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 }