package api import ( "context" "crypto/x509" "encoding/base64" "encoding/json" "fmt" "net" "net/http" "strings" "time" "github.com/go-chi/chi" "go.step.sm/crypto/randutil" "github.com/smallstep/certificates/acme" "github.com/smallstep/certificates/api/render" "github.com/smallstep/certificates/authority/policy" "github.com/smallstep/certificates/authority/provisioner" ) // NewOrderRequest represents the body for a NewOrder request. type NewOrderRequest struct { Identifiers []acme.Identifier `json:"identifiers"` NotBefore time.Time `json:"notBefore,omitempty"` NotAfter time.Time `json:"notAfter,omitempty"` } // Validate validates a new-order request body. func (n *NewOrderRequest) Validate() error { if len(n.Identifiers) == 0 { return acme.NewError(acme.ErrorMalformedType, "identifiers list cannot be empty") } for _, id := range n.Identifiers { if !(id.Type == acme.DNS || id.Type == acme.IP) { return acme.NewError(acme.ErrorMalformedType, "identifier type unsupported: %s", id.Type) } if id.Type == acme.IP && net.ParseIP(id.Value) == nil { return acme.NewError(acme.ErrorMalformedType, "invalid IP address: %s", id.Value) } // TODO(hs): add some validations for DNS domains? // TODO(hs): combine the errors from this with allow/deny policy, like example error in https://datatracker.ietf.org/doc/html/rfc8555#section-6.7.1 } return nil } // FinalizeRequest captures the body for a Finalize order request. type FinalizeRequest struct { CSR string `json:"csr"` csr *x509.CertificateRequest } // Validate validates a finalize request body. func (f *FinalizeRequest) Validate() error { var err error csrBytes, err := base64.RawURLEncoding.DecodeString(f.CSR) if err != nil { return acme.WrapError(acme.ErrorMalformedType, err, "error base64url decoding csr") } f.csr, err = x509.ParseCertificateRequest(csrBytes) if err != nil { return acme.WrapError(acme.ErrorMalformedType, err, "unable to parse csr") } if err = f.csr.CheckSignature(); err != nil { return acme.WrapError(acme.ErrorMalformedType, err, "csr failed signature check") } return nil } var defaultOrderExpiry = time.Hour * 24 var defaultOrderBackdate = time.Minute // NewOrder ACME api for creating a new order. func (h *Handler) NewOrder(w http.ResponseWriter, r *http.Request) { ctx := r.Context() acc, err := accountFromContext(ctx) if err != nil { render.Error(w, err) return } prov, err := provisionerFromContext(ctx) if err != nil { render.Error(w, err) return } payload, err := payloadFromContext(ctx) if err != nil { render.Error(w, err) return } var nor NewOrderRequest if err := json.Unmarshal(payload.value, &nor); err != nil { render.Error(w, acme.WrapError(acme.ErrorMalformedType, err, "failed to unmarshal new-order request payload")) return } if err := nor.Validate(); err != nil { render.Error(w, err) return } // TODO(hs): gather all errors, so that we can build one response with subproblems; include the nor.Validate() // error here too, like in example? eak, err := h.db.GetExternalAccountKeyByAccountID(ctx, prov.GetID(), acc.ID) if err != nil { render.Error(w, acme.WrapErrorISE(err, "error retrieving external account binding key")) return } acmePolicy, err := newACMEPolicyEngine(eak) if err != nil { render.Error(w, acme.WrapErrorISE(err, "error creating ACME policy engine")) return } for _, identifier := range nor.Identifiers { // evalue the ACME account level policy if err = isIdentifierAllowed(acmePolicy, identifier); err != nil { render.Error(w, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized")) return } // evaluate the provisioner level policy orderIdentifier := provisioner.ACMEIdentifier{Type: provisioner.ACMEIdentifierType(identifier.Type), Value: identifier.Value} if err = prov.AuthorizeOrderIdentifier(ctx, orderIdentifier); err != nil { render.Error(w, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized")) return } // evaluate the authority level policy if err = h.ca.AreSANsAllowed(ctx, []string{identifier.Value}); err != nil { render.Error(w, acme.WrapError(acme.ErrorRejectedIdentifierType, err, "not authorized")) return } } now := clock.Now() // New order. o := &acme.Order{ AccountID: acc.ID, ProvisionerID: prov.GetID(), Status: acme.StatusPending, Identifiers: nor.Identifiers, ExpiresAt: now.Add(defaultOrderExpiry), AuthorizationIDs: make([]string, len(nor.Identifiers)), NotBefore: nor.NotBefore, NotAfter: nor.NotAfter, } for i, identifier := range o.Identifiers { az := &acme.Authorization{ AccountID: acc.ID, Identifier: identifier, ExpiresAt: o.ExpiresAt, Status: acme.StatusPending, } if err := h.newAuthorization(ctx, az); err != nil { render.Error(w, err) return } o.AuthorizationIDs[i] = az.ID } if o.NotBefore.IsZero() { o.NotBefore = now } if o.NotAfter.IsZero() { o.NotAfter = o.NotBefore.Add(prov.DefaultTLSCertDuration()) } // If request NotBefore was empty then backdate the order.NotBefore (now) // to avoid timing issues. if nor.NotBefore.IsZero() { o.NotBefore = o.NotBefore.Add(-defaultOrderBackdate) } if err := h.db.CreateOrder(ctx, o); err != nil { render.Error(w, acme.WrapErrorISE(err, "error creating order")) return } h.linker.LinkOrder(ctx, o) w.Header().Set("Location", h.linker.GetLink(ctx, OrderLinkType, o.ID)) render.JSONStatus(w, o, http.StatusCreated) } func isIdentifierAllowed(acmePolicy policy.X509Policy, identifier acme.Identifier) error { if acmePolicy == nil { return nil } allowed, err := acmePolicy.AreSANsAllowed([]string{identifier.Value}) if err != nil { return err } if !allowed { return fmt.Errorf("acme identifier '%s' not allowed", identifier.Value) } return nil } func newACMEPolicyEngine(eak *acme.ExternalAccountKey) (policy.X509Policy, error) { if eak == nil { return nil, nil } return policy.NewX509PolicyEngine(eak.Policy) } func (h *Handler) newAuthorization(ctx context.Context, az *acme.Authorization) error { if strings.HasPrefix(az.Identifier.Value, "*.") { az.Wildcard = true az.Identifier = acme.Identifier{ Value: strings.TrimPrefix(az.Identifier.Value, "*."), Type: az.Identifier.Type, } } chTypes := challengeTypes(az) var err error az.Token, err = randutil.Alphanumeric(32) if err != nil { return acme.WrapErrorISE(err, "error generating random alphanumeric ID") } az.Challenges = make([]*acme.Challenge, len(chTypes)) for i, typ := range chTypes { ch := &acme.Challenge{ AccountID: az.AccountID, Value: az.Identifier.Value, Type: typ, Token: az.Token, Status: acme.StatusPending, } if err := h.db.CreateChallenge(ctx, ch); err != nil { return acme.WrapErrorISE(err, "error creating challenge") } az.Challenges[i] = ch } if err = h.db.CreateAuthorization(ctx, az); err != nil { return acme.WrapErrorISE(err, "error creating authorization") } return nil } // GetOrder ACME api for retrieving an order. func (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) { ctx := r.Context() acc, err := accountFromContext(ctx) if err != nil { render.Error(w, err) return } prov, err := provisionerFromContext(ctx) if err != nil { render.Error(w, err) return } o, err := h.db.GetOrder(ctx, chi.URLParam(r, "ordID")) if err != nil { render.Error(w, acme.WrapErrorISE(err, "error retrieving order")) return } if acc.ID != o.AccountID { render.Error(w, acme.NewError(acme.ErrorUnauthorizedType, "account '%s' does not own order '%s'", acc.ID, o.ID)) return } if prov.GetID() != o.ProvisionerID { render.Error(w, acme.NewError(acme.ErrorUnauthorizedType, "provisioner '%s' does not own order '%s'", prov.GetID(), o.ID)) return } if err = o.UpdateStatus(ctx, h.db); err != nil { render.Error(w, acme.WrapErrorISE(err, "error updating order status")) return } h.linker.LinkOrder(ctx, o) w.Header().Set("Location", h.linker.GetLink(ctx, OrderLinkType, o.ID)) render.JSON(w, o) } // FinalizeOrder attemptst to finalize an order and create a certificate. func (h *Handler) FinalizeOrder(w http.ResponseWriter, r *http.Request) { ctx := r.Context() acc, err := accountFromContext(ctx) if err != nil { render.Error(w, err) return } prov, err := provisionerFromContext(ctx) if err != nil { render.Error(w, err) return } payload, err := payloadFromContext(ctx) if err != nil { render.Error(w, err) return } var fr FinalizeRequest if err := json.Unmarshal(payload.value, &fr); err != nil { render.Error(w, acme.WrapError(acme.ErrorMalformedType, err, "failed to unmarshal finalize-order request payload")) return } if err := fr.Validate(); err != nil { render.Error(w, err) return } o, err := h.db.GetOrder(ctx, chi.URLParam(r, "ordID")) if err != nil { render.Error(w, acme.WrapErrorISE(err, "error retrieving order")) return } if acc.ID != o.AccountID { render.Error(w, acme.NewError(acme.ErrorUnauthorizedType, "account '%s' does not own order '%s'", acc.ID, o.ID)) return } if prov.GetID() != o.ProvisionerID { render.Error(w, acme.NewError(acme.ErrorUnauthorizedType, "provisioner '%s' does not own order '%s'", prov.GetID(), o.ID)) return } if err = o.Finalize(ctx, h.db, fr.csr, h.ca, prov); err != nil { render.Error(w, acme.WrapErrorISE(err, "error finalizing order")) return } h.linker.LinkOrder(ctx, o) w.Header().Set("Location", h.linker.GetLink(ctx, OrderLinkType, o.ID)) render.JSON(w, o) } // challengeTypes determines the types of challenges that should be used // for the ACME authorization request. func challengeTypes(az *acme.Authorization) []acme.ChallengeType { var chTypes []acme.ChallengeType switch az.Identifier.Type { case acme.IP: chTypes = []acme.ChallengeType{acme.HTTP01, acme.TLSALPN01} case acme.DNS: chTypes = []acme.ChallengeType{acme.DNS01} // HTTP and TLS challenges can only be used for identifiers without wildcards. if !az.Wildcard { chTypes = append(chTypes, []acme.ChallengeType{acme.HTTP01, acme.TLSALPN01}...) } default: chTypes = []acme.ChallengeType{} } return chTypes }