boulder/identifier/identifier.go

264 lines
8.0 KiB
Go

// The identifier package defines types for RFC 8555 ACME identifiers.
//
// It exists as a separate package to prevent an import loop between the core
// and probs packages.
//
// Function naming conventions:
// - "New" creates a new instance from one or more simple base type inputs.
// - "From" and "To" extract information from, or compose, a more complex object.
package identifier
import (
"crypto/x509"
"fmt"
"net"
"net/netip"
"slices"
"strings"
corepb "github.com/letsencrypt/boulder/core/proto"
)
// IdentifierType is a named string type for registered ACME identifier types.
// See https://tools.ietf.org/html/rfc8555#section-9.7.7
type IdentifierType string
const (
// TypeDNS is specified in RFC 8555 for TypeDNS type identifiers.
TypeDNS = IdentifierType("dns")
// TypeIP is specified in RFC 8738
TypeIP = IdentifierType("ip")
)
// IsValid tests whether the identifier type is known
func (i IdentifierType) IsValid() bool {
switch i {
case TypeDNS, TypeIP:
return true
default:
return false
}
}
// ACMEIdentifier is a struct encoding an identifier that can be validated. The
// protocol allows for different types of identifier to be supported (DNS
// names, IP addresses, etc.), but currently we only support RFC 8555 DNS type
// identifiers for domain names.
type ACMEIdentifier struct {
// Type is the registered IdentifierType of the identifier.
Type IdentifierType `json:"type"`
// Value is the value of the identifier. For a DNS type identifier it is
// a domain name.
Value string `json:"value"`
}
// ACMEIdentifiers is a named type for a slice of ACME identifiers, so that
// methods can be applied to these slices.
type ACMEIdentifiers []ACMEIdentifier
func (i ACMEIdentifier) ToProto() *corepb.Identifier {
return &corepb.Identifier{
Type: string(i.Type),
Value: i.Value,
}
}
func FromProto(ident *corepb.Identifier) ACMEIdentifier {
return ACMEIdentifier{
Type: IdentifierType(ident.Type),
Value: ident.Value,
}
}
// ToProtoSlice is a convenience function for converting a slice of
// ACMEIdentifier into a slice of *corepb.Identifier, to use for RPCs.
func (idents ACMEIdentifiers) ToProtoSlice() []*corepb.Identifier {
var pbIdents []*corepb.Identifier
for _, ident := range idents {
pbIdents = append(pbIdents, ident.ToProto())
}
return pbIdents
}
// FromProtoSlice is a convenience function for converting a slice of
// *corepb.Identifier from RPCs into a slice of ACMEIdentifier.
func FromProtoSlice(pbIdents []*corepb.Identifier) ACMEIdentifiers {
var idents ACMEIdentifiers
for _, pbIdent := range pbIdents {
idents = append(idents, FromProto(pbIdent))
}
return idents
}
// NewDNS is a convenience function for creating an ACMEIdentifier with Type
// "dns" for a given domain name.
func NewDNS(domain string) ACMEIdentifier {
return ACMEIdentifier{
Type: TypeDNS,
Value: domain,
}
}
// NewDNSSlice is a convenience function for creating a slice of ACMEIdentifier
// with Type "dns" for a given slice of domain names.
func NewDNSSlice(input []string) ACMEIdentifiers {
var out ACMEIdentifiers
for _, in := range input {
out = append(out, NewDNS(in))
}
return out
}
// ToDNSSlice returns a list of DNS names from the input if the input contains
// only DNS identifiers. Otherwise, it returns an error.
//
// TODO(#8023): Remove this when we no longer have any bare dnsNames slices.
func (idents ACMEIdentifiers) ToDNSSlice() ([]string, error) {
var out []string
for _, in := range idents {
if in.Type != "dns" {
return nil, fmt.Errorf("identifier '%s' is of type '%s', not DNS", in.Value, in.Type)
}
out = append(out, in.Value)
}
return out, nil
}
// NewIP is a convenience function for creating an ACMEIdentifier with Type "ip"
// for a given IP address.
func NewIP(ip netip.Addr) ACMEIdentifier {
return ACMEIdentifier{
Type: TypeIP,
// RFC 8738, Sec. 3: The identifier value MUST contain the textual form
// of the address as defined in RFC 1123, Sec. 2.1 for IPv4 and in RFC
// 5952, Sec. 4 for IPv6.
Value: ip.String(),
}
}
// fromX509 extracts the Subject Alternative Names from a certificate or CSR's fields, and
// returns a slice of ACMEIdentifiers.
func fromX509(commonName string, dnsNames []string, ipAddresses []net.IP) ACMEIdentifiers {
var sans ACMEIdentifiers
for _, name := range dnsNames {
sans = append(sans, NewDNS(name))
}
if commonName != "" {
// Boulder won't generate certificates with a CN that's not also present
// in the SANs, but such a certificate is possible. If appended, this is
// deduplicated later with Normalize(). We assume the CN is a DNSName,
// because CNs are untyped strings without metadata, and we will never
// configure a Boulder profile to issue a certificate that contains both
// an IP address identifier and a CN.
sans = append(sans, NewDNS(commonName))
}
for _, ip := range ipAddresses {
sans = append(sans, ACMEIdentifier{
Type: TypeIP,
Value: ip.String(),
})
}
return Normalize(sans)
}
// FromCert extracts the Subject Common Name and Subject Alternative Names from
// a certificate, and returns a slice of ACMEIdentifiers.
func FromCert(cert *x509.Certificate) ACMEIdentifiers {
return fromX509(cert.Subject.CommonName, cert.DNSNames, cert.IPAddresses)
}
// FromCSR extracts the Subject Common Name and Subject Alternative Names from a
// CSR, and returns a slice of ACMEIdentifiers.
func FromCSR(csr *x509.CertificateRequest) ACMEIdentifiers {
return fromX509(csr.Subject.CommonName, csr.DNSNames, csr.IPAddresses)
}
// Normalize returns the set of all unique ACME identifiers in the input after
// all of them are lowercased. The returned identifier values will be in their
// lowercased form and sorted alphabetically by value. DNS identifiers will
// precede IP address identifiers.
func Normalize(idents ACMEIdentifiers) ACMEIdentifiers {
for i := range idents {
idents[i].Value = strings.ToLower(idents[i].Value)
}
slices.SortFunc(idents, func(a, b ACMEIdentifier) int {
if a.Type == b.Type {
if a.Value == b.Value {
return 0
}
if a.Value < b.Value {
return -1
}
return 1
}
if a.Type == "dns" && b.Type == "ip" {
return -1
}
return 1
})
return slices.Compact(idents)
}
// ToValues returns a slice of DNS names and a slice of IP addresses in the
// input. If an identifier type or IP address is invalid, it returns an error.
func (idents ACMEIdentifiers) ToValues() ([]string, []net.IP, error) {
var dnsNames []string
var ipAddresses []net.IP
for _, ident := range idents {
switch ident.Type {
case TypeDNS:
dnsNames = append(dnsNames, ident.Value)
case TypeIP:
ip := net.ParseIP(ident.Value)
if ip == nil {
return nil, nil, fmt.Errorf("parsing IP address: %s", ident.Value)
}
ipAddresses = append(ipAddresses, ip)
default:
return nil, nil, fmt.Errorf("evaluating identifier type: %s for %s", ident.Type, ident.Value)
}
}
return dnsNames, ipAddresses, nil
}
// hasIdentifier matches any protobuf struct that has both Identifier and
// DnsName fields, like Authorization, Order, or many SA requests. This lets us
// convert these to ACMEIdentifier, vice versa, etc.
type hasIdentifier interface {
GetIdentifier() *corepb.Identifier
GetDnsName() string
}
// FromProtoWithDefault can be removed after DnsNames are no longer used in RPCs.
// TODO(#8023)
func FromProtoWithDefault(input hasIdentifier) ACMEIdentifier {
if input.GetIdentifier() != nil {
return FromProto(input.GetIdentifier())
}
return NewDNS(input.GetDnsName())
}
// hasIdentifiers matches any protobuf struct that has both Identifiers and
// DnsNames fields, like NewOrderRequest or many SA requests. This lets us
// convert these to ACMEIdentifiers, vice versa, etc.
type hasIdentifiers interface {
GetIdentifiers() []*corepb.Identifier
GetDnsNames() []string
}
// FromProtoSliceWithDefault can be removed after DnsNames are no longer used in
// RPCs. TODO(#8023)
func FromProtoSliceWithDefault(input hasIdentifiers) ACMEIdentifiers {
if len(input.GetIdentifiers()) > 0 {
return FromProtoSlice(input.GetIdentifiers())
}
return NewDNSSlice(input.GetDnsNames())
}