// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package pki import ( "context" "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/sha256" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/base64" "fmt" "math/big" "net/http" "net/http/httptest" "strconv" "strings" "testing" "time" "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/builtin/logical/pki/dnstest" "github.com/stretchr/testify/require" ) type keyAuthorizationTestCase struct { keyAuthz string token string thumbprint string shouldFail bool } var keyAuthorizationTestCases = []keyAuthorizationTestCase{ { // Entirely empty "", "non-empty-token", "non-empty-thumbprint", true, }, { // Both empty ".", "non-empty-token", "non-empty-thumbprint", true, }, { // Not equal "non-.non-", "non-empty-token", "non-empty-thumbprint", true, }, { // Empty thumbprint "non-.", "non-empty-token", "non-empty-thumbprint", true, }, { // Empty token ".non-", "non-empty-token", "non-empty-thumbprint", true, }, { // Wrong order "non-empty-thumbprint.non-empty-token", "non-empty-token", "non-empty-thumbprint", true, }, { // Too many pieces "one.two.three", "non-empty-token", "non-empty-thumbprint", true, }, { // Valid "non-empty-token.non-empty-thumbprint", "non-empty-token", "non-empty-thumbprint", false, }, } func TestAcmeValidateKeyAuthorization(t *testing.T) { t.Parallel() for index, tc := range keyAuthorizationTestCases { t.Run("subtest-"+strconv.Itoa(index), func(st *testing.T) { isValid, err := ValidateKeyAuthorization(tc.keyAuthz, tc.token, tc.thumbprint) if !isValid && err == nil { st.Fatalf("[%d] expected failure to give reason via err (%v / %v)", index, isValid, err) } expectedValid := !tc.shouldFail if expectedValid != isValid { st.Fatalf("[%d] got ret=%v, expected ret=%v (shouldFail=%v)", index, isValid, expectedValid, tc.shouldFail) } }) } } func TestAcmeValidateHTTP01Challenge(t *testing.T) { t.Parallel() for index, tc := range keyAuthorizationTestCases { validFunc := func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(tc.keyAuthz)) } withPadding := func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(" " + tc.keyAuthz + " ")) } withRedirect := func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/.well-known/") { http.Redirect(w, r, "/my-http-01-challenge-response", 301) return } w.Write([]byte(tc.keyAuthz)) } withSleep := func(w http.ResponseWriter, r *http.Request) { // Long enough to ensure any excessively short timeouts are hit, // not long enough to trigger a failure (hopefully). time.Sleep(5 * time.Second) w.Write([]byte(tc.keyAuthz)) } validHandlers := []http.HandlerFunc{ http.HandlerFunc(validFunc), http.HandlerFunc(withPadding), http.HandlerFunc(withRedirect), http.HandlerFunc(withSleep), } for handlerIndex, handler := range validHandlers { func() { ts := httptest.NewServer(handler) defer ts.Close() host := ts.URL[7:] isValid, err := ValidateHTTP01Challenge(host, tc.token, tc.thumbprint, &acmeConfigEntry{}) if !isValid && err == nil { t.Fatalf("[tc=%d/handler=%d] expected failure to give reason via err (%v / %v)", index, handlerIndex, isValid, err) } expectedValid := !tc.shouldFail if expectedValid != isValid { t.Fatalf("[tc=%d/handler=%d] got ret=%v (err=%v), expected ret=%v (shouldFail=%v)", index, handlerIndex, isValid, err, expectedValid, tc.shouldFail) } }() } } // Negative test cases for various HTTP-specific scenarios. redirectLoop := func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/my-http-01-challenge-response", 301) } publicRedirect := func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "http://hashicorp.com/", 301) } noData := func(w http.ResponseWriter, r *http.Request) {} noContent := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } notFound := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) } simulateHang := func(w http.ResponseWriter, r *http.Request) { time.Sleep(30 * time.Second) w.Write([]byte("my-token.my-thumbprint")) } tooLarge := func(w http.ResponseWriter, r *http.Request) { for i := 0; i < 512; i++ { w.Write([]byte("my-token.my-thumbprint\n")) } } validHandlers := []http.HandlerFunc{ http.HandlerFunc(redirectLoop), http.HandlerFunc(publicRedirect), http.HandlerFunc(noData), http.HandlerFunc(noContent), http.HandlerFunc(notFound), http.HandlerFunc(simulateHang), http.HandlerFunc(tooLarge), } for handlerIndex, handler := range validHandlers { func() { ts := httptest.NewServer(handler) defer ts.Close() host := ts.URL[7:] isValid, err := ValidateHTTP01Challenge(host, "my-token", "my-thumbprint", &acmeConfigEntry{}) if isValid || err == nil { t.Fatalf("[handler=%d] expected failure validating challenge (%v / %v)", handlerIndex, isValid, err) } }() } } func TestAcmeValidateDNS01Challenge(t *testing.T) { t.Parallel() host := "dadgarcorp.com" resolver := dnstest.SetupResolver(t, host) defer resolver.Cleanup() t.Logf("DNS Server Address: %v", resolver.GetLocalAddr()) config := &acmeConfigEntry{ DNSResolver: resolver.GetLocalAddr(), } for index, tc := range keyAuthorizationTestCases { checksum := sha256.Sum256([]byte(tc.keyAuthz)) authz := base64.RawURLEncoding.EncodeToString(checksum[:]) resolver.AddRecord(DNSChallengePrefix+host, "TXT", authz) resolver.PushConfig() isValid, err := ValidateDNS01Challenge(host, tc.token, tc.thumbprint, config) if !isValid && err == nil { t.Fatalf("[tc=%d] expected failure to give reason via err (%v / %v)", index, isValid, err) } expectedValid := !tc.shouldFail if expectedValid != isValid { t.Fatalf("[tc=%d] got ret=%v (err=%v), expected ret=%v (shouldFail=%v)", index, isValid, err, expectedValid, tc.shouldFail) } resolver.RemoveAllRecords() } } func TestAcmeValidateTLSALPN01Challenge(t *testing.T) { // This test is not parallel because we modify ALPNPort to use a custom // non-standard port _just for testing purposes_. host := "localhost" config := &acmeConfigEntry{} log := hclog.L() returnedProtocols := []string{ALPNProtocol} var certificates []*x509.Certificate var privateKey crypto.PrivateKey tlsCfg := &tls.Config{} tlsCfg.GetConfigForClient = func(*tls.ClientHelloInfo) (*tls.Config, error) { var retCfg tls.Config = *tlsCfg retCfg.NextProtos = returnedProtocols log.Info(fmt.Sprintf("[alpn-server] returned protocol: %v", returnedProtocols)) return &retCfg, nil } tlsCfg.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) { var ret tls.Certificate for index, cert := range certificates { ret.Certificate = append(ret.Certificate, cert.Raw) if index == 0 { ret.Leaf = cert } } ret.PrivateKey = privateKey log.Info(fmt.Sprintf("[alpn-server] returned certificates: %v", ret)) return &ret, nil } ln, err := tls.Listen("tcp", host+":0", tlsCfg) require.NoError(t, err, "failed to listen with TLS config") doOneAccept := func() { log.Info("[alpn-server] starting accept...") connRaw, err := ln.Accept() require.NoError(t, err, "failed to accept TLS connection") log.Info("[alpn-server] got connection...") conn := tls.Server(connRaw.(*tls.Conn), tlsCfg) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) defer func() { log.Info("[alpn-server] canceling listener connection...") cancel() }() log.Info("[alpn-server] starting handshake...") if err := conn.HandshakeContext(ctx); err != nil { log.Info("[alpn-server] got non-fatal error while handshaking connection: %v", err) } log.Info("[alpn-server] closing connection...") if err := conn.Close(); err != nil { log.Info("[alpn-server] got non-fatal error while closing connection: %v", err) } } ALPNPort = strings.Split(ln.Addr().String(), ":")[1] type alpnTestCase struct { name string certificates []*x509.Certificate privateKey crypto.PrivateKey protocols []string token string thumbprint string shouldFail bool } var alpnTestCases []alpnTestCase // Add all of our keyAuthorizationTestCases into alpnTestCases for index, tc := range keyAuthorizationTestCases { log.Info(fmt.Sprintf("using keyAuthorizationTestCase [tc=%d] as alpnTestCase [tc=%d]...", index, len(alpnTestCases))) // Properly encode the authorization. checksum := sha256.Sum256([]byte(tc.keyAuthz)) authz, err := asn1.Marshal(checksum[:]) require.NoError(t, err, "failed asn.1 marshalling authz") // Build a self-signed certificate. key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err, "failed generating private key") tmpl := &x509.Certificate{ Subject: pkix.Name{ CommonName: host, }, Issuer: pkix.Name{ CommonName: host, }, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, PublicKey: key.Public(), SerialNumber: big.NewInt(1), DNSNames: []string{host}, ExtraExtensions: []pkix.Extension{ { Id: OIDACMEIdentifier, Critical: true, Value: authz, }, }, BasicConstraintsValid: true, IsCA: false, } certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key) require.NoError(t, err, "failed to create certificate") cert, err := x509.ParseCertificate(certBytes) require.NoError(t, err, "failed to parse newly generated certificate") newTc := alpnTestCase{ name: fmt.Sprintf("keyAuthorizationTestCase[%d]", index), certificates: []*x509.Certificate{cert}, privateKey: key, protocols: []string{ALPNProtocol}, token: tc.token, thumbprint: tc.thumbprint, shouldFail: tc.shouldFail, } alpnTestCases = append(alpnTestCases, newTc) } { // Test case: Longer chain // Build a self-signed certificate. rootKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err, "failed generating root private key") tmpl := &x509.Certificate{ Subject: pkix.Name{ CommonName: "Root CA", }, Issuer: pkix.Name{ CommonName: "Root CA", }, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, PublicKey: rootKey.Public(), SerialNumber: big.NewInt(1), BasicConstraintsValid: true, IsCA: true, } rootCertBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, rootKey.Public(), rootKey) require.NoError(t, err, "failed to create root certificate") rootCert, err := x509.ParseCertificate(rootCertBytes) require.NoError(t, err, "failed to parse newly generated root certificate") // Compute our authorization. checksum := sha256.Sum256([]byte("valid.valid")) authz, err := asn1.Marshal(checksum[:]) require.NoError(t, err, "failed to marshal authz with asn.1 ") // Build a leaf certificate which _could_ pass validation key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err, "failed generating leaf private key") tmpl = &x509.Certificate{ Subject: pkix.Name{ CommonName: host, }, Issuer: pkix.Name{ CommonName: "Root CA", }, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, PublicKey: key.Public(), SerialNumber: big.NewInt(2), DNSNames: []string{host}, ExtraExtensions: []pkix.Extension{ { Id: OIDACMEIdentifier, Critical: true, Value: authz, }, }, BasicConstraintsValid: true, IsCA: false, } certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, rootCert, key.Public(), rootKey) require.NoError(t, err, "failed to create leaf certificate") cert, err := x509.ParseCertificate(certBytes) require.NoError(t, err, "failed to parse newly generated leaf certificate") newTc := alpnTestCase{ name: "longer chain with valid leaf", certificates: []*x509.Certificate{cert, rootCert}, privateKey: key, protocols: []string{ALPNProtocol}, token: "valid", thumbprint: "valid", shouldFail: true, } alpnTestCases = append(alpnTestCases, newTc) } { // Test case: cert without DNSSan // Compute our authorization. checksum := sha256.Sum256([]byte("valid.valid")) authz, err := asn1.Marshal(checksum[:]) require.NoError(t, err, "failed to marshal authz with asn.1 ") // Build a leaf certificate without a DNSSan key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err, "failed generating leaf private key") tmpl := &x509.Certificate{ Subject: pkix.Name{ CommonName: host, }, Issuer: pkix.Name{ CommonName: host, }, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, PublicKey: key.Public(), SerialNumber: big.NewInt(2), // NO DNSNames ExtraExtensions: []pkix.Extension{ { Id: OIDACMEIdentifier, Critical: true, Value: authz, }, }, BasicConstraintsValid: true, IsCA: false, } certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key) require.NoError(t, err, "failed to create leaf certificate") cert, err := x509.ParseCertificate(certBytes) require.NoError(t, err, "failed to parse newly generated leaf certificate") newTc := alpnTestCase{ name: "valid keyauthz without valid dnsname", certificates: []*x509.Certificate{cert}, privateKey: key, protocols: []string{ALPNProtocol}, token: "valid", thumbprint: "valid", shouldFail: true, } alpnTestCases = append(alpnTestCases, newTc) } { // Test case: cert without matching DNSSan // Compute our authorization. checksum := sha256.Sum256([]byte("valid.valid")) authz, err := asn1.Marshal(checksum[:]) require.NoError(t, err, "failed to marshal authz with asn.1 ") // Build a leaf certificate which fails validation due to bad DNSName key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err, "failed generating leaf private key") tmpl := &x509.Certificate{ Subject: pkix.Name{ CommonName: host, }, Issuer: pkix.Name{ CommonName: host, }, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, PublicKey: key.Public(), SerialNumber: big.NewInt(2), DNSNames: []string{host + ".dadgarcorp.com" /* not matching host! */}, ExtraExtensions: []pkix.Extension{ { Id: OIDACMEIdentifier, Critical: true, Value: authz, }, }, BasicConstraintsValid: true, IsCA: false, } certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key) require.NoError(t, err, "failed to create leaf certificate") cert, err := x509.ParseCertificate(certBytes) require.NoError(t, err, "failed to parse newly generated leaf certificate") newTc := alpnTestCase{ name: "valid keyauthz without matching dnsname", certificates: []*x509.Certificate{cert}, privateKey: key, protocols: []string{ALPNProtocol}, token: "valid", thumbprint: "valid", shouldFail: true, } alpnTestCases = append(alpnTestCases, newTc) } { // Test case: cert with additional SAN // Compute our authorization. checksum := sha256.Sum256([]byte("valid.valid")) authz, err := asn1.Marshal(checksum[:]) require.NoError(t, err, "failed to marshal authz with asn.1 ") // Build a leaf certificate which has an invalid additional SAN key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err, "failed generating leaf private key") tmpl := &x509.Certificate{ Subject: pkix.Name{ CommonName: host, }, Issuer: pkix.Name{ CommonName: host, }, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, PublicKey: key.Public(), SerialNumber: big.NewInt(2), DNSNames: []string{host}, EmailAddresses: []string{"webmaster@" + host}, /* unexpected */ ExtraExtensions: []pkix.Extension{ { Id: OIDACMEIdentifier, Critical: true, Value: authz, }, }, BasicConstraintsValid: true, IsCA: false, } certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key) require.NoError(t, err, "failed to create leaf certificate") cert, err := x509.ParseCertificate(certBytes) require.NoError(t, err, "failed to parse newly generated leaf certificate") newTc := alpnTestCase{ name: "valid keyauthz with additional email SANs", certificates: []*x509.Certificate{cert}, privateKey: key, protocols: []string{ALPNProtocol}, token: "valid", thumbprint: "valid", shouldFail: true, } alpnTestCases = append(alpnTestCases, newTc) } { // Test case: cert without CN // Compute our authorization. checksum := sha256.Sum256([]byte("valid.valid")) authz, err := asn1.Marshal(checksum[:]) require.NoError(t, err, "failed to marshal authz with asn.1 ") // Build a leaf certificate which should pass validation key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err, "failed generating leaf private key") tmpl := &x509.Certificate{ Subject: pkix.Name{}, Issuer: pkix.Name{}, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, PublicKey: key.Public(), SerialNumber: big.NewInt(2), DNSNames: []string{host}, ExtraExtensions: []pkix.Extension{ { Id: OIDACMEIdentifier, Critical: true, Value: authz, }, }, BasicConstraintsValid: true, IsCA: false, } certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key) require.NoError(t, err, "failed to create leaf certificate") cert, err := x509.ParseCertificate(certBytes) require.NoError(t, err, "failed to parse newly generated leaf certificate") newTc := alpnTestCase{ name: "valid certificate; no Subject/Issuer (missing CN)", certificates: []*x509.Certificate{cert}, privateKey: key, protocols: []string{ALPNProtocol}, token: "valid", thumbprint: "valid", shouldFail: false, } alpnTestCases = append(alpnTestCases, newTc) } { // Test case: cert without the extension // Build a leaf certificate which should fail validation key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err, "failed generating leaf private key") tmpl := &x509.Certificate{ Subject: pkix.Name{}, Issuer: pkix.Name{}, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, PublicKey: key.Public(), SerialNumber: big.NewInt(1), DNSNames: []string{host}, BasicConstraintsValid: true, IsCA: true, } certBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key) require.NoError(t, err, "failed to create leaf certificate") cert, err := x509.ParseCertificate(certBytes) require.NoError(t, err, "failed to parse newly generated leaf certificate") newTc := alpnTestCase{ name: "missing required acmeIdentifier extension", certificates: []*x509.Certificate{cert}, privateKey: key, protocols: []string{ALPNProtocol}, token: "valid", thumbprint: "valid", shouldFail: true, } alpnTestCases = append(alpnTestCases, newTc) } { // Test case: root without a leaf // Build a self-signed certificate. rootKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err, "failed generating root private key") tmpl := &x509.Certificate{ Subject: pkix.Name{ CommonName: "Root CA", }, Issuer: pkix.Name{ CommonName: "Root CA", }, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, PublicKey: rootKey.Public(), SerialNumber: big.NewInt(1), BasicConstraintsValid: true, IsCA: true, } rootCertBytes, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, rootKey.Public(), rootKey) require.NoError(t, err, "failed to create root certificate") rootCert, err := x509.ParseCertificate(rootCertBytes) require.NoError(t, err, "failed to parse newly generated root certificate") newTc := alpnTestCase{ name: "root without leaf", certificates: []*x509.Certificate{rootCert}, privateKey: rootKey, protocols: []string{ALPNProtocol}, token: "valid", thumbprint: "valid", shouldFail: true, } alpnTestCases = append(alpnTestCases, newTc) } for index, tc := range alpnTestCases { log.Info(fmt.Sprintf("\n\n[tc=%d/name=%s] starting validation", index, tc.name)) certificates = tc.certificates privateKey = tc.privateKey returnedProtocols = tc.protocols // Attempt to validate the challenge. go doOneAccept() isValid, err := ValidateTLSALPN01Challenge(host, tc.token, tc.thumbprint, config) if !isValid && err == nil { t.Fatalf("[tc=%d/name=%s] expected failure to give reason via err (%v / %v)", index, tc.name, isValid, err) } expectedValid := !tc.shouldFail if expectedValid != isValid { t.Fatalf("[tc=%d/name=%s] got ret=%v (err=%v), expected ret=%v (shouldFail=%v)", index, tc.name, isValid, err, expectedValid, tc.shouldFail) } else if err != nil { log.Info(fmt.Sprintf("[tc=%d/name=%s] got expected failure: err=%v", index, tc.name, err)) } } } // TestAcmeValidateHttp01TLSRedirect verify that we allow a http-01 challenge to redirect // to a TLS server and not validate the certificate chain is valid. We don't validate the // TLS chain as we would have accepted the auth over a non-secured channel anyway had // the original request not redirected us. func TestAcmeValidateHttp01TLSRedirect(t *testing.T) { t.Parallel() for index, tc := range keyAuthorizationTestCases { t.Run("subtest-"+strconv.Itoa(index), func(st *testing.T) { validFunc := func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/.well-known/") { w.Write([]byte(tc.keyAuthz)) return } http.Error(w, "status not found", http.StatusNotFound) } tlsTs := httptest.NewTLSServer(http.HandlerFunc(validFunc)) defer tlsTs.Close() // Set up a http server that will redirect to our TLS server ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, tlsTs.URL+r.URL.Path, 301) })) defer ts.Close() host := ts.URL[len("http://"):] isValid, err := ValidateHTTP01Challenge(host, tc.token, tc.thumbprint, &acmeConfigEntry{}) if !isValid && err == nil { st.Fatalf("[tc=%d] expected failure to give reason via err (%v / %v)", index, isValid, err) } expectedValid := !tc.shouldFail if expectedValid != isValid { st.Fatalf("[tc=%d] got ret=%v (err=%v), expected ret=%v (shouldFail=%v)", index, isValid, err, expectedValid, tc.shouldFail) } }) } }