open-vault/builtin/logical/pki/path_acme_order.go
Alexander Scheel 3c8c46e172
Better ACME wildcard validation (#20289)
* 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>
2023-04-21 12:54:19 -04:00

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
}