| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package pki |
| |
| import ( |
| "crypto/x509" |
| "encoding/base64" |
| "encoding/pem" |
| "fmt" |
| "net" |
| "net/http" |
| "sort" |
| "strings" |
| "time" |
| |
| "github.com/hashicorp/vault/sdk/helper/strutil" |
| |
| "github.com/hashicorp/vault/sdk/helper/certutil" |
| |
| "github.com/hashicorp/vault/sdk/framework" |
| "github.com/hashicorp/vault/sdk/logical" |
| "golang.org/x/net/idna" |
| ) |
| |
| var maxAcmeCertTTL = 90 * (24 * time.Hour) |
| |
| func pathAcmeListOrders(b *backend) []*framework.Path { |
| return buildAcmeFrameworkPaths(b, patternAcmeListOrders, "/orders") |
| } |
| |
| func pathAcmeGetOrder(b *backend) []*framework.Path { |
| return buildAcmeFrameworkPaths(b, patternAcmeGetOrder, "/order/"+uuidNameRegex("order_id")) |
| } |
| |
| func pathAcmeNewOrder(b *backend) []*framework.Path { |
| return buildAcmeFrameworkPaths(b, patternAcmeNewOrder, "/new-order") |
| } |
| |
| func pathAcmeFinalizeOrder(b *backend) []*framework.Path { |
| return buildAcmeFrameworkPaths(b, patternAcmeFinalizeOrder, "/order/"+uuidNameRegex("order_id")+"/finalize") |
| } |
| |
| func pathAcmeFetchOrderCert(b *backend) []*framework.Path { |
| return buildAcmeFrameworkPaths(b, patternAcmeFetchOrderCert, "/order/"+uuidNameRegex("order_id")+"/cert") |
| } |
| |
| func patternAcmeNewOrder(b *backend, pattern string) *framework.Path { |
| fields := map[string]*framework.FieldSchema{} |
| addFieldsForACMEPath(fields, pattern) |
| addFieldsForACMERequest(fields) |
| |
| return &framework.Path{ |
| Pattern: pattern, |
| Fields: fields, |
| Operations: map[logical.Operation]framework.OperationHandler{ |
| logical.UpdateOperation: &framework.PathOperation{ |
| Callback: b.acmeAccountRequiredWrapper(b.acmeNewOrderHandler), |
| ForwardPerformanceSecondary: false, |
| ForwardPerformanceStandby: true, |
| }, |
| }, |
| |
| HelpSynopsis: pathAcmeHelpSync, |
| HelpDescription: pathAcmeHelpDesc, |
| } |
| } |
| |
| func patternAcmeListOrders(b *backend, pattern string) *framework.Path { |
| fields := map[string]*framework.FieldSchema{} |
| addFieldsForACMEPath(fields, pattern) |
| addFieldsForACMERequest(fields) |
| |
| return &framework.Path{ |
| Pattern: pattern, |
| Fields: fields, |
| Operations: map[logical.Operation]framework.OperationHandler{ |
| logical.UpdateOperation: &framework.PathOperation{ |
| Callback: b.acmeAccountRequiredWrapper(b.acmeListOrdersHandler), |
| ForwardPerformanceSecondary: false, |
| ForwardPerformanceStandby: true, |
| }, |
| }, |
| |
| HelpSynopsis: pathAcmeHelpSync, |
| HelpDescription: pathAcmeHelpDesc, |
| } |
| } |
| |
| func patternAcmeGetOrder(b *backend, pattern string) *framework.Path { |
| fields := map[string]*framework.FieldSchema{} |
| addFieldsForACMEPath(fields, pattern) |
| addFieldsForACMERequest(fields) |
| addFieldsForACMEOrder(fields) |
| |
| return &framework.Path{ |
| Pattern: pattern, |
| Fields: fields, |
| Operations: map[logical.Operation]framework.OperationHandler{ |
| logical.UpdateOperation: &framework.PathOperation{ |
| Callback: b.acmeAccountRequiredWrapper(b.acmeGetOrderHandler), |
| ForwardPerformanceSecondary: false, |
| ForwardPerformanceStandby: true, |
| }, |
| }, |
| |
| HelpSynopsis: pathAcmeHelpSync, |
| HelpDescription: pathAcmeHelpDesc, |
| } |
| } |
| |
| func patternAcmeFinalizeOrder(b *backend, pattern string) *framework.Path { |
| fields := map[string]*framework.FieldSchema{} |
| addFieldsForACMEPath(fields, pattern) |
| addFieldsForACMERequest(fields) |
| addFieldsForACMEOrder(fields) |
| |
| return &framework.Path{ |
| Pattern: pattern, |
| Fields: fields, |
| Operations: map[logical.Operation]framework.OperationHandler{ |
| logical.UpdateOperation: &framework.PathOperation{ |
| Callback: b.acmeAccountRequiredWrapper(b.acmeFinalizeOrderHandler), |
| ForwardPerformanceSecondary: false, |
| ForwardPerformanceStandby: true, |
| }, |
| }, |
| |
| HelpSynopsis: pathAcmeHelpSync, |
| HelpDescription: pathAcmeHelpDesc, |
| } |
| } |
| |
| func patternAcmeFetchOrderCert(b *backend, pattern string) *framework.Path { |
| fields := map[string]*framework.FieldSchema{} |
| addFieldsForACMEPath(fields, pattern) |
| addFieldsForACMERequest(fields) |
| addFieldsForACMEOrder(fields) |
| |
| return &framework.Path{ |
| Pattern: pattern, |
| Fields: fields, |
| Operations: map[logical.Operation]framework.OperationHandler{ |
| logical.UpdateOperation: &framework.PathOperation{ |
| Callback: b.acmeAccountRequiredWrapper(b.acmeFetchCertOrderHandler), |
| ForwardPerformanceSecondary: false, |
| ForwardPerformanceStandby: true, |
| }, |
| }, |
| |
| HelpSynopsis: pathAcmeHelpSync, |
| HelpDescription: pathAcmeHelpDesc, |
| } |
| } |
| |
| func addFieldsForACMEOrder(fields map[string]*framework.FieldSchema) { |
| fields["order_id"] = &framework.FieldSchema{ |
| Type: framework.TypeString, |
| Description: `The ACME order identifier to fetch`, |
| Required: true, |
| } |
| } |
| |
| func (b *backend) acmeFetchCertOrderHandler(ac *acmeContext, _ *logical.Request, fields *framework.FieldData, uc *jwsCtx, data map[string]interface{}, _ *acmeAccount) (*logical.Response, error) { |
| orderId := fields.Get("order_id").(string) |
| |
| order, err := b.acmeState.LoadOrder(ac, uc, orderId) |
| if err != nil { |
| return nil, err |
| } |
| |
| if order.Status != ACMEOrderValid { |
| return nil, fmt.Errorf("%w: order is status %s, needs to be in valid state", ErrOrderNotReady, order.Status) |
| } |
| |
| if len(order.IssuerId) == 0 || len(order.CertificateSerialNumber) == 0 { |
| return nil, fmt.Errorf("order is missing required fields to load certificate") |
| } |
| |
| certEntry, err := fetchCertBySerial(ac.sc, "certs/", order.CertificateSerialNumber) |
| if err != nil { |
| return nil, fmt.Errorf("failed reading certificate %s from storage: %w", order.CertificateSerialNumber, err) |
| } |
| if certEntry == nil || len(certEntry.Value) == 0 { |
| return nil, fmt.Errorf("missing certificate %s from storage", order.CertificateSerialNumber) |
| } |
| |
| cert, err := x509.ParseCertificate(certEntry.Value) |
| if err != nil { |
| return nil, fmt.Errorf("failed parsing certificate %s: %w", order.CertificateSerialNumber, err) |
| } |
| |
| issuer, err := ac.sc.fetchIssuerById(order.IssuerId) |
| if err != nil { |
| return nil, fmt.Errorf("failed loading certificate issuer %s from storage: %w", order.IssuerId, err) |
| } |
| |
| allPems, err := func() ([]byte, error) { |
| leafPEM := pem.EncodeToMemory(&pem.Block{ |
| Type: "CERTIFICATE", |
| Bytes: cert.Raw, |
| }) |
| |
| chains := []byte(issuer.Certificate) |
| for _, chainVal := range issuer.CAChain { |
| if chainVal == issuer.Certificate { |
| continue |
| } |
| chains = append(chains, []byte(chainVal)...) |
| } |
| |
| return append(leafPEM, chains...), nil |
| }() |
| if err != nil { |
| return nil, fmt.Errorf("failed encoding certificate ca chain: %w", err) |
| } |
| |
| return &logical.Response{ |
| Data: map[string]interface{}{ |
| logical.HTTPContentType: "application/pem-certificate-chain", |
| logical.HTTPStatusCode: http.StatusOK, |
| logical.HTTPRawBody: allPems, |
| }, |
| }, nil |
| } |
| |
| func (b *backend) acmeFinalizeOrderHandler(ac *acmeContext, _ *logical.Request, fields *framework.FieldData, uc *jwsCtx, data map[string]interface{}, account *acmeAccount) (*logical.Response, error) { |
| orderId := fields.Get("order_id").(string) |
| |
| csr, err := parseCsrFromFinalize(data) |
| if err != nil { |
| return nil, err |
| } |
| |
| order, err := b.acmeState.LoadOrder(ac, uc, orderId) |
| if err != nil { |
| return nil, err |
| } |
| |
| order.Status, err = computeOrderStatus(ac, uc, order) |
| if err != nil { |
| return nil, err |
| } |
| |
| if order.Status != ACMEOrderReady { |
| return nil, fmt.Errorf("%w: order is status %s, needs to be in ready state", ErrOrderNotReady, order.Status) |
| } |
| |
| now := time.Now() |
| if !order.Expires.IsZero() && now.After(order.Expires) { |
| return nil, fmt.Errorf("%w: order %s is expired", ErrMalformed, orderId) |
| } |
| |
| if err = validateCsrMatchesOrder(csr, order); err != nil { |
| return nil, err |
| } |
| |
| if err = validateCsrNotUsingAccountKey(csr, uc); err != nil { |
| return nil, err |
| } |
| |
| signedCertBundle, issuerId, err := issueCertFromCsr(ac, csr) |
| if err != nil { |
| return nil, err |
| } |
| |
| hyphenSerialNumber := normalizeSerialFromBigInt(signedCertBundle.Certificate.SerialNumber) |
| err = storeCertificate(ac.sc, signedCertBundle) |
| if err != nil { |
| return nil, err |
| } |
| |
| if err := b.acmeState.TrackIssuedCert(ac, order.AccountId, hyphenSerialNumber, order.OrderId); err != nil { |
| b.Logger().Warn("orphaned generated ACME certificate due to error saving account->cert->order reference", "serial_number", hyphenSerialNumber, "error", err) |
| return nil, err |
| } |
| |
| order.Status = ACMEOrderValid |
| order.CertificateSerialNumber = hyphenSerialNumber |
| order.CertificateExpiry = signedCertBundle.Certificate.NotAfter |
| order.IssuerId = issuerId |
| |
| err = b.acmeState.SaveOrder(ac, order) |
| if err != nil { |
| b.Logger().Warn("orphaned generated ACME certificate due to error saving order", "serial_number", hyphenSerialNumber, "error", err) |
| return nil, fmt.Errorf("failed saving updated order: %w", err) |
| } |
| |
| if err := b.doTrackBilling(ac.sc.Context, order.Identifiers); err != nil { |
| b.Logger().Error("failed to track billing for order", "order", orderId, "error", err) |
| err = nil |
| } |
| |
| return formatOrderResponse(ac, order), nil |
| } |
| |
| func computeOrderStatus(ac *acmeContext, uc *jwsCtx, order *acmeOrder) (ACMEOrderStatusType, error) { |
| // If we reached a final stage, no use computing anything else |
| if order.Status == ACMEOrderInvalid || order.Status == ACMEOrderValid { |
| return order.Status, nil |
| } |
| |
| // We aren't in a final state yet, check for expiry |
| if time.Now().After(order.Expires) { |
| return ACMEOrderInvalid, nil |
| } |
| |
| // Intermediary steps passed authorizations should short circuit us as well |
| if order.Status == ACMEOrderReady || order.Status == ACMEOrderProcessing { |
| return order.Status, nil |
| } |
| |
| // If we have no authorizations attached to the order, nothing to compute either |
| if len(order.AuthorizationIds) == 0 { |
| return ACMEOrderPending, nil |
| } |
| |
| anyFailed := false |
| allPassed := true |
| for _, authId := range order.AuthorizationIds { |
| authorization, err := ac.getAcmeState().LoadAuthorization(ac, uc, authId) |
| if err != nil { |
| return order.Status, fmt.Errorf("failed loading authorization: %s: %w", authId, err) |
| } |
| |
| if authorization.Status == ACMEAuthorizationPending { |
| allPassed = false |
| continue |
| } |
| |
| if authorization.Status != ACMEAuthorizationValid { |
| // Per RFC 8555 - 7.1.6. Status Changes |
| // The order also moves to the "invalid" state if it expires or |
| // one of its authorizations enters a final state other than |
| // "valid" ("expired", "revoked", or "deactivated"). |
| allPassed = false |
| anyFailed = true |
| break |
| } |
| } |
| |
| if anyFailed { |
| return ACMEOrderInvalid, nil |
| } |
| |
| if allPassed { |
| return ACMEOrderReady, nil |
| } |
| |
| // The order has not expired, no authorizations have yet to be marked as failed |
| // nor have we passed them all. |
| return ACMEOrderPending, nil |
| } |
| |
| func validateCsrNotUsingAccountKey(csr *x509.CertificateRequest, uc *jwsCtx) error { |
| csrKey := csr.PublicKey |
| userKey := uc.Key.Public().Key |
| |
| sameKey, err := certutil.ComparePublicKeysAndType(csrKey, userKey) |
| if err != nil { |
| return err |
| } |
| |
| if sameKey { |
| return fmt.Errorf("%w: certificate public key must not match account key", ErrBadCSR) |
| } |
| |
| return nil |
| } |
| |
| func validateCsrMatchesOrder(csr *x509.CertificateRequest, order *acmeOrder) error { |
| csrDNSIdentifiers, csrIPIdentifiers := getIdentifiersFromCSR(csr) |
| orderDNSIdentifiers := strutil.RemoveDuplicates(order.getIdentifierDNSValues(), true) |
| orderIPIdentifiers := removeDuplicatesAndSortIps(order.getIdentifierIPValues()) |
| |
| if len(orderDNSIdentifiers) == 0 && len(orderIPIdentifiers) == 0 { |
| return fmt.Errorf("%w: order did not include any identifiers", ErrServerInternal) |
| } |
| |
| if len(orderDNSIdentifiers) != len(csrDNSIdentifiers) { |
| return fmt.Errorf("%w: Order (%v) and CSR (%v) mismatch on number of DNS identifiers", ErrBadCSR, len(orderDNSIdentifiers), len(csrDNSIdentifiers)) |
| } |
| |
| if len(orderIPIdentifiers) != len(csrIPIdentifiers) { |
| return fmt.Errorf("%w: Order (%v) and CSR (%v) mismatch on number of IP identifiers", ErrBadCSR, len(orderIPIdentifiers), len(csrIPIdentifiers)) |
| } |
| |
| for i, identifier := range orderDNSIdentifiers { |
| if identifier != csrDNSIdentifiers[i] { |
| return fmt.Errorf("%w: CSR is missing order DNS identifier %s", ErrBadCSR, identifier) |
| } |
| } |
| |
| for i, identifier := range orderIPIdentifiers { |
| if !identifier.Equal(csrIPIdentifiers[i]) { |
| return fmt.Errorf("%w: CSR is missing order IP identifier %s", ErrBadCSR, identifier.String()) |
| } |
| } |
| |
| // Since we do not support NotBefore/NotAfter dates at this time no need to validate CSR/Order match. |
| |
| return nil |
| } |
| |
| func (b *backend) validateIdentifiersAgainstRole(role *roleEntry, identifiers []*ACMEIdentifier) error { |
| for _, identifier := range identifiers { |
| switch identifier.Type { |
| case ACMEDNSIdentifier: |
| data := &inputBundle{ |
| role: role, |
| req: &logical.Request{}, |
| apiData: &framework.FieldData{}, |
| } |
| |
| if validateNames(b, data, []string{identifier.OriginalValue}) != "" { |
| return fmt.Errorf("%w: role (%s) will not issue certificate for name %v", |
| ErrRejectedIdentifier, role.Name, identifier.OriginalValue) |
| } |
| case ACMEIPIdentifier: |
| if !role.AllowIPSANs { |
| return fmt.Errorf("%w: role (%s) does not allow IP sans, so cannot issue certificate for %v", |
| ErrRejectedIdentifier, role.Name, identifier.OriginalValue) |
| } |
| default: |
| return fmt.Errorf("unknown type of identifier: %v for %v", identifier.Type, identifier.OriginalValue) |
| } |
| } |
| |
| return nil |
| } |
| |
| func getIdentifiersFromCSR(csr *x509.CertificateRequest) ([]string, []net.IP) { |
| dnsIdentifiers := append([]string(nil), csr.DNSNames...) |
| ipIdentifiers := append([]net.IP(nil), csr.IPAddresses...) |
| |
| if csr.Subject.CommonName != "" { |
| ip := net.ParseIP(csr.Subject.CommonName) |
| if ip != nil { |
| ipIdentifiers = append(ipIdentifiers, ip) |
| } else { |
| dnsIdentifiers = append(dnsIdentifiers, csr.Subject.CommonName) |
| } |
| } |
| |
| return strutil.RemoveDuplicates(dnsIdentifiers, true), removeDuplicatesAndSortIps(ipIdentifiers) |
| } |
| |
| func removeDuplicatesAndSortIps(ipIdentifiers []net.IP) []net.IP { |
| var uniqueIpIdentifiers []net.IP |
| for _, ip := range ipIdentifiers { |
| found := false |
| for _, curIp := range uniqueIpIdentifiers { |
| if curIp.Equal(ip) { |
| found = true |
| } |
| } |
| |
| if !found { |
| uniqueIpIdentifiers = append(uniqueIpIdentifiers, ip) |
| } |
| } |
| |
| sort.Slice(uniqueIpIdentifiers, func(i, j int) bool { |
| return uniqueIpIdentifiers[i].String() < uniqueIpIdentifiers[j].String() |
| }) |
| return uniqueIpIdentifiers |
| } |
| |
| func storeCertificate(sc *storageContext, signedCertBundle *certutil.ParsedCertBundle) error { |
| hyphenSerialNumber := normalizeSerialFromBigInt(signedCertBundle.Certificate.SerialNumber) |
| key := "certs/" + hyphenSerialNumber |
| certsCounted := sc.Backend.certsCounted.Load() |
| err := sc.Storage.Put(sc.Context, &logical.StorageEntry{ |
| Key: key, |
| Value: signedCertBundle.CertificateBytes, |
| }) |
| if err != nil { |
| return fmt.Errorf("unable to store certificate locally: %w", err) |
| } |
| sc.Backend.ifCountEnabledIncrementTotalCertificatesCount(certsCounted, key) |
| return nil |
| } |
| |
| func maybeAugmentReqDataWithSuitableCN(ac *acmeContext, csr *x509.CertificateRequest, data *framework.FieldData) { |
| // Role doesn't require a CN, so we don't care. |
| if !ac.role.RequireCN { |
| return |
| } |
| |
| // CSR contains a CN, so use that one. |
| if csr.Subject.CommonName != "" { |
| return |
| } |
| |
| // Choose a CN in the order wildcard -> DNS -> IP -> fail. |
| for _, name := range csr.DNSNames { |
| if strings.Contains(name, "*") { |
| data.Raw["common_name"] = name |
| return |
| } |
| } |
| if len(csr.DNSNames) > 0 { |
| data.Raw["common_name"] = csr.DNSNames[0] |
| return |
| } |
| if len(csr.IPAddresses) > 0 { |
| data.Raw["common_name"] = csr.IPAddresses[0].String() |
| return |
| } |
| } |
| |
| func issueCertFromCsr(ac *acmeContext, csr *x509.CertificateRequest) (*certutil.ParsedCertBundle, issuerID, error) { |
| pemBlock := &pem.Block{ |
| Type: "CERTIFICATE REQUEST", |
| Headers: nil, |
| Bytes: csr.Raw, |
| } |
| pemCsr := string(pem.EncodeToMemory(pemBlock)) |
| |
| data := &framework.FieldData{ |
| Raw: map[string]interface{}{ |
| "csr": pemCsr, |
| }, |
| Schema: getCsrSignVerbatimSchemaFields(), |
| } |
| |
| // XXX: Usability hack: by default, minimalist roles have require_cn=true, |
| // but some ACME clients do not provision one in the certificate as modern |
| // (TLS) clients are mostly verifying against server's DNS SANs. |
| maybeAugmentReqDataWithSuitableCN(ac, csr, data) |
| |
| signingBundle, issuerId, err := ac.sc.fetchCAInfoWithIssuer(ac.issuer.ID.String(), IssuanceUsage) |
| if err != nil { |
| return nil, "", fmt.Errorf("failed loading CA %s: %w", ac.issuer.ID.String(), err) |
| } |
| |
| // ACME issued cert will override the TTL values to truncate to the issuer's |
| // expiration if we go beyond, no matter the setting |
| if signingBundle.LeafNotAfterBehavior == certutil.ErrNotAfterBehavior { |
| signingBundle.LeafNotAfterBehavior = certutil.TruncateNotAfterBehavior |
| } |
| |
| input := &inputBundle{ |
| req: &logical.Request{}, |
| apiData: data, |
| role: ac.role, |
| } |
| |
| normalNotAfter, _, err := getCertificateNotAfter(ac.sc.Backend, input, signingBundle) |
| if err != nil { |
| return nil, "", fmt.Errorf("failed computing certificate TTL from role/mount: %v: %w", err, ErrMalformed) |
| } |
| |
| // Force a maximum 90 day TTL or lower for ACME |
| if time.Now().Add(maxAcmeCertTTL).Before(normalNotAfter) { |
| input.apiData.Raw["ttl"] = maxAcmeCertTTL |
| } |
| |
| if csr.PublicKeyAlgorithm == x509.UnknownPublicKeyAlgorithm || csr.PublicKey == nil { |
| return nil, "", fmt.Errorf("%w: Refusing to sign CSR with empty PublicKey", ErrBadCSR) |
| } |
| |
| // UseCSRValues as defined in certutil/helpers.go accepts the following |
| // fields off of the CSR: |
| // |
| // 1. Subject fields, |
| // 2. SANs, |
| // 3. Extensions (except for a BasicConstraint extension) |
| // |
| // Because we have stricter validation of subject parameters, and no way |
| // to validate or allow extensions, we do not wish to use the CSR's |
| // parameters for these values. If a CSR sets, e.g., an organizational |
| // unit, we have no way of validating this (via ACME here, without perhaps |
| // an external policy engine), and thus should not be setting it on our |
| // final issued certificate. |
| parsedBundle, _, err := signCert(ac.sc.Backend, input, signingBundle, false /* is_ca=false */, false /* use_csr_values */) |
| if err != nil { |
| return nil, "", fmt.Errorf("%w: refusing to sign CSR: %s", ErrBadCSR, err.Error()) |
| } |
| |
| if err = parsedBundle.Verify(); err != nil { |
| return nil, "", fmt.Errorf("verification of parsed bundle failed: %w", err) |
| } |
| |
| // We only allow ServerAuth key usage from ACME issued certs |
| // when configuration does not allow usage of ExtKeyusage field. |
| config, err := ac.sc.Backend.acmeState.getConfigWithUpdate(ac.sc) |
| if err != nil { |
| return nil, "", fmt.Errorf("failed to fetch ACME configuration: %w", err) |
| } |
| |
| if !config.AllowRoleExtKeyUsage { |
| for _, usage := range parsedBundle.Certificate.ExtKeyUsage { |
| if usage != x509.ExtKeyUsageServerAuth { |
| return nil, "", fmt.Errorf("%w: ACME certs only allow ServerAuth key usage", ErrBadCSR) |
| } |
| } |
| } |
| |
| return parsedBundle, issuerId, err |
| } |
| |
| func parseCsrFromFinalize(data map[string]interface{}) (*x509.CertificateRequest, error) { |
| csrInterface, present := data["csr"] |
| if !present { |
| return nil, fmt.Errorf("%w: missing csr in payload", ErrMalformed) |
| } |
| |
| base64Csr, ok := csrInterface.(string) |
| if !ok { |
| return nil, fmt.Errorf("%w: csr in payload not the expected type: %T", ErrMalformed, csrInterface) |
| } |
| |
| derCsr, err := base64.RawURLEncoding.DecodeString(base64Csr) |
| if err != nil { |
| return nil, fmt.Errorf("%w: failed base64 decoding csr: %s", ErrMalformed, err.Error()) |
| } |
| |
| csr, err := x509.ParseCertificateRequest(derCsr) |
| if err != nil { |
| return nil, fmt.Errorf("%w: failed to parse csr: %s", ErrMalformed, err.Error()) |
| } |
| |
| if csr.PublicKey == nil || csr.PublicKeyAlgorithm == x509.UnknownPublicKeyAlgorithm { |
| return nil, fmt.Errorf("%w: failed to parse csr no public key info or unknown key algorithm used", ErrBadCSR) |
| } |
| |
| for _, ext := range csr.Extensions { |
| if ext.Id.Equal(certutil.ExtensionBasicConstraintsOID) { |
| isCa, _, err := certutil.ParseBasicConstraintExtension(ext) |
| if err != nil { |
| return nil, fmt.Errorf("%w: refusing to accept CSR with Basic Constraints extension: %v", ErrBadCSR, err.Error()) |
| } |
| |
| if isCa { |
| return nil, fmt.Errorf("%w: refusing to accept CSR with Basic Constraints extension with CA set to true", ErrBadCSR) |
| } |
| } |
| } |
| |
| return csr, nil |
| } |
| |
| func (b *backend) acmeGetOrderHandler(ac *acmeContext, _ *logical.Request, fields *framework.FieldData, uc *jwsCtx, _ map[string]interface{}, _ *acmeAccount) (*logical.Response, error) { |
| orderId := fields.Get("order_id").(string) |
| |
| order, err := b.acmeState.LoadOrder(ac, uc, orderId) |
| if err != nil { |
| return nil, err |
| } |
| |
| order.Status, err = computeOrderStatus(ac, uc, order) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Per RFC 8555 -> 7.1.3. Order Objects |
| // For final orders (in the "valid" or "invalid" state), the authorizations that were completed. |
| // |
| // Otherwise, for "pending" orders we will return our list as it was originally saved. |
| requiresFiltering := order.Status == ACMEOrderValid || order.Status == ACMEOrderInvalid |
| if requiresFiltering { |
| filteredAuthorizationIds := []string{} |
| |
| for _, authId := range order.AuthorizationIds { |
| authorization, err := b.acmeState.LoadAuthorization(ac, uc, authId) |
| if err != nil { |
| return nil, err |
| } |
| |
| if (order.Status == ACMEOrderInvalid || order.Status == ACMEOrderValid) && |
| authorization.Status == ACMEAuthorizationValid { |
| filteredAuthorizationIds = append(filteredAuthorizationIds, authId) |
| } |
| } |
| |
| order.AuthorizationIds = filteredAuthorizationIds |
| } |
| |
| return formatOrderResponse(ac, order), nil |
| } |
| |
| func (b *backend) acmeListOrdersHandler(ac *acmeContext, _ *logical.Request, _ *framework.FieldData, uc *jwsCtx, _ map[string]interface{}, acct *acmeAccount) (*logical.Response, error) { |
| orderIds, err := b.acmeState.ListOrderIds(ac.sc, acct.KeyId) |
| if err != nil { |
| return nil, err |
| } |
| |
| orderUrls := []string{} |
| for _, orderId := range orderIds { |
| order, err := b.acmeState.LoadOrder(ac, uc, orderId) |
| if err != nil { |
| return nil, err |
| } |
| |
| if order.Status == ACMEOrderInvalid { |
| // Per RFC8555 -> 7.1.2.1 - Orders List |
| // The server SHOULD include pending orders and SHOULD NOT |
| // include orders that are invalid in the array of URLs. |
| continue |
| } |
| |
| orderUrls = append(orderUrls, buildOrderUrl(ac, orderId)) |
| } |
| |
| resp := &logical.Response{ |
| Data: map[string]interface{}{ |
| "orders": orderUrls, |
| }, |
| } |
| |
| return resp, nil |
| } |
| |
| func (b *backend) acmeNewOrderHandler(ac *acmeContext, _ *logical.Request, _ *framework.FieldData, _ *jwsCtx, data map[string]interface{}, account *acmeAccount) (*logical.Response, error) { |
| identifiers, err := parseOrderIdentifiers(data) |
| if err != nil { |
| return nil, err |
| } |
| |
| notBefore, err := parseOptRFC3339Field(data, "notBefore") |
| if err != nil { |
| return nil, err |
| } |
| |
| notAfter, err := parseOptRFC3339Field(data, "notAfter") |
| if err != nil { |
| return nil, err |
| } |
| |
| if !notBefore.IsZero() || !notAfter.IsZero() { |
| return nil, fmt.Errorf("%w: NotBefore and NotAfter are not supported", ErrMalformed) |
| } |
| |
| err = validateAcmeProvidedOrderDates(notBefore, notAfter) |
| if err != nil { |
| return nil, err |
| } |
| |
| err = b.validateIdentifiersAgainstRole(ac.role, identifiers) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Per RFC 8555 -> 7.1.3. Order Objects |
| // For pending orders, the authorizations that the client needs to complete before the |
| // requested certificate can be issued (see Section 7.5), including |
| // unexpired authorizations that the client has completed in the past |
| // for identifiers specified in the order. |
| // |
| // Since we are generating all authorizations here, there is no need to filter them out |
| // IF/WHEN we support pre-authz workflows and associate existing authorizations to this |
| // order they will need filtering. |
| var authorizations []*ACMEAuthorization |
| var authorizationIds []string |
| for _, identifier := range identifiers { |
| authz, err := generateAuthorization(account, identifier) |
| if err != nil { |
| return nil, fmt.Errorf("error generating authorizations: %w", err) |
| } |
| authorizations = append(authorizations, authz) |
| |
| err = b.acmeState.SaveAuthorization(ac, authz) |
| if err != nil { |
| return nil, fmt.Errorf("failed storing authorization: %w", err) |
| } |
| |
| authorizationIds = append(authorizationIds, authz.Id) |
| } |
| |
| order := &acmeOrder{ |
| OrderId: genUuid(), |
| AccountId: account.KeyId, |
| Status: ACMEOrderPending, |
| Expires: time.Now().Add(24 * time.Hour), // TODO: Readjust this based on authz and/or config |
| Identifiers: identifiers, |
| AuthorizationIds: authorizationIds, |
| } |
| |
| err = b.acmeState.SaveOrder(ac, order) |
| if err != nil { |
| return nil, fmt.Errorf("failed storing order: %w", err) |
| } |
| |
| resp := formatOrderResponse(ac, order) |
| |
| // Per RFC 8555 Section 7.4. Applying for Certificate Issuance: |
| // |
| // > If the server is willing to issue the requested certificate, it |
| // > responds with a 201 (Created) response. |
| resp.Data[logical.HTTPStatusCode] = http.StatusCreated |
| return resp, nil |
| } |
| |
| func validateAcmeProvidedOrderDates(notBefore time.Time, notAfter time.Time) error { |
| if !notBefore.IsZero() && !notAfter.IsZero() { |
| if notBefore.Equal(notAfter) { |
| return fmt.Errorf("%w: provided notBefore and notAfter dates can not be equal", ErrMalformed) |
| } |
| |
| if notBefore.After(notAfter) { |
| return fmt.Errorf("%w: provided notBefore can not be greater than notAfter", ErrMalformed) |
| } |
| } |
| |
| if !notAfter.IsZero() { |
| if time.Now().After(notAfter) { |
| return fmt.Errorf("%w: provided notAfter can not be in the past", ErrMalformed) |
| } |
| } |
| |
| return nil |
| } |
| |
| func formatOrderResponse(acmeCtx *acmeContext, order *acmeOrder) *logical.Response { |
| baseOrderUrl := buildOrderUrl(acmeCtx, order.OrderId) |
| |
| var authorizationUrls []string |
| for _, authId := range order.AuthorizationIds { |
| authorizationUrls = append(authorizationUrls, buildAuthorizationUrl(acmeCtx, authId)) |
| } |
| |
| var identifiers []map[string]interface{} |
| for _, identifier := range order.Identifiers { |
| identifiers = append(identifiers, identifier.NetworkMarshal( /* use original value */ true)) |
| } |
| |
| resp := &logical.Response{ |
| Data: map[string]interface{}{ |
| "status": order.Status, |
| "expires": order.Expires.Format(time.RFC3339), |
| "identifiers": identifiers, |
| "authorizations": authorizationUrls, |
| "finalize": baseOrderUrl + "/finalize", |
| }, |
| Headers: map[string][]string{ |
| "Location": {baseOrderUrl}, |
| }, |
| } |
| |
| // Only reply with the certificate URL if we are in a valid order state. |
| if order.Status == ACMEOrderValid { |
| resp.Data["certificate"] = baseOrderUrl + "/cert" |
| } |
| |
| return resp |
| } |
| |
| func buildAuthorizationUrl(acmeCtx *acmeContext, authId string) string { |
| return acmeCtx.baseUrl.JoinPath("authorization", authId).String() |
| } |
| |
| func buildOrderUrl(acmeCtx *acmeContext, orderId string) string { |
| return acmeCtx.baseUrl.JoinPath("order", orderId).String() |
| } |
| |
| func generateAuthorization(acct *acmeAccount, identifier *ACMEIdentifier) (*ACMEAuthorization, error) { |
| authId := genUuid() |
| |
| // Certain challenges have certain restrictions: DNS challenges cannot |
| // be used to validate IP addresses, and only DNS challenges can be used |
| // to validate wildcards. |
| allowedChallenges := []ACMEChallengeType{ACMEHTTPChallenge, ACMEDNSChallenge, ACMEALPNChallenge} |
| if identifier.Type == ACMEIPIdentifier { |
| allowedChallenges = []ACMEChallengeType{ACMEHTTPChallenge} |
| } else if identifier.IsWildcard { |
| allowedChallenges = []ACMEChallengeType{ACMEDNSChallenge} |
| } |
| |
| var challenges []*ACMEChallenge |
| for _, challengeType := range allowedChallenges { |
| token, err := getACMEToken() |
| if err != nil { |
| return nil, err |
| } |
| |
| challenge := &ACMEChallenge{ |
| Type: challengeType, |
| Status: ACMEChallengePending, |
| ChallengeFields: map[string]interface{}{ |
| "token": token, |
| }, |
| } |
| |
| challenges = append(challenges, challenge) |
| } |
| |
| return &ACMEAuthorization{ |
| Id: authId, |
| AccountId: acct.KeyId, |
| Identifier: identifier, |
| Status: ACMEAuthorizationPending, |
| Expires: "", // only populated when it switches to valid. |
| Challenges: challenges, |
| Wildcard: identifier.IsWildcard, |
| }, nil |
| } |
| |
| func parseOptRFC3339Field(data map[string]interface{}, keyName string) (time.Time, error) { |
| var timeVal time.Time |
| var err error |
| |
| rawBefore, present := data[keyName] |
| if present { |
| beforeStr, ok := rawBefore.(string) |
| if !ok { |
| return timeVal, fmt.Errorf("invalid type (%T) for field '%s': %w", rawBefore, keyName, ErrMalformed) |
| } |
| timeVal, err = time.Parse(time.RFC3339, beforeStr) |
| if err != nil { |
| return timeVal, fmt.Errorf("failed parsing field '%s' (%s): %s: %w", keyName, rawBefore, err.Error(), ErrMalformed) |
| } |
| |
| if timeVal.IsZero() { |
| return timeVal, fmt.Errorf("provided time value is invalid '%s' (%s): %w", keyName, rawBefore, ErrMalformed) |
| } |
| } |
| |
| return timeVal, nil |
| } |
| |
| func parseOrderIdentifiers(data map[string]interface{}) ([]*ACMEIdentifier, error) { |
| rawIdentifiers, present := data["identifiers"] |
| if !present { |
| return nil, fmt.Errorf("missing required identifiers argument: %w", ErrMalformed) |
| } |
| |
| listIdentifiers, ok := rawIdentifiers.([]interface{}) |
| if !ok { |
| return nil, fmt.Errorf("invalid type (%T) for field 'identifiers': %w", rawIdentifiers, ErrMalformed) |
| } |
| |
| var identifiers []*ACMEIdentifier |
| for _, rawIdentifier := range listIdentifiers { |
| mapIdentifier, ok := rawIdentifier.(map[string]interface{}) |
| if !ok { |
| return nil, fmt.Errorf("invalid type (%T) for value in 'identifiers': %w", rawIdentifier, ErrMalformed) |
| } |
| |
| typeVal, present := mapIdentifier["type"] |
| if !present { |
| return nil, fmt.Errorf("missing type argument for value in 'identifiers': %w", ErrMalformed) |
| } |
| typeStr, ok := typeVal.(string) |
| if !ok { |
| return nil, fmt.Errorf("invalid type for type argument (%T) for value in 'identifiers': %w", typeStr, ErrMalformed) |
| } |
| |
| valueVal, present := mapIdentifier["value"] |
| if !present { |
| return nil, fmt.Errorf("missing value argument for value in 'identifiers': %w", ErrMalformed) |
| } |
| valueStr, ok := valueVal.(string) |
| if !ok { |
| return nil, fmt.Errorf("invalid type for value argument (%T) for value in 'identifiers': %w", valueStr, ErrMalformed) |
| } |
| |
| if len(valueStr) == 0 { |
| return nil, fmt.Errorf("value argument for value in 'identifiers' can not be blank: %w", ErrMalformed) |
| } |
| |
| identifier := &ACMEIdentifier{ |
| Value: valueStr, |
| OriginalValue: valueStr, |
| } |
| |
| switch typeStr { |
| case string(ACMEIPIdentifier): |
| identifier.Type = ACMEIPIdentifier |
| ip := net.ParseIP(valueStr) |
| if ip == nil { |
| return nil, fmt.Errorf("value argument (%s) failed validation: failed parsing as IP: %w", valueStr, ErrMalformed) |
| } |
| case string(ACMEDNSIdentifier): |
| identifier.Type = ACMEDNSIdentifier |
| |
| // This check modifies the identifier if it is a wildcard, |
| // removing the non-wildcard portion. We do this before the |
| // IP address checks, in case of an attempt to bypass the IP/DNS |
| // check via including a leading wildcard (e.g., *.127.0.0.1). |
| // |
| // Per RFC 8555 Section 7.1.4. Authorization Objects: |
| // |
| // > Wildcard domain names (with "*" as the first label) MUST NOT |
| // > be included in authorization objects. |
| if _, _, err := identifier.MaybeParseWildcard(); err != nil { |
| return nil, fmt.Errorf("value argument (%s) failed validation: invalid wildcard: %v: %w", valueStr, err, ErrMalformed) |
| } |
| |
| if isIP := net.ParseIP(identifier.Value); isIP != nil { |
| return nil, fmt.Errorf("refusing to accept argument (%s) as DNS type identifier: parsed OK as IP address: %w", valueStr, ErrMalformed) |
| } |
| |
| // Use the reduced (identifier.Value) in case this was a wildcard |
| // domain. |
| p := idna.New(idna.ValidateForRegistration()) |
| converted, err := p.ToASCII(identifier.Value) |
| if err != nil { |
| return nil, fmt.Errorf("value argument (%s) failed validation: %s: %w", valueStr, err.Error(), ErrMalformed) |
| } |
| |
| // Per RFC 8555 Section 7.1.4. Authorization Objects: |
| // |
| // > The domain name MUST be encoded in the form in which it |
| // > would appear in a certificate. That is, it MUST be encoded |
| // > according to the rules in Section 7 of [RFC5280]. Servers |
| // > MUST verify any identifier values that begin with the |
| // > ASCII-Compatible Encoding prefix "xn--" as defined in |
| // > [RFC5890] are properly encoded. |
| if identifier.Value != converted { |
| return nil, fmt.Errorf("value argument (%s) failed IDNA round-tripping to ASCII: %w", valueStr, ErrMalformed) |
| } |
| default: |
| return nil, fmt.Errorf("unsupported identifier type %s: %w", typeStr, ErrUnsupportedIdentifier) |
| } |
| |
| identifiers = append(identifiers, identifier) |
| } |
| |
| return identifiers, nil |
| } |
| |
| func (b *backend) acmeTidyOrder(sc *storageContext, accountId string, orderPath string, certTidyBuffer time.Duration) (bool, time.Time, error) { |
| // First we get the order; note that the orderPath includes the account |
| // It's only accessed at acme/orders/<order_id> with the account context |
| // It's saved at acme/<account_id>/orders/<orderId> |
| entry, err := sc.Storage.Get(sc.Context, orderPath) |
| if err != nil { |
| return false, time.Time{}, fmt.Errorf("error loading order: %w", err) |
| } |
| if entry == nil { |
| return false, time.Time{}, fmt.Errorf("order does not exist: %w", ErrMalformed) |
| } |
| var order acmeOrder |
| err = entry.DecodeJSON(&order) |
| if err != nil { |
| return false, time.Time{}, fmt.Errorf("error decoding order: %w", err) |
| } |
| |
| // Determine whether we should tidy this order |
| shouldTidy := false |
| |
| // Track either the order expiry or certificate expiry to return to the caller, this |
| // can be used to influence the account's expiry |
| orderExpiry := order.CertificateExpiry |
| |
| // It is faster to check certificate information on the order entry rather than fetch the cert entry to parse: |
| if !order.CertificateExpiry.IsZero() { |
| // This implies that a certificate exists |
| // When a certificate exists, we want to expire and tidy the order when we tidy the certificate: |
| if time.Now().After(order.CertificateExpiry.Add(certTidyBuffer)) { // It's time to clean |
| shouldTidy = true |
| } |
| } else { |
| // This implies that no certificate exists |
| // In this case, we want to expire the order after it has expired (+ some safety buffer) |
| if time.Now().After(order.Expires) { |
| shouldTidy = true |
| } |
| orderExpiry = order.Expires |
| } |
| if shouldTidy == false { |
| return shouldTidy, orderExpiry, nil |
| } |
| |
| // Tidy this Order |
| // That includes any certificate acme/<account_id>/orders/orderPath/cert |
| // That also includes any related authorizations: acme/<account_id>/authorizations/<auth_id> |
| |
| // First Authorizations |
| for _, authorizationId := range order.AuthorizationIds { |
| err = sc.Storage.Delete(sc.Context, getAuthorizationPath(accountId, authorizationId)) |
| if err != nil { |
| return false, orderExpiry, err |
| } |
| } |
| |
| // Normal Tidy will Take Care of the Certificate, we need to clean up the certificate to account tracker though |
| err = sc.Storage.Delete(sc.Context, getAcmeSerialToAccountTrackerPath(accountId, order.CertificateSerialNumber)) |
| if err != nil { |
| return false, orderExpiry, err |
| } |
| |
| // And Finally, the order: |
| err = sc.Storage.Delete(sc.Context, orderPath) |
| if err != nil { |
| return false, orderExpiry, err |
| } |
| b.tidyStatusIncDelAcmeOrderCount() |
| |
| return true, orderExpiry, nil |
| } |