Initial ACME new-nonce API (#19822)
* Initial ACME new-nonce API implementation * Return proper HTTP status codes for ACME new-nonce API handler
This commit is contained in:
parent
71071fd954
commit
91d1628bb5
|
@ -20,11 +20,11 @@ type ACMEState struct {
|
|||
nonces *sync.Map // map[string]time.Time
|
||||
}
|
||||
|
||||
func NewACMEState() (*ACMEState, error) {
|
||||
func NewACMEState() *ACMEState {
|
||||
return &ACMEState{
|
||||
nextExpiry: new(atomic.Int64),
|
||||
nonces: new(sync.Map),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func generateNonce() (string, error) {
|
||||
|
|
|
@ -9,8 +9,7 @@ import (
|
|||
func TestAcmeNonces(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
a, err := NewACMEState()
|
||||
require.NoError(t, err)
|
||||
a := NewACMEState()
|
||||
|
||||
// Simple operation should succeed.
|
||||
nonce, _, err := a.GetNonce()
|
||||
|
|
|
@ -12,6 +12,8 @@ import (
|
|||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/builtin/logical/pki/acme"
|
||||
|
||||
atomic2 "go.uber.org/atomic"
|
||||
|
||||
"github.com/hashicorp/vault/helper/constants"
|
||||
|
@ -218,6 +220,11 @@ func Backend(conf *logical.BackendConfig) *backend {
|
|||
pathAcmeRoleDirectory(&b),
|
||||
pathAcmeIssuerDirectory(&b),
|
||||
pathAcmeIssuerAndRoleDirectory(&b),
|
||||
|
||||
pathAcmeRootNonce(&b),
|
||||
pathAcmeRoleNonce(&b),
|
||||
pathAcmeIssuerNonce(&b),
|
||||
pathAcmeIssuerAndRoleNonce(&b),
|
||||
},
|
||||
|
||||
Secrets: []*framework.Secret{
|
||||
|
@ -282,6 +289,7 @@ func Backend(conf *logical.BackendConfig) *backend {
|
|||
|
||||
b.unifiedTransferStatus = newUnifiedTransferStatus()
|
||||
|
||||
b.acmeState = acme.NewACMEState()
|
||||
return &b
|
||||
}
|
||||
|
||||
|
@ -314,6 +322,7 @@ type backend struct {
|
|||
|
||||
// Write lock around issuers and keys.
|
||||
issuersLock sync.RWMutex
|
||||
acmeState *acme.ACMEState
|
||||
}
|
||||
|
||||
type roleOperation func(ctx context.Context, req *logical.Request, data *framework.FieldData, role *roleEntry) (*logical.Response, error)
|
||||
|
|
|
@ -6811,6 +6811,7 @@ func TestProperAuthing(t *testing.T) {
|
|||
// Add ACME based paths to the test suite
|
||||
for _, acmePrefix := range []string{"", "issuer/default/", "roles/test/", "issuer/default/roles/test/"} {
|
||||
paths[acmePrefix+"acme/directory"] = shouldBeUnauthedReadList
|
||||
paths[acmePrefix+"acme/new-nonce"] = shouldBeUnauthedReadList
|
||||
}
|
||||
|
||||
for path, checkerType := range paths {
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
package pki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
func pathAcmeRootNonce(b *backend) *framework.Path {
|
||||
return patternAcmeNonce(b, "acme/new-nonce", false /* requireRole */, false /* requireIssuer */)
|
||||
}
|
||||
|
||||
func pathAcmeRoleNonce(b *backend) *framework.Path {
|
||||
return patternAcmeNonce(b, "roles/"+framework.GenericNameRegex("role")+"/acme/new-nonce",
|
||||
true /* requireRole */, false /* requireIssuer */)
|
||||
}
|
||||
|
||||
func pathAcmeIssuerNonce(b *backend) *framework.Path {
|
||||
return patternAcmeNonce(b, "issuer/"+framework.GenericNameRegex(issuerRefParam)+"/acme/new-nonce",
|
||||
false /* requireRole */, true /* requireIssuer */)
|
||||
}
|
||||
|
||||
func pathAcmeIssuerAndRoleNonce(b *backend) *framework.Path {
|
||||
return patternAcmeNonce(b,
|
||||
"issuer/"+framework.GenericNameRegex(issuerRefParam)+"/roles/"+framework.GenericNameRegex(
|
||||
"role")+"/acme/new-nonce",
|
||||
true /* requireRole */, true /* requireIssuer */)
|
||||
}
|
||||
|
||||
func patternAcmeNonce(b *backend, pattern string, requireRole, requireIssuer bool) *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,
|
||||
}
|
||||
}
|
||||
return &framework.Path{
|
||||
Pattern: pattern,
|
||||
Fields: fields,
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.HeaderOperation: &framework.PathOperation{
|
||||
Callback: b.acmeWrapper(b.acmeNonceHandler),
|
||||
ForwardPerformanceSecondary: false,
|
||||
ForwardPerformanceStandby: true,
|
||||
},
|
||||
logical.ReadOperation: &framework.PathOperation{
|
||||
Callback: b.acmeWrapper(b.acmeNonceHandler),
|
||||
ForwardPerformanceSecondary: false,
|
||||
ForwardPerformanceStandby: true,
|
||||
},
|
||||
},
|
||||
|
||||
HelpSynopsis: pathAcmeDirectoryHelpSync,
|
||||
HelpDescription: pathAcmeDirectoryHelpDesc,
|
||||
}
|
||||
}
|
||||
|
||||
func (b *backend) acmeNonceHandler(ctx acmeContext, r *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
||||
nonce, _, err := b.acmeState.GetNonce()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Header operations return 200, GET return 204.
|
||||
httpStatus := http.StatusOK
|
||||
if r.Operation == logical.ReadOperation {
|
||||
httpStatus = http.StatusNoContent
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Headers: map[string][]string{
|
||||
"Cache-Control": {"no-store"},
|
||||
"Replay-Nonce": {nonce},
|
||||
"Link": genAcmeLinkHeader(ctx),
|
||||
},
|
||||
Data: map[string]interface{}{
|
||||
logical.HTTPStatusCode: httpStatus,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func genAcmeLinkHeader(ctx acmeContext) []string {
|
||||
path := fmt.Sprintf("<%s>;rel=\"index\"", ctx.baseUrl.JoinPath("/acme/directory").String())
|
||||
return []string{path}
|
||||
}
|
|
@ -1,9 +1,12 @@
|
|||
package pki
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/square/go-jose.v2/json"
|
||||
)
|
||||
|
@ -12,16 +15,7 @@ import (
|
|||
// are available and produce the correct responses.
|
||||
func TestAcmeDirectory(t *testing.T) {
|
||||
t.Parallel()
|
||||
b, s := CreateBackendWithStorage(t)
|
||||
|
||||
// Setting templated AIAs should succeed.
|
||||
pathConfig := "https://localhost:8200/v1/pki"
|
||||
|
||||
_, err := CBWrite(b, s, "config/cluster", map[string]interface{}{
|
||||
"path": pathConfig,
|
||||
"aia_path": "http://localhost:8200/cdn/pki",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
b, s, pathConfig := setupAcmeBackend(t)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
|
@ -65,6 +59,65 @@ func TestAcmeDirectory(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAcmeNonce(t *testing.T) {
|
||||
t.Parallel()
|
||||
b, s, pathConfig := setupAcmeBackend(t)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
prefixUrl string
|
||||
directoryUrl string
|
||||
}{
|
||||
{"root", "", "acme/new-nonce"},
|
||||
{"role", "/roles/test-role", "roles/test-role/acme/new-nonce"},
|
||||
{"issuer", "/issuer/default", "issuer/default/acme/new-nonce"},
|
||||
{"issuer_role", "/issuer/default/roles/test-role", "issuer/default/roles/test-role/acme/new-nonce"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
for _, httpOp := range []string{"get", "header"} {
|
||||
t.Run(fmt.Sprintf("%s-%s", tc.name, httpOp), func(t *testing.T) {
|
||||
var resp *logical.Response
|
||||
var err error
|
||||
switch httpOp {
|
||||
case "get":
|
||||
resp, err = CBRead(b, s, tc.directoryUrl)
|
||||
case "header":
|
||||
resp, err = CBHeader(b, s, tc.directoryUrl)
|
||||
}
|
||||
require.NoError(t, err, "failed %s op for new-nouce", httpOp)
|
||||
|
||||
// Proper Status Code
|
||||
switch httpOp {
|
||||
case "get":
|
||||
require.Equal(t, http.StatusNoContent, resp.Data["http_status_code"])
|
||||
case "header":
|
||||
require.Equal(t, http.StatusOK, resp.Data["http_status_code"])
|
||||
}
|
||||
|
||||
// Make sure we return the Cache-Control header
|
||||
require.Contains(t, resp.Headers, "Cache-Control", "missing Cache-Control header")
|
||||
require.Contains(t, resp.Headers["Cache-Control"], "no-store",
|
||||
"missing Cache-Control header with no-store header value")
|
||||
require.Len(t, resp.Headers["Cache-Control"], 1,
|
||||
"Cache-Control header should have only a single header")
|
||||
|
||||
// Test for our nonce header value
|
||||
require.Contains(t, resp.Headers, "Replay-Nonce", "missing Replay-Nonce header")
|
||||
require.NotEmpty(t, resp.Headers["Replay-Nonce"], "missing Replay-Nonce header with an actual value")
|
||||
require.Len(t, resp.Headers["Replay-Nonce"], 1,
|
||||
"Replay-Nonce header should have only a single header")
|
||||
|
||||
// Test Link header value
|
||||
require.Contains(t, resp.Headers, "Link", "missing Link header")
|
||||
expectedLinkHeader := fmt.Sprintf("<%s>;rel=\"index\"", pathConfig+tc.prefixUrl+"/acme/directory")
|
||||
require.Contains(t, resp.Headers["Link"], expectedLinkHeader,
|
||||
"different value for link header than expected")
|
||||
require.Len(t, resp.Headers["Link"], 1, "Link header should have only a single header")
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestAcmeClusterPathNotConfigured basic testing of the ACME error handler.
|
||||
func TestAcmeClusterPathNotConfigured(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
@ -102,3 +155,17 @@ func TestAcmeClusterPathNotConfigured(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setupAcmeBackend(t *testing.T) (*backend, logical.Storage, string) {
|
||||
b, s := CreateBackendWithStorage(t)
|
||||
|
||||
// Setting templated AIAs should succeed.
|
||||
pathConfig := "https://localhost:8200/v1/pki"
|
||||
|
||||
_, err := CBWrite(b, s, "config/cluster", map[string]interface{}{
|
||||
"path": pathConfig,
|
||||
"aia_path": "http://localhost:8200/cdn/pki",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return b, s, pathConfig
|
||||
}
|
|
@ -211,6 +211,10 @@ func CBReq(b *backend, s logical.Storage, operation logical.Operation, path stri
|
|||
return resp, nil
|
||||
}
|
||||
|
||||
func CBHeader(b *backend, s logical.Storage, path string) (*logical.Response, error) {
|
||||
return CBReq(b, s, logical.HeaderOperation, path, make(map[string]interface{}))
|
||||
}
|
||||
|
||||
func CBRead(b *backend, s logical.Storage, path string) (*logical.Response, error) {
|
||||
return CBReq(b, s, logical.ReadOperation, path, make(map[string]interface{}))
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue