2023-04-14 18:12:31 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
2023-03-29 16:29:19 +00:00
|
|
|
package pki
|
|
|
|
|
|
|
|
import (
|
2023-04-03 13:38:20 +00:00
|
|
|
"context"
|
2023-04-12 13:05:42 +00:00
|
|
|
"crypto"
|
2023-04-21 13:38:06 +00:00
|
|
|
"crypto/ecdsa"
|
|
|
|
"crypto/elliptic"
|
2023-04-12 13:05:42 +00:00
|
|
|
"crypto/rand"
|
|
|
|
"crypto/rsa"
|
2023-04-21 13:38:06 +00:00
|
|
|
"crypto/x509"
|
|
|
|
"crypto/x509/pkix"
|
|
|
|
"encoding/base64"
|
2023-03-29 18:22:48 +00:00
|
|
|
"fmt"
|
2023-04-03 13:38:20 +00:00
|
|
|
"io"
|
2023-03-29 16:29:19 +00:00
|
|
|
"net/http"
|
2023-04-21 13:38:06 +00:00
|
|
|
"path"
|
2023-04-12 13:05:42 +00:00
|
|
|
"strings"
|
2023-03-29 16:29:19 +00:00
|
|
|
"testing"
|
2023-04-14 14:54:48 +00:00
|
|
|
"time"
|
|
|
|
|
2023-04-21 13:38:06 +00:00
|
|
|
"github.com/hashicorp/vault/sdk/helper/jsonutil"
|
|
|
|
|
2023-04-14 14:54:48 +00:00
|
|
|
"github.com/go-test/deep"
|
2023-03-29 16:29:19 +00:00
|
|
|
|
2023-04-12 13:05:42 +00:00
|
|
|
"github.com/hashicorp/go-cleanhttp"
|
|
|
|
"golang.org/x/crypto/acme"
|
|
|
|
"golang.org/x/net/http2"
|
|
|
|
|
2023-04-03 13:38:20 +00:00
|
|
|
"github.com/hashicorp/vault/api"
|
|
|
|
vaulthttp "github.com/hashicorp/vault/http"
|
|
|
|
"github.com/hashicorp/vault/vault"
|
|
|
|
|
2023-03-29 18:22:48 +00:00
|
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
|
|
|
2023-03-29 16:29:19 +00:00
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"gopkg.in/square/go-jose.v2/json"
|
|
|
|
)
|
|
|
|
|
2023-04-12 13:05:42 +00:00
|
|
|
// TestAcmeBasicWorkflow a basic test that will validate a basic ACME workflow using the Golang ACME client.
|
|
|
|
func TestAcmeBasicWorkflow(t *testing.T) {
|
2023-03-29 16:29:19 +00:00
|
|
|
t.Parallel()
|
2023-04-12 13:05:42 +00:00
|
|
|
cluster, client, _ := setupAcmeBackend(t)
|
2023-04-03 13:38:20 +00:00
|
|
|
defer cluster.Cleanup()
|
2023-03-29 16:29:19 +00:00
|
|
|
cases := []struct {
|
2023-04-12 13:05:42 +00:00
|
|
|
name string
|
|
|
|
prefixUrl string
|
2023-03-29 16:29:19 +00:00
|
|
|
}{
|
2023-04-12 13:05:42 +00:00
|
|
|
{"root", ""},
|
|
|
|
{"role", "/roles/test-role"},
|
2023-04-21 13:38:06 +00:00
|
|
|
{"issuer", "/issuer/int-ca"},
|
|
|
|
{"issuer_role", "/issuer/int-ca/roles/test-role"},
|
2023-04-12 13:05:42 +00:00
|
|
|
{"issuer_role_acme", "/issuer/acme/roles/acme"},
|
2023-03-29 16:29:19 +00:00
|
|
|
}
|
2023-04-03 13:38:20 +00:00
|
|
|
testCtx := context.Background()
|
2023-03-29 16:29:19 +00:00
|
|
|
|
|
|
|
for _, tc := range cases {
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
2023-04-12 13:05:42 +00:00
|
|
|
baseAcmeURL := "/v1/pki" + tc.prefixUrl + "/acme/"
|
2023-04-21 13:38:06 +00:00
|
|
|
accountKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
2023-04-12 13:05:42 +00:00
|
|
|
require.NoError(t, err, "failed creating rsa key")
|
|
|
|
|
2023-04-21 13:38:06 +00:00
|
|
|
acmeClient := getAcmeClientForCluster(t, cluster, baseAcmeURL, accountKey)
|
2023-04-12 13:05:42 +00:00
|
|
|
|
2023-04-14 14:54:48 +00:00
|
|
|
t.Logf("Testing discover on %s", baseAcmeURL)
|
2023-04-12 13:05:42 +00:00
|
|
|
discovery, err := acmeClient.Discover(testCtx)
|
|
|
|
require.NoError(t, err, "failed acme discovery call")
|
|
|
|
|
|
|
|
discoveryBaseUrl := client.Address() + baseAcmeURL
|
|
|
|
require.Equal(t, discoveryBaseUrl+"new-nonce", discovery.NonceURL)
|
|
|
|
require.Equal(t, discoveryBaseUrl+"new-account", discovery.RegURL)
|
|
|
|
require.Equal(t, discoveryBaseUrl+"new-order", discovery.OrderURL)
|
|
|
|
require.Equal(t, discoveryBaseUrl+"revoke-cert", discovery.RevokeURL)
|
|
|
|
require.Equal(t, discoveryBaseUrl+"key-change", discovery.KeyChangeURL)
|
|
|
|
|
|
|
|
// Attempt to update prior to creating an account
|
2023-04-14 14:54:48 +00:00
|
|
|
t.Logf("Testing updates with no proper account fail on %s", baseAcmeURL)
|
2023-04-12 13:05:42 +00:00
|
|
|
_, err = acmeClient.UpdateReg(testCtx, &acme.Account{Contact: []string{"mailto:shouldfail@example.com"}})
|
|
|
|
require.ErrorIs(t, err, acme.ErrNoAccount, "expected failure attempting to update prior to account registration")
|
|
|
|
|
|
|
|
// Create new account
|
2023-04-14 14:54:48 +00:00
|
|
|
t.Logf("Testing register on %s", baseAcmeURL)
|
2023-04-12 13:05:42 +00:00
|
|
|
acct, err := acmeClient.Register(testCtx, &acme.Account{
|
|
|
|
Contact: []string{"mailto:test@example.com", "mailto:test2@test.com"},
|
|
|
|
}, func(tosURL string) bool { return true })
|
|
|
|
require.NoError(t, err, "failed registering account")
|
|
|
|
require.Equal(t, acme.StatusValid, acct.Status)
|
|
|
|
require.Contains(t, acct.Contact, "mailto:test@example.com")
|
|
|
|
require.Contains(t, acct.Contact, "mailto:test2@test.com")
|
|
|
|
require.Len(t, acct.Contact, 2)
|
|
|
|
|
|
|
|
// Call register again we should get existing account
|
2023-04-14 14:54:48 +00:00
|
|
|
t.Logf("Testing duplicate register returns existing account on %s", baseAcmeURL)
|
2023-04-12 13:05:42 +00:00
|
|
|
_, err = acmeClient.Register(testCtx, acct, func(tosURL string) bool { return true })
|
|
|
|
require.ErrorIs(t, err, acme.ErrAccountAlreadyExists,
|
|
|
|
"We should have returned a 200 status code which would have triggered an error in the golang acme"+
|
|
|
|
" library")
|
|
|
|
|
|
|
|
// Update contact
|
2023-04-14 14:54:48 +00:00
|
|
|
t.Logf("Testing Update account contacts on %s", baseAcmeURL)
|
2023-04-12 13:05:42 +00:00
|
|
|
acct.Contact = []string{"mailto:test3@example.com"}
|
|
|
|
acct2, err := acmeClient.UpdateReg(testCtx, acct)
|
|
|
|
require.NoError(t, err, "failed updating account")
|
|
|
|
require.Equal(t, acme.StatusValid, acct2.Status)
|
|
|
|
// We should get this back, not the original values.
|
|
|
|
require.Contains(t, acct2.Contact, "mailto:test3@example.com")
|
|
|
|
require.Len(t, acct2.Contact, 1)
|
|
|
|
|
2023-04-21 13:38:06 +00:00
|
|
|
// Make sure order's do not accept dates
|
|
|
|
_, err = acmeClient.AuthorizeOrder(testCtx, []acme.AuthzID{{Type: "dns", Value: "localhost"}},
|
|
|
|
acme.WithOrderNotBefore(time.Now().Add(10*time.Minute)))
|
|
|
|
require.Error(t, err, "should have rejected a new order with NotBefore set")
|
|
|
|
|
|
|
|
_, err = acmeClient.AuthorizeOrder(testCtx, []acme.AuthzID{{Type: "dns", Value: "localhost"}},
|
|
|
|
acme.WithOrderNotAfter(time.Now().Add(10*time.Minute)))
|
|
|
|
require.Error(t, err, "should have rejected a new order with NotAfter set")
|
|
|
|
|
2023-04-21 16:54:19 +00:00
|
|
|
// Make sure DNS identifiers cannot include IP addresses
|
|
|
|
_, err = acmeClient.AuthorizeOrder(testCtx, []acme.AuthzID{{Type: "dns", Value: "127.0.0.1"}},
|
|
|
|
acme.WithOrderNotAfter(time.Now().Add(10*time.Minute)))
|
|
|
|
require.Error(t, err, "should have rejected a new order with IP-like DNS-type identifier")
|
|
|
|
_, err = acmeClient.AuthorizeOrder(testCtx, []acme.AuthzID{{Type: "dns", Value: "*.127.0.0.1"}},
|
|
|
|
acme.WithOrderNotAfter(time.Now().Add(10*time.Minute)))
|
|
|
|
require.Error(t, err, "should have rejected a new order with IP-like DNS-type identifier")
|
|
|
|
|
2023-04-14 14:54:48 +00:00
|
|
|
// Create an order
|
|
|
|
t.Logf("Testing Authorize Order on %s", baseAcmeURL)
|
2023-04-21 16:54:19 +00:00
|
|
|
identifiers := []string{"localhost", "*.localhost"}
|
|
|
|
createOrder, err := acmeClient.AuthorizeOrder(testCtx, []acme.AuthzID{
|
|
|
|
{Type: "dns", Value: identifiers[0]},
|
|
|
|
{Type: "dns", Value: identifiers[1]},
|
|
|
|
})
|
2023-04-14 14:54:48 +00:00
|
|
|
require.NoError(t, err, "failed creating order")
|
|
|
|
require.Equal(t, acme.StatusPending, createOrder.Status)
|
2023-04-17 17:52:54 +00:00
|
|
|
require.Empty(t, createOrder.CertURL)
|
|
|
|
require.Equal(t, createOrder.URI+"/finalize", createOrder.FinalizeURL)
|
2023-04-21 16:54:19 +00:00
|
|
|
require.Len(t, createOrder.AuthzURLs, 2, "expected two authzurls")
|
2023-04-14 14:54:48 +00:00
|
|
|
|
2023-04-17 17:52:54 +00:00
|
|
|
// Get order
|
2023-04-14 14:54:48 +00:00
|
|
|
t.Logf("Testing GetOrder on %s", baseAcmeURL)
|
|
|
|
getOrder, err := acmeClient.GetOrder(testCtx, createOrder.URI)
|
|
|
|
require.NoError(t, err, "failed fetching order")
|
|
|
|
require.Equal(t, acme.StatusPending, createOrder.Status)
|
|
|
|
if diffs := deep.Equal(createOrder, getOrder); diffs != nil {
|
|
|
|
t.Fatalf("Differences exist between create and get order: \n%v", strings.Join(diffs, "\n"))
|
|
|
|
}
|
|
|
|
|
2023-04-17 17:52:54 +00:00
|
|
|
// Load authorization
|
|
|
|
auth, err := acmeClient.GetAuthorization(testCtx, getOrder.AuthzURLs[0])
|
|
|
|
require.NoError(t, err, "failed fetching authorization")
|
|
|
|
require.Equal(t, acme.StatusPending, auth.Status)
|
|
|
|
require.Equal(t, "dns", auth.Identifier.Type)
|
2023-04-19 16:31:19 +00:00
|
|
|
require.Equal(t, "localhost", auth.Identifier.Value)
|
2023-04-17 17:52:54 +00:00
|
|
|
require.False(t, auth.Wildcard, "should not be a wildcard")
|
|
|
|
require.True(t, auth.Expires.IsZero(), "authorization should only have expiry set on valid status")
|
|
|
|
|
2023-04-21 16:54:19 +00:00
|
|
|
require.Len(t, auth.Challenges, 2, "expected two challenges")
|
2023-04-17 17:52:54 +00:00
|
|
|
require.Equal(t, acme.StatusPending, auth.Challenges[0].Status)
|
|
|
|
require.True(t, auth.Challenges[0].Validated.IsZero(), "validated time should be 0 on challenge")
|
|
|
|
require.Equal(t, "http-01", auth.Challenges[0].Type)
|
2023-04-17 19:23:04 +00:00
|
|
|
require.NotEmpty(t, auth.Challenges[0].Token, "missing challenge token")
|
2023-04-21 16:54:19 +00:00
|
|
|
require.Equal(t, acme.StatusPending, auth.Challenges[1].Status)
|
|
|
|
require.True(t, auth.Challenges[1].Validated.IsZero(), "validated time should be 0 on challenge")
|
|
|
|
require.Equal(t, "dns-01", auth.Challenges[1].Type)
|
|
|
|
require.NotEmpty(t, auth.Challenges[1].Token, "missing challenge token")
|
2023-04-17 17:52:54 +00:00
|
|
|
|
2023-04-19 16:31:19 +00:00
|
|
|
// Load a challenge directly; this triggers validation to start.
|
2023-04-17 17:52:54 +00:00
|
|
|
challenge, err := acmeClient.GetChallenge(testCtx, auth.Challenges[0].URI)
|
|
|
|
require.NoError(t, err, "failed to load challenge")
|
2023-04-19 16:31:19 +00:00
|
|
|
require.Equal(t, acme.StatusProcessing, challenge.Status)
|
2023-04-17 17:52:54 +00:00
|
|
|
require.True(t, challenge.Validated.IsZero(), "validated time should be 0 on challenge")
|
|
|
|
require.Equal(t, "http-01", challenge.Type)
|
|
|
|
|
2023-04-17 19:23:04 +00:00
|
|
|
require.NotEmpty(t, challenge.Token, "missing challenge token")
|
2023-04-17 17:52:54 +00:00
|
|
|
|
2023-04-21 13:38:06 +00:00
|
|
|
// HACK: Update authorization/challenge to completed as we can't really do it properly in this workflow
|
|
|
|
// test.
|
|
|
|
pkiMount := findStorageMountUuid(t, client, "pki")
|
|
|
|
accountId := acct.URI[strings.LastIndex(acct.URI, "/"):]
|
2023-04-21 16:54:19 +00:00
|
|
|
for _, authURI := range getOrder.AuthzURLs {
|
|
|
|
authId := authURI[strings.LastIndex(authURI, "/"):]
|
|
|
|
|
|
|
|
rawPath := path.Join("/sys/raw/logical/", pkiMount, getAuthorizationPath(accountId, authId))
|
|
|
|
resp, err := client.Logical().ReadWithContext(testCtx, rawPath)
|
|
|
|
require.NoError(t, err, "failed looking up authorization storage")
|
|
|
|
require.NotNil(t, resp, "sys raw response was nil")
|
|
|
|
require.NotEmpty(t, resp.Data["value"], "no value field in sys raw response")
|
|
|
|
|
|
|
|
var authz ACMEAuthorization
|
|
|
|
err = jsonutil.DecodeJSON([]byte(resp.Data["value"].(string)), &authz)
|
|
|
|
require.NoError(t, err, "error decoding authorization: %w", err)
|
|
|
|
authz.Status = ACMEAuthorizationValid
|
|
|
|
for _, challenge := range authz.Challenges {
|
|
|
|
challenge.Status = ACMEChallengeValid
|
|
|
|
}
|
|
|
|
|
|
|
|
encodeJSON, err := jsonutil.EncodeJSON(authz)
|
|
|
|
require.NoError(t, err, "failed encoding authz json")
|
|
|
|
_, err = client.Logical().WriteWithContext(testCtx, rawPath, map[string]interface{}{
|
|
|
|
"value": base64.StdEncoding.EncodeToString(encodeJSON),
|
|
|
|
"encoding": "base64",
|
|
|
|
})
|
|
|
|
require.NoError(t, err, "failed writing authorization storage")
|
2023-04-21 13:38:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure sending a CSR with the account key gets rejected.
|
|
|
|
goodCr := &x509.CertificateRequest{
|
2023-04-21 16:54:19 +00:00
|
|
|
Subject: pkix.Name{CommonName: identifiers[1]},
|
|
|
|
DNSNames: []string{identifiers[0], identifiers[1]},
|
2023-04-21 13:38:06 +00:00
|
|
|
}
|
2023-04-21 16:54:19 +00:00
|
|
|
t.Logf("csr: %v", goodCr)
|
2023-04-21 13:38:06 +00:00
|
|
|
|
|
|
|
// We want to make sure people are not using the same keys for CSR/Certs and their ACME account.
|
|
|
|
csrSignedWithAccountKey, err := x509.CreateCertificateRequest(rand.Reader, goodCr, accountKey)
|
|
|
|
require.NoError(t, err, "failed generating csr")
|
|
|
|
_, _, err = acmeClient.CreateOrderCert(testCtx, createOrder.FinalizeURL, csrSignedWithAccountKey, true)
|
|
|
|
require.Error(t, err, "should not be allowed to use the account key for a CSR")
|
|
|
|
|
|
|
|
csrKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
|
|
require.NoError(t, err, "failed generated key for CSR")
|
|
|
|
|
|
|
|
// Validate we reject CSRs that contain names that aren't in the original order
|
|
|
|
badCr := &x509.CertificateRequest{
|
|
|
|
Subject: pkix.Name{CommonName: createOrder.Identifiers[0].Value},
|
|
|
|
DNSNames: []string{"www.notinorder.com"},
|
|
|
|
}
|
|
|
|
|
|
|
|
csrWithBadName, err := x509.CreateCertificateRequest(rand.Reader, badCr, csrKey)
|
|
|
|
require.NoError(t, err, "failed generating csr with bad name")
|
|
|
|
|
|
|
|
_, _, err = acmeClient.CreateOrderCert(testCtx, createOrder.FinalizeURL, csrWithBadName, true)
|
|
|
|
require.Error(t, err, "should not be allowed to csr with different names than order")
|
|
|
|
|
2023-04-21 16:54:19 +00:00
|
|
|
// Validate we reject CSRs that contains fewer names than in the original order.
|
|
|
|
badCr = &x509.CertificateRequest{
|
|
|
|
Subject: pkix.Name{CommonName: identifiers[0]},
|
|
|
|
}
|
|
|
|
|
|
|
|
csrWithBadName, err = x509.CreateCertificateRequest(rand.Reader, badCr, csrKey)
|
|
|
|
require.NoError(t, err, "failed generating csr with bad name")
|
|
|
|
|
|
|
|
_, _, err = acmeClient.CreateOrderCert(testCtx, createOrder.FinalizeURL, csrWithBadName, true)
|
|
|
|
require.Error(t, err, "should not be allowed to csr with different names than order")
|
|
|
|
|
2023-04-21 13:38:06 +00:00
|
|
|
// Finally test a proper CSR, with the correct name and signed with a different key works.
|
|
|
|
csr, err := x509.CreateCertificateRequest(rand.Reader, goodCr, csrKey)
|
|
|
|
require.NoError(t, err, "failed generating csr")
|
|
|
|
|
|
|
|
certs, _, err := acmeClient.CreateOrderCert(testCtx, createOrder.FinalizeURL, csr, true)
|
|
|
|
require.NoError(t, err, "failed finalizing order")
|
|
|
|
require.Len(t, certs, 3, "expected three items within the returned certs")
|
|
|
|
|
|
|
|
testAcmeCertSignedByCa(t, client, certs, "int-ca")
|
|
|
|
|
2023-04-12 13:05:42 +00:00
|
|
|
// Deactivate account
|
2023-04-14 14:54:48 +00:00
|
|
|
t.Logf("Testing deactivate account on %s", baseAcmeURL)
|
2023-04-12 13:05:42 +00:00
|
|
|
err = acmeClient.DeactivateReg(testCtx)
|
|
|
|
require.NoError(t, err, "failed deactivating account")
|
|
|
|
|
|
|
|
// Make sure we get an unauthorized error trying to update the account again.
|
2023-04-14 14:54:48 +00:00
|
|
|
t.Logf("Testing update on deactivated account fails on %s", baseAcmeURL)
|
2023-04-12 13:05:42 +00:00
|
|
|
_, err = acmeClient.UpdateReg(testCtx, acct)
|
|
|
|
require.Error(t, err, "expected account to be deactivated")
|
|
|
|
require.IsType(t, &acme.Error{}, err, "expected acme error type")
|
|
|
|
acmeErr := err.(*acme.Error)
|
|
|
|
require.Equal(t, "urn:ietf:params:acme:error:unauthorized", acmeErr.ProblemType)
|
2023-03-29 16:29:19 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-03 13:38:20 +00:00
|
|
|
// TestAcmeNonce a basic test that will validate we get back a nonce with the proper status codes
|
|
|
|
// based on the
|
2023-03-29 18:22:48 +00:00
|
|
|
func TestAcmeNonce(t *testing.T) {
|
|
|
|
t.Parallel()
|
2023-04-03 13:38:20 +00:00
|
|
|
cluster, client, pathConfig := setupAcmeBackend(t)
|
|
|
|
defer cluster.Cleanup()
|
2023-03-29 18:22:48 +00:00
|
|
|
|
|
|
|
cases := []struct {
|
|
|
|
name string
|
|
|
|
prefixUrl string
|
|
|
|
directoryUrl string
|
|
|
|
}{
|
2023-04-03 13:38:20 +00:00
|
|
|
{"root", "", "pki/acme/new-nonce"},
|
|
|
|
{"role", "/roles/test-role", "pki/roles/test-role/acme/new-nonce"},
|
|
|
|
{"issuer", "/issuer/default", "pki/issuer/default/acme/new-nonce"},
|
|
|
|
{"issuer_role", "/issuer/default/roles/test-role", "pki/issuer/default/roles/test-role/acme/new-nonce"},
|
2023-03-29 18:22:48 +00:00
|
|
|
}
|
2023-04-03 13:38:20 +00:00
|
|
|
|
2023-03-29 18:22:48 +00:00
|
|
|
for _, tc := range cases {
|
|
|
|
for _, httpOp := range []string{"get", "header"} {
|
|
|
|
t.Run(fmt.Sprintf("%s-%s", tc.name, httpOp), func(t *testing.T) {
|
2023-04-03 13:38:20 +00:00
|
|
|
var req *api.Request
|
2023-03-29 18:22:48 +00:00
|
|
|
switch httpOp {
|
|
|
|
case "get":
|
2023-04-03 13:38:20 +00:00
|
|
|
req = client.NewRequest(http.MethodGet, "/v1/"+tc.directoryUrl)
|
2023-03-29 18:22:48 +00:00
|
|
|
case "header":
|
2023-04-03 13:38:20 +00:00
|
|
|
req = client.NewRequest(http.MethodHead, "/v1/"+tc.directoryUrl)
|
2023-03-29 18:22:48 +00:00
|
|
|
}
|
2023-04-03 13:38:20 +00:00
|
|
|
res, err := client.RawRequestWithContext(ctx, req)
|
|
|
|
require.NoError(t, err, "failed sending raw request")
|
|
|
|
_ = res.Body.Close()
|
2023-03-29 18:22:48 +00:00
|
|
|
|
|
|
|
// Proper Status Code
|
|
|
|
switch httpOp {
|
|
|
|
case "get":
|
2023-04-03 13:38:20 +00:00
|
|
|
require.Equal(t, http.StatusNoContent, res.StatusCode)
|
2023-03-29 18:22:48 +00:00
|
|
|
case "header":
|
2023-04-03 13:38:20 +00:00
|
|
|
require.Equal(t, http.StatusOK, res.StatusCode)
|
2023-03-29 18:22:48 +00:00
|
|
|
}
|
|
|
|
|
2023-04-03 13:38:20 +00:00
|
|
|
// Make sure we don't have a Content-Type header.
|
|
|
|
require.Equal(t, "", res.Header.Get("Content-Type"))
|
|
|
|
|
2023-03-29 18:22:48 +00:00
|
|
|
// Make sure we return the Cache-Control header
|
2023-04-03 13:38:20 +00:00
|
|
|
require.Contains(t, res.Header.Get("Cache-Control"), "no-store",
|
2023-03-29 18:22:48 +00:00
|
|
|
"missing Cache-Control header with no-store header value")
|
|
|
|
|
|
|
|
// Test for our nonce header value
|
2023-04-03 13:38:20 +00:00
|
|
|
require.NotEmpty(t, res.Header.Get("Replay-Nonce"), "missing Replay-Nonce header with an actual value")
|
2023-03-29 18:22:48 +00:00
|
|
|
|
|
|
|
// Test Link header value
|
|
|
|
expectedLinkHeader := fmt.Sprintf("<%s>;rel=\"index\"", pathConfig+tc.prefixUrl+"/acme/directory")
|
2023-04-03 13:38:20 +00:00
|
|
|
require.Contains(t, res.Header.Get("Link"), expectedLinkHeader,
|
2023-03-29 18:22:48 +00:00
|
|
|
"different value for link header than expected")
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-29 16:29:19 +00:00
|
|
|
// TestAcmeClusterPathNotConfigured basic testing of the ACME error handler.
|
|
|
|
func TestAcmeClusterPathNotConfigured(t *testing.T) {
|
|
|
|
t.Parallel()
|
2023-04-03 13:38:20 +00:00
|
|
|
cluster, client := setupTestPkiCluster(t)
|
|
|
|
defer cluster.Cleanup()
|
2023-03-29 16:29:19 +00:00
|
|
|
|
|
|
|
// Do not fill in the path option within the local cluster configuration
|
|
|
|
cases := []struct {
|
|
|
|
name string
|
|
|
|
directoryUrl string
|
|
|
|
}{
|
2023-04-03 13:38:20 +00:00
|
|
|
{"root", "pki/acme/directory"},
|
|
|
|
{"role", "pki/roles/test-role/acme/directory"},
|
|
|
|
{"issuer", "pki/issuer/default/acme/directory"},
|
|
|
|
{"issuer_role", "pki/issuer/default/roles/test-role/acme/directory"},
|
2023-03-29 16:29:19 +00:00
|
|
|
}
|
2023-04-03 13:38:20 +00:00
|
|
|
testCtx := context.Background()
|
|
|
|
|
2023-03-29 16:29:19 +00:00
|
|
|
for _, tc := range cases {
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
2023-04-03 13:38:20 +00:00
|
|
|
dirResp, err := client.Logical().ReadRawWithContext(testCtx, tc.directoryUrl)
|
|
|
|
require.Error(t, err, "expected failure reading ACME directory configuration got none")
|
2023-03-29 16:29:19 +00:00
|
|
|
|
2023-04-03 13:38:20 +00:00
|
|
|
require.Equal(t, "application/problem+json", dirResp.Header.Get("Content-Type"))
|
|
|
|
require.Equal(t, http.StatusInternalServerError, dirResp.StatusCode)
|
2023-03-29 16:29:19 +00:00
|
|
|
|
2023-04-03 13:38:20 +00:00
|
|
|
rawBodyBytes, err := io.ReadAll(dirResp.Body)
|
|
|
|
require.NoError(t, err, "failed reading from directory response body")
|
|
|
|
_ = dirResp.Body.Close()
|
2023-03-29 16:29:19 +00:00
|
|
|
|
|
|
|
respType := map[string]interface{}{}
|
|
|
|
err = json.Unmarshal(rawBodyBytes, &respType)
|
|
|
|
require.NoError(t, err, "failed unmarshalling ACME directory response body")
|
|
|
|
|
|
|
|
require.Equal(t, "urn:ietf:params:acme:error:serverInternal", respType["type"])
|
|
|
|
require.NotEmpty(t, respType["detail"])
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
2023-03-29 18:22:48 +00:00
|
|
|
|
2023-04-03 13:38:20 +00:00
|
|
|
func setupAcmeBackend(t *testing.T) (*vault.TestCluster, *api.Client, string) {
|
|
|
|
cluster, client := setupTestPkiCluster(t)
|
2023-03-29 18:22:48 +00:00
|
|
|
|
|
|
|
// Setting templated AIAs should succeed.
|
2023-04-12 13:05:42 +00:00
|
|
|
pathConfig := client.Address() + "/v1/pki"
|
2023-03-29 18:22:48 +00:00
|
|
|
|
2023-04-03 13:38:20 +00:00
|
|
|
_, err := client.Logical().WriteWithContext(context.Background(), "pki/config/cluster", map[string]interface{}{
|
2023-03-29 18:22:48 +00:00
|
|
|
"path": pathConfig,
|
|
|
|
"aia_path": "http://localhost:8200/cdn/pki",
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
2023-04-03 13:38:20 +00:00
|
|
|
|
|
|
|
// Allow certain headers to pass through for ACME support
|
|
|
|
_, err = client.Logical().WriteWithContext(context.Background(), "sys/mounts/pki/tune", map[string]interface{}{
|
2023-04-12 13:05:42 +00:00
|
|
|
"allowed_response_headers": []string{"Last-Modified", "Replay-Nonce", "Link", "Location"},
|
2023-04-03 13:38:20 +00:00
|
|
|
})
|
|
|
|
require.NoError(t, err, "failed tuning mount response headers")
|
|
|
|
|
2023-04-21 13:38:06 +00:00
|
|
|
_, err = client.Logical().WriteWithContext(context.Background(), "/pki/issuers/generate/root/internal", map[string]interface{}{
|
|
|
|
"issuer_name": "root-ca",
|
|
|
|
"key_name": "root-key",
|
|
|
|
"key_type": "ec",
|
|
|
|
"common_name": "root.com",
|
|
|
|
"ttl": "10h",
|
|
|
|
})
|
|
|
|
require.NoError(t, err, "failed creating root CA")
|
|
|
|
|
|
|
|
resp, err := client.Logical().WriteWithContext(context.Background(), "/pki/issuers/generate/intermediate/internal",
|
|
|
|
map[string]interface{}{
|
|
|
|
"key_name": "int-key",
|
|
|
|
"key_type": "ec",
|
|
|
|
"common_name": "test.com",
|
|
|
|
})
|
|
|
|
require.NoError(t, err, "failed creating intermediary CSR")
|
|
|
|
intermediateCSR := resp.Data["csr"].(string)
|
|
|
|
|
|
|
|
// Sign the intermediate CSR using /pki
|
|
|
|
resp, err = client.Logical().Write("pki/issuer/root-ca/sign-intermediate", map[string]interface{}{
|
|
|
|
"csr": intermediateCSR,
|
|
|
|
"ttl": "5h",
|
|
|
|
})
|
|
|
|
require.NoError(t, err, "failed signing intermediary CSR")
|
|
|
|
intermediateCertPEM := resp.Data["certificate"].(string)
|
|
|
|
|
|
|
|
// Configure the intermediate cert as the CA in /pki2
|
|
|
|
resp, err = client.Logical().Write("/pki/issuers/import/cert", map[string]interface{}{
|
|
|
|
"pem_bundle": intermediateCertPEM,
|
|
|
|
})
|
|
|
|
require.NoError(t, err, "failed importing intermediary cert")
|
|
|
|
importedIssuersRaw := resp.Data["imported_issuers"].([]interface{})
|
|
|
|
require.Len(t, importedIssuersRaw, 1)
|
|
|
|
intCaUuid := importedIssuersRaw[0].(string)
|
|
|
|
|
|
|
|
_, err = client.Logical().Write("/pki/issuer/"+intCaUuid, map[string]interface{}{
|
|
|
|
"issuer_name": "int-ca",
|
|
|
|
})
|
|
|
|
|
|
|
|
_, err = client.Logical().Write("/pki/config/issuers", map[string]interface{}{
|
|
|
|
"default": "int-ca",
|
|
|
|
})
|
|
|
|
|
2023-04-03 13:38:20 +00:00
|
|
|
return cluster, client, pathConfig
|
|
|
|
}
|
|
|
|
|
2023-04-21 13:38:06 +00:00
|
|
|
func testAcmeCertSignedByCa(t *testing.T, client *api.Client, derCerts [][]byte, issuerRef string) {
|
|
|
|
t.Helper()
|
|
|
|
require.NotEmpty(t, derCerts)
|
|
|
|
acmeCert, err := x509.ParseCertificate(derCerts[0])
|
|
|
|
require.NoError(t, err, "failed parsing acme cert bytes")
|
|
|
|
|
|
|
|
resp, err := client.Logical().ReadWithContext(context.Background(), "pki/issuer/"+issuerRef)
|
|
|
|
require.NoError(t, err, "failed reading issuer with name %s", issuerRef)
|
|
|
|
issuerCert := parseCert(t, resp.Data["certificate"].(string))
|
|
|
|
issuerChainRaw := resp.Data["ca_chain"].([]interface{})
|
|
|
|
|
|
|
|
err = acmeCert.CheckSignatureFrom(issuerCert)
|
|
|
|
require.NoError(t, err, "issuer %s did not sign provided cert", issuerRef)
|
|
|
|
|
|
|
|
expectedCerts := [][]byte{derCerts[0]}
|
|
|
|
|
|
|
|
for _, entry := range issuerChainRaw {
|
|
|
|
chainCert := parseCert(t, entry.(string))
|
|
|
|
expectedCerts = append(expectedCerts, chainCert.Raw)
|
|
|
|
}
|
|
|
|
|
|
|
|
if diffs := deep.Equal(expectedCerts, derCerts); diffs != nil {
|
|
|
|
t.Fatalf("diffs were found between the acme chain returned and the expected value: \n%v", diffs)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-03 13:38:20 +00:00
|
|
|
func setupTestPkiCluster(t *testing.T) (*vault.TestCluster, *api.Client) {
|
|
|
|
coreConfig := &vault.CoreConfig{
|
|
|
|
LogicalBackends: map[string]logical.Factory{
|
|
|
|
"pki": Factory,
|
|
|
|
},
|
2023-04-21 13:38:06 +00:00
|
|
|
EnableRaw: true,
|
2023-04-03 13:38:20 +00:00
|
|
|
}
|
|
|
|
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
|
|
|
|
HandlerFunc: vaulthttp.Handler,
|
|
|
|
})
|
|
|
|
cluster.Start()
|
|
|
|
client := cluster.Cores[0].Client
|
|
|
|
mountPKIEndpoint(t, client, "pki")
|
|
|
|
return cluster, client
|
2023-03-29 18:22:48 +00:00
|
|
|
}
|
2023-04-12 13:05:42 +00:00
|
|
|
|
|
|
|
func getAcmeClientForCluster(t *testing.T, cluster *vault.TestCluster, baseUrl string, key crypto.Signer) acme.Client {
|
|
|
|
coreAddr := cluster.Cores[0].Listeners[0].Address
|
|
|
|
tlsConfig := cluster.Cores[0].TLSConfig()
|
|
|
|
|
|
|
|
transport := cleanhttp.DefaultPooledTransport()
|
|
|
|
transport.TLSClientConfig = tlsConfig.Clone()
|
|
|
|
if err := http2.ConfigureTransport(transport); err != nil {
|
|
|
|
t.Fatal(err)
|
|
|
|
}
|
|
|
|
httpClient := &http.Client{Transport: transport}
|
|
|
|
if baseUrl[0] == '/' {
|
|
|
|
baseUrl = baseUrl[1:]
|
|
|
|
}
|
|
|
|
if !strings.HasPrefix(baseUrl, "v1/") {
|
|
|
|
baseUrl = "v1/" + baseUrl
|
|
|
|
}
|
|
|
|
baseAcmeURL := fmt.Sprintf("https://%s/%s", coreAddr.String(), baseUrl)
|
|
|
|
return acme.Client{
|
|
|
|
Key: key,
|
|
|
|
HTTPClient: httpClient,
|
|
|
|
DirectoryURL: baseAcmeURL + "directory",
|
|
|
|
}
|
|
|
|
}
|