2022-01-24 13:03:56 +00:00
package api
import (
"context"
"encoding/json"
"github.com/smallstep/certificates/acme"
"go.step.sm/crypto/jose"
)
// ExternalAccountBinding represents the ACME externalAccountBinding JWS
type ExternalAccountBinding struct {
Protected string ` json:"protected" `
Payload string ` json:"payload" `
Sig string ` json:"signature" `
}
// validateExternalAccountBinding validates the externalAccountBinding property in a call to new-account.
2022-04-27 22:42:26 +00:00
func validateExternalAccountBinding ( ctx context . Context , nar * NewAccountRequest ) ( * acme . ExternalAccountKey , error ) {
2022-01-24 13:03:56 +00:00
acmeProv , err := acmeProvisionerFromContext ( ctx )
if err != nil {
return nil , acme . WrapErrorISE ( err , "could not load ACME provisioner from context" )
}
if ! acmeProv . RequireEAB {
return nil , nil
}
if nar . ExternalAccountBinding == nil {
return nil , acme . NewError ( acme . ErrorExternalAccountRequiredType , "no external account binding provided" )
}
eabJSONBytes , err := json . Marshal ( nar . ExternalAccountBinding )
if err != nil {
return nil , acme . WrapErrorISE ( err , "error marshaling externalAccountBinding into bytes" )
}
eabJWS , err := jose . ParseJWS ( string ( eabJSONBytes ) )
if err != nil {
return nil , acme . WrapErrorISE ( err , "error parsing externalAccountBinding jws" )
}
// TODO(hs): implement strategy pattern to allow for different ways of verification (i.e. webhook call) based on configuration?
keyID , acmeErr := validateEABJWS ( ctx , eabJWS )
if acmeErr != nil {
return nil , acmeErr
}
2022-04-27 22:42:26 +00:00
db := acme . MustFromContext ( ctx )
externalAccountKey , err := db . GetExternalAccountKey ( ctx , acmeProv . ID , keyID )
2022-01-24 13:03:56 +00:00
if err != nil {
if _ , ok := err . ( * acme . Error ) ; ok {
return nil , acme . WrapError ( acme . ErrorUnauthorizedType , err , "the field 'kid' references an unknown key" )
}
return nil , acme . WrapErrorISE ( err , "error retrieving external account key" )
}
if externalAccountKey . AlreadyBound ( ) {
return nil , acme . NewError ( acme . ErrorUnauthorizedType , "external account binding key with id '%s' was already bound to account '%s' on %s" , keyID , externalAccountKey . AccountID , externalAccountKey . BoundAt )
}
payload , err := eabJWS . Verify ( externalAccountKey . KeyBytes )
if err != nil {
return nil , acme . WrapErrorISE ( err , "error verifying externalAccountBinding signature" )
}
jwk , err := jwkFromContext ( ctx )
if err != nil {
return nil , err
}
var payloadJWK * jose . JSONWebKey
if err = json . Unmarshal ( payload , & payloadJWK ) ; err != nil {
return nil , acme . WrapError ( acme . ErrorMalformedType , err , "error unmarshaling payload into jwk" )
}
if ! keysAreEqual ( jwk , payloadJWK ) {
return nil , acme . NewError ( acme . ErrorUnauthorizedType , "keys in jws and eab payload do not match" )
}
return externalAccountKey , nil
}
// keysAreEqual performs an equality check on two JWKs by comparing
// the (base64 encoding) of the Key IDs.
func keysAreEqual ( x , y * jose . JSONWebKey ) bool {
if x == nil || y == nil {
return false
}
digestX , errX := acme . KeyToID ( x )
digestY , errY := acme . KeyToID ( y )
if errX != nil || errY != nil {
return false
}
return digestX == digestY
}
// validateEABJWS verifies the contents of the External Account Binding JWS.
// The protected header of the JWS MUST meet the following criteria:
// o The "alg" field MUST indicate a MAC-based algorithm
// o The "kid" field MUST contain the key identifier provided by the CA
// o The "nonce" field MUST NOT be present
// o The "url" field MUST be set to the same value as the outer JWS
func validateEABJWS ( ctx context . Context , jws * jose . JSONWebSignature ) ( string , * acme . Error ) {
if jws == nil {
return "" , acme . NewErrorISE ( "no JWS provided" )
}
if len ( jws . Signatures ) != 1 {
return "" , acme . NewError ( acme . ErrorMalformedType , "JWS must have one signature" )
}
header := jws . Signatures [ 0 ] . Protected
algorithm := header . Algorithm
keyID := header . KeyID
nonce := header . Nonce
if ! ( algorithm == jose . HS256 || algorithm == jose . HS384 || algorithm == jose . HS512 ) {
return "" , acme . NewError ( acme . ErrorMalformedType , "'alg' field set to invalid algorithm '%s'" , algorithm )
}
if keyID == "" {
return "" , acme . NewError ( acme . ErrorMalformedType , "'kid' field is required" )
}
if nonce != "" {
return "" , acme . NewError ( acme . ErrorMalformedType , "'nonce' must not be present" )
}
jwsURL , ok := header . ExtraHeaders [ "url" ]
if ! ok {
return "" , acme . NewError ( acme . ErrorMalformedType , "'url' field is required" )
}
outerJWS , err := jwsFromContext ( ctx )
if err != nil {
return "" , acme . WrapErrorISE ( err , "could not retrieve outer JWS from context" )
}
if len ( outerJWS . Signatures ) != 1 {
return "" , acme . NewError ( acme . ErrorMalformedType , "outer JWS must have one signature" )
}
outerJWSURL , ok := outerJWS . Signatures [ 0 ] . Protected . ExtraHeaders [ "url" ]
if ! ok {
return "" , acme . NewError ( acme . ErrorMalformedType , "'url' field must be set in outer JWS" )
}
if jwsURL != outerJWSURL {
return "" , acme . NewError ( acme . ErrorMalformedType , "'url' field is not the same value as the outer JWS" )
}
return keyID , nil
}