2019-05-27 00:41:10 +00:00
package api
import (
2021-07-17 15:35:44 +00:00
"context"
2019-05-27 00:41:10 +00:00
"encoding/json"
"net/http"
2021-03-06 21:06:43 +00:00
"github.com/go-chi/chi"
2019-05-27 00:41:10 +00:00
"github.com/smallstep/certificates/acme"
"github.com/smallstep/certificates/api"
"github.com/smallstep/certificates/logging"
2021-12-07 11:17:41 +00:00
"go.step.sm/crypto/jose"
2019-05-27 00:41:10 +00:00
)
2021-08-10 10:39:11 +00:00
// ExternalAccountBinding represents the ACME externalAccountBinding JWS
type ExternalAccountBinding struct {
Protected string ` json:"protected" `
Payload string ` json:"payload" `
Sig string ` json:"signature" `
}
2019-05-27 00:41:10 +00:00
// NewAccountRequest represents the payload for a new account request.
type NewAccountRequest struct {
2021-08-10 10:39:11 +00:00
Contact [ ] string ` json:"contact" `
OnlyReturnExisting bool ` json:"onlyReturnExisting" `
TermsOfServiceAgreed bool ` json:"termsOfServiceAgreed" `
ExternalAccountBinding * ExternalAccountBinding ` json:"externalAccountBinding,omitempty" `
2019-05-27 00:41:10 +00:00
}
func validateContacts ( cs [ ] string ) error {
for _ , c := range cs {
2021-10-08 18:59:57 +00:00
if c == "" {
2021-03-03 23:16:25 +00:00
return acme . NewError ( acme . ErrorMalformedType , "contact cannot be empty string" )
2019-05-27 00:41:10 +00:00
}
}
return nil
}
// Validate validates a new-account request body.
func ( n * NewAccountRequest ) Validate ( ) error {
if n . OnlyReturnExisting && len ( n . Contact ) > 0 {
2021-03-03 23:16:25 +00:00
return acme . NewError ( acme . ErrorMalformedType , "incompatible input; onlyReturnExisting must be alone" )
2019-05-27 00:41:10 +00:00
}
return validateContacts ( n . Contact )
}
// UpdateAccountRequest represents an update-account request.
type UpdateAccountRequest struct {
2021-03-05 07:10:46 +00:00
Contact [ ] string ` json:"contact" `
Status acme . Status ` json:"status" `
2019-05-27 00:41:10 +00:00
}
// Validate validates a update-account request body.
func ( u * UpdateAccountRequest ) Validate ( ) error {
switch {
case len ( u . Status ) > 0 && len ( u . Contact ) > 0 :
2021-03-03 23:16:25 +00:00
return acme . NewError ( acme . ErrorMalformedType , "incompatible input; contact and " +
"status updates are mutually exclusive" )
2019-05-27 00:41:10 +00:00
case len ( u . Contact ) > 0 :
if err := validateContacts ( u . Contact ) ; err != nil {
return err
}
return nil
case len ( u . Status ) > 0 :
2021-03-05 07:10:46 +00:00
if u . Status != acme . StatusDeactivated {
2021-03-03 23:16:25 +00:00
return acme . NewError ( acme . ErrorMalformedType , "cannot update account " +
"status to %s, only deactivated" , u . Status )
2019-05-27 00:41:10 +00:00
}
return nil
default :
2020-05-08 18:52:30 +00:00
// According to the ACME spec (https://tools.ietf.org/html/rfc8555#section-7.3.2)
// accountUpdate should ignore any fields not recognized by the server.
return nil
2019-05-27 00:41:10 +00:00
}
}
// NewAccount is the handler resource for creating new ACME accounts.
func ( h * Handler ) NewAccount ( w http . ResponseWriter , r * http . Request ) {
2021-03-05 07:14:56 +00:00
ctx := r . Context ( )
payload , err := payloadFromContext ( ctx )
2019-05-27 00:41:10 +00:00
if err != nil {
api . WriteError ( w , err )
return
}
var nar NewAccountRequest
if err := json . Unmarshal ( payload . value , & nar ) ; err != nil {
2021-03-05 07:10:46 +00:00
api . WriteError ( w , acme . WrapError ( acme . ErrorMalformedType , err ,
2021-03-03 23:16:25 +00:00
"failed to unmarshal new-account request payload" ) )
2019-05-27 00:41:10 +00:00
return
}
if err := nar . Validate ( ) ; err != nil {
api . WriteError ( w , err )
return
}
2021-08-09 08:26:31 +00:00
prov , err := acmeProvisionerFromContext ( ctx )
if err != nil {
api . WriteError ( w , err )
return
}
2019-05-27 00:41:10 +00:00
httpStatus := http . StatusCreated
2021-12-07 11:17:41 +00:00
acc , err := accountFromContext ( ctx )
2019-05-27 00:41:10 +00:00
if err != nil {
acmeErr , ok := err . ( * acme . Error )
2020-02-02 01:35:41 +00:00
if ! ok || acmeErr . Status != http . StatusBadRequest {
2019-05-27 00:41:10 +00:00
// Something went wrong ...
api . WriteError ( w , err )
return
}
// Account does not exist //
if nar . OnlyReturnExisting {
2021-03-03 23:16:25 +00:00
api . WriteError ( w , acme . NewError ( acme . ErrorAccountDoesNotExistType ,
"account does not exist" ) )
2019-05-27 00:41:10 +00:00
return
}
2021-12-07 11:17:41 +00:00
2021-03-05 07:14:56 +00:00
jwk , err := jwkFromContext ( ctx )
2019-05-27 00:41:10 +00:00
if err != nil {
api . WriteError ( w , err )
return
}
2021-12-07 11:17:41 +00:00
eak , err := h . validateExternalAccountBinding ( ctx , & nar )
if err != nil {
api . WriteError ( w , err )
return
}
2021-03-11 21:10:14 +00:00
acc = & acme . Account {
2019-05-27 00:41:10 +00:00
Key : jwk ,
Contact : nar . Contact ,
2021-03-01 06:49:20 +00:00
Status : acme . StatusValid ,
2021-03-05 07:10:46 +00:00
}
2021-03-05 07:14:56 +00:00
if err := h . db . CreateAccount ( ctx , acc ) ; err != nil {
2021-03-05 07:10:46 +00:00
api . WriteError ( w , acme . WrapErrorISE ( err , "error creating account" ) )
2019-05-27 00:41:10 +00:00
return
}
2021-12-07 11:17:41 +00:00
2021-07-22 21:48:41 +00:00
if eak != nil { // means that we have a (valid) External Account Binding key that should be bound, updated and sent in the response
2021-10-08 11:18:23 +00:00
err := eak . BindTo ( acc )
if err != nil {
api . WriteError ( w , err )
return
}
2022-01-07 15:59:55 +00:00
if err := h . db . UpdateExternalAccountKey ( ctx , prov . ID , eak ) ; err != nil {
2021-07-17 17:02:47 +00:00
api . WriteError ( w , acme . WrapErrorISE ( err , "error updating external account binding key" ) )
return
}
acc . ExternalAccountBinding = nar . ExternalAccountBinding
}
2019-05-27 00:41:10 +00:00
} else {
2021-07-22 21:48:41 +00:00
// Account exists
2019-05-27 00:41:10 +00:00
httpStatus = http . StatusOK
}
2021-03-05 07:10:46 +00:00
h . linker . LinkAccount ( ctx , acc )
2021-04-14 22:11:15 +00:00
w . Header ( ) . Set ( "Location" , h . linker . GetLink ( r . Context ( ) , AccountLinkType , acc . ID ) )
2019-05-27 00:41:10 +00:00
api . JSONStatus ( w , acc , httpStatus )
}
2021-04-13 02:06:07 +00:00
// GetOrUpdateAccount is the api for updating an ACME account.
func ( h * Handler ) GetOrUpdateAccount ( w http . ResponseWriter , r * http . Request ) {
2021-03-05 07:14:56 +00:00
ctx := r . Context ( )
acc , err := accountFromContext ( ctx )
2019-05-27 00:41:10 +00:00
if err != nil {
api . WriteError ( w , err )
return
}
2021-03-05 07:14:56 +00:00
payload , err := payloadFromContext ( ctx )
2019-05-27 00:41:10 +00:00
if err != nil {
api . WriteError ( w , err )
return
}
2020-05-08 18:52:30 +00:00
// If PostAsGet just respond with the account, otherwise process like a
// normal Post request.
2019-05-27 00:41:10 +00:00
if ! payload . isPostAsGet {
var uar UpdateAccountRequest
if err := json . Unmarshal ( payload . value , & uar ) ; err != nil {
2021-03-05 07:10:46 +00:00
api . WriteError ( w , acme . WrapError ( acme . ErrorMalformedType , err ,
2021-03-03 23:16:25 +00:00
"failed to unmarshal new-account request payload" ) )
2019-05-27 00:41:10 +00:00
return
}
if err := uar . Validate ( ) ; err != nil {
api . WriteError ( w , err )
return
}
2021-03-12 08:16:48 +00:00
if len ( uar . Status ) > 0 || len ( uar . Contact ) > 0 {
if len ( uar . Status ) > 0 {
acc . Status = uar . Status
} else if len ( uar . Contact ) > 0 {
acc . Contact = uar . Contact
}
if err := h . db . UpdateAccount ( ctx , acc ) ; err != nil {
api . WriteError ( w , acme . WrapErrorISE ( err , "error updating account" ) )
return
}
2019-05-27 00:41:10 +00:00
}
}
2021-03-05 07:10:46 +00:00
h . linker . LinkAccount ( ctx , acc )
2021-04-14 22:11:15 +00:00
w . Header ( ) . Set ( "Location" , h . linker . GetLink ( ctx , AccountLinkType , acc . ID ) )
2019-05-27 00:41:10 +00:00
api . JSON ( w , acc )
}
func logOrdersByAccount ( w http . ResponseWriter , oids [ ] string ) {
if rl , ok := w . ( logging . ResponseLogger ) ; ok {
m := map [ string ] interface { } {
"orders" : oids ,
}
rl . WithFields ( m )
}
}
2021-03-09 06:35:57 +00:00
// GetOrdersByAccountID ACME api for retrieving the list of order urls belonging to an account.
func ( h * Handler ) GetOrdersByAccountID ( w http . ResponseWriter , r * http . Request ) {
2021-03-06 21:06:43 +00:00
ctx := r . Context ( )
acc , err := accountFromContext ( ctx )
if err != nil {
api . WriteError ( w , err )
return
}
accID := chi . URLParam ( r , "accID" )
if acc . ID != accID {
api . WriteError ( w , acme . NewError ( acme . ErrorUnauthorizedType , "account ID '%s' does not match url param '%s'" , acc . ID , accID ) )
return
}
orders , err := h . db . GetOrdersByAccountID ( ctx , acc . ID )
if err != nil {
api . WriteError ( w , err )
return
}
h . linker . LinkOrdersByAccountID ( ctx , orders )
api . JSON ( w , orders )
logOrdersByAccount ( w , orders )
2019-05-27 00:41:10 +00:00
}
2021-07-17 15:35:44 +00:00
2021-08-09 08:26:31 +00:00
// validateExternalAccountBinding validates the externalAccountBinding property in a call to new-account.
2021-07-17 17:02:47 +00:00
func ( h * Handler ) validateExternalAccountBinding ( ctx context . Context , nar * NewAccountRequest ) ( * acme . ExternalAccountKey , error ) {
2021-07-22 21:48:41 +00:00
acmeProv , err := acmeProvisionerFromContext ( ctx )
2021-07-17 15:35:44 +00:00
if err != nil {
2021-07-22 21:48:41 +00:00
return nil , acme . WrapErrorISE ( err , "could not load ACME provisioner from context" )
2021-07-17 15:35:44 +00:00
}
2021-07-22 21:48:41 +00:00
if ! acmeProv . RequireEAB {
2021-07-17 17:02:47 +00:00
return nil , nil
2021-07-17 15:35:44 +00:00
}
if nar . ExternalAccountBinding == nil {
2021-07-17 17:02:47 +00:00
return nil , acme . NewError ( acme . ErrorExternalAccountRequiredType , "no external account binding provided" )
2021-07-17 15:35:44 +00:00
}
eabJSONBytes , err := json . Marshal ( nar . ExternalAccountBinding )
if err != nil {
2021-09-16 21:09:24 +00:00
return nil , acme . WrapErrorISE ( err , "error marshaling externalAccountBinding into bytes" )
2021-07-17 15:35:44 +00:00
}
2021-12-07 11:17:41 +00:00
eabJWS , err := jose . ParseJWS ( string ( eabJSONBytes ) )
2021-07-17 15:35:44 +00:00
if err != nil {
2021-07-17 17:02:47 +00:00
return nil , acme . WrapErrorISE ( err , "error parsing externalAccountBinding jws" )
2021-07-17 15:35:44 +00:00
}
2021-12-07 11:17:41 +00:00
// 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
}
2021-07-17 15:35:44 +00:00
2022-01-07 15:59:55 +00:00
externalAccountKey , err := h . db . GetExternalAccountKey ( ctx , acmeProv . ID , keyID )
2021-07-17 15:35:44 +00:00
if err != nil {
2021-08-09 08:26:31 +00:00
if _ , ok := err . ( * acme . Error ) ; ok {
2021-12-07 11:17:41 +00:00
return nil , acme . WrapError ( acme . ErrorUnauthorizedType , err , "the field 'kid' references an unknown key" )
2021-08-09 08:26:31 +00:00
}
2021-07-17 17:02:47 +00:00
return nil , acme . WrapErrorISE ( err , "error retrieving external account key" )
}
2021-07-22 21:48:41 +00:00
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 )
2021-07-17 15:35:44 +00:00
}
payload , err := eabJWS . Verify ( externalAccountKey . KeyBytes )
if err != nil {
2021-07-17 17:02:47 +00:00
return nil , acme . WrapErrorISE ( err , "error verifying externalAccountBinding signature" )
2021-07-17 15:35:44 +00:00
}
jwk , err := jwkFromContext ( ctx )
if err != nil {
2021-07-17 17:02:47 +00:00
return nil , err
2021-07-17 15:35:44 +00:00
}
2021-12-07 11:17:41 +00:00
var payloadJWK * jose . JSONWebKey
2021-12-20 13:30:01 +00:00
if err = json . Unmarshal ( payload , & payloadJWK ) ; err != nil {
2021-07-17 18:39:12 +00:00
return nil , acme . WrapError ( acme . ErrorMalformedType , err , "error unmarshaling payload into jwk" )
2021-07-17 15:35:44 +00:00
}
2021-07-17 18:29:12 +00:00
if ! keysAreEqual ( jwk , payloadJWK ) {
2021-12-07 11:17:41 +00:00
return nil , acme . NewError ( acme . ErrorUnauthorizedType , "keys in jws and eab payload do not match" )
2021-07-17 15:35:44 +00:00
}
2021-07-17 17:02:47 +00:00
return externalAccountKey , nil
2021-07-17 15:35:44 +00:00
}
2021-07-17 18:29:12 +00:00
2021-08-09 08:26:31 +00:00
// keysAreEqual performs an equality check on two JWKs by comparing
// the (base64 encoding) of the Key IDs.
2021-12-07 11:17:41 +00:00
func keysAreEqual ( x , y * jose . JSONWebKey ) bool {
2021-07-17 18:29:12 +00:00
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
}
2021-12-07 11:17:41 +00:00
// 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
}