369 lines
13 KiB
Go
369 lines
13 KiB
Go
package va
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/asn1"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/miekg/dns"
|
|
|
|
"github.com/letsencrypt/boulder/core"
|
|
berrors "github.com/letsencrypt/boulder/errors"
|
|
"github.com/letsencrypt/boulder/identifier"
|
|
)
|
|
|
|
const (
|
|
// ALPN protocol ID for TLS-ALPN-01 challenge
|
|
// https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01#section-5.2
|
|
ACMETLS1Protocol = "acme-tls/1"
|
|
)
|
|
|
|
var (
|
|
// As defined in https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-04#section-5.1
|
|
// id-pe OID + 31 (acmeIdentifier)
|
|
IdPeAcmeIdentifier = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 31}
|
|
// OID for the Subject Alternative Name extension, as defined in
|
|
// https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6
|
|
IdCeSubjectAltName = asn1.ObjectIdentifier{2, 5, 29, 17}
|
|
)
|
|
|
|
// certAltNames collects up all of a certificate's subject names (Subject CN and
|
|
// Subject Alternate Names) and reduces them to a unique, sorted set, typically for an
|
|
// error message
|
|
func certAltNames(cert *x509.Certificate) []string {
|
|
var names []string
|
|
if cert.Subject.CommonName != "" {
|
|
names = append(names, cert.Subject.CommonName)
|
|
}
|
|
names = append(names, cert.DNSNames...)
|
|
names = append(names, cert.EmailAddresses...)
|
|
for _, id := range cert.IPAddresses {
|
|
names = append(names, id.String())
|
|
}
|
|
for _, id := range cert.URIs {
|
|
names = append(names, id.String())
|
|
}
|
|
names = core.UniqueLowerNames(names)
|
|
return names
|
|
}
|
|
|
|
func (va *ValidationAuthorityImpl) tryGetChallengeCert(
|
|
ctx context.Context,
|
|
ident identifier.ACMEIdentifier,
|
|
) (*x509.Certificate, *tls.ConnectionState, core.ValidationRecord, error) {
|
|
validationRecord := core.ValidationRecord{
|
|
Hostname: ident.Value,
|
|
Port: strconv.Itoa(va.tlsPort),
|
|
}
|
|
|
|
var addrs []netip.Addr
|
|
switch ident.Type {
|
|
case identifier.TypeDNS:
|
|
// Resolve IP addresses for the identifier
|
|
dnsAddrs, dnsResolvers, err := va.getAddrs(ctx, ident.Value)
|
|
if err != nil {
|
|
return nil, nil, validationRecord, err
|
|
}
|
|
addrs, validationRecord.ResolverAddrs = dnsAddrs, dnsResolvers
|
|
validationRecord.AddressesResolved = addrs
|
|
case identifier.TypeIP:
|
|
netIP, err := netip.ParseAddr(ident.Value)
|
|
if err != nil {
|
|
return nil, nil, validationRecord, fmt.Errorf("can't parse IP address %q: %s", ident.Value, err)
|
|
}
|
|
addrs = []netip.Addr{netIP}
|
|
default:
|
|
// This should never happen. The calling function should check the
|
|
// identifier type.
|
|
return nil, nil, validationRecord, fmt.Errorf("unknown identifier type: %s", ident.Type)
|
|
}
|
|
|
|
// Split the available addresses into v4 and v6 addresses
|
|
v4, v6 := availableAddresses(addrs)
|
|
addresses := append(v4, v6...)
|
|
|
|
// This shouldn't happen, but be defensive about it anyway
|
|
if len(addresses) < 1 {
|
|
return nil, nil, validationRecord, berrors.MalformedError("no IP addresses found for %q", ident.Value)
|
|
}
|
|
|
|
// If there is at least one IPv6 address then try it first
|
|
if len(v6) > 0 {
|
|
address := net.JoinHostPort(v6[0].String(), validationRecord.Port)
|
|
validationRecord.AddressUsed = v6[0]
|
|
|
|
cert, cs, err := va.getChallengeCert(ctx, address, ident)
|
|
|
|
// If there is no problem, return immediately
|
|
if err == nil {
|
|
return cert, cs, validationRecord, nil
|
|
}
|
|
|
|
// Otherwise, we note that we tried an address and fall back to trying IPv4
|
|
validationRecord.AddressesTried = append(validationRecord.AddressesTried, validationRecord.AddressUsed)
|
|
va.metrics.ipv4FallbackCounter.Inc()
|
|
}
|
|
|
|
// If there are no IPv4 addresses and we tried an IPv6 address return
|
|
// an error - there's nothing left to try
|
|
if len(v4) == 0 && len(validationRecord.AddressesTried) > 0 {
|
|
return nil, nil, validationRecord, berrors.MalformedError("Unable to contact %q at %q, no IPv4 addresses to try as fallback",
|
|
validationRecord.Hostname, validationRecord.AddressesTried[0])
|
|
} else if len(v4) == 0 && len(validationRecord.AddressesTried) == 0 {
|
|
// It shouldn't be possible that there are no IPv4 addresses and no previous
|
|
// attempts at an IPv6 address connection but be defensive about it anyway
|
|
return nil, nil, validationRecord, berrors.MalformedError("No IP addresses found for %q", validationRecord.Hostname)
|
|
}
|
|
|
|
// Otherwise if there are no IPv6 addresses, or there was an error
|
|
// talking to the first IPv6 address, try the first IPv4 address
|
|
validationRecord.AddressUsed = v4[0]
|
|
address := net.JoinHostPort(v4[0].String(), validationRecord.Port)
|
|
cert, cs, err := va.getChallengeCert(ctx, address, ident)
|
|
return cert, cs, validationRecord, err
|
|
}
|
|
|
|
func (va *ValidationAuthorityImpl) getChallengeCert(
|
|
ctx context.Context,
|
|
hostPort string,
|
|
ident identifier.ACMEIdentifier,
|
|
) (*x509.Certificate, *tls.ConnectionState, error) {
|
|
var serverName string
|
|
switch ident.Type {
|
|
case identifier.TypeDNS:
|
|
serverName = ident.Value
|
|
case identifier.TypeIP:
|
|
reverseIP, err := dns.ReverseAddr(ident.Value)
|
|
if err != nil {
|
|
va.log.Infof("%s Failed to parse IP address %s.", core.ChallengeTypeTLSALPN01, ident.Value)
|
|
return nil, nil, fmt.Errorf("failed to parse IP address")
|
|
}
|
|
serverName = reverseIP
|
|
default:
|
|
// This should never happen. The calling function should check the
|
|
// identifier type.
|
|
va.log.Infof("%s Unknown identifier type '%s' for %s.", core.ChallengeTypeTLSALPN01, ident.Type, ident.Value)
|
|
return nil, nil, fmt.Errorf("unknown identifier type: %s", ident.Type)
|
|
}
|
|
|
|
va.log.Info(fmt.Sprintf("%s [%s] Attempting to validate for %s %s", core.ChallengeTypeTLSALPN01, ident, hostPort, serverName))
|
|
|
|
dialCtx, cancel := context.WithTimeout(ctx, va.singleDialTimeout)
|
|
defer cancel()
|
|
|
|
dialer := &tls.Dialer{Config: &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
NextProtos: []string{ACMETLS1Protocol},
|
|
ServerName: serverName,
|
|
// We expect a self-signed challenge certificate, do not verify it here.
|
|
InsecureSkipVerify: true,
|
|
}}
|
|
// TODO(#8041): This could be a good place for a backstop check for reserved IP
|
|
// addresses.
|
|
conn, err := dialer.DialContext(dialCtx, "tcp", hostPort)
|
|
if err != nil {
|
|
va.log.Infof("%s connection failure for %s. err=[%#v] errStr=[%s]", core.ChallengeTypeTLSALPN01, ident, err, err)
|
|
host, _, splitErr := net.SplitHostPort(hostPort)
|
|
netIP, ipErr := netip.ParseAddr(host)
|
|
if splitErr == nil && ipErr == nil {
|
|
// Wrap the validation error and the IP of the remote host in an
|
|
// IPError so we can display the IP in the problem details returned
|
|
// to the client.
|
|
return nil, nil, ipError{netIP, err}
|
|
}
|
|
return nil, nil, err
|
|
}
|
|
defer conn.Close()
|
|
|
|
// tls.Dialer.DialContext guarantees that the *net.Conn it returns is a *tls.Conn.
|
|
cs := conn.(*tls.Conn).ConnectionState()
|
|
certs := cs.PeerCertificates
|
|
if len(certs) == 0 {
|
|
va.log.Infof("%s challenge for %s resulted in no certificates", core.ChallengeTypeTLSALPN01, ident.Value)
|
|
return nil, nil, berrors.UnauthorizedError("No certs presented for %s challenge", core.ChallengeTypeTLSALPN01)
|
|
}
|
|
for i, cert := range certs {
|
|
va.log.AuditInfof("%s challenge for %s received certificate (%d of %d): cert=[%s]",
|
|
core.ChallengeTypeTLSALPN01, ident.Value, i+1, len(certs), hex.EncodeToString(cert.Raw))
|
|
}
|
|
return certs[0], &cs, nil
|
|
}
|
|
|
|
func checkExpectedSAN(cert *x509.Certificate, ident identifier.ACMEIdentifier) error {
|
|
var expectedSANBytes []byte
|
|
switch ident.Type {
|
|
case identifier.TypeDNS:
|
|
if len(cert.DNSNames) != 1 || len(cert.IPAddresses) != 0 {
|
|
return errors.New("wrong number of identifiers")
|
|
}
|
|
if !strings.EqualFold(cert.DNSNames[0], ident.Value) {
|
|
return errors.New("identifier does not match expected identifier")
|
|
}
|
|
bytes, err := asn1.Marshal([]asn1.RawValue{
|
|
{Tag: 2, Class: 2, Bytes: []byte(ident.Value)},
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("composing SAN extension: %w", err)
|
|
}
|
|
expectedSANBytes = bytes
|
|
case identifier.TypeIP:
|
|
if len(cert.IPAddresses) != 1 || len(cert.DNSNames) != 0 {
|
|
return errors.New("wrong number of identifiers")
|
|
}
|
|
if !cert.IPAddresses[0].Equal(net.ParseIP(ident.Value)) {
|
|
return errors.New("identifier does not match expected identifier")
|
|
}
|
|
netipAddr, err := netip.ParseAddr(ident.Value)
|
|
if err != nil {
|
|
return fmt.Errorf("parsing IP address identifier: %w", err)
|
|
}
|
|
netipBytes, err := netipAddr.MarshalBinary()
|
|
if err != nil {
|
|
return fmt.Errorf("marshalling IP address identifier: %w", err)
|
|
}
|
|
bytes, err := asn1.Marshal([]asn1.RawValue{
|
|
{Tag: 7, Class: 2, Bytes: netipBytes},
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("composing SAN extension: %w", err)
|
|
}
|
|
expectedSANBytes = bytes
|
|
default:
|
|
// This should never happen. The calling function should check the
|
|
// identifier type.
|
|
return fmt.Errorf("unknown identifier type: %s", ident.Type)
|
|
}
|
|
|
|
for _, ext := range cert.Extensions {
|
|
if IdCeSubjectAltName.Equal(ext.Id) {
|
|
if !bytes.Equal(ext.Value, expectedSANBytes) {
|
|
return errors.New("SAN extension does not match expected bytes")
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Confirm that of the OIDs provided, all of them are in the provided list of
|
|
// extensions. Also confirms that of the extensions provided that none are
|
|
// repeated. Per RFC8737, allows unexpected extensions.
|
|
func checkAcceptableExtensions(exts []pkix.Extension, requiredOIDs []asn1.ObjectIdentifier) error {
|
|
oidSeen := make(map[string]bool)
|
|
|
|
for _, ext := range exts {
|
|
if oidSeen[ext.Id.String()] {
|
|
return fmt.Errorf("Extension OID %s seen twice", ext.Id)
|
|
}
|
|
oidSeen[ext.Id.String()] = true
|
|
}
|
|
|
|
for _, required := range requiredOIDs {
|
|
if !oidSeen[required.String()] {
|
|
return fmt.Errorf("Required extension OID %s is not present", required)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (va *ValidationAuthorityImpl) validateTLSALPN01(ctx context.Context, ident identifier.ACMEIdentifier, keyAuthorization string) ([]core.ValidationRecord, error) {
|
|
if ident.Type != identifier.TypeDNS && ident.Type != identifier.TypeIP {
|
|
va.log.Info(fmt.Sprintf("Identifier type for TLS-ALPN-01 challenge was not DNS or IP: %s", ident))
|
|
return nil, berrors.MalformedError("Identifier type for TLS-ALPN-01 challenge was not DNS or IP")
|
|
}
|
|
|
|
cert, cs, tvr, err := va.tryGetChallengeCert(ctx, ident)
|
|
// Copy the single validationRecord into the slice that we have to return, and
|
|
// get a reference to it so we can modify it if we have to.
|
|
validationRecords := []core.ValidationRecord{tvr}
|
|
validationRecord := &validationRecords[0]
|
|
if err != nil {
|
|
return validationRecords, err
|
|
}
|
|
|
|
if cs.NegotiatedProtocol != ACMETLS1Protocol {
|
|
return validationRecords, berrors.UnauthorizedError(
|
|
"Cannot negotiate ALPN protocol %q for %s challenge",
|
|
ACMETLS1Protocol,
|
|
core.ChallengeTypeTLSALPN01)
|
|
}
|
|
|
|
badCertErr := func(msg string) error {
|
|
hostPort := net.JoinHostPort(validationRecord.AddressUsed.String(), validationRecord.Port)
|
|
|
|
return berrors.UnauthorizedError(
|
|
"Incorrect validation certificate for %s challenge. "+
|
|
"Requested %s from %s. %s",
|
|
core.ChallengeTypeTLSALPN01, ident.Value, hostPort, msg)
|
|
}
|
|
|
|
// The certificate must be self-signed.
|
|
err = cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature)
|
|
if err != nil || !bytes.Equal(cert.RawSubject, cert.RawIssuer) {
|
|
return validationRecords, badCertErr(
|
|
"Received certificate which is not self-signed.")
|
|
}
|
|
|
|
// The certificate must have the subjectAltName and acmeIdentifier
|
|
// extensions, and only one of each.
|
|
allowedOIDs := []asn1.ObjectIdentifier{
|
|
IdPeAcmeIdentifier, IdCeSubjectAltName,
|
|
}
|
|
err = checkAcceptableExtensions(cert.Extensions, allowedOIDs)
|
|
if err != nil {
|
|
return validationRecords, badCertErr(
|
|
fmt.Sprintf("Received certificate with unexpected extensions: %q", err))
|
|
}
|
|
|
|
// The certificate returned must have a subjectAltName extension containing
|
|
// only the identifier being validated and no other entries.
|
|
err = checkExpectedSAN(cert, ident)
|
|
if err != nil {
|
|
names := strings.Join(certAltNames(cert), ", ")
|
|
return validationRecords, badCertErr(
|
|
fmt.Sprintf("Received certificate with unexpected identifiers (%q): %q", names, err))
|
|
}
|
|
|
|
// Verify key authorization in acmeValidation extension
|
|
h := sha256.Sum256([]byte(keyAuthorization))
|
|
for _, ext := range cert.Extensions {
|
|
if IdPeAcmeIdentifier.Equal(ext.Id) {
|
|
va.metrics.tlsALPNOIDCounter.WithLabelValues(IdPeAcmeIdentifier.String()).Inc()
|
|
if !ext.Critical {
|
|
return validationRecords, badCertErr(
|
|
"Received certificate with acmeValidationV1 extension that is not Critical.")
|
|
}
|
|
var extValue []byte
|
|
rest, err := asn1.Unmarshal(ext.Value, &extValue)
|
|
if err != nil || len(rest) > 0 || len(h) != len(extValue) {
|
|
return validationRecords, badCertErr(
|
|
"Received certificate with malformed acmeValidationV1 extension value.")
|
|
}
|
|
if subtle.ConstantTimeCompare(h[:], extValue) != 1 {
|
|
return validationRecords, badCertErr(fmt.Sprintf(
|
|
"Received certificate with acmeValidationV1 extension value %s but expected %s.",
|
|
hex.EncodeToString(extValue),
|
|
hex.EncodeToString(h[:]),
|
|
))
|
|
}
|
|
return validationRecords, nil
|
|
}
|
|
}
|
|
|
|
return validationRecords, badCertErr(
|
|
"Received certificate with no acmeValidationV1 extension.")
|
|
}
|