mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 02:28:09 +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 | ||||
| } | ||||
|  | ||||
| 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 { | ||||
|   | ||||
							
								
								
									
										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 | ||||
| 
 | ||||
| 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{})) | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Steven Clark
					Steven Clark