mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 10:37:56 +00:00 
			
		
		
		
	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:
		| @@ -20,11 +20,11 @@ type ACMEState struct { | |||||||
| 	nonces     *sync.Map // map[string]time.Time | 	nonces     *sync.Map // map[string]time.Time | ||||||
| } | } | ||||||
|  |  | ||||||
| func NewACMEState() (*ACMEState, error) { | func NewACMEState() *ACMEState { | ||||||
| 	return &ACMEState{ | 	return &ACMEState{ | ||||||
| 		nextExpiry: new(atomic.Int64), | 		nextExpiry: new(atomic.Int64), | ||||||
| 		nonces:     new(sync.Map), | 		nonces:     new(sync.Map), | ||||||
| 	}, nil | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func generateNonce() (string, error) { | func generateNonce() (string, error) { | ||||||
|   | |||||||
| @@ -9,8 +9,7 @@ import ( | |||||||
| func TestAcmeNonces(t *testing.T) { | func TestAcmeNonces(t *testing.T) { | ||||||
| 	t.Parallel() | 	t.Parallel() | ||||||
|  |  | ||||||
| 	a, err := NewACMEState() | 	a := NewACMEState() | ||||||
| 	require.NoError(t, err) |  | ||||||
|  |  | ||||||
| 	// Simple operation should succeed. | 	// Simple operation should succeed. | ||||||
| 	nonce, _, err := a.GetNonce() | 	nonce, _, err := a.GetNonce() | ||||||
|   | |||||||
| @@ -12,6 +12,8 @@ import ( | |||||||
| 	"sync/atomic" | 	"sync/atomic" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/hashicorp/vault/builtin/logical/pki/acme" | ||||||
|  |  | ||||||
| 	atomic2 "go.uber.org/atomic" | 	atomic2 "go.uber.org/atomic" | ||||||
|  |  | ||||||
| 	"github.com/hashicorp/vault/helper/constants" | 	"github.com/hashicorp/vault/helper/constants" | ||||||
| @@ -218,6 +220,11 @@ func Backend(conf *logical.BackendConfig) *backend { | |||||||
| 			pathAcmeRoleDirectory(&b), | 			pathAcmeRoleDirectory(&b), | ||||||
| 			pathAcmeIssuerDirectory(&b), | 			pathAcmeIssuerDirectory(&b), | ||||||
| 			pathAcmeIssuerAndRoleDirectory(&b), | 			pathAcmeIssuerAndRoleDirectory(&b), | ||||||
|  |  | ||||||
|  | 			pathAcmeRootNonce(&b), | ||||||
|  | 			pathAcmeRoleNonce(&b), | ||||||
|  | 			pathAcmeIssuerNonce(&b), | ||||||
|  | 			pathAcmeIssuerAndRoleNonce(&b), | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| 		Secrets: []*framework.Secret{ | 		Secrets: []*framework.Secret{ | ||||||
| @@ -282,6 +289,7 @@ func Backend(conf *logical.BackendConfig) *backend { | |||||||
|  |  | ||||||
| 	b.unifiedTransferStatus = newUnifiedTransferStatus() | 	b.unifiedTransferStatus = newUnifiedTransferStatus() | ||||||
|  |  | ||||||
|  | 	b.acmeState = acme.NewACMEState() | ||||||
| 	return &b | 	return &b | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -314,6 +322,7 @@ type backend struct { | |||||||
|  |  | ||||||
| 	// Write lock around issuers and keys. | 	// Write lock around issuers and keys. | ||||||
| 	issuersLock sync.RWMutex | 	issuersLock sync.RWMutex | ||||||
|  | 	acmeState   *acme.ACMEState | ||||||
| } | } | ||||||
|  |  | ||||||
| type roleOperation func(ctx context.Context, req *logical.Request, data *framework.FieldData, role *roleEntry) (*logical.Response, error) | 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 | 	// Add ACME based paths to the test suite | ||||||
| 	for _, acmePrefix := range []string{"", "issuer/default/", "roles/test/", "issuer/default/roles/test/"} { | 	for _, acmePrefix := range []string{"", "issuer/default/", "roles/test/", "issuer/default/roles/test/"} { | ||||||
| 		paths[acmePrefix+"acme/directory"] = shouldBeUnauthedReadList | 		paths[acmePrefix+"acme/directory"] = shouldBeUnauthedReadList | ||||||
|  | 		paths[acmePrefix+"acme/new-nonce"] = shouldBeUnauthedReadList | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for path, checkerType := range paths { | 	for path, checkerType := range paths { | ||||||
|   | |||||||
							
								
								
									
										96
									
								
								builtin/logical/pki/path_acme_nonce.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								builtin/logical/pki/path_acme_nonce.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | package pki | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/hashicorp/vault/sdk/logical" | ||||||
|  | 
 | ||||||
| 	"github.com/stretchr/testify/require" | 	"github.com/stretchr/testify/require" | ||||||
| 	"gopkg.in/square/go-jose.v2/json" | 	"gopkg.in/square/go-jose.v2/json" | ||||||
| ) | ) | ||||||
| @@ -12,16 +15,7 @@ import ( | |||||||
| // are available and produce the correct responses. | // are available and produce the correct responses. | ||||||
| func TestAcmeDirectory(t *testing.T) { | func TestAcmeDirectory(t *testing.T) { | ||||||
| 	t.Parallel() | 	t.Parallel() | ||||||
| 	b, s := CreateBackendWithStorage(t) | 	b, s, pathConfig := setupAcmeBackend(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) |  | ||||||
| 
 | 
 | ||||||
| 	cases := []struct { | 	cases := []struct { | ||||||
| 		name         string | 		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. | // TestAcmeClusterPathNotConfigured basic testing of the ACME error handler. | ||||||
| func TestAcmeClusterPathNotConfigured(t *testing.T) { | func TestAcmeClusterPathNotConfigured(t *testing.T) { | ||||||
| 	t.Parallel() | 	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 | 	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) { | func CBRead(b *backend, s logical.Storage, path string) (*logical.Response, error) { | ||||||
| 	return CBReq(b, s, logical.ReadOperation, path, make(map[string]interface{})) | 	return CBReq(b, s, logical.ReadOperation, path, make(map[string]interface{})) | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Steven Clark
					Steven Clark