github.com/letsencrypt/boulder@v0.20251208.0/identifier/identifier.go (about)

     1  // The identifier package defines types for RFC 8555 ACME identifiers.
     2  //
     3  // It exists as a separate package to prevent an import loop between the core
     4  // and probs packages.
     5  //
     6  // Function naming conventions:
     7  // - "New" creates a new instance from one or more simple base type inputs.
     8  // - "From" and "To" extract information from, or compose, a more complex object.
     9  package identifier
    10  
    11  import (
    12  	"crypto/x509"
    13  	"fmt"
    14  	"net"
    15  	"net/netip"
    16  	"slices"
    17  	"strings"
    18  
    19  	corepb "github.com/letsencrypt/boulder/core/proto"
    20  )
    21  
    22  // IdentifierType is a named string type for registered ACME identifier types.
    23  // See https://tools.ietf.org/html/rfc8555#section-9.7.7
    24  type IdentifierType string
    25  
    26  const (
    27  	// TypeDNS is specified in RFC 8555 for TypeDNS type identifiers.
    28  	TypeDNS = IdentifierType("dns")
    29  	// TypeIP is specified in RFC 8738
    30  	TypeIP = IdentifierType("ip")
    31  )
    32  
    33  // IsValid tests whether the identifier type is known
    34  func (i IdentifierType) IsValid() bool {
    35  	switch i {
    36  	case TypeDNS, TypeIP:
    37  		return true
    38  	default:
    39  		return false
    40  	}
    41  }
    42  
    43  // ACMEIdentifier is a struct encoding an identifier that can be validated. The
    44  // protocol allows for different types of identifier to be supported (DNS
    45  // names, IP addresses, etc.), but currently we only support RFC 8555 DNS type
    46  // identifiers for domain names.
    47  type ACMEIdentifier struct {
    48  	// Type is the registered IdentifierType of the identifier.
    49  	Type IdentifierType `json:"type"`
    50  	// Value is the value of the identifier. For a DNS type identifier it is
    51  	// a domain name.
    52  	Value string `json:"value"`
    53  }
    54  
    55  // ACMEIdentifiers is a named type for a slice of ACME identifiers, so that
    56  // methods can be applied to these slices.
    57  type ACMEIdentifiers []ACMEIdentifier
    58  
    59  func (i ACMEIdentifier) ToProto() *corepb.Identifier {
    60  	return &corepb.Identifier{
    61  		Type:  string(i.Type),
    62  		Value: i.Value,
    63  	}
    64  }
    65  
    66  func FromProto(ident *corepb.Identifier) ACMEIdentifier {
    67  	return ACMEIdentifier{
    68  		Type:  IdentifierType(ident.Type),
    69  		Value: ident.Value,
    70  	}
    71  }
    72  
    73  // ToProtoSlice is a convenience function for converting a slice of
    74  // ACMEIdentifier into a slice of *corepb.Identifier, to use for RPCs.
    75  func (idents ACMEIdentifiers) ToProtoSlice() []*corepb.Identifier {
    76  	var pbIdents []*corepb.Identifier
    77  	for _, ident := range idents {
    78  		pbIdents = append(pbIdents, ident.ToProto())
    79  	}
    80  	return pbIdents
    81  }
    82  
    83  // FromProtoSlice is a convenience function for converting a slice of
    84  // *corepb.Identifier from RPCs into a slice of ACMEIdentifier.
    85  func FromProtoSlice(pbIdents []*corepb.Identifier) ACMEIdentifiers {
    86  	var idents ACMEIdentifiers
    87  
    88  	for _, pbIdent := range pbIdents {
    89  		idents = append(idents, FromProto(pbIdent))
    90  	}
    91  	return idents
    92  }
    93  
    94  // NewDNS is a convenience function for creating an ACMEIdentifier with Type
    95  // "dns" for a given domain name.
    96  func NewDNS(domain string) ACMEIdentifier {
    97  	return ACMEIdentifier{
    98  		Type:  TypeDNS,
    99  		Value: domain,
   100  	}
   101  }
   102  
   103  // NewDNSSlice is a convenience function for creating a slice of ACMEIdentifier
   104  // with Type "dns" for a given slice of domain names.
   105  func NewDNSSlice(input []string) ACMEIdentifiers {
   106  	var out ACMEIdentifiers
   107  	for _, in := range input {
   108  		out = append(out, NewDNS(in))
   109  	}
   110  	return out
   111  }
   112  
   113  // NewIP is a convenience function for creating an ACMEIdentifier with Type "ip"
   114  // for a given IP address.
   115  func NewIP(ip netip.Addr) ACMEIdentifier {
   116  	return ACMEIdentifier{
   117  		Type: TypeIP,
   118  		// RFC 8738, Sec. 3: The identifier value MUST contain the textual form
   119  		// of the address as defined in RFC 1123, Sec. 2.1 for IPv4 and in RFC
   120  		// 5952, Sec. 4 for IPv6.
   121  		Value: ip.WithZone("").String(),
   122  	}
   123  }
   124  
   125  // FromString converts a string to an ACMEIdentifier.
   126  func FromString(identStr string) ACMEIdentifier {
   127  	ip, err := netip.ParseAddr(identStr)
   128  	if err == nil {
   129  		return NewIP(ip)
   130  	}
   131  	return NewDNS(identStr)
   132  }
   133  
   134  // FromStringSlice converts a slice of strings to a slice of ACMEIdentifier.
   135  func FromStringSlice(identStrs []string) ACMEIdentifiers {
   136  	var idents ACMEIdentifiers
   137  	for _, identStr := range identStrs {
   138  		idents = append(idents, FromString(identStr))
   139  	}
   140  	return idents
   141  }
   142  
   143  // fromX509 extracts the Subject Alternative Names from a certificate or CSR's fields, and
   144  // returns a slice of ACMEIdentifiers.
   145  func fromX509(commonName string, dnsNames []string, ipAddresses []net.IP) ACMEIdentifiers {
   146  	var sans ACMEIdentifiers
   147  	for _, name := range dnsNames {
   148  		sans = append(sans, NewDNS(name))
   149  	}
   150  	if commonName != "" {
   151  		// Boulder won't generate certificates with a CN that's not also present
   152  		// in the SANs, but such a certificate is possible. If appended, this is
   153  		// deduplicated later with Normalize(). We assume the CN is a DNSName,
   154  		// because CNs are untyped strings without metadata, and we will never
   155  		// configure a Boulder profile to issue a certificate that contains both
   156  		// an IP address identifier and a CN.
   157  		sans = append(sans, NewDNS(commonName))
   158  	}
   159  
   160  	for _, ip := range ipAddresses {
   161  		sans = append(sans, ACMEIdentifier{
   162  			Type:  TypeIP,
   163  			Value: ip.String(),
   164  		})
   165  	}
   166  
   167  	return Normalize(sans)
   168  }
   169  
   170  // FromCert extracts the Subject Common Name and Subject Alternative Names from
   171  // a certificate, and returns a slice of ACMEIdentifiers.
   172  func FromCert(cert *x509.Certificate) ACMEIdentifiers {
   173  	return fromX509(cert.Subject.CommonName, cert.DNSNames, cert.IPAddresses)
   174  }
   175  
   176  // FromCSR extracts the Subject Common Name and Subject Alternative Names from a
   177  // CSR, and returns a slice of ACMEIdentifiers.
   178  func FromCSR(csr *x509.CertificateRequest) ACMEIdentifiers {
   179  	return fromX509(csr.Subject.CommonName, csr.DNSNames, csr.IPAddresses)
   180  }
   181  
   182  // Normalize returns the set of all unique ACME identifiers in the input after
   183  // all of them are lowercased. The returned identifier values will be in their
   184  // lowercased form and sorted alphabetically by value. DNS identifiers will
   185  // precede IP address identifiers.
   186  func Normalize(idents ACMEIdentifiers) ACMEIdentifiers {
   187  	for i := range idents {
   188  		idents[i].Value = strings.ToLower(idents[i].Value)
   189  	}
   190  
   191  	slices.SortFunc(idents, func(a, b ACMEIdentifier) int {
   192  		if a.Type == b.Type {
   193  			if a.Value == b.Value {
   194  				return 0
   195  			}
   196  			if a.Value < b.Value {
   197  				return -1
   198  			}
   199  			return 1
   200  		}
   201  		if a.Type == "dns" && b.Type == "ip" {
   202  			return -1
   203  		}
   204  		return 1
   205  	})
   206  
   207  	return slices.Compact(idents)
   208  }
   209  
   210  // ToValues returns a slice of DNS names and a slice of IP addresses in the
   211  // input. If an identifier type or IP address is invalid, it returns an error.
   212  func (idents ACMEIdentifiers) ToValues() ([]string, []net.IP, error) {
   213  	var dnsNames []string
   214  	var ipAddresses []net.IP
   215  
   216  	for _, ident := range idents {
   217  		switch ident.Type {
   218  		case TypeDNS:
   219  			dnsNames = append(dnsNames, ident.Value)
   220  		case TypeIP:
   221  			ip := net.ParseIP(ident.Value)
   222  			if ip == nil {
   223  				return nil, nil, fmt.Errorf("parsing IP address: %s", ident.Value)
   224  			}
   225  			ipAddresses = append(ipAddresses, ip)
   226  		default:
   227  			return nil, nil, fmt.Errorf("evaluating identifier type: %s for %s", ident.Type, ident.Value)
   228  		}
   229  	}
   230  
   231  	return dnsNames, ipAddresses, nil
   232  }