vault/builtin/credential/cert/backend_test.go
Steven Clark 2fe676e75b
Validate OCSP response is signed by expected issuer (#26091)
* Validate OCSP response is signed by expected issuer and serial number matches request

 - There was a bug in the OCSP response signature logic, it properly
   verified but kept around the ocspRes object around so we ignored
   the errors found and passed the response object back up the stack.
 - Now extract the verification logic into a dedicated function, if
   it returns an error, blank the ocspRes response as we can't trust it.
 - Address an issue that the OCSP requests from multiple servers were
   clobbering each others responses as the index loop variable was not
   properly captured.
 - Add a missing validation that the response was for the serial number
    we requested

* Add cl
2024-03-22 09:28:02 -04:00

2941 lines
94 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cert
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"io"
"io/ioutil"
"math/big"
mathrand "math/rand"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"reflect"
"testing"
"time"
"github.com/go-test/deep"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-rootcerts"
"github.com/hashicorp/go-sockaddr"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/builtin/logical/pki"
logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/helper/tokenutil"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/mapstructure"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ocsp"
"golang.org/x/net/http2"
)
const (
serverCertPath = "test-fixtures/cacert.pem"
serverKeyPath = "test-fixtures/cakey.pem"
serverCAPath = serverCertPath
testRootCACertPath1 = "test-fixtures/testcacert1.pem"
testRootCAKeyPath1 = "test-fixtures/testcakey1.pem"
testCertPath1 = "test-fixtures/testissuedcert4.pem"
testKeyPath1 = "test-fixtures/testissuedkey4.pem"
testIssuedCertCRL = "test-fixtures/issuedcertcrl"
testRootCACertPath2 = "test-fixtures/testcacert2.pem"
testRootCAKeyPath2 = "test-fixtures/testcakey2.pem"
testRootCertCRL = "test-fixtures/cacert2crl"
)
func generateTestCertAndConnState(t *testing.T, template *x509.Certificate) (string, tls.ConnectionState, error) {
t.Helper()
tempDir, err := ioutil.TempDir("", "vault-cert-auth-test-")
if err != nil {
t.Fatal(err)
}
t.Logf("test %s, temp dir %s", t.Name(), tempDir)
caCertTemplate := &x509.Certificate{
Subject: pkix.Name{
CommonName: "localhost",
},
DNSNames: []string{"localhost"},
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
KeyUsage: x509.KeyUsage(x509.KeyUsageCertSign | x509.KeyUsageCRLSign),
SerialNumber: big.NewInt(mathrand.Int63()),
NotBefore: time.Now().Add(-30 * time.Second),
NotAfter: time.Now().Add(262980 * time.Hour),
BasicConstraintsValid: true,
IsCA: true,
}
caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
caBytes, err := x509.CreateCertificate(rand.Reader, caCertTemplate, caCertTemplate, caKey.Public(), caKey)
if err != nil {
t.Fatal(err)
}
caCert, err := x509.ParseCertificate(caBytes)
if err != nil {
t.Fatal(err)
}
caCertPEMBlock := &pem.Block{
Type: "CERTIFICATE",
Bytes: caBytes,
}
err = ioutil.WriteFile(filepath.Join(tempDir, "ca_cert.pem"), pem.EncodeToMemory(caCertPEMBlock), 0o755)
if err != nil {
t.Fatal(err)
}
marshaledCAKey, err := x509.MarshalECPrivateKey(caKey)
if err != nil {
t.Fatal(err)
}
caKeyPEMBlock := &pem.Block{
Type: "EC PRIVATE KEY",
Bytes: marshaledCAKey,
}
err = ioutil.WriteFile(filepath.Join(tempDir, "ca_key.pem"), pem.EncodeToMemory(caKeyPEMBlock), 0o755)
if err != nil {
t.Fatal(err)
}
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
certBytes, err := x509.CreateCertificate(rand.Reader, template, caCert, key.Public(), caKey)
if err != nil {
t.Fatal(err)
}
certPEMBlock := &pem.Block{
Type: "CERTIFICATE",
Bytes: certBytes,
}
err = ioutil.WriteFile(filepath.Join(tempDir, "cert.pem"), pem.EncodeToMemory(certPEMBlock), 0o755)
if err != nil {
t.Fatal(err)
}
marshaledKey, err := x509.MarshalECPrivateKey(key)
if err != nil {
t.Fatal(err)
}
keyPEMBlock := &pem.Block{
Type: "EC PRIVATE KEY",
Bytes: marshaledKey,
}
err = ioutil.WriteFile(filepath.Join(tempDir, "key.pem"), pem.EncodeToMemory(keyPEMBlock), 0o755)
if err != nil {
t.Fatal(err)
}
connInfo, err := testConnState(filepath.Join(tempDir, "cert.pem"), filepath.Join(tempDir, "key.pem"), filepath.Join(tempDir, "ca_cert.pem"))
return tempDir, connInfo, err
}
// Unlike testConnState, this method does not use the same 'tls.Config' objects for
// both dialing and listening. Instead, it runs the server without specifying its CA.
// But the client, presents the CA cert of the server to trust the server.
// The client can present a cert and key which is completely independent of server's CA.
// The connection state returned will contain the certificate presented by the client.
func connectionState(serverCAPath, serverCertPath, serverKeyPath, clientCertPath, clientKeyPath string) (tls.ConnectionState, error) {
serverKeyPair, err := tls.LoadX509KeyPair(serverCertPath, serverKeyPath)
if err != nil {
return tls.ConnectionState{}, err
}
// Prepare the listener configuration with server's key pair
listenConf := &tls.Config{
Certificates: []tls.Certificate{serverKeyPair},
ClientAuth: tls.RequestClientCert,
}
clientKeyPair, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath)
if err != nil {
return tls.ConnectionState{}, err
}
// Load the CA cert required by the client to authenticate the server.
rootConfig := &rootcerts.Config{
CAFile: serverCAPath,
}
serverCAs, err := rootcerts.LoadCACerts(rootConfig)
if err != nil {
return tls.ConnectionState{}, err
}
// Prepare the dial configuration that the client uses to establish the connection.
dialConf := &tls.Config{
Certificates: []tls.Certificate{clientKeyPair},
RootCAs: serverCAs,
}
// Start the server.
list, err := tls.Listen("tcp", "127.0.0.1:0", listenConf)
if err != nil {
return tls.ConnectionState{}, err
}
defer list.Close()
// Accept connections.
serverErrors := make(chan error, 1)
connState := make(chan tls.ConnectionState)
go func() {
defer close(connState)
serverConn, err := list.Accept()
if err != nil {
serverErrors <- err
close(serverErrors)
return
}
defer serverConn.Close()
// Read the ping
buf := make([]byte, 4)
_, err = serverConn.Read(buf)
if (err != nil) && (err != io.EOF) {
serverErrors <- err
close(serverErrors)
return
}
close(serverErrors)
connState <- serverConn.(*tls.Conn).ConnectionState()
}()
// Establish a connection from the client side and write a few bytes.
clientErrors := make(chan error, 1)
go func() {
addr := list.Addr().String()
conn, err := tls.Dial("tcp", addr, dialConf)
if err != nil {
clientErrors <- err
close(clientErrors)
return
}
defer conn.Close()
// Write ping
_, err = conn.Write([]byte("ping"))
if err != nil {
clientErrors <- err
}
close(clientErrors)
}()
for err = range clientErrors {
if err != nil {
return tls.ConnectionState{}, fmt.Errorf("error in client goroutine:%v", err)
}
}
for err = range serverErrors {
if err != nil {
return tls.ConnectionState{}, fmt.Errorf("error in server goroutine:%v", err)
}
}
// Grab the current state
return <-connState, nil
}
func TestBackend_PermittedDNSDomainsIntermediateCA(t *testing.T) {
// Enable PKI secret engine and Cert auth method
coreConfig := &vault.CoreConfig{
CredentialBackends: map[string]logical.Factory{
"cert": Factory,
},
LogicalBackends: map[string]logical.Factory{
"pki": pki.Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
cores := cluster.Cores
vault.TestWaitActive(t, cores[0].Core)
client := cores[0].Client
var err error
// Mount /pki as a root CA
err = client.Sys().Mount("pki", &api.MountInput{
Type: "pki",
Config: api.MountConfigInput{
DefaultLeaseTTL: "16h",
MaxLeaseTTL: "32h",
},
})
if err != nil {
t.Fatal(err)
}
// Set the cluster's certificate as the root CA in /pki
pemBundleRootCA := string(cluster.CACertPEM) + string(cluster.CAKeyPEM)
_, err = client.Logical().Write("pki/config/ca", map[string]interface{}{
"pem_bundle": pemBundleRootCA,
})
if err != nil {
t.Fatal(err)
}
// Mount /pki2 to operate as an intermediate CA
err = client.Sys().Mount("pki2", &api.MountInput{
Type: "pki",
Config: api.MountConfigInput{
DefaultLeaseTTL: "16h",
MaxLeaseTTL: "32h",
},
})
if err != nil {
t.Fatal(err)
}
// Create a CSR for the intermediate CA
secret, err := client.Logical().Write("pki2/intermediate/generate/internal", nil)
if err != nil {
t.Fatal(err)
}
intermediateCSR := secret.Data["csr"].(string)
// Sign the intermediate CSR using /pki
secret, err = client.Logical().Write("pki/root/sign-intermediate", map[string]interface{}{
"permitted_dns_domains": ".myvault.com",
"csr": intermediateCSR,
})
if err != nil {
t.Fatal(err)
}
intermediateCertPEM := secret.Data["certificate"].(string)
// Configure the intermediate cert as the CA in /pki2
_, err = client.Logical().Write("pki2/intermediate/set-signed", map[string]interface{}{
"certificate": intermediateCertPEM,
})
if err != nil {
t.Fatal(err)
}
// Create a role on the intermediate CA mount
_, err = client.Logical().Write("pki2/roles/myvault-dot-com", map[string]interface{}{
"allowed_domains": "myvault.com",
"allow_subdomains": "true",
"max_ttl": "5m",
})
if err != nil {
t.Fatal(err)
}
// Issue a leaf cert using the intermediate CA
secret, err = client.Logical().Write("pki2/issue/myvault-dot-com", map[string]interface{}{
"common_name": "cert.myvault.com",
"format": "pem",
"ip_sans": "127.0.0.1",
})
if err != nil {
t.Fatal(err)
}
leafCertPEM := secret.Data["certificate"].(string)
leafCertKeyPEM := secret.Data["private_key"].(string)
// Enable the cert auth method
err = client.Sys().EnableAuthWithOptions("cert", &api.EnableAuthOptions{
Type: "cert",
})
if err != nil {
t.Fatal(err)
}
// Set the intermediate CA cert as a trusted certificate in the backend
_, err = client.Logical().Write("auth/cert/certs/myvault-dot-com", map[string]interface{}{
"display_name": "myvault.com",
"policies": "default",
"certificate": intermediateCertPEM,
})
if err != nil {
t.Fatal(err)
}
// Create temporary files for CA cert, client cert and client cert key.
// This is used to configure TLS in the api client.
caCertFile, err := ioutil.TempFile("", "caCert")
if err != nil {
t.Fatal(err)
}
defer os.Remove(caCertFile.Name())
if _, err := caCertFile.Write([]byte(cluster.CACertPEM)); err != nil {
t.Fatal(err)
}
if err := caCertFile.Close(); err != nil {
t.Fatal(err)
}
leafCertFile, err := ioutil.TempFile("", "leafCert")
if err != nil {
t.Fatal(err)
}
defer os.Remove(leafCertFile.Name())
if _, err := leafCertFile.Write([]byte(leafCertPEM)); err != nil {
t.Fatal(err)
}
if err := leafCertFile.Close(); err != nil {
t.Fatal(err)
}
leafCertKeyFile, err := ioutil.TempFile("", "leafCertKey")
if err != nil {
t.Fatal(err)
}
defer os.Remove(leafCertKeyFile.Name())
if _, err := leafCertKeyFile.Write([]byte(leafCertKeyPEM)); err != nil {
t.Fatal(err)
}
if err := leafCertKeyFile.Close(); err != nil {
t.Fatal(err)
}
// This function is a copy-pasta from the NewTestCluster, with the
// modification to reconfigure the TLS on the api client with the leaf
// certificate generated above.
getAPIClient := func(port int, tlsConfig *tls.Config) *api.Client {
transport := cleanhttp.DefaultPooledTransport()
transport.TLSClientConfig = tlsConfig.Clone()
if err := http2.ConfigureTransport(transport); err != nil {
t.Fatal(err)
}
client := &http.Client{
Transport: transport,
CheckRedirect: func(*http.Request, []*http.Request) error {
// This can of course be overridden per-test by using its own client
return fmt.Errorf("redirects not allowed in these tests")
},
}
config := api.DefaultConfig()
if config.Error != nil {
t.Fatal(config.Error)
}
config.Address = fmt.Sprintf("https://127.0.0.1:%d", port)
config.HttpClient = client
// Set the above issued certificates as the client certificates
config.ConfigureTLS(&api.TLSConfig{
CACert: caCertFile.Name(),
ClientCert: leafCertFile.Name(),
ClientKey: leafCertKeyFile.Name(),
})
apiClient, err := api.NewClient(config)
if err != nil {
t.Fatal(err)
}
return apiClient
}
// Create a new api client with the desired TLS configuration
newClient := getAPIClient(cores[0].Listeners[0].Address.Port, cores[0].TLSConfig())
secret, err = newClient.Logical().Write("auth/cert/login", map[string]interface{}{
"name": "myvault-dot-com",
})
if err != nil {
t.Fatal(err)
}
if secret.Auth == nil || secret.Auth.ClientToken == "" {
t.Fatalf("expected a successful authentication")
}
// testing pathLoginRenew for cert auth
oldAccessor := secret.Auth.Accessor
newClient.SetToken(client.Token())
secret, err = newClient.Logical().Write("auth/token/renew-accessor", map[string]interface{}{
"accessor": secret.Auth.Accessor,
"increment": 3600,
})
if err != nil {
t.Fatal(err)
}
if secret.Auth == nil || secret.Auth.ClientToken != "" || secret.Auth.LeaseDuration != 3600 || secret.Auth.Accessor != oldAccessor {
t.Fatalf("unexpected accessor renewal")
}
}
func TestBackend_MetadataBasedACLPolicy(t *testing.T) {
// Start cluster with cert auth method enabled
coreConfig := &vault.CoreConfig{
CredentialBackends: map[string]logical.Factory{
"cert": Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
cores := cluster.Cores
vault.TestWaitActive(t, cores[0].Core)
client := cores[0].Client
var err error
// Enable the cert auth method
err = client.Sys().EnableAuthWithOptions("cert", &api.EnableAuthOptions{
Type: "cert",
})
if err != nil {
t.Fatal(err)
}
// Enable metadata in aliases
_, err = client.Logical().Write("auth/cert/config", map[string]interface{}{
"enable_identity_alias_metadata": true,
})
if err != nil {
t.Fatal(err)
}
// Retrieve its accessor id
auths, err := client.Sys().ListAuth()
if err != nil {
t.Fatal(err)
}
var accessor string
for _, auth := range auths {
if auth.Type == "cert" {
accessor = auth.Accessor
}
}
if accessor == "" {
t.Fatal("failed to find cert auth accessor")
}
// Write ACL policy
err = client.Sys().PutPolicy("metadata-based", fmt.Sprintf(`
path "kv/cn/{{identity.entity.aliases.%s.metadata.common_name}}" {
capabilities = ["read"]
}
path "kv/ext/{{identity.entity.aliases.%s.metadata.2-1-1-1}}" {
capabilities = ["read"]
}
`, accessor, accessor))
if err != nil {
t.Fatalf("err: %v", err)
}
ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem")
if err != nil {
t.Fatalf("err: %v", err)
}
// Set the trusted certificate in the backend
_, err = client.Logical().Write("auth/cert/certs/test", map[string]interface{}{
"display_name": "test",
"policies": "metadata-based",
"certificate": string(ca),
"allowed_metadata_extensions": "2.1.1.1,1.2.3.45",
})
if err != nil {
t.Fatal(err)
}
// This function is a copy-paste from the NewTestCluster, with the
// modification to reconfigure the TLS on the api client with a
// specific client certificate.
getAPIClient := func(port int, tlsConfig *tls.Config) *api.Client {
transport := cleanhttp.DefaultPooledTransport()
transport.TLSClientConfig = tlsConfig.Clone()
if err := http2.ConfigureTransport(transport); err != nil {
t.Fatal(err)
}
client := &http.Client{
Transport: transport,
CheckRedirect: func(*http.Request, []*http.Request) error {
// This can of course be overridden per-test by using its own client
return fmt.Errorf("redirects not allowed in these tests")
},
}
config := api.DefaultConfig()
if config.Error != nil {
t.Fatal(config.Error)
}
config.Address = fmt.Sprintf("https://127.0.0.1:%d", port)
config.HttpClient = client
// Set the client certificates
config.ConfigureTLS(&api.TLSConfig{
CACertBytes: cluster.CACertPEM,
ClientCert: "test-fixtures/root/rootcawextcert.pem",
ClientKey: "test-fixtures/root/rootcawextkey.pem",
})
apiClient, err := api.NewClient(config)
if err != nil {
t.Fatal(err)
}
return apiClient
}
// Create a new api client with the desired TLS configuration
newClient := getAPIClient(cores[0].Listeners[0].Address.Port, cores[0].TLSConfig())
var secret *api.Secret
secret, err = newClient.Logical().Write("auth/cert/login", map[string]interface{}{
"name": "test",
})
if err != nil {
t.Fatal(err)
}
if secret.Auth == nil || secret.Auth.ClientToken == "" {
t.Fatalf("expected a successful authentication")
}
// Check paths guarded by ACL policy
newClient.SetToken(secret.Auth.ClientToken)
_, err = newClient.Logical().Read("kv/cn/example.com")
if err != nil {
t.Fatal(err)
}
_, err = newClient.Logical().Read("kv/cn/not.example.com")
if err == nil {
t.Fatal("expected access denied")
}
_, err = newClient.Logical().Read("kv/ext/A UTF8String Extension")
if err != nil {
t.Fatal(err)
}
_, err = newClient.Logical().Read("kv/ext/bar")
if err == nil {
t.Fatal("expected access denied")
}
}
func TestBackend_NonCAExpiry(t *testing.T) {
var resp *logical.Response
var err error
// Create a self-signed certificate and issue a leaf certificate using the
// CA cert
template := &x509.Certificate{
SerialNumber: big.NewInt(1234),
Subject: pkix.Name{
CommonName: "localhost",
Organization: []string{"hashicorp"},
OrganizationalUnit: []string{"vault"},
},
BasicConstraintsValid: true,
NotBefore: time.Now().Add(-30 * time.Second),
NotAfter: time.Now().Add(50 * time.Second),
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
KeyUsage: x509.KeyUsage(x509.KeyUsageCertSign | x509.KeyUsageCRLSign),
}
// Set IP SAN
parsedIP := net.ParseIP("127.0.0.1")
if parsedIP == nil {
t.Fatalf("failed to create parsed IP")
}
template.IPAddresses = []net.IP{parsedIP}
// Private key for CA cert
caPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
// Marshalling to be able to create PEM file
caPrivateKeyBytes := x509.MarshalPKCS1PrivateKey(caPrivateKey)
caPublicKey := &caPrivateKey.PublicKey
template.IsCA = true
caCertBytes, err := x509.CreateCertificate(rand.Reader, template, template, caPublicKey, caPrivateKey)
if err != nil {
t.Fatal(err)
}
caCert, err := x509.ParseCertificate(caCertBytes)
if err != nil {
t.Fatal(err)
}
parsedCaBundle := &certutil.ParsedCertBundle{
Certificate: caCert,
CertificateBytes: caCertBytes,
PrivateKeyBytes: caPrivateKeyBytes,
PrivateKeyType: certutil.RSAPrivateKey,
}
caCertBundle, err := parsedCaBundle.ToCertBundle()
if err != nil {
t.Fatal(err)
}
caCertFile, err := ioutil.TempFile("", "caCert")
if err != nil {
t.Fatal(err)
}
defer os.Remove(caCertFile.Name())
if _, err := caCertFile.Write([]byte(caCertBundle.Certificate)); err != nil {
t.Fatal(err)
}
if err := caCertFile.Close(); err != nil {
t.Fatal(err)
}
caKeyFile, err := ioutil.TempFile("", "caKey")
if err != nil {
t.Fatal(err)
}
defer os.Remove(caKeyFile.Name())
if _, err := caKeyFile.Write([]byte(caCertBundle.PrivateKey)); err != nil {
t.Fatal(err)
}
if err := caKeyFile.Close(); err != nil {
t.Fatal(err)
}
// Prepare template for non-CA cert
template.IsCA = false
template.SerialNumber = big.NewInt(5678)
template.KeyUsage = x509.KeyUsage(x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign)
issuedPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
issuedPrivateKeyBytes := x509.MarshalPKCS1PrivateKey(issuedPrivateKey)
issuedPublicKey := &issuedPrivateKey.PublicKey
// Keep a short certificate lifetime so logins can be tested both when
// cert is valid and when it gets expired
template.NotBefore = time.Now().Add(-2 * time.Second)
template.NotAfter = time.Now().Add(3 * time.Second)
issuedCertBytes, err := x509.CreateCertificate(rand.Reader, template, caCert, issuedPublicKey, caPrivateKey)
if err != nil {
t.Fatal(err)
}
issuedCert, err := x509.ParseCertificate(issuedCertBytes)
if err != nil {
t.Fatal(err)
}
parsedIssuedBundle := &certutil.ParsedCertBundle{
Certificate: issuedCert,
CertificateBytes: issuedCertBytes,
PrivateKeyBytes: issuedPrivateKeyBytes,
PrivateKeyType: certutil.RSAPrivateKey,
}
issuedCertBundle, err := parsedIssuedBundle.ToCertBundle()
if err != nil {
t.Fatal(err)
}
issuedCertFile, err := ioutil.TempFile("", "issuedCert")
if err != nil {
t.Fatal(err)
}
defer os.Remove(issuedCertFile.Name())
if _, err := issuedCertFile.Write([]byte(issuedCertBundle.Certificate)); err != nil {
t.Fatal(err)
}
if err := issuedCertFile.Close(); err != nil {
t.Fatal(err)
}
issuedKeyFile, err := ioutil.TempFile("", "issuedKey")
if err != nil {
t.Fatal(err)
}
defer os.Remove(issuedKeyFile.Name())
if _, err := issuedKeyFile.Write([]byte(issuedCertBundle.PrivateKey)); err != nil {
t.Fatal(err)
}
if err := issuedKeyFile.Close(); err != nil {
t.Fatal(err)
}
config := logical.TestBackendConfig()
storage := &logical.InmemStorage{}
config.StorageView = storage
b, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
// Register the Non-CA certificate of the client key pair
certData := map[string]interface{}{
"certificate": issuedCertBundle.Certificate,
"policies": "abc",
"display_name": "cert1",
"ttl": 10000,
}
certReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "certs/cert1",
Storage: storage,
Data: certData,
}
resp, err = b.HandleRequest(context.Background(), certReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
// Create connection state using the certificates generated
connState, err := connectionState(caCertFile.Name(), caCertFile.Name(), caKeyFile.Name(), issuedCertFile.Name(), issuedKeyFile.Name())
if err != nil {
t.Fatalf("error testing connection state:%v", err)
}
loginReq := &logical.Request{
Operation: logical.UpdateOperation,
Storage: storage,
Path: "login",
Connection: &logical.Connection{
ConnState: &connState,
},
}
// Login when the certificate is still valid. Login should succeed.
resp, err = b.HandleRequest(context.Background(), loginReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
// Wait until the certificate expires
time.Sleep(5 * time.Second)
// Login attempt after certificate expiry should fail
resp, err = b.HandleRequest(context.Background(), loginReq)
if err == nil {
t.Fatalf("expected error due to expired certificate")
}
}
func TestBackend_RegisteredNonCA_CRL(t *testing.T) {
config := logical.TestBackendConfig()
storage := &logical.InmemStorage{}
config.StorageView = storage
b, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
nonCACert, err := ioutil.ReadFile(testCertPath1)
if err != nil {
t.Fatal(err)
}
// Register the Non-CA certificate of the client key pair
certData := map[string]interface{}{
"certificate": nonCACert,
"policies": "abc",
"display_name": "cert1",
"ttl": 10000,
}
certReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "certs/cert1",
Storage: storage,
Data: certData,
}
resp, err := b.HandleRequest(context.Background(), certReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
// Connection state is presenting the client Non-CA cert and its key.
// This is exactly what is registered at the backend.
connState, err := connectionState(serverCAPath, serverCertPath, serverKeyPath, testCertPath1, testKeyPath1)
if err != nil {
t.Fatalf("error testing connection state:%v", err)
}
loginReq := &logical.Request{
Operation: logical.UpdateOperation,
Storage: storage,
Path: "login",
Connection: &logical.Connection{
ConnState: &connState,
},
}
// Login should succeed.
resp, err = b.HandleRequest(context.Background(), loginReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
// Register a CRL containing the issued client certificate used above.
issuedCRL, err := ioutil.ReadFile(testIssuedCertCRL)
if err != nil {
t.Fatal(err)
}
crlData := map[string]interface{}{
"crl": issuedCRL,
}
crlReq := &logical.Request{
Operation: logical.UpdateOperation,
Storage: storage,
Path: "crls/issuedcrl",
Data: crlData,
}
resp, err = b.HandleRequest(context.Background(), crlReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
// Ensure the CRL shows up on a list.
listReq := &logical.Request{
Operation: logical.ListOperation,
Storage: storage,
Path: "crls",
Data: map[string]interface{}{},
}
resp, err = b.HandleRequest(context.Background(), listReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
if len(resp.Data) != 1 || len(resp.Data["keys"].([]string)) != 1 || resp.Data["keys"].([]string)[0] != "issuedcrl" {
t.Fatalf("bad listing: resp:%v", resp)
}
// Attempt login with the same connection state but with the CRL registered
resp, err = b.HandleRequest(context.Background(), loginReq)
if err != nil {
t.Fatal(err)
}
if resp == nil || !resp.IsError() {
t.Fatalf("expected failure due to revoked certificate")
}
}
func TestBackend_CRLs(t *testing.T) {
config := logical.TestBackendConfig()
storage := &logical.InmemStorage{}
config.StorageView = storage
b, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
clientCA1, err := ioutil.ReadFile(testRootCACertPath1)
if err != nil {
t.Fatal(err)
}
// Register the CA certificate of the client key pair
certData := map[string]interface{}{
"certificate": clientCA1,
"policies": "abc",
"display_name": "cert1",
"ttl": 10000,
}
certReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "certs/cert1",
Storage: storage,
Data: certData,
}
resp, err := b.HandleRequest(context.Background(), certReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
// Connection state is presenting the client CA cert and its key.
// This is exactly what is registered at the backend.
connState, err := connectionState(serverCAPath, serverCertPath, serverKeyPath, testRootCACertPath1, testRootCAKeyPath1)
if err != nil {
t.Fatalf("error testing connection state:%v", err)
}
loginReq := &logical.Request{
Operation: logical.UpdateOperation,
Storage: storage,
Path: "login",
Connection: &logical.Connection{
ConnState: &connState,
},
}
resp, err = b.HandleRequest(context.Background(), loginReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
// Now, without changing the registered client CA cert, present from
// the client side, a cert issued using the registered CA.
connState, err = connectionState(serverCAPath, serverCertPath, serverKeyPath, testCertPath1, testKeyPath1)
if err != nil {
t.Fatalf("error testing connection state: %v", err)
}
loginReq.Connection.ConnState = &connState
// Attempt login with the updated connection
resp, err = b.HandleRequest(context.Background(), loginReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
// Register a CRL containing the issued client certificate used above.
issuedCRL, err := ioutil.ReadFile(testIssuedCertCRL)
if err != nil {
t.Fatal(err)
}
crlData := map[string]interface{}{
"crl": issuedCRL,
}
crlReq := &logical.Request{
Operation: logical.UpdateOperation,
Storage: storage,
Path: "crls/issuedcrl",
Data: crlData,
}
resp, err = b.HandleRequest(context.Background(), crlReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
// Attempt login with the revoked certificate.
resp, err = b.HandleRequest(context.Background(), loginReq)
if err != nil {
t.Fatal(err)
}
if resp == nil || !resp.IsError() {
t.Fatalf("expected failure due to revoked certificate")
}
// Register a different client CA certificate.
clientCA2, err := ioutil.ReadFile(testRootCACertPath2)
if err != nil {
t.Fatal(err)
}
certData["certificate"] = clientCA2
resp, err = b.HandleRequest(context.Background(), certReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
// Test login using a different client CA cert pair.
connState, err = connectionState(serverCAPath, serverCertPath, serverKeyPath, testRootCACertPath2, testRootCAKeyPath2)
if err != nil {
t.Fatalf("error testing connection state: %v", err)
}
loginReq.Connection.ConnState = &connState
// Attempt login with the updated connection
resp, err = b.HandleRequest(context.Background(), loginReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
// Register a CRL containing the root CA certificate used above.
rootCRL, err := ioutil.ReadFile(testRootCertCRL)
if err != nil {
t.Fatal(err)
}
crlData["crl"] = rootCRL
resp, err = b.HandleRequest(context.Background(), crlReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
// Attempt login with the same connection state but with the CRL registered
resp, err = b.HandleRequest(context.Background(), loginReq)
if err != nil {
t.Fatal(err)
}
if resp == nil || !resp.IsError() {
t.Fatalf("expected failure due to revoked certificate")
}
}
func testFactory(t *testing.T) logical.Backend {
storage := &logical.InmemStorage{}
b, err := Factory(context.Background(), &logical.BackendConfig{
System: &logical.StaticSystemView{
DefaultLeaseTTLVal: 1000 * time.Second,
MaxLeaseTTLVal: 1800 * time.Second,
},
StorageView: storage,
})
if err != nil {
t.Fatalf("error: %s", err)
}
if err := b.Initialize(context.Background(), &logical.InitializationRequest{
Storage: storage,
}); err != nil {
t.Fatalf("error: %s", err)
}
return b
}
// Test the certificates being registered to the backend
func TestBackend_CertWrites(t *testing.T) {
// CA cert
ca1, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem")
if err != nil {
t.Fatalf("err: %v", err)
}
// Non CA Cert
ca2, err := ioutil.ReadFile("test-fixtures/keys/cert.pem")
if err != nil {
t.Fatalf("err: %v", err)
}
// Non CA cert without TLS web client authentication
ca3, err := ioutil.ReadFile("test-fixtures/noclientauthcert.pem")
if err != nil {
t.Fatalf("err: %v", err)
}
tc := logicaltest.TestCase{
CredentialBackend: testFactory(t),
Steps: []logicaltest.TestStep{
testAccStepCert(t, "aaa", ca1, "foo", allowed{}, false),
testAccStepCert(t, "bbb", ca2, "foo", allowed{}, false),
testAccStepCert(t, "ccc", ca3, "foo", allowed{}, true),
},
}
tc.Steps = append(tc.Steps, testAccStepListCerts(t, []string{"aaa", "bbb"})...)
logicaltest.Test(t, tc)
}
// Test a client trusted by a CA
func TestBackend_basic_CA(t *testing.T) {
connState, err := testConnState("test-fixtures/keys/cert.pem",
"test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem")
if err != nil {
t.Fatalf("error testing connection state: %v", err)
}
ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem")
if err != nil {
t.Fatalf("err: %v", err)
}
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: testFactory(t),
Steps: []logicaltest.TestStep{
testAccStepCert(t, "web", ca, "foo", allowed{}, false),
testAccStepLogin(t, connState),
testAccStepCertLease(t, "web", ca, "foo"),
testAccStepCertTTL(t, "web", ca, "foo"),
testAccStepLogin(t, connState),
testAccStepCertMaxTTL(t, "web", ca, "foo"),
testAccStepLogin(t, connState),
testAccStepCertNoLease(t, "web", ca, "foo"),
testAccStepLoginDefaultLease(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{names: "*.example.com"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{names: "*.invalid.com"}, false),
testAccStepLoginInvalid(t, connState),
},
})
}
// Test CRL behavior
func TestBackend_Basic_CRLs(t *testing.T) {
connState, err := testConnState("test-fixtures/keys/cert.pem",
"test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem")
if err != nil {
t.Fatalf("error testing connection state: %v", err)
}
ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem")
if err != nil {
t.Fatalf("err: %v", err)
}
crl, err := ioutil.ReadFile("test-fixtures/root/root.crl")
if err != nil {
t.Fatalf("err: %v", err)
}
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: testFactory(t),
Steps: []logicaltest.TestStep{
testAccStepCertNoLease(t, "web", ca, "foo"),
testAccStepLoginDefaultLease(t, connState),
testAccStepAddCRL(t, crl, connState),
testAccStepReadCRL(t, connState),
testAccStepLoginInvalid(t, connState),
testAccStepDeleteCRL(t, connState),
testAccStepLoginDefaultLease(t, connState),
},
})
}
// Test a self-signed client (root CA) that is trusted
func TestBackend_basic_singleCert(t *testing.T) {
connState, err := testConnState("test-fixtures/root/rootcacert.pem",
"test-fixtures/root/rootcakey.pem", "test-fixtures/root/rootcacert.pem")
if err != nil {
t.Fatalf("error testing connection state: %v", err)
}
ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem")
if err != nil {
t.Fatalf("err: %v", err)
}
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: testFactory(t),
Steps: []logicaltest.TestStep{
testAccStepCert(t, "web", ca, "foo", allowed{}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{ext: "1.2.3.4:invalid"}, false),
testAccStepLoginInvalid(t, connState),
},
})
}
func TestBackend_common_name_singleCert(t *testing.T) {
connState, err := testConnState("test-fixtures/root/rootcacert.pem",
"test-fixtures/root/rootcakey.pem", "test-fixtures/root/rootcacert.pem")
if err != nil {
t.Fatalf("error testing connection state: %v", err)
}
ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem")
if err != nil {
t.Fatalf("err: %v", err)
}
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: testFactory(t),
Steps: []logicaltest.TestStep{
testAccStepCert(t, "web", ca, "foo", allowed{}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{common_names: "example.com"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{common_names: "invalid"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{ext: "1.2.3.4:invalid"}, false),
testAccStepLoginInvalid(t, connState),
},
})
}
// Test a self-signed client with custom ext (root CA) that is trusted
func TestBackend_ext_singleCert(t *testing.T) {
connState, err := testConnState(
"test-fixtures/root/rootcawextcert.pem",
"test-fixtures/root/rootcawextkey.pem",
"test-fixtures/root/rootcacert.pem",
)
if err != nil {
t.Fatalf("error testing connection state: %v", err)
}
ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem")
if err != nil {
t.Fatalf("err: %v", err)
}
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: testFactory(t),
Steps: []logicaltest.TestStep{
testAccStepCert(t, "web", ca, "foo", allowed{ext: "2.1.1.1:A UTF8String Extension"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{ext: "2.1.1.1:*,2.1.1.2:A UTF8*"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{ext: "1.2.3.45:*"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{ext: "2.1.1.1:The Wrong Value"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{ext: "2.1.1.1:*,2.1.1.2:The Wrong Value"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{ext: "2.1.1.1:"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{ext: "2.1.1.1:,2.1.1.2:*"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "2.1.1.1:A UTF8String Extension"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "2.1.1.1:*,2.1.1.2:A UTF8*"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "1.2.3.45:*"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "2.1.1.1:The Wrong Value"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "2.1.1.1:*,2.1.1.2:The Wrong Value"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid", ext: "2.1.1.1:A UTF8String Extension"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid", ext: "2.1.1.1:*,2.1.1.2:A UTF8*"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid", ext: "1.2.3.45:*"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid", ext: "2.1.1.1:The Wrong Value"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{names: "invalid", ext: "2.1.1.1:*,2.1.1.2:The Wrong Value"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "hex:2.5.29.17:*87047F000002*"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "hex:2.5.29.17:*87047F000001*"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{names: "example.com", ext: "2.5.29.17:"}, false),
testAccStepLogin(t, connState),
testAccStepReadConfig(t, config{EnableIdentityAliasMetadata: false}, connState),
testAccStepCert(t, "web", ca, "foo", allowed{metadata_ext: "2.1.1.1,1.2.3.45"}, false),
testAccStepLoginWithMetadata(t, connState, "web", map[string]string{"2-1-1-1": "A UTF8String Extension"}, false),
testAccStepCert(t, "web", ca, "foo", allowed{metadata_ext: "1.2.3.45"}, false),
testAccStepLoginWithMetadata(t, connState, "web", map[string]string{}, false),
testAccStepSetConfig(t, config{EnableIdentityAliasMetadata: true}, connState),
testAccStepReadConfig(t, config{EnableIdentityAliasMetadata: true}, connState),
testAccStepCert(t, "web", ca, "foo", allowed{metadata_ext: "2.1.1.1,1.2.3.45"}, false),
testAccStepLoginWithMetadata(t, connState, "web", map[string]string{"2-1-1-1": "A UTF8String Extension"}, true),
testAccStepCert(t, "web", ca, "foo", allowed{metadata_ext: "1.2.3.45"}, false),
testAccStepLoginWithMetadata(t, connState, "web", map[string]string{}, true),
},
})
}
// Test a self-signed client with URI alt names (root CA) that is trusted
func TestBackend_dns_singleCert(t *testing.T) {
certTemplate := &x509.Certificate{
Subject: pkix.Name{
CommonName: "example.com",
},
DNSNames: []string{"example.com"},
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageServerAuth,
x509.ExtKeyUsageClientAuth,
},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,
SerialNumber: big.NewInt(mathrand.Int63()),
NotBefore: time.Now().Add(-30 * time.Second),
NotAfter: time.Now().Add(262980 * time.Hour),
}
tempDir, connState, err := generateTestCertAndConnState(t, certTemplate)
if tempDir != "" {
defer os.RemoveAll(tempDir)
}
if err != nil {
t.Fatalf("error testing connection state: %v", err)
}
ca, err := ioutil.ReadFile(filepath.Join(tempDir, "ca_cert.pem"))
if err != nil {
t.Fatalf("err: %v", err)
}
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: testFactory(t),
Steps: []logicaltest.TestStep{
testAccStepCert(t, "web", ca, "foo", allowed{dns: "example.com"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{dns: "*ample.com"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{dns: "notincert.com"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{dns: "abc"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{dns: "*.example.com"}, false),
testAccStepLoginInvalid(t, connState),
},
})
}
// Test a self-signed client with URI alt names (root CA) that is trusted
func TestBackend_email_singleCert(t *testing.T) {
certTemplate := &x509.Certificate{
Subject: pkix.Name{
CommonName: "example.com",
},
EmailAddresses: []string{"valid@example.com"},
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageServerAuth,
x509.ExtKeyUsageClientAuth,
},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,
SerialNumber: big.NewInt(mathrand.Int63()),
NotBefore: time.Now().Add(-30 * time.Second),
NotAfter: time.Now().Add(262980 * time.Hour),
}
tempDir, connState, err := generateTestCertAndConnState(t, certTemplate)
if tempDir != "" {
defer os.RemoveAll(tempDir)
}
if err != nil {
t.Fatalf("error testing connection state: %v", err)
}
ca, err := ioutil.ReadFile(filepath.Join(tempDir, "ca_cert.pem"))
if err != nil {
t.Fatalf("err: %v", err)
}
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: testFactory(t),
Steps: []logicaltest.TestStep{
testAccStepCert(t, "web", ca, "foo", allowed{emails: "valid@example.com"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{emails: "*@example.com"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{emails: "invalid@notincert.com"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{emails: "abc"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{emails: "*.example.com"}, false),
testAccStepLoginInvalid(t, connState),
},
})
}
// Test a self-signed client with OU (root CA) that is trusted
func TestBackend_organizationalUnit_singleCert(t *testing.T) {
connState, err := testConnState(
"test-fixtures/root/rootcawoucert.pem",
"test-fixtures/root/rootcawoukey.pem",
"test-fixtures/root/rootcawoucert.pem",
)
if err != nil {
t.Fatalf("error testing connection state: %v", err)
}
ca, err := ioutil.ReadFile("test-fixtures/root/rootcawoucert.pem")
if err != nil {
t.Fatalf("err: %v", err)
}
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: testFactory(t),
Steps: []logicaltest.TestStep{
testAccStepCert(t, "web", ca, "foo", allowed{organizational_units: "engineering"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{organizational_units: "eng*"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{organizational_units: "engineering,finance"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{organizational_units: "foo"}, false),
testAccStepLoginInvalid(t, connState),
},
})
}
// Test a self-signed client with URI alt names (root CA) that is trusted
func TestBackend_uri_singleCert(t *testing.T) {
u, err := url.Parse("spiffe://example.com/host")
if err != nil {
t.Fatal(err)
}
certTemplate := &x509.Certificate{
Subject: pkix.Name{
CommonName: "example.com",
},
DNSNames: []string{"example.com"},
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
URIs: []*url.URL{u},
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageServerAuth,
x509.ExtKeyUsageClientAuth,
},
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,
SerialNumber: big.NewInt(mathrand.Int63()),
NotBefore: time.Now().Add(-30 * time.Second),
NotAfter: time.Now().Add(262980 * time.Hour),
}
tempDir, connState, err := generateTestCertAndConnState(t, certTemplate)
if tempDir != "" {
defer os.RemoveAll(tempDir)
}
if err != nil {
t.Fatalf("error testing connection state: %v", err)
}
ca, err := ioutil.ReadFile(filepath.Join(tempDir, "ca_cert.pem"))
if err != nil {
t.Fatalf("err: %v", err)
}
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: testFactory(t),
Steps: []logicaltest.TestStep{
testAccStepCert(t, "web", ca, "foo", allowed{uris: "spiffe://example.com/*"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{uris: "spiffe://example.com/host"}, false),
testAccStepLogin(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{uris: "spiffe://example.com/invalid"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{uris: "abc"}, false),
testAccStepLoginInvalid(t, connState),
testAccStepCert(t, "web", ca, "foo", allowed{uris: "http://www.google.com"}, false),
testAccStepLoginInvalid(t, connState),
},
})
}
// Test against a collection of matching and non-matching rules
func TestBackend_mixed_constraints(t *testing.T) {
connState, err := testConnState("test-fixtures/keys/cert.pem",
"test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem")
if err != nil {
t.Fatalf("error testing connection state: %v", err)
}
ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem")
if err != nil {
t.Fatalf("err: %v", err)
}
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: testFactory(t),
Steps: []logicaltest.TestStep{
testAccStepCert(t, "1unconstrained", ca, "foo", allowed{}, false),
testAccStepCert(t, "2matching", ca, "foo", allowed{names: "*.example.com,whatever"}, false),
testAccStepCert(t, "3invalid", ca, "foo", allowed{names: "invalid"}, false),
testAccStepLogin(t, connState),
// Assumes CertEntries are processed in alphabetical order (due to store.List), so we only match 2matching if 1unconstrained doesn't match
testAccStepLoginWithName(t, connState, "2matching"),
testAccStepLoginWithNameInvalid(t, connState, "3invalid"),
},
})
}
// Test an untrusted client
func TestBackend_untrusted(t *testing.T) {
connState, err := testConnState("test-fixtures/keys/cert.pem",
"test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem")
if err != nil {
t.Fatalf("error testing connection state: %v", err)
}
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: testFactory(t),
Steps: []logicaltest.TestStep{
testAccStepLoginInvalid(t, connState),
},
})
}
func TestBackend_validCIDR(t *testing.T) {
config := logical.TestBackendConfig()
storage := &logical.InmemStorage{}
config.StorageView = storage
b, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
connState, err := testConnState("test-fixtures/keys/cert.pem",
"test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem")
if err != nil {
t.Fatalf("error testing connection state: %v", err)
}
ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem")
if err != nil {
t.Fatalf("err: %v", err)
}
name := "web"
boundCIDRs := []string{"127.0.0.1", "128.252.0.0/16"}
addCertReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "certs/" + name,
Data: map[string]interface{}{
"certificate": string(ca),
"policies": "foo",
"display_name": name,
"allowed_names": "",
"required_extensions": "",
"lease": 1000,
"bound_cidrs": boundCIDRs,
},
Storage: storage,
Connection: &logical.Connection{ConnState: &connState},
}
_, err = b.HandleRequest(context.Background(), addCertReq)
if err != nil {
t.Fatal(err)
}
readCertReq := &logical.Request{
Operation: logical.ReadOperation,
Path: "certs/" + name,
Storage: storage,
Connection: &logical.Connection{ConnState: &connState},
}
readResult, err := b.HandleRequest(context.Background(), readCertReq)
if err != nil {
t.Fatal(err)
}
cidrsResult := readResult.Data["bound_cidrs"].([]*sockaddr.SockAddrMarshaler)
if cidrsResult[0].String() != boundCIDRs[0] ||
cidrsResult[1].String() != boundCIDRs[1] {
t.Fatalf("bound_cidrs couldn't be set correctly, EXPECTED: %v, ACTUAL: %v", boundCIDRs, cidrsResult)
}
loginReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "login",
Unauthenticated: true,
Data: map[string]interface{}{
"name": name,
},
Storage: storage,
Connection: &logical.Connection{ConnState: &connState},
}
// override the remote address with an IPV4 that is authorized
loginReq.Connection.RemoteAddr = "127.0.0.1/32"
_, err = b.HandleRequest(context.Background(), loginReq)
if err != nil {
t.Fatal(err.Error())
}
}
func TestBackend_invalidCIDR(t *testing.T) {
config := logical.TestBackendConfig()
storage := &logical.InmemStorage{}
config.StorageView = storage
b, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}
connState, err := testConnState("test-fixtures/keys/cert.pem",
"test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem")
if err != nil {
t.Fatalf("error testing connection state: %v", err)
}
ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem")
if err != nil {
t.Fatalf("err: %v", err)
}
name := "web"
addCertReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "certs/" + name,
Data: map[string]interface{}{
"certificate": string(ca),
"policies": "foo",
"display_name": name,
"allowed_names": "",
"required_extensions": "",
"lease": 1000,
"bound_cidrs": []string{"127.0.0.1/32", "128.252.0.0/16"},
},
Storage: storage,
Connection: &logical.Connection{ConnState: &connState},
}
_, err = b.HandleRequest(context.Background(), addCertReq)
if err != nil {
t.Fatal(err)
}
loginReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "login",
Unauthenticated: true,
Data: map[string]interface{}{
"name": name,
},
Storage: storage,
Connection: &logical.Connection{ConnState: &connState},
}
// override the remote address with an IPV4 that isn't authorized
loginReq.Connection.RemoteAddr = "127.0.0.1/8"
_, err = b.HandleRequest(context.Background(), loginReq)
if err == nil {
t.Fatal("expected \"ERROR: permission denied\"")
}
}
func testAccStepAddCRL(t *testing.T, crl []byte, connState tls.ConnectionState) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "crls/test",
ConnState: &connState,
Data: map[string]interface{}{
"crl": crl,
},
}
}
func testAccStepReadCRL(t *testing.T, connState tls.ConnectionState) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.ReadOperation,
Path: "crls/test",
ConnState: &connState,
Check: func(resp *logical.Response) error {
crlInfo := CRLInfo{}
err := mapstructure.Decode(resp.Data, &crlInfo)
if err != nil {
t.Fatalf("err: %v", err)
}
if len(crlInfo.Serials) != 1 {
t.Fatalf("bad: expected CRL with length 1, got %d", len(crlInfo.Serials))
}
if _, ok := crlInfo.Serials["637101449987587619778072672905061040630001617053"]; !ok {
t.Fatalf("bad: expected serial number not found in CRL")
}
return nil
},
}
}
func testAccStepDeleteCRL(t *testing.T, connState tls.ConnectionState) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.DeleteOperation,
Path: "crls/test",
ConnState: &connState,
}
}
func testAccStepSetConfig(t *testing.T, conf config, connState tls.ConnectionState) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "config",
ConnState: &connState,
Data: map[string]interface{}{
"enable_identity_alias_metadata": conf.EnableIdentityAliasMetadata,
},
}
}
func testAccStepReadConfig(t *testing.T, conf config, connState tls.ConnectionState) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.ReadOperation,
Path: "config",
ConnState: &connState,
Check: func(resp *logical.Response) error {
value, ok := resp.Data["enable_identity_alias_metadata"]
if !ok {
t.Fatalf("enable_identity_alias_metadata not found in response")
}
b, ok := value.(bool)
if !ok {
t.Fatalf("bad: expected enable_identity_alias_metadata to be a bool")
}
if b != conf.EnableIdentityAliasMetadata {
t.Fatalf("bad: expected enable_identity_alias_metadata to be %t, got %t", conf.EnableIdentityAliasMetadata, b)
}
return nil
},
}
}
func testAccStepLogin(t *testing.T, connState tls.ConnectionState) logicaltest.TestStep {
return testAccStepLoginWithName(t, connState, "")
}
func testAccStepLoginWithName(t *testing.T, connState tls.ConnectionState, certName string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "login",
Unauthenticated: true,
ConnState: &connState,
Check: func(resp *logical.Response) error {
if resp.Auth.TTL != 1000*time.Second {
t.Fatalf("bad lease length: %#v", resp.Auth)
}
if certName != "" && resp.Auth.DisplayName != ("mnt-"+certName) {
t.Fatalf("matched the wrong cert: %#v", resp.Auth.DisplayName)
}
fn := logicaltest.TestCheckAuth([]string{"default", "foo"})
return fn(resp)
},
Data: map[string]interface{}{
"name": certName,
},
}
}
func testAccStepLoginDefaultLease(t *testing.T, connState tls.ConnectionState) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "login",
Unauthenticated: true,
ConnState: &connState,
Check: func(resp *logical.Response) error {
if resp.Auth.TTL != 1000*time.Second {
t.Fatalf("bad lease length: %#v", resp.Auth)
}
fn := logicaltest.TestCheckAuth([]string{"default", "foo"})
return fn(resp)
},
}
}
func testAccStepLoginWithMetadata(t *testing.T, connState tls.ConnectionState, certName string, metadata map[string]string, expectAliasMetadata bool) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "login",
Unauthenticated: true,
ConnState: &connState,
Check: func(resp *logical.Response) error {
// Check for fixed metadata too
metadata["cert_name"] = certName
metadata["common_name"] = connState.PeerCertificates[0].Subject.CommonName
metadata["serial_number"] = connState.PeerCertificates[0].SerialNumber.String()
metadata["subject_key_id"] = certutil.GetHexFormatted(connState.PeerCertificates[0].SubjectKeyId, ":")
metadata["authority_key_id"] = certutil.GetHexFormatted(connState.PeerCertificates[0].AuthorityKeyId, ":")
for key, expected := range metadata {
value, ok := resp.Auth.Metadata[key]
if !ok {
t.Fatalf("missing metadata key: %s", key)
}
if value != expected {
t.Fatalf("expected metadata key %s to equal %s, but got: %s", key, expected, value)
}
if expectAliasMetadata {
value, ok = resp.Auth.Alias.Metadata[key]
if !ok {
t.Fatalf("missing alias metadata key: %s", key)
}
if value != expected {
t.Fatalf("expected metadata key %s to equal %s, but got: %s", key, expected, value)
}
} else {
if len(resp.Auth.Alias.Metadata) > 0 {
t.Fatal("found alias metadata keys, but should not have any")
}
}
}
fn := logicaltest.TestCheckAuth([]string{"default", "foo"})
return fn(resp)
},
Data: map[string]interface{}{
"metadata": metadata,
},
}
}
func testAccStepLoginInvalid(t *testing.T, connState tls.ConnectionState) logicaltest.TestStep {
return testAccStepLoginWithNameInvalid(t, connState, "")
}
func testAccStepLoginWithNameInvalid(t *testing.T, connState tls.ConnectionState, certName string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "login",
Unauthenticated: true,
ConnState: &connState,
Check: func(resp *logical.Response) error {
if resp.Auth != nil {
return fmt.Errorf("should not be authorized: %#v", resp)
}
return nil
},
Data: map[string]interface{}{
"name": certName,
},
ErrorOk: true,
}
}
func testAccStepListCerts(
t *testing.T, certs []string,
) []logicaltest.TestStep {
return []logicaltest.TestStep{
{
Operation: logical.ListOperation,
Path: "certs",
Check: func(resp *logical.Response) error {
if resp == nil {
return fmt.Errorf("nil response")
}
if resp.Data == nil {
return fmt.Errorf("nil data")
}
if resp.Data["keys"] == interface{}(nil) {
return fmt.Errorf("nil keys")
}
keys := resp.Data["keys"].([]string)
if !reflect.DeepEqual(keys, certs) {
return fmt.Errorf("mismatch: keys is %#v, certs is %#v", keys, certs)
}
return nil
},
}, {
Operation: logical.ListOperation,
Path: "certs/",
Check: func(resp *logical.Response) error {
if resp == nil {
return fmt.Errorf("nil response")
}
if resp.Data == nil {
return fmt.Errorf("nil data")
}
if resp.Data["keys"] == interface{}(nil) {
return fmt.Errorf("nil keys")
}
keys := resp.Data["keys"].([]string)
if !reflect.DeepEqual(keys, certs) {
return fmt.Errorf("mismatch: keys is %#v, certs is %#v", keys, certs)
}
return nil
},
},
}
}
type allowed struct {
names string // allowed names in the certificate, looks at common, name, dns, email [depricated]
common_names string // allowed common names in the certificate
dns string // allowed dns names in the SAN extension of the certificate
emails string // allowed email names in SAN extension of the certificate
uris string // allowed uris in SAN extension of the certificate
organizational_units string // allowed OUs in the certificate
ext string // required extensions in the certificate
metadata_ext string // allowed metadata extensions to add to identity alias
}
func testAccStepCert(t *testing.T, name string, cert []byte, policies string, testData allowed, expectError bool) logicaltest.TestStep {
return testAccStepCertWithExtraParams(t, name, cert, policies, testData, expectError, nil)
}
func testAccStepCertWithExtraParams(t *testing.T, name string, cert []byte, policies string, testData allowed, expectError bool, extraParams map[string]interface{}) logicaltest.TestStep {
data := map[string]interface{}{
"certificate": string(cert),
"policies": policies,
"display_name": name,
"allowed_names": testData.names,
"allowed_common_names": testData.common_names,
"allowed_dns_sans": testData.dns,
"allowed_email_sans": testData.emails,
"allowed_uri_sans": testData.uris,
"allowed_organizational_units": testData.organizational_units,
"required_extensions": testData.ext,
"allowed_metadata_extensions": testData.metadata_ext,
"lease": 1000,
}
for k, v := range extraParams {
data[k] = v
}
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "certs/" + name,
ErrorOk: expectError,
Data: data,
Check: func(resp *logical.Response) error {
if resp == nil && expectError {
return fmt.Errorf("expected error but received nil")
}
return nil
},
}
}
func testAccStepReadCertPolicy(t *testing.T, name string, expectError bool, expected map[string]interface{}) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.ReadOperation,
Path: "certs/" + name,
ErrorOk: expectError,
Data: nil,
Check: func(resp *logical.Response) error {
if (resp == nil || len(resp.Data) == 0) && expectError {
return fmt.Errorf("expected error but received nil")
}
for key, expectedValue := range expected {
actualValue := resp.Data[key]
if expectedValue != actualValue {
return fmt.Errorf("Expected to get [%v]=[%v] but read [%v]=[%v] from server for certs/%v: %v", key, expectedValue, key, actualValue, name, resp)
}
}
return nil
},
}
}
func testAccStepCertLease(
t *testing.T, name string, cert []byte, policies string,
) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "certs/" + name,
Data: map[string]interface{}{
"certificate": string(cert),
"policies": policies,
"display_name": name,
"lease": 1000,
},
}
}
func testAccStepCertTTL(
t *testing.T, name string, cert []byte, policies string,
) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "certs/" + name,
Data: map[string]interface{}{
"certificate": string(cert),
"policies": policies,
"display_name": name,
"ttl": "1000s",
},
}
}
func testAccStepCertMaxTTL(
t *testing.T, name string, cert []byte, policies string,
) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "certs/" + name,
Data: map[string]interface{}{
"certificate": string(cert),
"policies": policies,
"display_name": name,
"ttl": "1000s",
"max_ttl": "1200s",
},
}
}
func testAccStepCertNoLease(
t *testing.T, name string, cert []byte, policies string,
) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "certs/" + name,
Data: map[string]interface{}{
"certificate": string(cert),
"policies": policies,
"display_name": name,
},
}
}
func testConnState(certPath, keyPath, rootCertPath string) (tls.ConnectionState, error) {
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return tls.ConnectionState{}, err
}
rootConfig := &rootcerts.Config{
CAFile: rootCertPath,
}
rootCAs, err := rootcerts.LoadCACerts(rootConfig)
if err != nil {
return tls.ConnectionState{}, err
}
return testConnStateWithCert(cert, rootCAs)
}
func testConnStateWithCert(cert tls.Certificate, rootCAs *x509.CertPool) (tls.ConnectionState, error) {
listenConf := &tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.RequestClientCert,
InsecureSkipVerify: false,
RootCAs: rootCAs,
}
dialConf := listenConf.Clone()
// start a server
list, err := tls.Listen("tcp", "127.0.0.1:0", listenConf)
if err != nil {
return tls.ConnectionState{}, err
}
defer list.Close()
// Accept connections.
serverErrors := make(chan error, 1)
connState := make(chan tls.ConnectionState)
go func() {
defer close(connState)
serverConn, err := list.Accept()
serverErrors <- err
if err != nil {
close(serverErrors)
return
}
defer serverConn.Close()
// Read the ping
buf := make([]byte, 4)
_, err = serverConn.Read(buf)
if (err != nil) && (err != io.EOF) {
serverErrors <- err
close(serverErrors)
return
} else {
// EOF is a reasonable error condition, so swallow it.
serverErrors <- nil
}
close(serverErrors)
connState <- serverConn.(*tls.Conn).ConnectionState()
}()
// Establish a connection from the client side and write a few bytes.
clientErrors := make(chan error, 1)
go func() {
addr := list.Addr().String()
conn, err := tls.Dial("tcp", addr, dialConf)
clientErrors <- err
if err != nil {
close(clientErrors)
return
}
defer conn.Close()
// Write ping
_, err = conn.Write([]byte("ping"))
clientErrors <- err
close(clientErrors)
}()
for err = range clientErrors {
if err != nil {
return tls.ConnectionState{}, fmt.Errorf("error in client goroutine:%v", err)
}
}
for err = range serverErrors {
if err != nil {
return tls.ConnectionState{}, fmt.Errorf("error in server goroutine:%v", err)
}
}
// Grab the current state
return <-connState, nil
}
func Test_Renew(t *testing.T) {
storage := &logical.InmemStorage{}
lb, err := Factory(context.Background(), &logical.BackendConfig{
System: &logical.StaticSystemView{
DefaultLeaseTTLVal: 300 * time.Second,
MaxLeaseTTLVal: 1800 * time.Second,
},
StorageView: storage,
})
if err != nil {
t.Fatalf("error: %s", err)
}
b := lb.(*backend)
connState, err := testConnState("test-fixtures/keys/cert.pem",
"test-fixtures/keys/key.pem", "test-fixtures/root/rootcacert.pem")
if err != nil {
t.Fatalf("error testing connection state: %v", err)
}
ca, err := ioutil.ReadFile("test-fixtures/root/rootcacert.pem")
if err != nil {
t.Fatal(err)
}
req := &logical.Request{
Connection: &logical.Connection{
ConnState: &connState,
},
Storage: storage,
Auth: &logical.Auth{},
}
fd := &framework.FieldData{
Raw: map[string]interface{}{
"name": "test",
"certificate": ca,
"policies": "foo,bar",
},
Schema: pathCerts(b).Fields,
}
resp, err := b.pathCertWrite(context.Background(), req, fd)
if err != nil {
t.Fatal(err)
}
empty_login_fd := &framework.FieldData{
Raw: map[string]interface{}{},
Schema: pathLogin(b).Fields,
}
resp, err = b.pathLogin(context.Background(), req, empty_login_fd)
if err != nil {
t.Fatal(err)
}
if resp.IsError() {
t.Fatalf("got error: %#v", *resp)
}
req.Auth.InternalData = resp.Auth.InternalData
req.Auth.Metadata = resp.Auth.Metadata
req.Auth.LeaseOptions = resp.Auth.LeaseOptions
req.Auth.Policies = resp.Auth.Policies
req.Auth.TokenPolicies = req.Auth.Policies
req.Auth.Period = resp.Auth.Period
// Normal renewal
resp, err = b.pathLoginRenew(context.Background(), req, empty_login_fd)
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("got nil response from renew")
}
if resp.IsError() {
t.Fatalf("got error: %#v", *resp)
}
// Change the policies -- this should fail
fd.Raw["policies"] = "zip,zap"
resp, err = b.pathCertWrite(context.Background(), req, fd)
if err != nil {
t.Fatal(err)
}
resp, err = b.pathLoginRenew(context.Background(), req, empty_login_fd)
if err == nil {
t.Fatal("expected error")
}
// Put the policies back, this should be okay
fd.Raw["policies"] = "bar,foo"
resp, err = b.pathCertWrite(context.Background(), req, fd)
if err != nil {
t.Fatal(err)
}
resp, err = b.pathLoginRenew(context.Background(), req, empty_login_fd)
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("got nil response from renew")
}
if resp.IsError() {
t.Fatalf("got error: %#v", *resp)
}
// Add period value to cert entry
period := 350 * time.Second
fd.Raw["period"] = period.String()
resp, err = b.pathCertWrite(context.Background(), req, fd)
if err != nil {
t.Fatal(err)
}
resp, err = b.pathLoginRenew(context.Background(), req, empty_login_fd)
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("got nil response from renew")
}
if resp.IsError() {
t.Fatalf("got error: %#v", *resp)
}
if resp.Auth.Period != period {
t.Fatalf("expected a period value of %s in the response, got: %s", period, resp.Auth.Period)
}
// Delete CA, make sure we can't renew
resp, err = b.pathCertDelete(context.Background(), req, fd)
if err != nil {
t.Fatal(err)
}
resp, err = b.pathLoginRenew(context.Background(), req, empty_login_fd)
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("got nil response from renew")
}
if !resp.IsError() {
t.Fatal("expected error")
}
}
func TestBackend_CertUpgrade(t *testing.T) {
s := &logical.InmemStorage{}
config := logical.TestBackendConfig()
config.StorageView = s
ctx := context.Background()
b := Backend()
if b == nil {
t.Fatalf("failed to create backend")
}
if err := b.Setup(ctx, config); err != nil {
t.Fatal(err)
}
foo := &CertEntry{
Policies: []string{"foo"},
Period: time.Second,
TTL: time.Second,
MaxTTL: time.Second,
BoundCIDRs: []*sockaddr.SockAddrMarshaler{{SockAddr: sockaddr.MustIPAddr("127.0.0.1")}},
}
entry, err := logical.StorageEntryJSON("cert/foo", foo)
if err != nil {
t.Fatal(err)
}
err = s.Put(ctx, entry)
if err != nil {
t.Fatal(err)
}
certEntry, err := b.Cert(ctx, s, "foo")
if err != nil {
t.Fatal(err)
}
exp := &CertEntry{
Policies: []string{"foo"},
Period: time.Second,
TTL: time.Second,
MaxTTL: time.Second,
BoundCIDRs: []*sockaddr.SockAddrMarshaler{{SockAddr: sockaddr.MustIPAddr("127.0.0.1")}},
TokenParams: tokenutil.TokenParams{
TokenPolicies: []string{"foo"},
TokenPeriod: time.Second,
TokenTTL: time.Second,
TokenMaxTTL: time.Second,
TokenBoundCIDRs: []*sockaddr.SockAddrMarshaler{{SockAddr: sockaddr.MustIPAddr("127.0.0.1")}},
},
}
if diff := deep.Equal(certEntry, exp); diff != nil {
t.Fatal(diff)
}
}
// TestOCSPFailOpenWithBadIssuer validates we fail all different types of cert auth
// login scenarios if we encounter an OCSP verification error
func TestOCSPFailOpenWithBadIssuer(t *testing.T) {
caFile := "test-fixtures/root/rootcacert.pem"
pemCa, err := os.ReadFile(caFile)
require.NoError(t, err, "failed reading in file %s", caFile)
caTLS := loadCerts(t, caFile, "test-fixtures/root/rootcakey.pem")
leafTLS := loadCerts(t, "test-fixtures/keys/cert.pem", "test-fixtures/keys/key.pem")
rootConfig := &rootcerts.Config{
CAFile: caFile,
}
rootCAs, err := rootcerts.LoadCACerts(rootConfig)
connState, err := testConnStateWithCert(leafTLS, rootCAs)
require.NoError(t, err, "error testing connection state: %v", err)
badCa, badCaKey := createCa(t)
// Setup an OCSP handler
ocspHandler := func(ca *x509.Certificate, caKey crypto.Signer) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
now := time.Now()
ocspRes := ocsp.Response{
SerialNumber: leafTLS.Leaf.SerialNumber,
ThisUpdate: now.Add(-1 * time.Hour),
NextUpdate: now.Add(30 * time.Minute),
Status: ocsp.Good,
}
response, err := ocsp.CreateResponse(ca, ca, ocspRes, caKey)
if err != nil {
t.Fatalf("failed generating OCSP response: %v", err)
}
_, _ = w.Write(response)
})
}
goodTs := httptest.NewServer(ocspHandler(caTLS.Leaf, caTLS.PrivateKey.(crypto.Signer)))
badTs := httptest.NewServer(ocspHandler(badCa, badCaKey))
defer goodTs.Close()
defer badTs.Close()
steps := []logicaltest.TestStep{
// step 1/2: This should fail as we get a response from a bad root, even with ocsp_fail_open is set to true
testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false,
map[string]interface{}{
"ocsp_enabled": true,
"ocsp_servers_override": []string{badTs.URL},
"ocsp_query_all_servers": false,
"ocsp_fail_open": true,
}),
testAccStepLoginInvalid(t, connState),
// step 3/4: This should fail as we query all the servers which will get a response with an invalid signature
testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false,
map[string]interface{}{
"ocsp_enabled": true,
"ocsp_servers_override": []string{goodTs.URL, badTs.URL},
"ocsp_query_all_servers": true,
"ocsp_fail_open": true,
}),
testAccStepLoginInvalid(t, connState),
// step 5/6: This should fail as we will query the OCSP server with the bad root key first.
testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false,
map[string]interface{}{
"ocsp_enabled": true,
"ocsp_servers_override": []string{badTs.URL, goodTs.URL},
"ocsp_query_all_servers": false,
"ocsp_fail_open": true,
}),
testAccStepLoginInvalid(t, connState),
// step 7/8: This should pass as we will only query the first server with the valid root signature
testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false,
map[string]interface{}{
"ocsp_enabled": true,
"ocsp_servers_override": []string{goodTs.URL, badTs.URL},
"ocsp_query_all_servers": false,
"ocsp_fail_open": true,
}),
testAccStepLogin(t, connState),
}
// Setup a new factory everytime to avoid OCSP caching from influencing the test
for i := 0; i < len(steps); i += 2 {
setup := i
execute := i + 1
t.Run(fmt.Sprintf("steps-%d-%d", setup+1, execute+1), func(t *testing.T) {
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: testFactory(t),
Steps: []logicaltest.TestStep{steps[setup], steps[execute]},
})
})
}
}
// TestOCSPWithMixedValidResponses validates the expected behavior of multiple OCSP servers configured,
// with and without ocsp_query_all_servers enabled or disabled.
func TestOCSPWithMixedValidResponses(t *testing.T) {
caFile := "test-fixtures/root/rootcacert.pem"
pemCa, err := os.ReadFile(caFile)
require.NoError(t, err, "failed reading in file %s", caFile)
caTLS := loadCerts(t, caFile, "test-fixtures/root/rootcakey.pem")
leafTLS := loadCerts(t, "test-fixtures/keys/cert.pem", "test-fixtures/keys/key.pem")
rootConfig := &rootcerts.Config{
CAFile: caFile,
}
rootCAs, err := rootcerts.LoadCACerts(rootConfig)
connState, err := testConnStateWithCert(leafTLS, rootCAs)
require.NoError(t, err, "error testing connection state: %v", err)
// Setup an OCSP handler
ocspHandler := func(status int) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
now := time.Now()
ocspRes := ocsp.Response{
SerialNumber: leafTLS.Leaf.SerialNumber,
ThisUpdate: now.Add(-1 * time.Hour),
NextUpdate: now.Add(30 * time.Minute),
Status: status,
}
response, err := ocsp.CreateResponse(caTLS.Leaf, caTLS.Leaf, ocspRes, caTLS.PrivateKey.(crypto.Signer))
if err != nil {
t.Fatalf("failed generating OCSP response: %v", err)
}
_, _ = w.Write(response)
})
}
goodTs := httptest.NewServer(ocspHandler(ocsp.Good))
revokeTs := httptest.NewServer(ocspHandler(ocsp.Revoked))
defer goodTs.Close()
defer revokeTs.Close()
steps := []logicaltest.TestStep{
// step 1/2: This should pass as we will query the first server and get a valid good response, not testing
// the second configured server
testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false,
map[string]interface{}{
"ocsp_enabled": true,
"ocsp_servers_override": []string{goodTs.URL, revokeTs.URL},
"ocsp_query_all_servers": false,
}),
testAccStepLogin(t, connState),
// step 3/4: This should fail as we will query the revoking OCSP server first and get a revoke response
testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false,
map[string]interface{}{
"ocsp_enabled": true,
"ocsp_servers_override": []string{revokeTs.URL, goodTs.URL},
"ocsp_query_all_servers": false,
}),
testAccStepLoginInvalid(t, connState),
// step 5/6: This should fail as we will query all the OCSP servers and prefer the revoke response
testAccStepCertWithExtraParams(t, "web", pemCa, "foo",
allowed{names: "cert.example.com"}, false, map[string]interface{}{
"ocsp_enabled": true,
"ocsp_servers_override": []string{goodTs.URL, revokeTs.URL},
"ocsp_query_all_servers": true,
}),
testAccStepLoginInvalid(t, connState),
}
// Setup a new factory everytime to avoid OCSP caching from influencing the test
for i := 0; i < len(steps); i += 2 {
setup := i
execute := i + 1
t.Run(fmt.Sprintf("steps-%d-%d", setup+1, execute+1), func(t *testing.T) {
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: testFactory(t),
Steps: []logicaltest.TestStep{steps[setup], steps[execute]},
})
})
}
}
// TestOCSPFailOpenWithGoodResponse validates the expected behavior with multiple OCSP servers configured
// one that returns a Good response the other is not available, along with the ocsp_fail_open in multiple modes
func TestOCSPFailOpenWithGoodResponse(t *testing.T) {
caFile := "test-fixtures/root/rootcacert.pem"
pemCa, err := os.ReadFile(caFile)
require.NoError(t, err, "failed reading in file %s", caFile)
caTLS := loadCerts(t, caFile, "test-fixtures/root/rootcakey.pem")
leafTLS := loadCerts(t, "test-fixtures/keys/cert.pem", "test-fixtures/keys/key.pem")
rootConfig := &rootcerts.Config{
CAFile: caFile,
}
rootCAs, err := rootcerts.LoadCACerts(rootConfig)
connState, err := testConnStateWithCert(leafTLS, rootCAs)
require.NoError(t, err, "error testing connection state: %v", err)
// Setup an OCSP handler
ocspHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
now := time.Now()
ocspRes := ocsp.Response{
SerialNumber: leafTLS.Leaf.SerialNumber,
ThisUpdate: now.Add(-1 * time.Hour),
NextUpdate: now.Add(30 * time.Minute),
Status: ocsp.Good,
}
response, err := ocsp.CreateResponse(caTLS.Leaf, caTLS.Leaf, ocspRes, caTLS.PrivateKey.(crypto.Signer))
if err != nil {
t.Fatalf("failed generating OCSP response: %v", err)
}
_, _ = w.Write(response)
})
ts := httptest.NewServer(ocspHandler)
defer ts.Close()
steps := []logicaltest.TestStep{
// Step 1/2 With no proper responses from any OCSP server and fail_open to true, we should pass validation
// as fail_open is true
testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false,
map[string]interface{}{
"ocsp_enabled": true,
"ocsp_servers_override": []string{"http://127.0.0.1:30000", "http://127.0.0.1:30001"},
"ocsp_fail_open": true,
"ocsp_query_all_servers": false,
"ocsp_max_retries": 0,
}),
testAccStepLogin(t, connState),
// Step 3/4 With no proper responses from any OCSP server and fail_open to false we should fail validation
// as fail_open is false
testAccStepCertWithExtraParams(t, "web", pemCa, "foo",
allowed{names: "cert.example.com"}, false, map[string]interface{}{
"ocsp_enabled": true,
"ocsp_servers_override": []string{"http://127.0.0.1:30000", "http://127.0.0.1:30001"},
"ocsp_fail_open": false,
"ocsp_query_all_servers": false,
"ocsp_max_retries": 0,
}),
testAccStepLoginInvalid(t, connState),
// Step 5/6 With a single positive response, query all servers set to false and fail open true, pass validation
// as query all servers is false
testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false,
map[string]interface{}{
"ocsp_enabled": true,
"ocsp_servers_override": []string{ts.URL, "http://127.0.0.1:30001"},
"ocsp_fail_open": true,
"ocsp_query_all_servers": false,
"ocsp_max_retries": 0,
}),
testAccStepLogin(t, connState),
// Step 7/8 With a single positive response, query all servers set to false and fail open false, pass validation
// as query all servers is false
testAccStepCertWithExtraParams(t, "web", pemCa, "foo",
allowed{names: "cert.example.com"}, false, map[string]interface{}{
"ocsp_enabled": true,
"ocsp_servers_override": []string{ts.URL, "http://127.0.0.1:30001"},
"ocsp_fail_open": false,
"ocsp_query_all_servers": false,
"ocsp_max_retries": 0,
}),
testAccStepLogin(t, connState),
// Step 9/10 With a single positive response, query all servers set to true and fail open true, pass validation
// as fail open is true
testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false,
map[string]interface{}{
"ocsp_enabled": true,
"ocsp_servers_override": []string{ts.URL, "http://127.0.0.1:30001"},
"ocsp_fail_open": true,
"ocsp_query_all_servers": true,
"ocsp_max_retries": 0,
}),
testAccStepLogin(t, connState),
// Step 11/12 With a single positive response, query all servers set to true and fail open false, fail validation
// as not all servers agree
testAccStepCertWithExtraParams(t, "web", pemCa, "foo",
allowed{names: "cert.example.com"}, false, map[string]interface{}{
"ocsp_enabled": true,
"ocsp_servers_override": []string{ts.URL, "http://127.0.0.1:30001"},
"ocsp_fail_open": false,
"ocsp_query_all_servers": true,
"ocsp_max_retries": 0,
}),
testAccStepLoginInvalid(t, connState),
}
// Setup a new factory everytime to avoid OCSP caching from influencing the test
for i := 0; i < len(steps); i += 2 {
setup := i
execute := i + 1
t.Run(fmt.Sprintf("steps-%d-%d", setup+1, execute+1), func(t *testing.T) {
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: testFactory(t),
Steps: []logicaltest.TestStep{steps[setup], steps[execute]},
})
})
}
}
// TestOCSPFailOpenWithRevokeResponse validates the expected behavior with multiple OCSP servers configured
// one that returns a Revoke response the other is not available, along with the ocsp_fail_open in multiple modes
func TestOCSPFailOpenWithRevokeResponse(t *testing.T) {
caFile := "test-fixtures/root/rootcacert.pem"
pemCa, err := os.ReadFile(caFile)
require.NoError(t, err, "failed reading in file %s", caFile)
caTLS := loadCerts(t, caFile, "test-fixtures/root/rootcakey.pem")
leafTLS := loadCerts(t, "test-fixtures/keys/cert.pem", "test-fixtures/keys/key.pem")
rootConfig := &rootcerts.Config{
CAFile: caFile,
}
rootCAs, err := rootcerts.LoadCACerts(rootConfig)
connState, err := testConnStateWithCert(leafTLS, rootCAs)
require.NoError(t, err, "error testing connection state: %v", err)
// Setup an OCSP handler
ocspHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
now := time.Now()
ocspRes := ocsp.Response{
SerialNumber: leafTLS.Leaf.SerialNumber,
ThisUpdate: now.Add(-1 * time.Hour),
NextUpdate: now.Add(30 * time.Minute),
Status: ocsp.Revoked,
}
response, err := ocsp.CreateResponse(caTLS.Leaf, caTLS.Leaf, ocspRes, caTLS.PrivateKey.(crypto.Signer))
if err != nil {
t.Fatalf("failed generating OCSP response: %v", err)
}
_, _ = w.Write(response)
})
ts := httptest.NewServer(ocspHandler)
defer ts.Close()
// With no OCSP servers available, make sure that we behave as we expect
steps := []logicaltest.TestStep{
// Step 1/2 With a single revoke response, query all servers set to false and fail open true, fail validation
testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false,
map[string]interface{}{
"ocsp_enabled": true,
"ocsp_servers_override": []string{ts.URL, "http://127.0.0.1:30001"},
"ocsp_fail_open": true,
"ocsp_query_all_servers": false,
"ocsp_max_retries": 0,
}),
testAccStepLoginInvalid(t, connState),
// Step 3/4 With a single revoke response, query all servers set to false and fail open false, fail validation
testAccStepCertWithExtraParams(t, "web", pemCa, "foo",
allowed{names: "cert.example.com"}, false, map[string]interface{}{
"ocsp_enabled": true,
"ocsp_servers_override": []string{ts.URL, "http://127.0.0.1:30001"},
"ocsp_fail_open": false,
"ocsp_query_all_servers": false,
"ocsp_max_retries": 0,
}),
testAccStepLoginInvalid(t, connState),
// Step 5/6 With a single revoke response, query all servers set to true and fail open false, fail validation
testAccStepCertWithExtraParams(t, "web", pemCa, "foo",
allowed{names: "cert.example.com"}, false, map[string]interface{}{
"ocsp_enabled": true,
"ocsp_servers_override": []string{ts.URL, "http://127.0.0.1:30001"},
"ocsp_fail_open": false,
"ocsp_query_all_servers": true,
"ocsp_max_retries": 0,
}),
testAccStepLoginInvalid(t, connState),
// Step 7/8 With a single revoke response, query all servers set to true and fail open true, fail validation
testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false,
map[string]interface{}{
"ocsp_enabled": true,
"ocsp_servers_override": []string{ts.URL, "http://127.0.0.1:30001"},
"ocsp_fail_open": true,
"ocsp_query_all_servers": true,
"ocsp_max_retries": 0,
}),
testAccStepLoginInvalid(t, connState),
}
// Setup a new factory everytime to avoid OCSP caching from influencing the test
for i := 0; i < len(steps); i += 2 {
setup := i
execute := i + 1
t.Run(fmt.Sprintf("steps-%d-%d", setup+1, execute+1), func(t *testing.T) {
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: testFactory(t),
Steps: []logicaltest.TestStep{steps[setup], steps[execute]},
})
})
}
}
// TestOCSPFailOpenWithUnknownResponse validates the expected behavior with multiple OCSP servers configured
// one that returns an Unknown response the other is not available, along with the ocsp_fail_open in multiple modes
func TestOCSPFailOpenWithUnknownResponse(t *testing.T) {
caFile := "test-fixtures/root/rootcacert.pem"
pemCa, err := os.ReadFile(caFile)
require.NoError(t, err, "failed reading in file %s", caFile)
caTLS := loadCerts(t, caFile, "test-fixtures/root/rootcakey.pem")
leafTLS := loadCerts(t, "test-fixtures/keys/cert.pem", "test-fixtures/keys/key.pem")
rootConfig := &rootcerts.Config{
CAFile: caFile,
}
rootCAs, err := rootcerts.LoadCACerts(rootConfig)
connState, err := testConnStateWithCert(leafTLS, rootCAs)
require.NoError(t, err, "error testing connection state: %v", err)
// Setup an OCSP handler
ocspHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
now := time.Now()
ocspRes := ocsp.Response{
SerialNumber: leafTLS.Leaf.SerialNumber,
ThisUpdate: now.Add(-1 * time.Hour),
NextUpdate: now.Add(30 * time.Minute),
Status: ocsp.Unknown,
}
response, err := ocsp.CreateResponse(caTLS.Leaf, caTLS.Leaf, ocspRes, caTLS.PrivateKey.(crypto.Signer))
if err != nil {
t.Fatalf("failed generating OCSP response: %v", err)
}
_, _ = w.Write(response)
})
ts := httptest.NewServer(ocspHandler)
defer ts.Close()
// With no OCSP servers available, make sure that we behave as we expect
steps := []logicaltest.TestStep{
// Step 1/2 With a single unknown response, query all servers set to false and fail open true, pass validation
testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false,
map[string]interface{}{
"ocsp_enabled": true,
"ocsp_servers_override": []string{ts.URL, "http://127.0.0.1:30001"},
"ocsp_fail_open": true,
"ocsp_query_all_servers": false,
"ocsp_max_retries": 0,
}),
testAccStepLogin(t, connState),
// Step 3/4 With a single unknown response, query all servers set to false and fail open false, fail validation
testAccStepCertWithExtraParams(t, "web", pemCa, "foo",
allowed{names: "cert.example.com"}, false, map[string]interface{}{
"ocsp_enabled": true,
"ocsp_servers_override": []string{ts.URL, "http://127.0.0.1:30001"},
"ocsp_fail_open": false,
"ocsp_query_all_servers": false,
"ocsp_max_retries": 0,
}),
testAccStepLoginInvalid(t, connState),
// Step 5/6 With a single unknown response, query all servers set to true and fail open true, fail validation
testAccStepCertWithExtraParams(t, "web", pemCa, "foo", allowed{names: "cert.example.com"}, false,
map[string]interface{}{
"ocsp_enabled": true,
"ocsp_servers_override": []string{ts.URL, "http://127.0.0.1:30001"},
"ocsp_fail_open": true,
"ocsp_query_all_servers": true,
"ocsp_max_retries": 0,
}),
testAccStepLogin(t, connState),
// Step 7/8 With a single unknown response, query all servers set to true and fail open false, fail validation
testAccStepCertWithExtraParams(t, "web", pemCa, "foo",
allowed{names: "cert.example.com"}, false, map[string]interface{}{
"ocsp_enabled": true,
"ocsp_servers_override": []string{ts.URL, "http://127.0.0.1:30001"},
"ocsp_fail_open": false,
"ocsp_query_all_servers": true,
"ocsp_max_retries": 0,
}),
testAccStepLoginInvalid(t, connState),
}
// Setup a new factory everytime to avoid OCSP caching from influencing the test
for i := 0; i < len(steps); i += 2 {
setup := i
execute := i + 1
t.Run(fmt.Sprintf("steps-%d-%d", setup+1, execute+1), func(t *testing.T) {
logicaltest.Test(t, logicaltest.TestCase{
CredentialBackend: testFactory(t),
Steps: []logicaltest.TestStep{steps[setup], steps[execute]},
})
})
}
}
// TestOcspMaxRetriesUpdate verifies that the ocsp_max_retries field is properly initialized
// with our default value of 4, legacy roles have it initialized automatically to 4 and we
// can properly store and retrieve updates to the field.
func TestOcspMaxRetriesUpdate(t *testing.T) {
storage := &logical.InmemStorage{}
ctx := context.Background()
lb, err := Factory(context.Background(), &logical.BackendConfig{
System: &logical.StaticSystemView{
DefaultLeaseTTLVal: 300 * time.Second,
MaxLeaseTTLVal: 1800 * time.Second,
},
StorageView: storage,
})
require.NoError(t, err, "failed creating backend")
caFile := "test-fixtures/root/rootcacert.pem"
pemCa, err := os.ReadFile(caFile)
require.NoError(t, err, "failed reading in file %s", caFile)
data := map[string]interface{}{
"certificate": string(pemCa),
"display_name": "test",
}
// Test initial creation of role sets ocsp_max_retries to a default of 4
_, err = lb.HandleRequest(ctx, &logical.Request{
Operation: logical.UpdateOperation,
Path: "certs/test",
Data: data,
Storage: storage,
})
require.NoError(t, err, "failed initial role creation request")
resp, err := lb.HandleRequest(ctx, &logical.Request{
Operation: logical.ReadOperation,
Path: "certs/test",
Storage: storage,
})
require.NoError(t, err, "failed reading role request")
require.NotNil(t, resp)
require.Equal(t, 4, resp.Data["ocsp_max_retries"], "ocsp config didn't match expectations")
// Test we can update the field and read it back
data["ocsp_max_retries"] = 1
_, err = lb.HandleRequest(ctx, &logical.Request{
Operation: logical.UpdateOperation,
Path: "certs/test",
Data: data,
Storage: storage,
})
require.NoError(t, err, "failed updating role request")
resp, err = lb.HandleRequest(ctx, &logical.Request{
Operation: logical.ReadOperation,
Path: "certs/test",
Storage: storage,
})
require.NoError(t, err, "failed reading role request")
require.NotNil(t, resp)
require.Equal(t, 1, resp.Data["ocsp_max_retries"], "ocsp config didn't match expectations on update")
// Verify existing storage entries get updated with a value of 4
entry := &logical.StorageEntry{
Key: "cert/legacy",
Value: []byte(`{"token_bound_cidrs":null,"token_explicit_max_ttl":0,"token_max_ttl":0,
"token_no_default_policy":false,"token_num_uses":0,"token_period":0,
"token_policies":null,"token_type":0,"token_ttl":0,"Name":"test",
"Certificate":"-----BEGIN CERTIFICATE-----\nMIIDPDCCAiSgAwIBAgIUb5id+GcaMeMnYBv3MvdTGWigyJ0wDQYJKoZIhvcNAQEL\nBQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzI5WhcNMjYw\nMjI2MDIyNzU5WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBAOxTMvhTuIRc2YhxZpmPwegP86cgnqfT1mXxi1A7\nQ7qax24Nqbf00I3oDMQtAJlj2RB3hvRSCb0/lkF7i1Bub+TGxuM7NtZqp2F8FgG0\nz2md+W6adwW26rlxbQKjmRvMn66G9YPTkoJmPmxt2Tccb9+apmwW7lslL5j8H48x\nAHJTMb+PMP9kbOHV5Abr3PT4jXUPUr/mWBvBiKiHG0Xd/HEmlyOEPeAThxK+I5tb\n6m+eB+7cL9BsvQpy135+2bRAxUphvFi5NhryJ2vlAvoJ8UqigsNK3E28ut60FAoH\nSWRfFUFFYtfPgTDS1yOKU/z/XMU2giQv2HrleWt0mp4jqBUCAwEAAaOBgTB/MA4G\nA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSdxLNP/ocx\n7HK6JT3/sSAe76iTmzAfBgNVHSMEGDAWgBSdxLNP/ocx7HK6JT3/sSAe76iTmzAc\nBgNVHREEFTATggtleGFtcGxlLmNvbYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEA\nwHThDRsXJunKbAapxmQ6bDxSvTvkLA6m97TXlsFgL+Q3Jrg9HoJCNowJ0pUTwhP2\nU946dCnSCkZck0fqkwVi4vJ5EQnkvyEbfN4W5qVsQKOFaFVzep6Qid4rZT6owWPa\ncNNzNcXAee3/j6hgr6OQ/i3J6fYR4YouYxYkjojYyg+CMdn6q8BoV0BTsHdnw1/N\nScbnBHQIvIZMBDAmQueQZolgJcdOuBLYHe/kRy167z8nGg+PUFKIYOL8NaOU1+CJ\nt2YaEibVq5MRqCbRgnd9a2vG0jr5a3Mn4CUUYv+5qIjP3hUusYenW1/EWtn1s/gk\nzehNe5dFTjFpylg1o6b8Ow==\n-----END CERTIFICATE-----\n",
"DisplayName":"test","Policies":null,"TTL":0,"MaxTTL":0,"Period":0,
"AllowedNames":null,"AllowedCommonNames":null,"AllowedDNSSANs":null,
"AllowedEmailSANs":null,"AllowedURISANs":null,"AllowedOrganizationalUnits":null,
"RequiredExtensions":null,"AllowedMetadataExtensions":null,"BoundCIDRs":null,
"OcspCaCertificates":"","OcspEnabled":false,"OcspServersOverride":null,
"OcspFailOpen":false,"OcspQueryAllServers":false}`),
}
err = storage.Put(ctx, entry)
require.NoError(t, err, "failed putting legacy storage entry")
resp, err = lb.HandleRequest(ctx, &logical.Request{
Operation: logical.ReadOperation,
Path: "certs/legacy",
Storage: storage,
})
require.NoError(t, err, "failed reading role request")
require.NotNil(t, resp)
require.Equal(t, 4, resp.Data["ocsp_max_retries"], "ocsp config didn't match expectations on legacy entry")
}
func loadCerts(t *testing.T, certFile, certKey string) tls.Certificate {
caTLS, err := tls.LoadX509KeyPair(certFile, certKey)
require.NoError(t, err, "failed reading ca/key files")
caTLS.Leaf, err = x509.ParseCertificate(caTLS.Certificate[0])
require.NoError(t, err, "failed parsing certificate from file %s", certFile)
return caTLS
}
func createCa(t *testing.T) (*x509.Certificate, *ecdsa.PrivateKey) {
rootCaKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err, "failed generated root key for CA")
// Validate we reject CSRs that contain CN that aren't in the original order
cr := &x509.Certificate{
Subject: pkix.Name{CommonName: "Root Cert"},
SerialNumber: big.NewInt(1),
IsCA: true,
BasicConstraintsValid: true,
SignatureAlgorithm: x509.ECDSAWithSHA256,
NotBefore: time.Now().Add(-1 * time.Second),
NotAfter: time.Now().AddDate(1, 0, 0),
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageOCSPSigning},
}
rootCaBytes, err := x509.CreateCertificate(rand.Reader, cr, cr, &rootCaKey.PublicKey, rootCaKey)
require.NoError(t, err, "failed generating root ca")
rootCa, err := x509.ParseCertificate(rootCaBytes)
require.NoError(t, err, "failed parsing root ca")
return rootCa, rootCaKey
}