3c8c46e172
* Refactor wildcard validation checks Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add helper to determine if identifier is wildcard Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Better validate wildcard acceptance This correctly validates wildcards to contain only a leading prefix (*.) and must include another label, also ensuring that the remaining domain components pass non-IP and IDNA validation, and removing them from display on the authorization. Finally, we restrict the challenge types available for various combinations of IP, DNS, and wildcard addresses. This then updates the tests to validate this as well. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> --------- Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
870 lines
27 KiB
Go
870 lines
27 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"
|
|
"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"
|
|
)
|
|
|
|
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: "",
|
|
HelpDescription: "",
|
|
}
|
|
}
|
|
|
|
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: "",
|
|
HelpDescription: "",
|
|
}
|
|
}
|
|
|
|
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: "",
|
|
HelpDescription: "",
|
|
}
|
|
}
|
|
|
|
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: "",
|
|
HelpDescription: "",
|
|
}
|
|
}
|
|
|
|
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: "",
|
|
HelpDescription: "",
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
if order.Status == ACMEOrderPending {
|
|
// Lets see if we can update our order status to ready if all the authorizations have been completed.
|
|
if requiredAuthorizationsCompleted(b, ac, uc, order) {
|
|
order.Status = ACMEOrderReady
|
|
}
|
|
}
|
|
|
|
if order.Status != ACMEOrderReady {
|
|
return nil, fmt.Errorf("%w: order is status %s, needs to be in ready state", ErrOrderNotReady, order.Status)
|
|
}
|
|
|
|
if !order.Expires.IsZero() && time.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
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
return formatOrderResponse(ac, order), nil
|
|
}
|
|
|
|
func requiredAuthorizationsCompleted(b *backend, ac *acmeContext, uc *jwsCtx, order *acmeOrder) bool {
|
|
if len(order.AuthorizationIds) == 0 {
|
|
return false
|
|
}
|
|
|
|
for _, authId := range order.AuthorizationIds {
|
|
authorization, err := b.acmeState.LoadAuthorization(ac, uc, authId)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
if authorization.Status != ACMEAuthorizationValid {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
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 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 issueCertFromCsr(ac *acmeContext, csr *x509.CertificateRequest) (*certutil.ParsedCertBundle, issuerID, error) {
|
|
entry := &roleEntry{
|
|
AllowLocalhost: true,
|
|
AllowAnyName: true,
|
|
AllowIPSANs: true,
|
|
AllowWildcardCertificates: new(bool),
|
|
EnforceHostnames: false,
|
|
KeyType: "any",
|
|
UseCSRCommonName: true,
|
|
UseCSRSANs: true,
|
|
AllowedOtherSANs: []string{"*"},
|
|
AllowedSerialNumbers: []string{"*"},
|
|
AllowedURISANs: []string{"*"},
|
|
AllowedUserIDs: []string{"*"},
|
|
CNValidations: []string{"disabled"},
|
|
GenerateLease: new(bool),
|
|
KeyUsage: []string{},
|
|
ExtKeyUsage: []string{},
|
|
ExtKeyUsageOIDs: []string{},
|
|
SignatureBits: 0,
|
|
UsePSS: false,
|
|
Issuer: defaultRef,
|
|
}
|
|
*entry.AllowWildcardCertificates = true
|
|
*entry.GenerateLease = false
|
|
|
|
pemBlock := &pem.Block{
|
|
Type: "CERTIFICATE REQUEST",
|
|
Headers: nil,
|
|
Bytes: csr.Raw,
|
|
}
|
|
pemCsr := string(pem.EncodeToMemory(pemBlock))
|
|
|
|
signingBundle, issuerId, err := ac.sc.fetchCAInfoWithIssuer(entry.Issuer, IssuanceUsage)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("failed loading CA %s: %w", entry.Issuer, err)
|
|
}
|
|
|
|
input := &inputBundle{
|
|
req: &logical.Request{},
|
|
apiData: &framework.FieldData{
|
|
Raw: map[string]interface{}{
|
|
"csr": pemCsr,
|
|
"ttl": "1h",
|
|
},
|
|
Schema: map[string]*framework.FieldSchema{
|
|
"csr": {Type: framework.TypeString},
|
|
"serial_number": {Type: framework.TypeString},
|
|
"exclude_cn_from_sans": {Type: framework.TypeBool},
|
|
"other_sans": {Type: framework.TypeCommaStringSlice},
|
|
"ttl": {Type: framework.TypeDurationSecond},
|
|
},
|
|
},
|
|
role: entry,
|
|
}
|
|
|
|
if csr.PublicKeyAlgorithm == x509.UnknownPublicKeyAlgorithm || csr.PublicKey == nil {
|
|
return nil, "", fmt.Errorf("%w: Refusing to sign CSR with empty PublicKey", ErrBadCSR)
|
|
}
|
|
|
|
parsedBundle, _, err := signCert(ac.sc.Backend, input, signingBundle, false, true)
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// TODO: Implement checks against role here.
|
|
|
|
// 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))
|
|
}
|
|
|
|
resp := &logical.Response{
|
|
Data: map[string]interface{}{
|
|
"status": order.Status,
|
|
"expires": order.Expires.Format(time.RFC3339),
|
|
"identifiers": order.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}
|
|
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
|
|
}
|