2023-04-14 18:12:31 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
2023-04-14 14:54:48 +00:00
|
|
|
package pki
|
|
|
|
|
|
|
|
import (
|
2023-04-21 13:38:06 +00:00
|
|
|
"crypto/x509"
|
|
|
|
"encoding/base64"
|
|
|
|
"encoding/pem"
|
2023-04-14 14:54:48 +00:00
|
|
|
"fmt"
|
2023-04-21 13:38:06 +00:00
|
|
|
"net"
|
2023-04-14 14:54:48 +00:00
|
|
|
"net/http"
|
2023-04-21 13:38:06 +00:00
|
|
|
"sort"
|
2023-04-14 14:54:48 +00:00
|
|
|
"time"
|
|
|
|
|
2023-04-21 13:38:06 +00:00
|
|
|
"github.com/hashicorp/vault/sdk/helper/strutil"
|
|
|
|
|
|
|
|
"github.com/hashicorp/vault/sdk/helper/certutil"
|
|
|
|
|
2023-04-14 14:54:48 +00:00
|
|
|
"github.com/hashicorp/vault/sdk/framework"
|
|
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
|
|
"golang.org/x/net/idna"
|
|
|
|
)
|
|
|
|
|
2023-04-14 18:48:33 +00:00
|
|
|
func pathAcmeListOrders(b *backend) []*framework.Path {
|
|
|
|
return buildAcmeFrameworkPaths(b, patternAcmeListOrders, "/orders")
|
2023-04-14 14:54:48 +00:00
|
|
|
}
|
|
|
|
|
2023-04-14 18:48:33 +00:00
|
|
|
func pathAcmeGetOrder(b *backend) []*framework.Path {
|
|
|
|
return buildAcmeFrameworkPaths(b, patternAcmeGetOrder, "/order/"+uuidNameRegex("order_id"))
|
2023-04-14 14:54:48 +00:00
|
|
|
}
|
|
|
|
|
2023-04-14 18:48:33 +00:00
|
|
|
func pathAcmeNewOrder(b *backend) []*framework.Path {
|
|
|
|
return buildAcmeFrameworkPaths(b, patternAcmeNewOrder, "/new-order")
|
2023-04-14 14:54:48 +00:00
|
|
|
}
|
|
|
|
|
2023-04-21 13:38:06 +00:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
2023-04-14 14:54:48 +00:00
|
|
|
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)
|
2023-04-21 13:38:06 +00:00
|
|
|
addFieldsForACMEOrder(fields)
|
2023-04-14 14:54:48 +00:00
|
|
|
|
|
|
|
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: "",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-21 13:38:06 +00:00
|
|
|
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) {
|
2023-04-21 16:54:19 +00:00
|
|
|
return fmt.Errorf("%w: Order (%v) and CSR (%v) mismatch on number of DNS identifiers", ErrBadCSR, len(orderDNSIdentifiers), len(csrDNSIdentifiers))
|
2023-04-21 13:38:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if len(orderIPIdentifiers) != len(csrIPIdentifiers) {
|
2023-04-21 16:54:19 +00:00
|
|
|
return fmt.Errorf("%w: Order (%v) and CSR (%v) mismatch on number of IP identifiers", ErrBadCSR, len(orderIPIdentifiers), len(csrIPIdentifiers))
|
2023-04-21 13:38:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
|
|
|
pemBlock := &pem.Block{
|
|
|
|
Type: "CERTIFICATE REQUEST",
|
|
|
|
Headers: nil,
|
|
|
|
Bytes: csr.Raw,
|
|
|
|
}
|
|
|
|
pemCsr := string(pem.EncodeToMemory(pemBlock))
|
|
|
|
|
2023-04-25 13:29:07 +00:00
|
|
|
data := &framework.FieldData{
|
|
|
|
Raw: map[string]interface{}{
|
|
|
|
"csr": pemCsr,
|
|
|
|
},
|
|
|
|
Schema: getCsrSignVerbatimSchemaFields(),
|
|
|
|
}
|
|
|
|
|
|
|
|
signingBundle, issuerId, err := ac.sc.fetchCAInfoWithIssuer(ac.issuer.ID.String(), IssuanceUsage)
|
2023-04-21 13:38:06 +00:00
|
|
|
if err != nil {
|
2023-04-25 13:29:07 +00:00
|
|
|
return nil, "", fmt.Errorf("failed loading CA %s: %w", ac.issuer.ID.String(), err)
|
2023-04-21 13:38:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
input := &inputBundle{
|
2023-04-25 13:29:07 +00:00
|
|
|
req: &logical.Request{},
|
|
|
|
apiData: data,
|
|
|
|
role: ac.role,
|
2023-04-21 13:38:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2023-04-14 14:54:48 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-04-21 13:38:06 +00:00
|
|
|
func (b *backend) acmeNewOrderHandler(ac *acmeContext, _ *logical.Request, _ *framework.FieldData, _ *jwsCtx, data map[string]interface{}, account *acmeAccount) (*logical.Response, error) {
|
2023-04-14 14:54:48 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-04-21 13:38:06 +00:00
|
|
|
if !notBefore.IsZero() || !notAfter.IsZero() {
|
|
|
|
return nil, fmt.Errorf("%w: NotBefore and NotAfter are not supported", ErrMalformed)
|
|
|
|
}
|
|
|
|
|
2023-04-14 14:54:48 +00:00
|
|
|
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 {
|
2023-04-21 13:38:06 +00:00
|
|
|
authz, err := generateAuthorization(account, identifier)
|
2023-04-17 19:23:04 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error generating authorizations: %w", err)
|
|
|
|
}
|
2023-04-14 14:54:48 +00:00
|
|
|
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,
|
2023-04-21 13:38:06 +00:00
|
|
|
Expires: time.Now().Add(24 * time.Hour), // TODO: Readjust this based on authz and/or config
|
2023-04-14 14:54:48 +00:00
|
|
|
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 {
|
2023-04-17 17:52:54 +00:00
|
|
|
authorizationUrls = append(authorizationUrls, buildAuthorizationUrl(acmeCtx, authId))
|
2023-04-14 14:54:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
resp := &logical.Response{
|
|
|
|
Data: map[string]interface{}{
|
2023-04-21 13:38:06 +00:00
|
|
|
"status": order.Status,
|
|
|
|
"expires": order.Expires.Format(time.RFC3339),
|
2023-04-14 14:54:48 +00:00
|
|
|
"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
|
|
|
|
}
|
|
|
|
|
2023-04-17 17:52:54 +00:00
|
|
|
func buildAuthorizationUrl(acmeCtx *acmeContext, authId string) string {
|
|
|
|
return acmeCtx.baseUrl.JoinPath("authorization", authId).String()
|
|
|
|
}
|
|
|
|
|
2023-04-14 14:54:48 +00:00
|
|
|
func buildOrderUrl(acmeCtx *acmeContext, orderId string) string {
|
2023-04-17 17:52:54 +00:00
|
|
|
return acmeCtx.baseUrl.JoinPath("order", orderId).String()
|
2023-04-14 14:54:48 +00:00
|
|
|
}
|
|
|
|
|
2023-04-21 13:38:06 +00:00
|
|
|
func generateAuthorization(acct *acmeAccount, identifier *ACMEIdentifier) (*ACMEAuthorization, error) {
|
2023-04-17 17:52:54 +00:00
|
|
|
authId := genUuid()
|
2023-04-21 16:54:19 +00:00
|
|
|
|
|
|
|
// 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}
|
|
|
|
}
|
|
|
|
|
2023-04-17 19:23:04 +00:00
|
|
|
var challenges []*ACMEChallenge
|
2023-04-21 16:54:19 +00:00
|
|
|
for _, challengeType := range allowedChallenges {
|
2023-04-17 19:23:04 +00:00
|
|
|
token, err := getACMEToken()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-04-17 17:52:54 +00:00
|
|
|
|
2023-04-17 19:23:04 +00:00
|
|
|
challenge := &ACMEChallenge{
|
|
|
|
Type: challengeType,
|
|
|
|
Status: ACMEChallengePending,
|
|
|
|
ChallengeFields: map[string]interface{}{
|
|
|
|
"token": token,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
challenges = append(challenges, challenge)
|
2023-04-14 14:54:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return &ACMEAuthorization{
|
2023-04-17 17:52:54 +00:00
|
|
|
Id: authId,
|
2023-04-14 14:54:48 +00:00
|
|
|
AccountId: acct.KeyId,
|
|
|
|
Identifier: identifier,
|
|
|
|
Status: ACMEAuthorizationPending,
|
|
|
|
Expires: "", // only populated when it switches to valid.
|
|
|
|
Challenges: challenges,
|
2023-04-21 16:54:19 +00:00
|
|
|
Wildcard: identifier.IsWildcard,
|
2023-04-17 19:23:04 +00:00
|
|
|
}, nil
|
2023-04-14 14:54:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2023-04-21 16:54:19 +00:00
|
|
|
identifier := &ACMEIdentifier{
|
|
|
|
Value: valueStr,
|
|
|
|
OriginalValue: valueStr,
|
|
|
|
}
|
|
|
|
|
2023-04-21 13:38:06 +00:00
|
|
|
switch typeStr {
|
|
|
|
case string(ACMEIPIdentifier):
|
2023-04-21 16:54:19 +00:00
|
|
|
identifier.Type = ACMEIPIdentifier
|
2023-04-21 13:38:06 +00:00
|
|
|
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):
|
2023-04-21 16:54:19 +00:00
|
|
|
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.
|
2023-04-21 13:38:06 +00:00
|
|
|
p := idna.New(idna.ValidateForRegistration())
|
2023-04-21 16:54:19 +00:00
|
|
|
converted, err := p.ToASCII(identifier.Value)
|
2023-04-21 13:38:06 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("value argument (%s) failed validation: %s: %w", valueStr, err.Error(), ErrMalformed)
|
|
|
|
}
|
|
|
|
|
2023-04-21 16:54:19 +00:00
|
|
|
// 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)
|
|
|
|
}
|
2023-04-21 13:38:06 +00:00
|
|
|
default:
|
|
|
|
return nil, fmt.Errorf("unsupported identifier type %s: %w", typeStr, ErrUnsupportedIdentifier)
|
|
|
|
}
|
2023-04-21 16:54:19 +00:00
|
|
|
|
|
|
|
identifiers = append(identifiers, identifier)
|
2023-04-14 14:54:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return identifiers, nil
|
|
|
|
}
|