mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-17 12:07:02 +02:00
* Allow CSRs with basic constraint extension with IsCA=false - We previously forbid any CSR with a basic constraint extension within the CSR. - It was discovered that some ACME clients (Proxmox ACME client) do send us this extension with a value of IsCA to false. - So allow the extension to be set within the ACME CSR with a value of IsCA set to false - Add a test for both the IsCA=true and IsCA=false use-cases and make sure we don't actually set the extension back within the generated certificate. * PR feedback - Move basic constraint function to sdk, increase test coverage - Error out on extra characters being returned from the asn1 unmarshalling. * make fmt
1085 lines
34 KiB
Go
1085 lines
34 KiB
Go
// 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.
|
|
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, 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(ac *acmeContext, 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 := ac.sc.Storage.Get(ac.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 = ac.sc.Storage.Delete(ac.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 = ac.sc.Storage.Delete(ac.sc.Context, getAcmeSerialToAccountTrackerPath(accountId, order.CertificateSerialNumber))
|
|
if err != nil {
|
|
return false, orderExpiry, err
|
|
}
|
|
|
|
// And Finally, the order:
|
|
err = ac.sc.Storage.Delete(ac.sc.Context, orderPath)
|
|
if err != nil {
|
|
return false, orderExpiry, err
|
|
}
|
|
b.tidyStatusIncDelAcmeOrderCount()
|
|
|
|
return true, orderExpiry, nil
|
|
}
|