package acme import ( "bytes" "context" "crypto/x509" "encoding/json" "encoding/pem" "net" "os" "sort" "strings" "time" "github.com/smallstep/certificates/authority/provisioner" "go.step.sm/crypto/x509util" ) type IdentifierType string const ( // IP is the ACME ip identifier type IP IdentifierType = "ip" // DNS is the ACME dns identifier type DNS IdentifierType = "dns" // PermanentIdentifier is the ACME permanent-identifier identifier type // defined in https://datatracker.ietf.org/doc/html/draft-bweeks-acme-device-attest-00 PermanentIdentifier IdentifierType = "permanent-identifier" ) // Identifier encodes the type that an order pertains to. type Identifier struct { Type IdentifierType `json:"type"` Value string `json:"value"` } // Order contains order metadata for the ACME protocol order type. type Order struct { ID string `json:"id"` AccountID string `json:"-"` ProvisionerID string `json:"-"` Status Status `json:"status"` ExpiresAt time.Time `json:"expires"` Identifiers []Identifier `json:"identifiers"` NotBefore time.Time `json:"notBefore"` NotAfter time.Time `json:"notAfter"` Error *Error `json:"error,omitempty"` AuthorizationIDs []string `json:"-"` AuthorizationURLs []string `json:"authorizations"` FinalizeURL string `json:"finalize"` CertificateID string `json:"-"` CertificateURL string `json:"certificate,omitempty"` } // ToLog enables response logging. func (o *Order) ToLog() (interface{}, error) { b, err := json.Marshal(o) if err != nil { return nil, WrapErrorISE(err, "error marshaling order for logging") } return string(b), nil } // UpdateStatus updates the ACME Order Status if necessary. // Changes to the order are saved using the database interface. func (o *Order) UpdateStatus(ctx context.Context, db DB) error { now := clock.Now() switch o.Status { case StatusInvalid: return nil case StatusValid: return nil case StatusReady: // Check expiry if now.After(o.ExpiresAt) { o.Status = StatusInvalid o.Error = NewError(ErrorMalformedType, "order has expired") break } return nil case StatusPending: // Check expiry if now.After(o.ExpiresAt) { o.Status = StatusInvalid o.Error = NewError(ErrorMalformedType, "order has expired") break } var count = map[Status]int{ StatusValid: 0, StatusInvalid: 0, StatusPending: 0, } for _, azID := range o.AuthorizationIDs { az, err := db.GetAuthorization(ctx, azID) if err != nil { return WrapErrorISE(err, "error getting authorization ID %s", azID) } if err = az.UpdateStatus(ctx, db); err != nil { return WrapErrorISE(err, "error updating authorization ID %s", azID) } st := az.Status count[st]++ } switch { case count[StatusInvalid] > 0: o.Status = StatusInvalid // No change in the order status, so just return the order as is - // without writing any changes. case count[StatusPending] > 0: return nil case count[StatusValid] == len(o.AuthorizationIDs): o.Status = StatusReady default: return NewErrorISE("unexpected authz status") } default: return NewErrorISE("unrecognized order status: %s", o.Status) } if err := db.UpdateOrder(ctx, o); err != nil { return WrapErrorISE(err, "error updating order") } return nil } // Finalize signs a certificate if the necessary conditions for Order completion // have been met. // // TODO(mariano): Here or in the challenge validation we should perform some // external validation using the identifier value and the attestation data. From // a validation service we can get the list of SANs to set in the final // certificate. func (o *Order) Finalize(ctx context.Context, db DB, csr *x509.CertificateRequest, auth CertificateAuthority, p Provisioner) error { if err := o.UpdateStatus(ctx, db); err != nil { return err } switch o.Status { case StatusInvalid: return NewError(ErrorOrderNotReadyType, "order %s has been abandoned", o.ID) case StatusValid: return nil case StatusPending: return NewError(ErrorOrderNotReadyType, "order %s is not ready", o.ID) case StatusReady: break default: return NewErrorISE("unexpected status %s for order %s", o.Status, o.ID) } // canonicalize the CSR to allow for comparison csr = canonicalize(csr) // Template data data := x509util.NewTemplateData() data.SetCommonName(csr.Subject.CommonName) // TODO: support for multiple identifiers? var permanentIdentifier string for i := range o.Identifiers { if o.Identifiers[i].Type == PermanentIdentifier { permanentIdentifier = o.Identifiers[i].Value break } } var defaultTemplate string if permanentIdentifier != "" { defaultTemplate = x509util.DefaultAttestedLeafTemplate data.SetSubjectAlternativeNames(x509util.SubjectAlternativeName{ Type: x509util.PermanentIdentifierType, Value: permanentIdentifier, }) } else { defaultTemplate = x509util.DefaultLeafTemplate sans, err := o.sans(csr) if err != nil { return err } data.SetSubjectAlternativeNames(sans...) } // Get authorizations from the ACME provisioner. ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod) signOps, err := p.AuthorizeSign(ctx, "") if err != nil { return WrapErrorISE(err, "error retrieving authorization options from ACME provisioner") } templateOptions, err := provisioner.CustomTemplateOptions(p.GetOptions(), data, defaultTemplate) if err != nil { return WrapErrorISE(err, "error creating template options from ACME provisioner") } signOps = append(signOps, templateOptions) // Sign a new certificate. certChain, err := auth.Sign(csr, provisioner.SignOptions{ NotBefore: provisioner.NewTimeDuration(o.NotBefore), NotAfter: provisioner.NewTimeDuration(o.NotAfter), }, signOps...) if err != nil { return WrapErrorISE(err, "error signing certificate for order %s", o.ID) } pem.Encode(os.Stdout, &pem.Block{ Type: "CERTIFICATE REQUEST", Bytes: csr.Raw, }) pem.Encode(os.Stdout, &pem.Block{ Type: "CERTIFICATE", Bytes: certChain[0].Raw, }) cert := &Certificate{ AccountID: o.AccountID, OrderID: o.ID, Leaf: certChain[0], Intermediates: certChain[1:], } if err := db.CreateCertificate(ctx, cert); err != nil { return WrapErrorISE(err, "error creating certificate for order %s", o.ID) } o.CertificateID = cert.ID o.Status = StatusValid if err = db.UpdateOrder(ctx, o); err != nil { return WrapErrorISE(err, "error updating order %s", o.ID) } return nil } func (o *Order) sans(csr *x509.CertificateRequest) ([]x509util.SubjectAlternativeName, error) { var sans []x509util.SubjectAlternativeName if len(csr.EmailAddresses) > 0 || len(csr.URIs) > 0 { return sans, NewError(ErrorBadCSRType, "Only DNS names and IP addresses are allowed") } // order the DNS names and IP addresses, so that they can be compared against the canonicalized CSR orderNames := make([]string, numberOfIdentifierType(DNS, o.Identifiers)) orderIPs := make([]net.IP, numberOfIdentifierType(IP, o.Identifiers)) orderPIDs := make([]string, numberOfIdentifierType(PermanentIdentifier, o.Identifiers)) indexDNS, indexIP, indexPID := 0, 0, 0 for _, n := range o.Identifiers { switch n.Type { case DNS: orderNames[indexDNS] = n.Value indexDNS++ case IP: orderIPs[indexIP] = net.ParseIP(n.Value) // NOTE: this assumes are all valid IPs at this time; or will result in nil entries indexIP++ case PermanentIdentifier: orderPIDs[indexPID] = n.Value indexPID++ default: return sans, NewErrorISE("unsupported identifier type in order: %s", n.Type) } } orderNames = uniqueSortedLowerNames(orderNames) orderIPs = uniqueSortedIPs(orderIPs) totalNumberOfSANs := len(csr.DNSNames) + len(csr.IPAddresses) sans = make([]x509util.SubjectAlternativeName, totalNumberOfSANs) index := 0 // Validate identifier names against CSR alternative names. // // Note that with certificate templates we are not going to check for the // absence of other SANs as they will only be set if the template allows // them. if len(csr.DNSNames) != len(orderNames) { return sans, NewError(ErrorBadCSRType, "CSR names do not match identifiers exactly: "+ "CSR names = %v, Order names = %v", csr.DNSNames, orderNames) } for i := range csr.DNSNames { if csr.DNSNames[i] != orderNames[i] { return sans, NewError(ErrorBadCSRType, "CSR names do not match identifiers exactly: "+ "CSR names = %v, Order names = %v", csr.DNSNames, orderNames) } sans[index] = x509util.SubjectAlternativeName{ Type: x509util.DNSType, Value: csr.DNSNames[i], } index++ } if len(csr.IPAddresses) != len(orderIPs) { return sans, NewError(ErrorBadCSRType, "CSR IPs do not match identifiers exactly: "+ "CSR IPs = %v, Order IPs = %v", csr.IPAddresses, orderIPs) } for i := range csr.IPAddresses { if !ipsAreEqual(csr.IPAddresses[i], orderIPs[i]) { return sans, NewError(ErrorBadCSRType, "CSR IPs do not match identifiers exactly: "+ "CSR IPs = %v, Order IPs = %v", csr.IPAddresses, orderIPs) } sans[index] = x509util.SubjectAlternativeName{ Type: x509util.IPType, Value: csr.IPAddresses[i].String(), } index++ } return sans, nil } // numberOfIdentifierType returns the number of Identifiers that // are of type typ. func numberOfIdentifierType(typ IdentifierType, ids []Identifier) int { c := 0 for _, id := range ids { if id.Type == typ { c++ } } return c } // canonicalize canonicalizes a CSR so that it can be compared against an Order // NOTE: this effectively changes the order of SANs in the CSR, which may be OK, // but may not be expected. It also adds a Subject Common Name to either the IP // addresses or DNS names slice, depending on whether it can be parsed as an IP // or not. This might result in an additional SAN in the final certificate. func canonicalize(csr *x509.CertificateRequest) (canonicalized *x509.CertificateRequest) { // for clarity only; we're operating on the same object by pointer canonicalized = csr // RFC8555: The CSR MUST indicate the exact same set of requested // identifiers as the initial newOrder request. Identifiers of type "dns" // MUST appear either in the commonName portion of the requested subject // name or in an extensionRequest attribute [RFC2985] requesting a // subjectAltName extension, or both. Subject Common Names that can be // parsed as an IP are included as an IP address for the equality check. // If these were excluded, a certificate could contain an IP as the // common name without having been challenged. if csr.Subject.CommonName != "" { if ip := net.ParseIP(csr.Subject.CommonName); ip != nil { canonicalized.IPAddresses = append(canonicalized.IPAddresses, ip) } else { canonicalized.DNSNames = append(canonicalized.DNSNames, csr.Subject.CommonName) } } canonicalized.DNSNames = uniqueSortedLowerNames(canonicalized.DNSNames) canonicalized.IPAddresses = uniqueSortedIPs(canonicalized.IPAddresses) return canonicalized } // ipsAreEqual compares IPs to be equal. Nil values (i.e. invalid IPs) are // not considered equal. IPv6 representations of IPv4 addresses are // considered equal to the IPv4 address in this implementation, which is // standard Go behavior. An example is "::ffff:192.168.42.42", which // is equal to "192.168.42.42". This is considered a known issue within // step and is tracked here too: https://github.com/golang/go/issues/37921. func ipsAreEqual(x, y net.IP) bool { if x == nil || y == nil { return false } return x.Equal(y) } // uniqueSortedLowerNames returns the set of all unique names in the input after all // of them are lowercased. The returned names will be in their lowercased form // and sorted alphabetically. func uniqueSortedLowerNames(names []string) (unique []string) { nameMap := make(map[string]int, len(names)) for _, name := range names { nameMap[strings.ToLower(name)] = 1 } unique = make([]string, 0, len(nameMap)) for name := range nameMap { unique = append(unique, name) } sort.Strings(unique) return } // uniqueSortedIPs returns the set of all unique net.IPs in the input. They // are sorted by their bytes (octet) representation. func uniqueSortedIPs(ips []net.IP) (unique []net.IP) { type entry struct { ip net.IP } ipEntryMap := make(map[string]entry, len(ips)) for _, ip := range ips { // reparsing the IP results in the IP being represented using 16 bytes // for both IPv4 as well as IPv6, even when the ips slice contains IPs that // are represented by 4 bytes. This ensures a fair comparison and thus ordering. ipEntryMap[ip.String()] = entry{ip: net.ParseIP(ip.String())} } unique = make([]net.IP, 0, len(ipEntryMap)) for _, entry := range ipEntryMap { unique = append(unique, entry.ip) } sort.Slice(unique, func(i, j int) bool { return bytes.Compare(unique[i], unique[j]) < 0 }) return }