mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-18 04:27:02 +02:00
* Adds automated ACME tests using Caddy. * Do not use CheckSignatureFrom method to validate TLS-ALPN-01 challenges * Uncomment TLS-ALPN test. * Fix validation of tls-alpn-01 keyAuthz Surprisingly, this failure was not caught by our earlier, but unmerged acme.sh tests: > 2023-06-07T19:35:27.6963070Z [32mPASS[0m builtin/logical/pkiext/pkiext_binary.Test_ACME/group/acme.sh_tls-alpn (33.06s) from https://github.com/hashicorp/vault/pull/20987. Notably, we had two failures: 1. The extension's raw value is not used, but is instead an OCTET STRING encoded version: > The extension has the following ASN.1 [X.680] format : > > Authorization ::= OCTET STRING (SIZE (32)) > > The extnValue of the id-pe-acmeIdentifier extension is the ASN.1 > DER encoding [X.690] of the Authorization structure, which > contains the SHA-256 digest of the key authorization for the > challenge. 2. Unlike DNS, the SHA-256 is directly embedded in the authorization, as evidenced by the `SIZE (32)` annotation in the quote above: we were instead expecting this to be url base-64 encoded, which would have a different size. This failure was caught by Matt, testing with Caddy. :-) Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Quick gofmt run. * Fix challenge encoding in TLS-ALPN-01 challenge tests * Rename a PKI test helper that retrieves the Vault cluster listener's cert to distinguish it from the method that retrieves the PKI mount's CA cert. Combine a couple of Docker file copy commands into one. --------- Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> Co-authored-by: Steve Clark <steven.clark@hashicorp.com> Co-authored-by: Alexander Scheel <alex.scheel@hashicorp.com>
317 lines
8.6 KiB
Go
317 lines
8.6 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package pkiext_binary
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/vault/api"
|
|
"github.com/hashicorp/vault/builtin/logical/pki/dnstest"
|
|
dockhelper "github.com/hashicorp/vault/sdk/helper/docker"
|
|
"github.com/hashicorp/vault/sdk/helper/testcluster"
|
|
"github.com/hashicorp/vault/sdk/helper/testcluster/docker"
|
|
)
|
|
|
|
type VaultPkiCluster struct {
|
|
cluster *docker.DockerCluster
|
|
Dns *dnstest.TestServer
|
|
}
|
|
|
|
func NewVaultPkiCluster(t *testing.T) *VaultPkiCluster {
|
|
binary := os.Getenv("VAULT_BINARY")
|
|
if binary == "" {
|
|
t.Skip("only running docker test when $VAULT_BINARY present")
|
|
}
|
|
|
|
opts := &docker.DockerClusterOptions{
|
|
ImageRepo: "docker.mirror.hashicorp.services/hashicorp/vault",
|
|
// We're replacing the binary anyway, so we're not too particular about
|
|
// the docker image version tag.
|
|
ImageTag: "latest",
|
|
VaultBinary: binary,
|
|
ClusterOptions: testcluster.ClusterOptions{
|
|
VaultNodeConfig: &testcluster.VaultNodeConfig{
|
|
LogLevel: "TRACE",
|
|
},
|
|
NumCores: 3,
|
|
},
|
|
}
|
|
|
|
cluster := docker.NewTestDockerCluster(t, opts)
|
|
|
|
return &VaultPkiCluster{cluster: cluster}
|
|
}
|
|
|
|
func NewVaultPkiClusterWithDNS(t *testing.T) *VaultPkiCluster {
|
|
cluster := NewVaultPkiCluster(t)
|
|
dns := dnstest.SetupResolverOnNetwork(t, "dadgarcorp.com", cluster.GetContainerNetworkName())
|
|
cluster.Dns = dns
|
|
return cluster
|
|
}
|
|
|
|
func (vpc *VaultPkiCluster) Cleanup() {
|
|
vpc.cluster.Cleanup()
|
|
if vpc.Dns != nil {
|
|
vpc.Dns.Cleanup()
|
|
}
|
|
}
|
|
|
|
func (vpc *VaultPkiCluster) GetActiveClusterNode() *docker.DockerClusterNode {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
|
|
node, err := testcluster.WaitForActiveNode(ctx, vpc.cluster)
|
|
if err != nil {
|
|
panic(fmt.Sprintf("no cluster node became active in timeout window: %v", err))
|
|
}
|
|
|
|
return vpc.cluster.ClusterNodes[node]
|
|
}
|
|
|
|
func (vpc *VaultPkiCluster) GetNonActiveNodes() []*docker.DockerClusterNode {
|
|
nodes := []*docker.DockerClusterNode{}
|
|
for _, node := range vpc.cluster.ClusterNodes {
|
|
leader, err := node.APIClient().Sys().Leader()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if !leader.IsSelf {
|
|
nodes = append(nodes, node)
|
|
}
|
|
}
|
|
|
|
return nodes
|
|
}
|
|
|
|
func (vpc *VaultPkiCluster) GetActiveContainerHostPort() string {
|
|
return vpc.GetActiveClusterNode().HostPort
|
|
}
|
|
|
|
func (vpc *VaultPkiCluster) GetContainerNetworkName() string {
|
|
return vpc.cluster.ClusterNodes[0].ContainerNetworkName
|
|
}
|
|
|
|
func (vpc *VaultPkiCluster) GetActiveContainerIP() string {
|
|
return vpc.GetActiveClusterNode().ContainerIPAddress
|
|
}
|
|
|
|
func (vpc *VaultPkiCluster) GetActiveContainerID() string {
|
|
return vpc.GetActiveClusterNode().Container.ID
|
|
}
|
|
|
|
func (vpc *VaultPkiCluster) GetActiveNode() *api.Client {
|
|
return vpc.GetActiveClusterNode().APIClient()
|
|
}
|
|
|
|
// GetListenerCACertPEM returns the Vault cluster's PEM-encoded CA certificate.
|
|
func (vpc *VaultPkiCluster) GetListenerCACertPEM() []byte {
|
|
return vpc.cluster.CACertPEM
|
|
}
|
|
|
|
func (vpc *VaultPkiCluster) AddHostname(hostname, ip string) error {
|
|
if vpc.Dns != nil {
|
|
vpc.Dns.AddRecord(hostname, "A", ip)
|
|
vpc.Dns.PushConfig()
|
|
return nil
|
|
} else {
|
|
return vpc.AddNameToHostFiles(hostname, ip)
|
|
}
|
|
}
|
|
|
|
func (vpc *VaultPkiCluster) AddNameToHostFiles(hostname, ip string) error {
|
|
updateHostsCmd := []string{
|
|
"sh", "-c",
|
|
"echo '" + ip + " " + hostname + "' >> /etc/hosts",
|
|
}
|
|
for _, node := range vpc.cluster.ClusterNodes {
|
|
containerID := node.Container.ID
|
|
_, _, retcode, err := dockhelper.RunCmdWithOutput(vpc.cluster.DockerAPI, context.Background(), containerID, updateHostsCmd)
|
|
if err != nil {
|
|
return fmt.Errorf("failed updating container %s host file: %w", containerID, err)
|
|
}
|
|
|
|
if retcode != 0 {
|
|
return fmt.Errorf("expected zero retcode from updating vault host file in container %s got: %d", containerID, retcode)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (vpc *VaultPkiCluster) AddDNSRecord(hostname, recordType, ip string) error {
|
|
if vpc.Dns == nil {
|
|
return fmt.Errorf("no DNS server was provisioned on this cluster group; unable to provision custom records")
|
|
}
|
|
|
|
vpc.Dns.AddRecord(hostname, recordType, ip)
|
|
vpc.Dns.PushConfig()
|
|
return nil
|
|
}
|
|
|
|
func (vpc *VaultPkiCluster) RemoveDNSRecord(domain string, record string, value string) error {
|
|
if vpc.Dns == nil {
|
|
return fmt.Errorf("no DNS server was provisioned on this cluster group; unable to remove specific record")
|
|
}
|
|
|
|
vpc.Dns.RemoveRecord(domain, record, value)
|
|
return nil
|
|
}
|
|
|
|
func (vpc *VaultPkiCluster) RemoveDNSRecordsOfTypeForDomain(domain string, record string) error {
|
|
if vpc.Dns == nil {
|
|
return fmt.Errorf("no DNS server was provisioned on this cluster group; unable to remove all records of type")
|
|
}
|
|
|
|
vpc.Dns.RemoveRecordsOfTypeForDomain(domain, record)
|
|
return nil
|
|
}
|
|
|
|
func (vpc *VaultPkiCluster) RemoveDNSRecordsForDomain(domain string) error {
|
|
if vpc.Dns == nil {
|
|
return fmt.Errorf("no DNS server was provisioned on this cluster group; unable to remove records for domain")
|
|
}
|
|
|
|
vpc.Dns.RemoveRecordsForDomain(domain)
|
|
return nil
|
|
}
|
|
|
|
func (vpc *VaultPkiCluster) RemoveAllDNSRecords() error {
|
|
if vpc.Dns == nil {
|
|
return fmt.Errorf("no DNS server was provisioned on this cluster group; unable to remove all records")
|
|
}
|
|
|
|
vpc.Dns.RemoveAllRecords()
|
|
return nil
|
|
}
|
|
|
|
func (vpc *VaultPkiCluster) CreateMount(name string) (*VaultPkiMount, error) {
|
|
err := vpc.GetActiveNode().Sys().Mount(name, &api.MountInput{
|
|
Type: "pki",
|
|
Config: api.MountConfigInput{
|
|
DefaultLeaseTTL: "16h",
|
|
MaxLeaseTTL: "32h",
|
|
AllowedResponseHeaders: []string{
|
|
"Last-Modified", "Replay-Nonce",
|
|
"Link", "Location",
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &VaultPkiMount{
|
|
vpc,
|
|
name,
|
|
}, nil
|
|
}
|
|
|
|
func (vpc *VaultPkiCluster) CreateAcmeMount(mountName string) (*VaultPkiMount, error) {
|
|
pki, err := vpc.CreateMount(mountName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed creating mount %s: %w", mountName, err)
|
|
}
|
|
|
|
err = pki.UpdateClusterConfig(nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed updating cluster config: %w", err)
|
|
}
|
|
|
|
cfg := map[string]interface{}{
|
|
"eab_policy": "not-required",
|
|
}
|
|
if vpc.Dns != nil {
|
|
cfg["dns_resolver"] = vpc.Dns.GetRemoteAddr()
|
|
}
|
|
|
|
err = pki.UpdateAcmeConfig(true, cfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed updating acme config: %w", err)
|
|
}
|
|
|
|
// Setup root+intermediate CA hierarchy within this mount.
|
|
resp, err := pki.GenerateRootInternal(map[string]interface{}{
|
|
"common_name": "Root X1",
|
|
"country": "US",
|
|
"organization": "Dadgarcorp",
|
|
"ou": "QA",
|
|
"key_type": "ec",
|
|
"key_bits": 256,
|
|
"use_pss": false,
|
|
"issuer_name": "root",
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed generating root internal: %w", err)
|
|
}
|
|
if resp == nil || len(resp.Data) == 0 {
|
|
return nil, fmt.Errorf("failed generating root internal: nil or empty response but no error")
|
|
}
|
|
|
|
resp, err = pki.GenerateIntermediateInternal(map[string]interface{}{
|
|
"common_name": "Intermediate I1",
|
|
"country": "US",
|
|
"organization": "Dadgarcorp",
|
|
"ou": "QA",
|
|
"key_type": "ec",
|
|
"key_bits": 256,
|
|
"use_pss": false,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed generating int csr: %w", err)
|
|
}
|
|
if resp == nil || len(resp.Data) == 0 {
|
|
return nil, fmt.Errorf("failed generating int csr: nil or empty response but no error")
|
|
}
|
|
|
|
resp, err = pki.SignIntermediary("default", resp.Data["csr"], map[string]interface{}{
|
|
"common_name": "Intermediate I1",
|
|
"country": "US",
|
|
"organization": "Dadgarcorp",
|
|
"ou": "QA",
|
|
"key_type": "ec",
|
|
"csr": resp.Data["csr"],
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed signing int csr: %w", err)
|
|
}
|
|
if resp == nil || len(resp.Data) == 0 {
|
|
return nil, fmt.Errorf("failed signing int csr: nil or empty response but no error")
|
|
}
|
|
intCert := resp.Data["certificate"].(string)
|
|
|
|
resp, err = pki.ImportBundle(intCert, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed importing signed cert: %w", err)
|
|
}
|
|
if resp == nil || len(resp.Data) == 0 {
|
|
return nil, fmt.Errorf("failed importing signed cert: nil or empty response but no error")
|
|
}
|
|
|
|
err = pki.UpdateDefaultIssuer(resp.Data["imported_issuers"].([]interface{})[0].(string), nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to set intermediate as default: %w", err)
|
|
}
|
|
|
|
err = pki.UpdateIssuer("default", map[string]interface{}{
|
|
"leaf_not_after_behavior": "truncate",
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to update intermediate ttl behavior: %w", err)
|
|
}
|
|
|
|
err = pki.UpdateIssuer("root", map[string]interface{}{
|
|
"leaf_not_after_behavior": "truncate",
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to update root ttl behavior: %w", err)
|
|
}
|
|
|
|
return pki, nil
|
|
}
|