mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-30 18:17:55 +00:00 
			
		
		
		
	Add TLS-ALPN-01 Challenge Type to ACME (#20943)
* Add ACME TLS-ALPN-01 Challenge validator to PKI This adds support for verifying the last missing challenge type, TLS-ALPN-01 challenges, using Go's TLS library. We wish to add this as many servers (such as Caddy) support transparently renewing certificates via this protocol, without influencing the contents of sites served. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Enable suggesting, validating tls-alpn-01 in PKI Notably, while RFC 8737 is somewhat vague about what identifier types can be validated with this protocol, it does restrict SANs to be only DNSSans; from this, we can infer that it is not applicable for IP typed identifiers. Additionally, since this must resolve to a specific domain name, we cannot provision it for wildcard identifiers either. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Fix test expectations to allow ALPN challenges Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add tls-alpn-01 as a supported challenge to docs Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add test for tls-alpn-01 challenge verifier This hacks the challenge engine to allow non-standard (non-443) ports, letting us use a local server listener with custom implementation. In addition to the standard test cases, we run: - A test with a longer chain (bad), - A test without a DNSSan (bad), - A test with a bad DNSSan (bad), - A test with some other SANs (bad), - A test without a CN (good), - A test without any leaf (bad), and - A test without the extension (bad). Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add changelog entry Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Update builtin/logical/pki/acme_challenges.go Co-authored-by: Alexander Scheel <alex.scheel@hashicorp.com> --------- Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> Co-authored-by: Kit Haines <khaines@mit.edu>
This commit is contained in:
		| @@ -417,6 +417,22 @@ func (ace *ACMEChallengeEngine) _verifyChallenge(sc *storageContext, id string, | |||||||
| 			err = fmt.Errorf("%w: error validating dns-01 challenge %v: %v; %v", ErrIncorrectResponse, id, err, ChallengeAttemptFailedMsg) | 			err = fmt.Errorf("%w: error validating dns-01 challenge %v: %v; %v", ErrIncorrectResponse, id, err, ChallengeAttemptFailedMsg) | ||||||
| 			return ace._verifyChallengeRetry(sc, cv, authzPath, authz, challenge, err, id) | 			return ace._verifyChallengeRetry(sc, cv, authzPath, authz, challenge, err, id) | ||||||
| 		} | 		} | ||||||
|  | 	case ACMEALPNChallenge: | ||||||
|  | 		if authz.Identifier.Type != ACMEDNSIdentifier { | ||||||
|  | 			err = fmt.Errorf("unsupported identifier type for authorization %v/%v in challenge %v: %v", cv.Account, cv.Authorization, id, authz.Identifier.Type) | ||||||
|  | 			return ace._verifyChallengeCleanup(sc, err, id) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if authz.Wildcard { | ||||||
|  | 			err = fmt.Errorf("unable to validate wildcard authorization %v/%v in challenge %v via tls-alpn-01 challenge", cv.Account, cv.Authorization, id) | ||||||
|  | 			return ace._verifyChallengeCleanup(sc, err, id) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		valid, err = ValidateTLSALPN01Challenge(authz.Identifier.Value, cv.Token, cv.Thumbprint, config) | ||||||
|  | 		if err != nil { | ||||||
|  | 			err = fmt.Errorf("%w: error validating tls-alpn-01 challenge %v: %s", ErrIncorrectResponse, id, err.Error()) | ||||||
|  | 			return ace._verifyChallengeRetry(sc, cv, authzPath, authz, challenge, err, id) | ||||||
|  | 		} | ||||||
| 	default: | 	default: | ||||||
| 		err = fmt.Errorf("unsupported ACME challenge type %v for challenge %v", cv.ChallengeType, id) | 		err = fmt.Errorf("unsupported ACME challenge type %v for challenge %v", cv.ChallengeType, id) | ||||||
| 		return ace._verifyChallengeCleanup(sc, err, id) | 		return ace._verifyChallengeCleanup(sc, err, id) | ||||||
|   | |||||||
| @@ -1,8 +1,12 @@ | |||||||
| package pki | package pki | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"bytes" | ||||||
| 	"context" | 	"context" | ||||||
| 	"crypto/sha256" | 	"crypto/sha256" | ||||||
|  | 	"crypto/tls" | ||||||
|  | 	"crypto/x509" | ||||||
|  | 	"encoding/asn1" | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| @@ -12,7 +16,21 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const DNSChallengePrefix = "_acme-challenge." | const ( | ||||||
|  | 	DNSChallengePrefix = "_acme-challenge." | ||||||
|  | 	ALPNProtocol       = "acme-tls/1" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // While this should be a constant, there's no way to do a low-level test of | ||||||
|  | // ValidateTLSALPN01Challenge without spinning up a complicated Docker | ||||||
|  | // instance to build a custom responder. Because we already have a local | ||||||
|  | // toolchain, it is far easier to drive this through Go tests with a custom | ||||||
|  | // (high) port, rather than requiring permission to bind to port 443 (root-run | ||||||
|  | // tests are even worse). | ||||||
|  | var ALPNPort = "443" | ||||||
|  |  | ||||||
|  | // OID of the acmeIdentifier X.509 Certificate Extension. | ||||||
|  | var OIDACMEIdentifier = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31} | ||||||
|  |  | ||||||
| // ValidateKeyAuthorization validates that the given keyAuthz from a challenge | // ValidateKeyAuthorization validates that the given keyAuthz from a challenge | ||||||
| // matches our expectation, returning (true, nil) if so, or (false, err) if | // matches our expectation, returning (true, nil) if so, or (false, err) if | ||||||
| @@ -67,15 +85,28 @@ func buildResolver(config *acmeConfigEntry) (*net.Resolver, error) { | |||||||
| 	}, nil | 	}, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func buildDialerConfig(config *acmeConfigEntry) (*net.Dialer, error) { | ||||||
|  | 	resolver, err := buildResolver(config) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("failed to build resolver: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &net.Dialer{ | ||||||
|  | 		Timeout:   10 * time.Second, | ||||||
|  | 		KeepAlive: -1 * time.Second, | ||||||
|  | 		Resolver:  resolver, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
| // Validates a given ACME http-01 challenge against the specified domain, | // Validates a given ACME http-01 challenge against the specified domain, | ||||||
| // per RFC 8555. | // per RFC 8555. | ||||||
| // | // | ||||||
| // We attempt to be defensive here against timeouts, extra redirects, &c. | // We attempt to be defensive here against timeouts, extra redirects, &c. | ||||||
| func ValidateHTTP01Challenge(domain string, token string, thumbprint string, config *acmeConfigEntry) (bool, error) { | func ValidateHTTP01Challenge(domain string, token string, thumbprint string, config *acmeConfigEntry) (bool, error) { | ||||||
| 	path := "http://" + domain + "/.well-known/acme-challenge/" + token | 	path := "http://" + domain + "/.well-known/acme-challenge/" + token | ||||||
| 	resolver, err := buildResolver(config) | 	dialer, err := buildDialerConfig(config) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, fmt.Errorf("failed to build resolver: %w", err) | 		return false, fmt.Errorf("failed to build dialer: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	transport := &http.Transport{ | 	transport := &http.Transport{ | ||||||
| @@ -90,11 +121,7 @@ func ValidateHTTP01Challenge(domain string, token string, thumbprint string, con | |||||||
|  |  | ||||||
| 		// We'd rather timeout and re-attempt validation later than hang | 		// We'd rather timeout and re-attempt validation later than hang | ||||||
| 		// too many validators waiting for slow hosts. | 		// too many validators waiting for slow hosts. | ||||||
| 		DialContext: (&net.Dialer{ | 		DialContext:           dialer.DialContext, | ||||||
| 			Timeout:   10 * time.Second, |  | ||||||
| 			KeepAlive: -1 * time.Second, |  | ||||||
| 			Resolver:  resolver, |  | ||||||
| 		}).DialContext, |  | ||||||
| 		ResponseHeaderTimeout: 10 * time.Second, | 		ResponseHeaderTimeout: 10 * time.Second, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -188,3 +215,255 @@ func ValidateDNS01Challenge(domain string, token string, thumbprint string, conf | |||||||
|  |  | ||||||
| 	return false, fmt.Errorf("dns-01: challenge failed against %v records", len(results)) | 	return false, fmt.Errorf("dns-01: challenge failed against %v records", len(results)) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func ValidateTLSALPN01Challenge(domain string, token string, thumbprint string, config *acmeConfigEntry) (bool, error) { | ||||||
|  | 	// This RFC is defined in RFC 8737 Automated Certificate Management | ||||||
|  | 	// Environment (ACME) TLS Application‑Layer Protocol Negotiation | ||||||
|  | 	// (ALPN) Challenge Extension. | ||||||
|  | 	// | ||||||
|  | 	// This is conceptually similar to ValidateHTTP01Challenge, but | ||||||
|  | 	// uses a TLS connection on port 443 with the specified ALPN | ||||||
|  | 	// protocol. | ||||||
|  |  | ||||||
|  | 	cfg := &tls.Config{ | ||||||
|  | 		// Per RFC 8737 Section 3. TLS with Application-Layer Protocol | ||||||
|  | 		// Negotiation (TLS ALPN) Challenge, the name of the negotiated | ||||||
|  | 		// protocol is "acme-tls/1". | ||||||
|  | 		NextProtos: []string{ALPNProtocol}, | ||||||
|  |  | ||||||
|  | 		// Per RFC 8737 Section 3. TLS with Application-Layer Protocol | ||||||
|  | 		// Negotiation (TLS ALPN) Challenge: | ||||||
|  | 		// | ||||||
|  | 		// > ... and an SNI extension containing only the domain name | ||||||
|  | 		// > being validated during the TLS handshake. | ||||||
|  | 		// | ||||||
|  | 		// According to the Go docs, setting this option (even though | ||||||
|  | 		// InsecureSkipVerify=true is also specified), allows us to | ||||||
|  | 		// set the SNI extension to this value. | ||||||
|  | 		ServerName: domain, | ||||||
|  |  | ||||||
|  | 		VerifyConnection: func(connState tls.ConnectionState) error { | ||||||
|  | 			// We initiated a fresh connection with no session tickets; | ||||||
|  | 			// even if we did have a session ticket, we do not wish to | ||||||
|  | 			// use it. Verify that the server has not inadvertently | ||||||
|  | 			// reused connections between validation attempts or something. | ||||||
|  | 			if connState.DidResume { | ||||||
|  | 				return fmt.Errorf("server under test incorrectly reported that handshake was resumed when no session cache was provided; refusing to continue") | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Per RFC 8737 Section 3. TLS with Application-Layer Protocol | ||||||
|  | 			// Negotiation (TLS ALPN) Challenge: | ||||||
|  | 			// | ||||||
|  | 			// > The ACME server verifies that during the TLS handshake the | ||||||
|  | 			// > application-layer protocol "acme-tls/1" was successfully | ||||||
|  | 			// > negotiated (and that the ALPN extension contained only the | ||||||
|  | 			// > value "acme-tls/1"). | ||||||
|  | 			if connState.NegotiatedProtocol != ALPNProtocol { | ||||||
|  | 				return fmt.Errorf("server under test negotiated unexpected ALPN protocol %v", connState.NegotiatedProtocol) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Per RFC 8737 Section 3. TLS with Application-Layer Protocol | ||||||
|  | 			// Negotiation (TLS ALPN) Challenge: | ||||||
|  | 			// | ||||||
|  | 			// > and that the certificate returned | ||||||
|  | 			// | ||||||
|  | 			// Because this certificate MUST be self-signed (per earlier | ||||||
|  | 			// statement in RFC 8737 Section 3), there is no point in sending | ||||||
|  | 			// more than one certificate, and so we will err early here if | ||||||
|  | 			// we got more than one. | ||||||
|  | 			if len(connState.PeerCertificates) > 1 { | ||||||
|  | 				return fmt.Errorf("server under test returned multiple (%v) certificates when we expected only one", len(connState.PeerCertificates)) | ||||||
|  | 			} | ||||||
|  | 			cert := connState.PeerCertificates[0] | ||||||
|  |  | ||||||
|  | 			// Per RFC 8737 Section 3. TLS with Application-Layer Protocol | ||||||
|  | 			// Negotiation (TLS ALPN) Challenge: | ||||||
|  | 			// | ||||||
|  | 			// > The client prepares for validation by constructing a | ||||||
|  | 			// > self-signed certificate that MUST contain an acmeIdentifier | ||||||
|  | 			// > extension and a subjectAlternativeName extension [RFC5280]. | ||||||
|  | 			// | ||||||
|  | 			// Verify that this is a self-signed certificate that isn't signed | ||||||
|  | 			// by another certificate (i.e., with the same key material but | ||||||
|  | 			// different issuer). | ||||||
|  | 			if err := cert.CheckSignatureFrom(cert); err != nil { | ||||||
|  | 				return fmt.Errorf("server under test returned a non-self-signed certificate: %w", err) | ||||||
|  | 			} | ||||||
|  | 			if !bytes.Equal(cert.RawSubject, cert.RawIssuer) { | ||||||
|  | 				return fmt.Errorf("server under test returned a non-self-signed certificate: invalid subject (%v) <-> issuer (%v) match", cert.Subject.String(), cert.Issuer.String()) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Per RFC 8737 Section 3. TLS with Application-Layer Protocol | ||||||
|  | 			// Negotiation (TLS ALPN) Challenge: | ||||||
|  | 			// | ||||||
|  | 			// > The subjectAlternativeName extension MUST contain a single | ||||||
|  | 			// > dNSName entry where the value is the domain name being | ||||||
|  | 			// > validated. | ||||||
|  | 			// | ||||||
|  | 			// TODO: this does not validate that there are not other SANs | ||||||
|  | 			// with unknown (to Go) OIDs. | ||||||
|  | 			if len(cert.DNSNames) != 1 || len(cert.EmailAddresses) > 0 || len(cert.IPAddresses) > 0 || len(cert.URIs) > 0 { | ||||||
|  | 				return fmt.Errorf("server under test returned a certificate with incorrect SANs") | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Per RFC 8737 Section 3. TLS with Application-Layer Protocol | ||||||
|  | 			// Negotiation (TLS ALPN) Challenge: | ||||||
|  | 			// | ||||||
|  | 			// > The comparison of dNSNames MUST be case insensitive | ||||||
|  | 			// > [RFC4343]. Note that as ACME doesn't support Unicode | ||||||
|  | 			// > identifiers, all dNSNames MUST be encoded using the rules | ||||||
|  | 			// > of [RFC3492]. | ||||||
|  | 			if !strings.EqualFold(cert.DNSNames[0], domain) { | ||||||
|  | 				return fmt.Errorf("server under test returned a certificate with unexpected identifier: %v", cert.DNSNames[0]) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Per above, verify that the acmeIdentifier extension is present | ||||||
|  | 			// exactly once and has the correct value. | ||||||
|  | 			var foundACMEId bool | ||||||
|  | 			for _, ext := range cert.Extensions { | ||||||
|  | 				if !ext.Id.Equal(OIDACMEIdentifier) { | ||||||
|  | 					continue | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				// There must be only a single ACME extension. | ||||||
|  | 				if foundACMEId { | ||||||
|  | 					return fmt.Errorf("server under test returned a certificate with multiple acmeIdentifier extensions") | ||||||
|  | 				} | ||||||
|  | 				foundACMEId = true | ||||||
|  |  | ||||||
|  | 				// Per RFC 8737 Section 3. TLS with Application-Layer Protocol | ||||||
|  | 				// Negotiation (TLS ALPN) Challenge: | ||||||
|  | 				// | ||||||
|  | 				// > a critical acmeIdentifier extension | ||||||
|  | 				if !ext.Critical { | ||||||
|  | 					return fmt.Errorf("server under test returned a certificate with an acmeIdentifier extension marked non-Critical") | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				keyAuthz := string(ext.Value) | ||||||
|  | 				ok, err := ValidateSHA256KeyAuthorization(keyAuthz, token, thumbprint) | ||||||
|  | 				if !ok || err != nil { | ||||||
|  | 					return fmt.Errorf("server under test returned a certificate with an invalid key authorization (%w)", err) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Per RFC 8737 Section 3. TLS with Application-Layer Protocol | ||||||
|  | 			// Negotiation (TLS ALPN) Challenge: | ||||||
|  | 			// | ||||||
|  | 			// > The ACME server verifies that ... the certificate returned | ||||||
|  | 			// > contains: ... a critical acmeIdentifier extension containing | ||||||
|  | 			// > the expected SHA-256 digest computed in step 1. | ||||||
|  | 			if !foundACMEId { | ||||||
|  | 				return fmt.Errorf("server under test returned a certificate without the required acmeIdentifier extension") | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// Remove the handled critical extension and validate that we | ||||||
|  | 			// have no additional critical extensions left unhandled. | ||||||
|  | 			var index int = -1 | ||||||
|  | 			for oidIndex, oid := range cert.UnhandledCriticalExtensions { | ||||||
|  | 				if oid.Equal(OIDACMEIdentifier) { | ||||||
|  | 					index = oidIndex | ||||||
|  | 					break | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			if index != -1 { | ||||||
|  | 				// Unlike the foundACMEId case, this is not a failure; if Go | ||||||
|  | 				// updates to "understand" this critical extension, we do not | ||||||
|  | 				// wish to fail. | ||||||
|  | 				cert.UnhandledCriticalExtensions = append(cert.UnhandledCriticalExtensions[0:index], cert.UnhandledCriticalExtensions[index+1:]...) | ||||||
|  | 			} | ||||||
|  | 			if len(cert.UnhandledCriticalExtensions) > 0 { | ||||||
|  | 				return fmt.Errorf("server under test returned a certificate with additional unknown critical extensions (%v)", cert.UnhandledCriticalExtensions) | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			// All good! | ||||||
|  | 			return nil | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		// We never want to resume a connection; do not provide session | ||||||
|  | 		// cache storage. | ||||||
|  | 		ClientSessionCache: nil, | ||||||
|  |  | ||||||
|  | 		// Do not trust any system trusted certificates; we're going to be | ||||||
|  | 		// manually validating the chain, so specifying a non-empty pool | ||||||
|  | 		// here could only cause additional, unnecessary work. | ||||||
|  | 		RootCAs: x509.NewCertPool(), | ||||||
|  |  | ||||||
|  | 		// Do not bother validating the client's chain; we know it should be | ||||||
|  | 		// self-signed. This also disables hostname verification, but we do | ||||||
|  | 		// this verification as part of VerifyConnection(...) ourselves. | ||||||
|  | 		// | ||||||
|  | 		// Per Go docs, this option is only safe in conjunction with | ||||||
|  | 		// VerifyConnection which we define above. | ||||||
|  | 		InsecureSkipVerify: true, | ||||||
|  |  | ||||||
|  | 		// RFC 8737 Section 4. acme-tls/1 Protocol Definition: | ||||||
|  | 		// | ||||||
|  | 		// > ACME servers that implement "acme-tls/1" MUST only negotiate | ||||||
|  | 		// > TLS 1.2 [RFC5246] or higher when connecting to clients for | ||||||
|  | 		// > validation. | ||||||
|  | 		MinVersion: tls.VersionTLS12, | ||||||
|  |  | ||||||
|  | 		// While RFC 8737 does not place restrictions around allowed cipher | ||||||
|  | 		// suites, we wish to restrict ourselves to secure defaults. Specify | ||||||
|  | 		// the Intermediate guideline from Mozilla's TLS config generator to | ||||||
|  | 		// disable obviously weak ciphers. | ||||||
|  | 		// | ||||||
|  | 		// See also: https://ssl-config.mozilla.org/#server=go&version=1.14.4&config=intermediate&guideline=5.7 | ||||||
|  | 		CipherSuites: []uint16{ | ||||||
|  | 			tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, | ||||||
|  | 			tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, | ||||||
|  | 			tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, | ||||||
|  | 			tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, | ||||||
|  | 			tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, | ||||||
|  | 			tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Build a dialer using our custom DNS resolver, to ensure domains get | ||||||
|  | 	// resolved according to configuration. | ||||||
|  | 	dialer, err := buildDialerConfig(config) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return false, fmt.Errorf("failed to build dialer: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Per RFC 8737 Section 3. TLS with Application-Layer Protocol | ||||||
|  | 	// Negotiation (TLS ALPN) Challenge: | ||||||
|  | 	// | ||||||
|  | 	// > 2. The ACME server resolves the domain name being validated and | ||||||
|  | 	// >    chooses one of the IP addresses returned for validation (the | ||||||
|  | 	// >    server MAY validate against multiple addresses if more than | ||||||
|  | 	// >    one is returned). | ||||||
|  | 	// > 3. The ACME server initiates a TLS connection to the chosen IP | ||||||
|  | 	// >    address. This connection MUST use TCP port 443. | ||||||
|  | 	address := fmt.Sprintf("%v:"+ALPNPort, domain) | ||||||
|  | 	conn, err := dialer.Dial("tcp", address) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return false, fmt.Errorf("tls-alpn-01: failed to dial host: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Initiate the connection to the remote peer. | ||||||
|  | 	client := tls.Client(conn, cfg) | ||||||
|  |  | ||||||
|  | 	// We intentionally swallow this error as it isn't useful to the | ||||||
|  | 	// underlying protocol we perform here. Notably, per RFC 8737 | ||||||
|  | 	// Section 4. acme-tls/1 Protocol Definition: | ||||||
|  | 	// | ||||||
|  | 	// > Once the handshake is completed, the client MUST NOT exchange | ||||||
|  | 	// > any further data with the server and MUST immediately close the | ||||||
|  | 	// > connection. ... Because of this, an ACME server MAY choose to | ||||||
|  | 	// > withhold authorization if either the certificate signature is | ||||||
|  | 	// > invalid or the handshake doesn't fully complete. | ||||||
|  | 	defer client.Close() | ||||||
|  |  | ||||||
|  | 	// We wish to put time bounds on the total time the handshake can | ||||||
|  | 	// stall for, so build a connection context here. | ||||||
|  | 	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) | ||||||
|  | 	defer cancel() | ||||||
|  |  | ||||||
|  | 	// See note above about why we can allow Handshake to complete | ||||||
|  | 	// successfully. | ||||||
|  | 	if err := client.HandshakeContext(ctx); err != nil { | ||||||
|  | 		return false, fmt.Errorf("tls-alpn-01: failed to perform handshake: %w", err) | ||||||
|  | 	} | ||||||
|  | 	return true, nil | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,8 +1,18 @@ | |||||||
| package pki | package pki | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"crypto" | ||||||
|  | 	"crypto/ecdsa" | ||||||
|  | 	"crypto/elliptic" | ||||||
|  | 	"crypto/rand" | ||||||
| 	"crypto/sha256" | 	"crypto/sha256" | ||||||
|  | 	"crypto/tls" | ||||||
|  | 	"crypto/x509" | ||||||
|  | 	"crypto/x509/pkix" | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
|  | 	"fmt" | ||||||
|  | 	"math/big" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/http/httptest" | 	"net/http/httptest" | ||||||
| 	"strings" | 	"strings" | ||||||
| @@ -10,6 +20,8 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/hashicorp/vault/builtin/logical/pki/dnstest" | 	"github.com/hashicorp/vault/builtin/logical/pki/dnstest" | ||||||
|  |  | ||||||
|  | 	"github.com/stretchr/testify/require" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type keyAuthorizationTestCase struct { | type keyAuthorizationTestCase struct { | ||||||
| @@ -190,7 +202,7 @@ func TestAcmeValidateHTTP01Challenge(t *testing.T) { | |||||||
| func TestAcmeValidateDNS01Challenge(t *testing.T) { | func TestAcmeValidateDNS01Challenge(t *testing.T) { | ||||||
| 	t.Parallel() | 	t.Parallel() | ||||||
|  |  | ||||||
| 	host := "alsdkjfasldkj.com" | 	host := "dadgarcorp.com" | ||||||
| 	resolver := dnstest.SetupResolver(t, host) | 	resolver := dnstest.SetupResolver(t, host) | ||||||
| 	defer resolver.Cleanup() | 	defer resolver.Cleanup() | ||||||
|  |  | ||||||
| @@ -219,3 +231,473 @@ func TestAcmeValidateDNS01Challenge(t *testing.T) { | |||||||
| 		resolver.RemoveAllRecords() | 		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{} | ||||||
|  |  | ||||||
|  | 	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 | ||||||
|  | 		t.Logf("[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 | ||||||
|  | 		t.Logf("[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() { | ||||||
|  | 		t.Logf("[alpn-server] starting accept...") | ||||||
|  | 		connRaw, err := ln.Accept() | ||||||
|  | 		require.NoError(t, err, "failed to accept TLS connection") | ||||||
|  |  | ||||||
|  | 		t.Logf("[alpn-server] got connection...") | ||||||
|  | 		conn := tls.Server(connRaw.(*tls.Conn), tlsCfg) | ||||||
|  |  | ||||||
|  | 		ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) | ||||||
|  | 		defer func() { | ||||||
|  | 			t.Logf("[alpn-server] defer context cancel executing") | ||||||
|  | 			cancel() | ||||||
|  | 		}() | ||||||
|  |  | ||||||
|  | 		t.Logf("[alpn-server] starting handshake...") | ||||||
|  | 		if err := conn.HandshakeContext(ctx); err != nil { | ||||||
|  | 			t.Logf("[alpn-server] got non-fatal error while handshaking connection: %v", err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		t.Logf("[alpn-server] closing connection...") | ||||||
|  | 		if err := conn.Close(); err != nil { | ||||||
|  | 			t.Logf("[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 { | ||||||
|  | 		t.Logf("using keyAuthorizationTestCase [tc=%d] as alpnTestCase [tc=%d]...", index, len(alpnTestCases)) | ||||||
|  | 		// Properly encode the authorization. | ||||||
|  | 		checksum := sha256.Sum256([]byte(tc.keyAuthz)) | ||||||
|  | 		authz := base64.RawURLEncoding.EncodeToString(checksum[:]) | ||||||
|  |  | ||||||
|  | 		// 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:    []byte(authz), | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			BasicConstraintsValid: true, | ||||||
|  | 			IsCA:                  true, | ||||||
|  | 		} | ||||||
|  | 		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 := base64.RawURLEncoding.EncodeToString(checksum[:]) | ||||||
|  |  | ||||||
|  | 		// 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:    []byte(authz), | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			BasicConstraintsValid: true, | ||||||
|  | 			IsCA:                  true, | ||||||
|  | 		} | ||||||
|  | 		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 := base64.RawURLEncoding.EncodeToString(checksum[:]) | ||||||
|  |  | ||||||
|  | 		// 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:    []byte(authz), | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			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:         "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 := base64.RawURLEncoding.EncodeToString(checksum[:]) | ||||||
|  |  | ||||||
|  | 		// 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:    []byte(authz), | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			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:         "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 := base64.RawURLEncoding.EncodeToString(checksum[:]) | ||||||
|  |  | ||||||
|  | 		// 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:    []byte(authz), | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			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:         "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 := base64.RawURLEncoding.EncodeToString(checksum[:]) | ||||||
|  |  | ||||||
|  | 		// 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:    []byte(authz), | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			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:         "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 { | ||||||
|  | 		t.Logf("\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 { | ||||||
|  | 			t.Logf("[tc=%d/name=%s] got expected failure: err=%v", index, tc.name, err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -845,7 +845,7 @@ func generateAuthorization(acct *acmeAccount, identifier *ACMEIdentifier) (*ACME | |||||||
| 	// Certain challenges have certain restrictions: DNS challenges cannot | 	// Certain challenges have certain restrictions: DNS challenges cannot | ||||||
| 	// be used to validate IP addresses, and only DNS challenges can be used | 	// be used to validate IP addresses, and only DNS challenges can be used | ||||||
| 	// to validate wildcards. | 	// to validate wildcards. | ||||||
| 	allowedChallenges := []ACMEChallengeType{ACMEHTTPChallenge, ACMEDNSChallenge} | 	allowedChallenges := []ACMEChallengeType{ACMEHTTPChallenge, ACMEDNSChallenge, ACMEALPNChallenge} | ||||||
| 	if identifier.Type == ACMEIPIdentifier { | 	if identifier.Type == ACMEIPIdentifier { | ||||||
| 		allowedChallenges = []ACMEChallengeType{ACMEHTTPChallenge} | 		allowedChallenges = []ACMEChallengeType{ACMEHTTPChallenge} | ||||||
| 	} else if identifier.IsWildcard { | 	} else if identifier.IsWildcard { | ||||||
|   | |||||||
| @@ -185,7 +185,7 @@ func TestAcmeBasicWorkflow(t *testing.T) { | |||||||
| 			require.False(t, domainAuth.Wildcard, "should not be a wildcard") | 			require.False(t, domainAuth.Wildcard, "should not be a wildcard") | ||||||
| 			require.True(t, domainAuth.Expires.IsZero(), "authorization should only have expiry set on valid status") | 			require.True(t, domainAuth.Expires.IsZero(), "authorization should only have expiry set on valid status") | ||||||
|  |  | ||||||
| 			require.Len(t, domainAuth.Challenges, 2, "expected two challenges") | 			require.Len(t, domainAuth.Challenges, 3, "expected three challenges") | ||||||
| 			require.Equal(t, acme.StatusPending, domainAuth.Challenges[0].Status) | 			require.Equal(t, acme.StatusPending, domainAuth.Challenges[0].Status) | ||||||
| 			require.True(t, domainAuth.Challenges[0].Validated.IsZero(), "validated time should be 0 on challenge") | 			require.True(t, domainAuth.Challenges[0].Validated.IsZero(), "validated time should be 0 on challenge") | ||||||
| 			require.Equal(t, "http-01", domainAuth.Challenges[0].Type) | 			require.Equal(t, "http-01", domainAuth.Challenges[0].Type) | ||||||
| @@ -194,6 +194,10 @@ func TestAcmeBasicWorkflow(t *testing.T) { | |||||||
| 			require.True(t, domainAuth.Challenges[1].Validated.IsZero(), "validated time should be 0 on challenge") | 			require.True(t, domainAuth.Challenges[1].Validated.IsZero(), "validated time should be 0 on challenge") | ||||||
| 			require.Equal(t, "dns-01", domainAuth.Challenges[1].Type) | 			require.Equal(t, "dns-01", domainAuth.Challenges[1].Type) | ||||||
| 			require.NotEmpty(t, domainAuth.Challenges[1].Token, "missing challenge token") | 			require.NotEmpty(t, domainAuth.Challenges[1].Token, "missing challenge token") | ||||||
|  | 			require.Equal(t, acme.StatusPending, domainAuth.Challenges[2].Status) | ||||||
|  | 			require.True(t, domainAuth.Challenges[2].Validated.IsZero(), "validated time should be 0 on challenge") | ||||||
|  | 			require.Equal(t, "tls-alpn-01", domainAuth.Challenges[2].Type) | ||||||
|  | 			require.NotEmpty(t, domainAuth.Challenges[2].Token, "missing challenge token") | ||||||
|  |  | ||||||
| 			// Test the values for the wildcard authentication | 			// Test the values for the wildcard authentication | ||||||
| 			require.Equal(t, acme.StatusPending, wildcardAuth.Status) | 			require.Equal(t, acme.StatusPending, wildcardAuth.Status) | ||||||
| @@ -202,7 +206,7 @@ func TestAcmeBasicWorkflow(t *testing.T) { | |||||||
| 			require.True(t, wildcardAuth.Wildcard, "should be a wildcard") | 			require.True(t, wildcardAuth.Wildcard, "should be a wildcard") | ||||||
| 			require.True(t, wildcardAuth.Expires.IsZero(), "authorization should only have expiry set on valid status") | 			require.True(t, wildcardAuth.Expires.IsZero(), "authorization should only have expiry set on valid status") | ||||||
|  |  | ||||||
| 			require.Len(t, wildcardAuth.Challenges, 1, "expected two challenges") | 			require.Len(t, wildcardAuth.Challenges, 1, "expected one challenge") | ||||||
| 			require.Equal(t, acme.StatusPending, domainAuth.Challenges[0].Status) | 			require.Equal(t, acme.StatusPending, domainAuth.Challenges[0].Status) | ||||||
| 			require.True(t, wildcardAuth.Challenges[0].Validated.IsZero(), "validated time should be 0 on challenge") | 			require.True(t, wildcardAuth.Challenges[0].Validated.IsZero(), "validated time should be 0 on challenge") | ||||||
| 			require.Equal(t, "dns-01", wildcardAuth.Challenges[0].Type) | 			require.Equal(t, "dns-01", wildcardAuth.Challenges[0].Type) | ||||||
| @@ -1255,7 +1259,7 @@ func TestAcmeValidationError(t *testing.T) { | |||||||
| 		authorizations = append(authorizations, auth) | 		authorizations = append(authorizations, auth) | ||||||
| 	} | 	} | ||||||
| 	require.Len(t, authorizations, 1, "expected a certain number of authorizations") | 	require.Len(t, authorizations, 1, "expected a certain number of authorizations") | ||||||
| 	require.Len(t, authorizations[0].Challenges, 2, "expected a certain number of challenges associated with authorization") | 	require.Len(t, authorizations[0].Challenges, 3, "expected a certain number of challenges associated with authorization") | ||||||
|  |  | ||||||
| 	acceptedAuth, err := acmeClient.Accept(testCtx, authorizations[0].Challenges[0]) | 	acceptedAuth, err := acmeClient.Accept(testCtx, authorizations[0].Challenges[0]) | ||||||
| 	require.NoError(t, err, "Should have been allowed to accept challenge 1") | 	require.NoError(t, err, "Should have been allowed to accept challenge 1") | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								changelog/20943.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								changelog/20943.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | ```release-note:improvement | ||||||
|  | secrets/pki: Support TLS-ALPN-01 challenge type in ACME for DNS certificate identifiers. | ||||||
|  | ``` | ||||||
| @@ -165,8 +165,9 @@ identifiers. | |||||||
|  |  | ||||||
| Vault supports the following ACME challenge types presently: | Vault supports the following ACME challenge types presently: | ||||||
|  |  | ||||||
|  - `http-01`, supporting both `dns` and `ip` identifiers, |  - `http-01`, supporting both `dns` and `ip` identifiers. | ||||||
|  - `dns-01`, supporting `dns` identifiers including wildcards. |  - `dns-01`, supporting `dns` identifiers including wildcards. | ||||||
|  |  - `tls-alpn-01`, supporting only non-wildcard `dns` identifiers. | ||||||
|  |  | ||||||
| A custom DNS resolver used by the server for looking up DNS names for use | A custom DNS resolver used by the server for looking up DNS names for use | ||||||
| with both mechanisms can be added via the [ACME configuration](#set-acme-configuration). | with both mechanisms can be added via the [ACME configuration](#set-acme-configuration). | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Alexander Scheel
					Alexander Scheel