Skip to content

Commit

Permalink
Add ACME new account creation handlers (hashicorp#19820)
Browse files Browse the repository at this point in the history
* Identify whether JWKs existed or were created, set KIDs

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Reclassify ErrAccountDoesNotExist as 400 per spec

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add additional stub methods for ACME accounts

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Start adding ACME newAccount handlers

This handler supports two pieces of functionality:

 1. Searching for whether an existing account already exists.
 2. Creating a new account.

One side effect of our JWS parsing logic is that we needed a way to
differentiate between whether a JWK existed on disk from an account or
if it was specified in the request. This technically means we're
potentially responding to certain requests with positive results (e.g.,
key search based on kid) versus erring earlier like other
implementations do.

No account storage has been done as part of this commit.

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Unify path fields handling, fix newAccount method

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

---------

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
  • Loading branch information
cipherboy committed Mar 29, 2023
1 parent 2054fe2 commit 7144174
Show file tree
Hide file tree
Showing 8 changed files with 271 additions and 64 deletions.
2 changes: 1 addition & 1 deletion builtin/logical/pki/acme/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ var errIdMappings = map[error]string{

// Mapping of err->status codes; see table in RFC 8555 Section 6.7. Errors.
var errCodeMappings = map[error]int{
ErrAccountDoesNotExist: http.StatusNotFound,
ErrAccountDoesNotExist: http.StatusBadRequest, // See RFC 8555 Section 7.3.1. Finding an Account URL Given a Key.
ErrAlreadyRevoked: http.StatusBadRequest,
ErrBadCSR: http.StatusBadRequest,
ErrBadNonce: http.StatusBadRequest,
Expand Down
27 changes: 21 additions & 6 deletions builtin/logical/pki/acme/jws.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package acme

import (
"crypto"
"encoding/base64"
"encoding/json"
"fmt"

Expand All @@ -22,12 +24,13 @@ var AllowedOuterJWSTypes = map[string]interface{}{

// This wraps a JWS message structure.
type JWSCtx struct {
Algo string `json:"alg"`
Kid string `json:"kid"`
jwk json.RawMessage `json:"jwk"`
Nonce string `json:"nonce"`
Url string `json:"url"`
key jose.JSONWebKey `json:"-"`
Algo string `json:"alg"`
Kid string `json:"kid"`
jwk json.RawMessage `json:"jwk"`
Nonce string `json:"nonce"`
Url string `json:"url"`
key jose.JSONWebKey `json:"-"`
Existing bool `json:"-"`
}

func (c *JWSCtx) UnmarshalJSON(a *ACMEState, jws []byte) error {
Expand Down Expand Up @@ -71,6 +74,7 @@ func (c *JWSCtx) UnmarshalJSON(a *ACMEState, jws []byte) error {
if err != nil {
return err
}
c.Existing = true
}

if err = c.key.UnmarshalJSON(c.jwk); err != nil {
Expand All @@ -81,6 +85,17 @@ func (c *JWSCtx) UnmarshalJSON(a *ACMEState, jws []byte) error {
return fmt.Errorf("received invalid jwk")
}

if c.Kid != "" {
// Create a key ID
kid, err := c.key.Thumbprint(crypto.SHA256)
if err != nil {
return fmt.Errorf("failed creating thumbprint: %w", err)
}

c.Kid = base64.URLEncoding.EncodeToString(kid)
c.Existing = false
}

return nil
}

Expand Down
14 changes: 12 additions & 2 deletions builtin/logical/pki/acme/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,23 @@ func (a *ACMEState) TidyNonces() {
a.nextExpiry.Store(nextRun.Unix())
}

func (a *ACMEState) LoadKey(keyID string) (map[string]interface{}, error) {
func (a *ACMEState) CreateAccount(c *JWSCtx, contact []string, termsOfServiceAgreed bool) (map[string]interface{}, error) {
// TODO
return nil, nil
}

func (a *ACMEState) LoadAccount(keyID string) (map[string]interface{}, error) {
// TODO
return nil, nil
}

func (a *ACMEState) DoesAccountExist(keyId string) bool {
account, err := a.LoadAccount(keyId)
return err == nil && len(account) > 0
}

func (a *ACMEState) LoadJWK(keyID string) ([]byte, error) {
key, err := a.LoadKey(keyID)
key, err := a.LoadAccount(keyID)
if err != nil {
return nil, err
}
Expand Down
22 changes: 13 additions & 9 deletions builtin/logical/pki/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,17 @@ import (
"sync/atomic"
"time"

"github.com/hashicorp/vault/builtin/logical/pki/acme"

atomic2 "go.uber.org/atomic"

"github.com/hashicorp/vault/helper/constants"

"github.com/hashicorp/go-multierror"

"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/builtin/logical/pki/acme"

"github.com/armon/go-metrics"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/vault/helper/constants"
"github.com/hashicorp/vault/helper/metricsutil"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/logical"
)

Expand Down Expand Up @@ -220,11 +217,14 @@ func Backend(conf *logical.BackendConfig) *backend {
pathAcmeRoleDirectory(&b),
pathAcmeIssuerDirectory(&b),
pathAcmeIssuerAndRoleDirectory(&b),

pathAcmeRootNonce(&b),
pathAcmeRoleNonce(&b),
pathAcmeIssuerNonce(&b),
pathAcmeIssuerAndRoleNonce(&b),
pathAcmeRootNewAccount(&b),
pathAcmeRoleNewAccount(&b),
pathAcmeIssuerNewAccount(&b),
pathAcmeIssuerAndRoleNewAccount(&b),
},

Secrets: []*framework.Secret{
Expand All @@ -241,6 +241,7 @@ func Backend(conf *logical.BackendConfig) *backend {
for _, acmePrefix := range []string{"", "issuer/+/", "roles/+/", "issuer/+/roles/+/"} {
b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, acmePrefix+"acme/directory")
b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, acmePrefix+"acme/new-nonce")
b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, acmePrefix+"acme/new-account")
b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, acmePrefix+"acme/new-order")
b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, acmePrefix+"acme/revoke-cert")
b.PathsSpecial.Unauthenticated = append(b.PathsSpecial.Unauthenticated, acmePrefix+"acme/key-change")
Expand Down Expand Up @@ -322,7 +323,9 @@ type backend struct {

// Write lock around issuers and keys.
issuersLock sync.RWMutex
acmeState *acme.ACMEState

// Context around ACME operations
acmeState *acme.ACMEState
}

type roleOperation func(ctx context.Context, req *logical.Request, data *framework.FieldData, role *roleEntry) (*logical.Response, error)
Expand Down Expand Up @@ -410,6 +413,7 @@ func (b *backend) initialize(ctx context.Context, _ *logical.InitializationReque
b.Logger().Error("Could not initialize stored certificate counts", err)
b.certCountError = err.Error()
}

return nil
}

Expand Down
1 change: 1 addition & 0 deletions builtin/logical/pki/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6812,6 +6812,7 @@ func TestProperAuthing(t *testing.T) {
for _, acmePrefix := range []string{"", "issuer/default/", "roles/test/", "issuer/default/roles/test/"} {
paths[acmePrefix+"acme/directory"] = shouldBeUnauthedReadList
paths[acmePrefix+"acme/new-nonce"] = shouldBeUnauthedReadList
paths[acmePrefix+"acme/new-account"] = shouldBeUnauthedWriteOnly
}

for path, checkerType := range paths {
Expand Down
31 changes: 8 additions & 23 deletions builtin/logical/pki/path_acme_directory.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,42 +19,27 @@ const (
)

func pathAcmeRootDirectory(b *backend) *framework.Path {
return patternAcmeDirectory(b, "acme/directory", false /* requireRole */, false /* requireIssuer */)
return patternAcmeDirectory(b, "acme/directory")
}

func pathAcmeRoleDirectory(b *backend) *framework.Path {
return patternAcmeDirectory(b, "roles/"+framework.GenericNameRegex("role")+"/acme/directory",
true /* requireRole */, false /* requireIssuer */)
return patternAcmeDirectory(b, "roles/"+framework.GenericNameRegex("role")+"/acme/directory")
}

func pathAcmeIssuerDirectory(b *backend) *framework.Path {
return patternAcmeDirectory(b, "issuer/"+framework.GenericNameRegex(issuerRefParam)+"/acme/directory",
false /* requireRole */, true /* requireIssuer */)
return patternAcmeDirectory(b, "issuer/"+framework.GenericNameRegex(issuerRefParam)+"/acme/directory")
}

func pathAcmeIssuerAndRoleDirectory(b *backend) *framework.Path {
return patternAcmeDirectory(b,
"issuer/"+framework.GenericNameRegex(issuerRefParam)+"/roles/"+framework.GenericNameRegex(
"role")+"/acme/directory",
true /* requireRole */, true /* requireIssuer */)
"issuer/"+framework.GenericNameRegex(issuerRefParam)+
"/roles/"+framework.GenericNameRegex("role")+"/acme/directory")
}

func patternAcmeDirectory(b *backend, pattern string, requireRole, requireIssuer bool) *framework.Path {
func patternAcmeDirectory(b *backend, pattern string) *framework.Path {
fields := map[string]*framework.FieldSchema{}
if requireRole {
fields["role"] = &framework.FieldSchema{
Type: framework.TypeString,
Description: `The desired role for the acme request`,
Required: true,
}
}
if requireIssuer {
fields[issuerRefParam] = &framework.FieldSchema{
Type: framework.TypeString,
Description: `Reference to an existing issuer name or issuer id`,
Required: true,
}
}
addFieldsForACMEPath(fields, pattern)

return &framework.Path{
Pattern: pattern,
Fields: fields,
Expand Down

0 comments on commit 7144174

Please sign in to comment.