2023-04-14 18:12:31 +00:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package pki
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
2023-04-26 17:21:00 +00:00
"os"
"strconv"
2023-04-14 18:12:31 +00:00
"strings"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
type acmeContext struct {
2023-04-17 17:52:54 +00:00
// baseUrl is the combination of the configured cluster local URL and the acmePath up to /acme/
2023-04-25 20:47:52 +00:00
baseUrl * url . URL
clusterUrl * url . URL
sc * storageContext
role * roleEntry
issuer * issuerEntry
2023-04-26 16:47:31 +00:00
// acmeDirectory is a string that can distinguish the various acme directories we have configured
// if something needs to remain locked into a directory path structure.
acmeDirectory string
2023-04-14 18:12:31 +00:00
}
type (
acmeOperation func ( acmeCtx * acmeContext , r * logical . Request , _ * framework . FieldData ) ( * logical . Response , error )
acmeParsedOperation func ( acmeCtx * acmeContext , r * logical . Request , fields * framework . FieldData , userCtx * jwsCtx , data map [ string ] interface { } ) ( * logical . Response , error )
acmeAccountRequiredOperation func ( acmeCtx * acmeContext , r * logical . Request , fields * framework . FieldData , userCtx * jwsCtx , data map [ string ] interface { } , acct * acmeAccount ) ( * logical . Response , error )
)
2023-04-26 16:47:31 +00:00
// acmeErrorWrapper the lowest level wrapper that will translate errors into proper ACME error responses
2023-04-14 18:12:31 +00:00
func acmeErrorWrapper ( op framework . OperationFunc ) framework . OperationFunc {
return func ( ctx context . Context , r * logical . Request , data * framework . FieldData ) ( * logical . Response , error ) {
resp , err := op ( ctx , r , data )
if err != nil {
return TranslateError ( err )
}
return resp , nil
}
}
2023-04-26 16:47:31 +00:00
// acmeWrapper a basic wrapper that all ACME handlers should leverage as the basis.
// This will create a basic ACME context, validate basic ACME configuration is setup
// for operations. This pulls in acmeErrorWrapper to translate error messages for users,
// but does not enforce any sort of ACME authentication.
2023-04-14 18:12:31 +00:00
func ( b * backend ) acmeWrapper ( op acmeOperation ) framework . OperationFunc {
return acmeErrorWrapper ( func ( ctx context . Context , r * logical . Request , data * framework . FieldData ) ( * logical . Response , error ) {
sc := b . makeStorageContext ( ctx , r . Storage )
2023-04-27 20:31:13 +00:00
config , err := sc . Backend . acmeState . getConfigWithUpdate ( sc )
if err != nil {
return nil , fmt . Errorf ( "failed to fetch ACME configuration: %w" , err )
}
if isAcmeDisabled ( sc , config ) {
2023-04-26 17:21:00 +00:00
return nil , ErrAcmeDisabled
2023-04-14 18:12:31 +00:00
}
2023-04-25 13:29:07 +00:00
if b . useLegacyBundleCaStorage ( ) {
return nil , fmt . Errorf ( "%w: Can not perform ACME operations until migration has completed" , ErrServerInternal )
}
2023-04-25 20:47:52 +00:00
acmeBaseUrl , clusterBase , err := getAcmeBaseUrl ( sc , r . Path )
2023-04-14 18:12:31 +00:00
if err != nil {
return nil , err
}
2023-04-27 20:31:13 +00:00
role , issuer , err := getAcmeRoleAndIssuer ( sc , data , config )
2023-04-25 13:29:07 +00:00
if err != nil {
return nil , err
}
2023-04-26 16:47:31 +00:00
acmeDirectory := getAcmeDirectory ( data )
2023-04-14 18:12:31 +00:00
acmeCtx := & acmeContext {
2023-04-26 16:47:31 +00:00
baseUrl : acmeBaseUrl ,
clusterUrl : clusterBase ,
sc : sc ,
role : role ,
issuer : issuer ,
acmeDirectory : acmeDirectory ,
2023-04-14 18:12:31 +00:00
}
return op ( acmeCtx , r , data )
} )
}
2023-04-26 16:47:31 +00:00
// acmeParsedWrapper is an ACME wrapper that will parse out the ACME request parameters, validate
// that we have a proper signature and pass to the operation a decoded map of arguments received.
// This wrapper builds on top of acmeWrapper. Note that this does perform signature verification
// it does not enforce the account being in a valid state nor existing.
2023-04-14 18:12:31 +00:00
func ( b * backend ) acmeParsedWrapper ( op acmeParsedOperation ) framework . OperationFunc {
return b . acmeWrapper ( func ( acmeCtx * acmeContext , r * logical . Request , fields * framework . FieldData ) ( * logical . Response , error ) {
2023-04-25 20:47:52 +00:00
user , data , err := b . acmeState . ParseRequestParams ( acmeCtx , r , fields )
2023-04-14 18:12:31 +00:00
if err != nil {
return nil , err
}
resp , err := op ( acmeCtx , r , fields , user , data )
// Our response handlers might not add the necessary headers.
if resp != nil {
if resp . Headers == nil {
resp . Headers = map [ string ] [ ] string { }
}
if _ , ok := resp . Headers [ "Replay-Nonce" ] ; ! ok {
nonce , _ , err := b . acmeState . GetNonce ( )
if err != nil {
return nil , err
}
resp . Headers [ "Replay-Nonce" ] = [ ] string { nonce }
}
if _ , ok := resp . Headers [ "Link" ] ; ! ok {
resp . Headers [ "Link" ] = genAcmeLinkHeader ( acmeCtx )
} else {
directory := genAcmeLinkHeader ( acmeCtx ) [ 0 ]
addDirectory := true
for _ , item := range resp . Headers [ "Link" ] {
if item == directory {
addDirectory = false
break
}
}
if addDirectory {
resp . Headers [ "Link" ] = append ( resp . Headers [ "Link" ] , directory )
}
}
// ACME responses don't understand Vault's default encoding
// format. Rather than expecting everything to handle creating
// ACME-formatted responses, do the marshaling in one place.
if _ , ok := resp . Data [ logical . HTTPRawBody ] ; ! ok {
ignored_values := map [ string ] bool { logical . HTTPContentType : true , logical . HTTPStatusCode : true }
fields := map [ string ] interface { } { }
body := map [ string ] interface { } {
logical . HTTPContentType : "application/json" ,
logical . HTTPStatusCode : http . StatusOK ,
}
for key , value := range resp . Data {
if _ , present := ignored_values [ key ] ; ! present {
fields [ key ] = value
} else {
body [ key ] = value
}
}
rawBody , err := json . Marshal ( fields )
if err != nil {
return nil , fmt . Errorf ( "Error marshaling JSON body: %w" , err )
}
body [ logical . HTTPRawBody ] = rawBody
resp . Data = body
}
}
return resp , err
} )
}
2023-04-26 16:47:31 +00:00
// acmeAccountRequiredWrapper builds on top of acmeParsedWrapper, enforcing the
// request has a proper signature for an existing account, and that account is
// in a valid status. It passes to the operation a decoded form of the request
// parameters as well as the ACME account the request is for.
2023-04-14 18:12:31 +00:00
func ( b * backend ) acmeAccountRequiredWrapper ( op acmeAccountRequiredOperation ) framework . OperationFunc {
return b . acmeParsedWrapper ( func ( acmeCtx * acmeContext , r * logical . Request , fields * framework . FieldData , uc * jwsCtx , data map [ string ] interface { } ) ( * logical . Response , error ) {
if ! uc . Existing {
return nil , fmt . Errorf ( "cannot process request without a 'kid': %w" , ErrMalformed )
}
account , err := b . acmeState . LoadAccount ( acmeCtx , uc . Kid )
if err != nil {
return nil , fmt . Errorf ( "error loading account: %w" , err )
}
if account . Status != StatusValid {
// Treating "revoked" and "deactivated" as the same here.
return nil , fmt . Errorf ( "%w: account in status: %s" , ErrUnauthorized , account . Status )
}
return op ( acmeCtx , r , fields , uc , data , account )
} )
}
2023-04-14 18:48:33 +00:00
// A helper function that will build up the various path patterns we want for ACME APIs.
func buildAcmeFrameworkPaths ( b * backend , patternFunc func ( b * backend , pattern string ) * framework . Path , acmeApi string ) [ ] * framework . Path {
var patterns [ ] * framework . Path
for _ , baseUrl := range [ ] string {
"acme" ,
"roles/" + framework . GenericNameRegex ( "role" ) + "/acme" ,
"issuer/" + framework . GenericNameRegex ( issuerRefParam ) + "/acme" ,
"issuer/" + framework . GenericNameRegex ( issuerRefParam ) + "/roles/" + framework . GenericNameRegex ( "role" ) + "/acme" ,
} {
if ! strings . HasPrefix ( acmeApi , "/" ) {
acmeApi = "/" + acmeApi
}
path := patternFunc ( b , baseUrl + acmeApi )
patterns = append ( patterns , path )
}
return patterns
}
2023-04-25 20:47:52 +00:00
func getAcmeBaseUrl ( sc * storageContext , path string ) ( * url . URL , * url . URL , error ) {
2023-04-14 18:12:31 +00:00
cfg , err := sc . getClusterConfig ( )
if err != nil {
2023-04-25 20:47:52 +00:00
return nil , nil , fmt . Errorf ( "failed loading cluster config: %w" , err )
2023-04-14 18:12:31 +00:00
}
if cfg . Path == "" {
2023-04-25 20:47:52 +00:00
return nil , nil , fmt . Errorf ( "ACME feature requires local cluster path configuration to be set: %w" , ErrServerInternal )
2023-04-14 18:12:31 +00:00
}
baseUrl , err := url . Parse ( cfg . Path )
if err != nil {
2023-04-25 20:47:52 +00:00
return nil , nil , fmt . Errorf ( "ACME feature a proper URL configured in local cluster path: %w" , ErrServerInternal )
2023-04-14 18:12:31 +00:00
}
directoryPrefix := ""
lastIndex := strings . LastIndex ( path , "/acme/" )
if lastIndex != - 1 {
directoryPrefix = path [ 0 : lastIndex ]
}
2023-04-25 20:47:52 +00:00
return baseUrl . JoinPath ( directoryPrefix , "/acme/" ) , baseUrl , nil
2023-04-14 18:12:31 +00:00
}
2023-04-26 16:47:31 +00:00
func getAcmeIssuer ( sc * storageContext , issuerName string ) ( * issuerEntry , error ) {
if issuerName == "" {
issuerName = defaultRef
}
issuerId , err := sc . resolveIssuerReference ( issuerName )
if err != nil {
return nil , fmt . Errorf ( "%w: issuer does not exist" , ErrMalformed )
}
issuer , err := sc . fetchIssuerById ( issuerId )
if err != nil {
return nil , fmt . Errorf ( "issuer failed to load: %w" , err )
}
if issuer . Usage . HasUsage ( IssuanceUsage ) && len ( issuer . KeyID ) > 0 {
return issuer , nil
}
return nil , fmt . Errorf ( "%w: issuer missing proper issuance usage or key" , ErrServerInternal )
}
func getAcmeDirectory ( data * framework . FieldData ) string {
requestedIssuer := getRequestedAcmeIssuerFromPath ( data )
requestedRole := getRequestedAcmeRoleFromPath ( data )
return fmt . Sprintf ( "issuer-%s::role-%s" , requestedIssuer , requestedRole )
}
2023-04-27 20:31:13 +00:00
func getAcmeRoleAndIssuer ( sc * storageContext , data * framework . FieldData , config * acmeConfigEntry ) ( * roleEntry , * issuerEntry , error ) {
2023-04-26 16:47:31 +00:00
requestedIssuer := getRequestedAcmeIssuerFromPath ( data )
requestedRole := getRequestedAcmeRoleFromPath ( data )
issuerToLoad := requestedIssuer
2023-04-27 20:31:13 +00:00
var wasVerbatim bool
2023-04-26 16:47:31 +00:00
var role * roleEntry
2023-04-27 20:31:13 +00:00
if len ( requestedRole ) > 0 || len ( config . DefaultRole ) > 0 {
if len ( requestedRole ) == 0 {
requestedRole = config . DefaultRole
}
2023-04-26 16:47:31 +00:00
var err error
role , err = sc . Backend . getRole ( sc . Context , sc . Storage , requestedRole )
if err != nil {
return nil , nil , fmt . Errorf ( "%w: err loading role" , ErrServerInternal )
}
if role == nil {
return nil , nil , fmt . Errorf ( "%w: role does not exist" , ErrMalformed )
}
if role . NoStore {
return nil , nil , fmt . Errorf ( "%w: role can not be used as NoStore is set to true" , ErrServerInternal )
}
// If we haven't loaded an issuer directly from our path and the specified
// role does specify an issuer prefer the role's issuer rather than the default issuer.
if len ( role . Issuer ) > 0 && len ( requestedIssuer ) == 0 {
issuerToLoad = role . Issuer
}
} else {
role = buildSignVerbatimRoleWithNoData ( & roleEntry {
Issuer : requestedIssuer ,
NoStore : false ,
Name : requestedRole ,
} )
2023-04-27 20:31:13 +00:00
wasVerbatim = true
}
allowAnyRole := len ( config . AllowedRoles ) == 1 && config . AllowedRoles [ 0 ] == "*"
if ! allowAnyRole {
if wasVerbatim {
return nil , nil , fmt . Errorf ( "%w: using the default directory without specifying a role is not supported by this configuration; specify 'default_role' in the acme config to the default directories" , ErrServerInternal )
}
var foundRole bool
for _ , name := range config . AllowedRoles {
if name == role . Name {
foundRole = true
break
}
}
if ! foundRole {
return nil , nil , fmt . Errorf ( "%w: specified role not allowed by ACME policy" , ErrServerInternal )
}
2023-04-26 16:47:31 +00:00
}
issuer , err := getAcmeIssuer ( sc , issuerToLoad )
if err != nil {
return nil , nil , err
}
2023-04-27 20:31:13 +00:00
allowAnyIssuer := len ( config . AllowedIssuers ) == 1 && config . AllowedIssuers [ 0 ] == "*"
if ! allowAnyIssuer {
var foundIssuer bool
for index , name := range config . AllowedIssuers {
candidateId , err := sc . resolveIssuerReference ( name )
if err != nil {
return nil , nil , fmt . Errorf ( "failed to resolve reference for allowed_issuer entry %d: %w" , index , err )
}
if candidateId == issuer . ID {
foundIssuer = true
break
}
}
if ! foundIssuer {
return nil , nil , fmt . Errorf ( "%w: specified issuer not allowed by ACME policy" , ErrServerInternal )
}
}
2023-04-26 16:47:31 +00:00
return role , issuer , nil
}
func getRequestedAcmeRoleFromPath ( data * framework . FieldData ) string {
requestedRole := ""
roleNameRaw , present := data . GetOk ( "role" )
if present {
requestedRole = roleNameRaw . ( string )
}
return requestedRole
}
func getRequestedAcmeIssuerFromPath ( data * framework . FieldData ) string {
requestedIssuer := ""
requestedIssuerRaw , present := data . GetOk ( issuerRefParam )
if present {
requestedIssuer = requestedIssuerRaw . ( string )
}
return requestedIssuer
}
2023-04-26 17:21:00 +00:00
2023-04-27 20:31:13 +00:00
func isAcmeDisabled ( sc * storageContext , config * acmeConfigEntry ) bool {
if ! config . Enabled {
return true
}
2023-04-26 17:21:00 +00:00
if disableAcmeRaw := os . Getenv ( "VAULT_DISABLE_PUBLIC_ACME" ) ; disableAcmeRaw != "" {
disableAcme , err := strconv . ParseBool ( disableAcmeRaw )
if err != nil {
sc . Backend . Logger ( ) . Warn ( "could not parse env var VAULT_DISABLE_PUBLIC_ACME" , "error" , err )
disableAcme = false
}
// The OS environment if true will override any configuration option.
if disableAcme {
2023-04-27 20:31:13 +00:00
// TODO: If EAB is enforced in the configuration, don't mark
// ACME as disabled.
2023-04-26 17:21:00 +00:00
return true
}
}
return false
}