| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: MPL-2.0 |
| |
| package pki |
| |
| import ( |
| "bytes" |
| "context" |
| "crypto" |
| "crypto/ecdsa" |
| "crypto/ed25519" |
| "crypto/elliptic" |
| "crypto/rand" |
| "crypto/rsa" |
| "crypto/x509" |
| "crypto/x509/pkix" |
| "encoding/base64" |
| "encoding/hex" |
| "encoding/json" |
| "encoding/pem" |
| "fmt" |
| "math" |
| "math/big" |
| mathrand "math/rand" |
| "net" |
| "net/url" |
| "os" |
| "reflect" |
| "sort" |
| "strconv" |
| "strings" |
| "sync" |
| "testing" |
| "time" |
| |
| "github.com/hashicorp/vault/helper/testhelpers/teststorage" |
| |
| "github.com/hashicorp/vault/helper/testhelpers" |
| |
| "github.com/hashicorp/vault/sdk/helper/testhelpers/schema" |
| |
| "github.com/stretchr/testify/require" |
| |
| "github.com/armon/go-metrics" |
| "github.com/fatih/structs" |
| "github.com/go-test/deep" |
| "github.com/hashicorp/go-secure-stdlib/strutil" |
| "github.com/hashicorp/vault/api" |
| auth "github.com/hashicorp/vault/api/auth/userpass" |
| "github.com/hashicorp/vault/builtin/credential/userpass" |
| logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical" |
| vaulthttp "github.com/hashicorp/vault/http" |
| "github.com/hashicorp/vault/sdk/helper/certutil" |
| "github.com/hashicorp/vault/sdk/logical" |
| "github.com/hashicorp/vault/vault" |
| "github.com/mitchellh/mapstructure" |
| "golang.org/x/net/idna" |
| ) |
| |
| var stepCount = 0 |
| |
| // From builtin/credential/cert/test-fixtures/root/rootcacert.pem |
| const ( |
| rootCACertPEM = `-----BEGIN CERTIFICATE----- |
| MIIDPDCCAiSgAwIBAgIUb5id+GcaMeMnYBv3MvdTGWigyJ0wDQYJKoZIhvcNAQEL |
| BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMTYwMjI5MDIyNzI5WhcNMjYw |
| MjI2MDIyNzU5WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN |
| AQEBBQADggEPADCCAQoCggEBAOxTMvhTuIRc2YhxZpmPwegP86cgnqfT1mXxi1A7 |
| Q7qax24Nqbf00I3oDMQtAJlj2RB3hvRSCb0/lkF7i1Bub+TGxuM7NtZqp2F8FgG0 |
| z2md+W6adwW26rlxbQKjmRvMn66G9YPTkoJmPmxt2Tccb9+apmwW7lslL5j8H48x |
| AHJTMb+PMP9kbOHV5Abr3PT4jXUPUr/mWBvBiKiHG0Xd/HEmlyOEPeAThxK+I5tb |
| 6m+eB+7cL9BsvQpy135+2bRAxUphvFi5NhryJ2vlAvoJ8UqigsNK3E28ut60FAoH |
| SWRfFUFFYtfPgTDS1yOKU/z/XMU2giQv2HrleWt0mp4jqBUCAwEAAaOBgTB/MA4G |
| A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSdxLNP/ocx |
| 7HK6JT3/sSAe76iTmzAfBgNVHSMEGDAWgBSdxLNP/ocx7HK6JT3/sSAe76iTmzAc |
| BgNVHREEFTATggtleGFtcGxlLmNvbYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEA |
| wHThDRsXJunKbAapxmQ6bDxSvTvkLA6m97TXlsFgL+Q3Jrg9HoJCNowJ0pUTwhP2 |
| U946dCnSCkZck0fqkwVi4vJ5EQnkvyEbfN4W5qVsQKOFaFVzep6Qid4rZT6owWPa |
| cNNzNcXAee3/j6hgr6OQ/i3J6fYR4YouYxYkjojYyg+CMdn6q8BoV0BTsHdnw1/N |
| ScbnBHQIvIZMBDAmQueQZolgJcdOuBLYHe/kRy167z8nGg+PUFKIYOL8NaOU1+CJ |
| t2YaEibVq5MRqCbRgnd9a2vG0jr5a3Mn4CUUYv+5qIjP3hUusYenW1/EWtn1s/gk |
| zehNe5dFTjFpylg1o6b8Ow== |
| -----END CERTIFICATE-----` |
| rootCAKeyPEM = `-----BEGIN RSA PRIVATE KEY----- |
| MIIEpQIBAAKCAQEA7FMy+FO4hFzZiHFmmY/B6A/zpyCep9PWZfGLUDtDuprHbg2p |
| t/TQjegMxC0AmWPZEHeG9FIJvT+WQXuLUG5v5MbG4zs21mqnYXwWAbTPaZ35bpp3 |
| BbbquXFtAqOZG8yfrob1g9OSgmY+bG3ZNxxv35qmbBbuWyUvmPwfjzEAclMxv48w |
| /2Rs4dXkBuvc9PiNdQ9Sv+ZYG8GIqIcbRd38cSaXI4Q94BOHEr4jm1vqb54H7twv |
| 0Gy9CnLXfn7ZtEDFSmG8WLk2GvIna+UC+gnxSqKCw0rcTby63rQUCgdJZF8VQUVi |
| 18+BMNLXI4pT/P9cxTaCJC/YeuV5a3SaniOoFQIDAQABAoIBAQCoGZJC84JnnIgb |
| ttZNWuWKBXbCJcDVDikOQJ9hBZbqsFg1X0CfGmQS3MHf9Ubc1Ro8zVjQh15oIEfn |
| 8lIpdzTeXcpxLdiW8ix3ekVJF20F6pnXY8ZP6UnTeOwamXY6QPZAtb0D9UXcvY+f |
| nw+IVRD6082XS0Rmzu+peYWVXDy+FDN+HJRANBcdJZz8gOmNBIe0qDWx1b85d/s8 |
| 2Kk1Wwdss1IwAGeSddTSwzBNaaHdItZaMZOqPW1gRyBfVSkcUQIE6zn2RKw2b70t |
| grkIvyRcTdfmiKbqkkJ+eR+ITOUt0cBZSH4cDjlQA+r7hulvoBpQBRj068Toxkcc |
| bTagHaPBAoGBAPWPGVkHqhTbJ/DjmqDIStxby2M1fhhHt4xUGHinhUYjQjGOtDQ9 |
| 0mfaB7HObudRiSLydRAVGAHGyNJdQcTeFxeQbovwGiYKfZSA1IGpea7dTxPpGEdN |
| ksA0pzSp9MfKzX/MdLuAkEtO58aAg5YzsgX9hDNxo4MhH/gremZhEGZlAoGBAPZf |
| lqdYvAL0fjHGJ1FUEalhzGCGE9PH2iOqsxqLCXK7bDbzYSjvuiHkhYJHAOgVdiW1 |
| lB34UHHYAqZ1VVoFqJ05gax6DE2+r7K5VV3FUCaC0Zm3pavxchU9R/TKP82xRrBj |
| AFWwdgDTxUyvQEmgPR9sqorftO71Iz2tiwyTpIfxAoGBAIhEMLzHFAse0rtKkrRG |
| ccR27BbRyHeQ1Lp6sFnEHKEfT8xQdI/I/snCpCJ3e/PBu2g5Q9z416mktiyGs8ib |
| thTNgYsGYnxZtfaCx2pssanoBcn2wBJRae5fSapf5gY49HDG9MBYR7qCvvvYtSzU |
| 4yWP2ZzyotpRt3vwJKxLkN5BAoGAORHpZvhiDNkvxj3da7Rqpu7VleJZA2y+9hYb |
| iOF+HcqWhaAY+I+XcTRrTMM/zYLzLEcEeXDEyao86uwxCjpXVZw1kotvAC9UqbTO |
| tnr3VwRkoxPsV4kFYTAh0+1pnC8dbcxxDmhi3Uww3tOVs7hfkEDuvF6XnebA9A+Y |
| LyCgMzECgYEA6cCU8QODOivIKWFRXucvWckgE6MYDBaAwe6qcLsd1Q/gpE2e3yQc |
| 4RB3bcyiPROLzMLlXFxf1vSNJQdIaVfrRv+zJeGIiivLPU8+Eq4Lrb+tl1LepcOX |
| OzQeADTSCn5VidOfjDkIst9UXjMlrFfV9/oJEw5Eiqa6lkNPCGDhfA8= |
| -----END RSA PRIVATE KEY-----` |
| ) |
| |
| func TestPKI_RequireCN(t *testing.T) { |
| t.Parallel() |
| b, s := CreateBackendWithStorage(t) |
| |
| resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ |
| "common_name": "myvault.com", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("expected ca info") |
| } |
| |
| // Create a role which does require CN (default) |
| _, err = CBWrite(b, s, "roles/example", map[string]interface{}{ |
| "allowed_domains": "foobar.com,zipzap.com,abc.com,xyz.com", |
| "allow_bare_domains": true, |
| "allow_subdomains": true, |
| "max_ttl": "2h", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Issue a cert with require_cn set to true and with common name supplied. |
| // It should succeed. |
| resp, err = CBWrite(b, s, "issue/example", map[string]interface{}{ |
| "common_name": "foobar.com", |
| }) |
| schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("issue/example"), logical.UpdateOperation), resp, true) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Issue a cert with require_cn set to true and with out supplying the |
| // common name. It should error out. |
| _, err = CBWrite(b, s, "issue/example", map[string]interface{}{}) |
| if err == nil { |
| t.Fatalf("expected an error due to missing common_name") |
| } |
| |
| // Modify the role to make the common name optional |
| _, err = CBWrite(b, s, "roles/example", map[string]interface{}{ |
| "allowed_domains": "foobar.com,zipzap.com,abc.com,xyz.com", |
| "allow_bare_domains": true, |
| "allow_subdomains": true, |
| "max_ttl": "2h", |
| "require_cn": false, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Issue a cert with require_cn set to false and without supplying the |
| // common name. It should succeed. |
| resp, err = CBWrite(b, s, "issue/example", map[string]interface{}{}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| if resp.Data["certificate"] == "" { |
| t.Fatalf("expected a cert to be generated") |
| } |
| |
| // Issue a cert with require_cn set to false and with a common name. It |
| // should succeed. |
| resp, err = CBWrite(b, s, "issue/example", map[string]interface{}{}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| if resp.Data["certificate"] == "" { |
| t.Fatalf("expected a cert to be generated") |
| } |
| } |
| |
| func TestPKI_DeviceCert(t *testing.T) { |
| t.Parallel() |
| b, s := CreateBackendWithStorage(t) |
| |
| resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ |
| "common_name": "myvault.com", |
| "not_after": "9999-12-31T23:59:59Z", |
| "not_before_duration": "2h", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("expected ca info") |
| } |
| var certBundle certutil.CertBundle |
| err = mapstructure.Decode(resp.Data, &certBundle) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| parsedCertBundle, err := certBundle.ToParsedCertBundle() |
| if err != nil { |
| t.Fatal(err) |
| } |
| cert := parsedCertBundle.Certificate |
| notAfter := cert.NotAfter.Format(time.RFC3339) |
| if notAfter != "9999-12-31T23:59:59Z" { |
| t.Fatalf("not after from certificate: %v is not matching with input parameter: %v", cert.NotAfter, "9999-12-31T23:59:59Z") |
| } |
| if math.Abs(float64(time.Now().Add(-2*time.Hour).Unix()-cert.NotBefore.Unix())) > 10 { |
| t.Fatalf("root/generate/internal did not properly set validity period (notBefore): was %v vs expected %v", cert.NotBefore, time.Now().Add(-2*time.Hour)) |
| } |
| |
| // Create a role which does require CN (default) |
| _, err = CBWrite(b, s, "roles/example", map[string]interface{}{ |
| "allowed_domains": "foobar.com,zipzap.com,abc.com,xyz.com", |
| "allow_bare_domains": true, |
| "allow_subdomains": true, |
| "not_after": "9999-12-31T23:59:59Z", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Issue a cert with require_cn set to true and with common name supplied. |
| // It should succeed. |
| resp, err = CBWrite(b, s, "issue/example", map[string]interface{}{ |
| "common_name": "foobar.com", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| err = mapstructure.Decode(resp.Data, &certBundle) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| parsedCertBundle, err = certBundle.ToParsedCertBundle() |
| if err != nil { |
| t.Fatal(err) |
| } |
| cert = parsedCertBundle.Certificate |
| notAfter = cert.NotAfter.Format(time.RFC3339) |
| if notAfter != "9999-12-31T23:59:59Z" { |
| t.Fatal(fmt.Errorf("not after from certificate is not matching with input parameter")) |
| } |
| } |
| |
| func TestBackend_InvalidParameter(t *testing.T) { |
| t.Parallel() |
| b, s := CreateBackendWithStorage(t) |
| |
| _, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ |
| "common_name": "myvault.com", |
| "not_after": "9999-12-31T23:59:59Z", |
| "ttl": "25h", |
| }) |
| if err == nil { |
| t.Fatal(err) |
| } |
| |
| _, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{ |
| "common_name": "myvault.com", |
| "not_after": "9999-12-31T23:59:59", |
| }) |
| if err == nil { |
| t.Fatal(err) |
| } |
| } |
| |
| func TestBackend_CSRValues(t *testing.T) { |
| t.Parallel() |
| initTest.Do(setCerts) |
| b, _ := CreateBackendWithStorage(t) |
| |
| testCase := logicaltest.TestCase{ |
| LogicalBackend: b, |
| Steps: []logicaltest.TestStep{}, |
| } |
| |
| intdata := map[string]interface{}{} |
| reqdata := map[string]interface{}{} |
| testCase.Steps = append(testCase.Steps, generateCSRSteps(t, ecCACert, ecCAKey, intdata, reqdata)...) |
| |
| logicaltest.Test(t, testCase) |
| } |
| |
| func TestBackend_URLsCRUD(t *testing.T) { |
| t.Parallel() |
| initTest.Do(setCerts) |
| b, _ := CreateBackendWithStorage(t) |
| |
| testCase := logicaltest.TestCase{ |
| LogicalBackend: b, |
| Steps: []logicaltest.TestStep{}, |
| } |
| |
| intdata := map[string]interface{}{} |
| reqdata := map[string]interface{}{} |
| testCase.Steps = append(testCase.Steps, generateURLSteps(t, ecCACert, ecCAKey, intdata, reqdata)...) |
| |
| logicaltest.Test(t, testCase) |
| } |
| |
| // Generates and tests steps that walk through the various possibilities |
| // of role flags to ensure that they are properly restricted |
| func TestBackend_Roles(t *testing.T) { |
| t.Parallel() |
| cases := []struct { |
| name string |
| key, cert *string |
| useCSR bool |
| }{ |
| {"RSA", &rsaCAKey, &rsaCACert, false}, |
| {"RSACSR", &rsaCAKey, &rsaCACert, true}, |
| {"EC", &ecCAKey, &ecCACert, false}, |
| {"ECCSR", &ecCAKey, &ecCACert, true}, |
| {"ED", &edCAKey, &edCACert, false}, |
| {"EDCSR", &edCAKey, &edCACert, true}, |
| } |
| |
| for _, tc := range cases { |
| tc := tc |
| |
| t.Run(tc.name, func(t *testing.T) { |
| initTest.Do(setCerts) |
| b, _ := CreateBackendWithStorage(t) |
| |
| testCase := logicaltest.TestCase{ |
| LogicalBackend: b, |
| Steps: []logicaltest.TestStep{ |
| { |
| Operation: logical.UpdateOperation, |
| Path: "config/ca", |
| Data: map[string]interface{}{ |
| "pem_bundle": *tc.key + "\n" + *tc.cert, |
| }, |
| }, |
| }, |
| } |
| |
| testCase.Steps = append(testCase.Steps, generateRoleSteps(t, tc.useCSR)...) |
| if len(os.Getenv("VAULT_VERBOSE_PKITESTS")) > 0 { |
| for i, v := range testCase.Steps { |
| data := map[string]interface{}{} |
| var keys []string |
| for k := range v.Data { |
| keys = append(keys, k) |
| } |
| sort.Strings(keys) |
| for _, k := range keys { |
| interf := v.Data[k] |
| switch v := interf.(type) { |
| case bool: |
| if !v { |
| continue |
| } |
| case int: |
| if v == 0 { |
| continue |
| } |
| case []string: |
| if len(v) == 0 { |
| continue |
| } |
| case string: |
| if v == "" { |
| continue |
| } |
| lines := strings.Split(v, "\n") |
| if len(lines) > 1 { |
| data[k] = lines[0] + " ... (truncated)" |
| continue |
| } |
| } |
| data[k] = interf |
| |
| } |
| t.Logf("Step %d:\n%s %s err=%v %+v\n\n", i+1, v.Operation, v.Path, v.ErrorOk, data) |
| } |
| } |
| |
| logicaltest.Test(t, testCase) |
| }) |
| } |
| } |
| |
| // Performs some validity checking on the returned bundles |
| func checkCertsAndPrivateKey(keyType string, key crypto.Signer, usage x509.KeyUsage, extUsage x509.ExtKeyUsage, validity time.Duration, certBundle *certutil.CertBundle) (*certutil.ParsedCertBundle, error) { |
| parsedCertBundle, err := certBundle.ToParsedCertBundle() |
| if err != nil { |
| return nil, fmt.Errorf("error parsing cert bundle: %s", err) |
| } |
| |
| if key != nil { |
| switch keyType { |
| case "rsa": |
| parsedCertBundle.PrivateKeyType = certutil.RSAPrivateKey |
| parsedCertBundle.PrivateKey = key |
| parsedCertBundle.PrivateKeyBytes = x509.MarshalPKCS1PrivateKey(key.(*rsa.PrivateKey)) |
| case "ec": |
| parsedCertBundle.PrivateKeyType = certutil.ECPrivateKey |
| parsedCertBundle.PrivateKey = key |
| parsedCertBundle.PrivateKeyBytes, err = x509.MarshalECPrivateKey(key.(*ecdsa.PrivateKey)) |
| if err != nil { |
| return nil, fmt.Errorf("error parsing EC key: %s", err) |
| } |
| case "ed25519": |
| parsedCertBundle.PrivateKeyType = certutil.Ed25519PrivateKey |
| parsedCertBundle.PrivateKey = key |
| parsedCertBundle.PrivateKeyBytes, err = x509.MarshalPKCS8PrivateKey(key.(ed25519.PrivateKey)) |
| if err != nil { |
| return nil, fmt.Errorf("error parsing Ed25519 key: %s", err) |
| } |
| } |
| } |
| |
| switch { |
| case parsedCertBundle.Certificate == nil: |
| return nil, fmt.Errorf("did not find a certificate in the cert bundle") |
| case len(parsedCertBundle.CAChain) == 0 || parsedCertBundle.CAChain[0].Certificate == nil: |
| return nil, fmt.Errorf("did not find a CA in the cert bundle") |
| case parsedCertBundle.PrivateKey == nil: |
| return nil, fmt.Errorf("did not find a private key in the cert bundle") |
| case parsedCertBundle.PrivateKeyType == certutil.UnknownPrivateKey: |
| return nil, fmt.Errorf("could not figure out type of private key") |
| } |
| |
| switch { |
| case parsedCertBundle.PrivateKeyType == certutil.Ed25519PrivateKey && keyType != "ed25519": |
| fallthrough |
| case parsedCertBundle.PrivateKeyType == certutil.RSAPrivateKey && keyType != "rsa": |
| fallthrough |
| case parsedCertBundle.PrivateKeyType == certutil.ECPrivateKey && keyType != "ec": |
| return nil, fmt.Errorf("given key type does not match type found in bundle") |
| } |
| |
| cert := parsedCertBundle.Certificate |
| |
| if usage != cert.KeyUsage { |
| return nil, fmt.Errorf("expected usage of %#v, got %#v; ext usage is %#v", usage, cert.KeyUsage, cert.ExtKeyUsage) |
| } |
| |
| // There should only be one ext usage type, because only one is requested |
| // in the tests |
| if len(cert.ExtKeyUsage) != 1 { |
| return nil, fmt.Errorf("got wrong size key usage in generated cert; expected 1, values are %#v", cert.ExtKeyUsage) |
| } |
| switch extUsage { |
| case x509.ExtKeyUsageEmailProtection: |
| if cert.ExtKeyUsage[0] != x509.ExtKeyUsageEmailProtection { |
| return nil, fmt.Errorf("bad extended key usage") |
| } |
| case x509.ExtKeyUsageServerAuth: |
| if cert.ExtKeyUsage[0] != x509.ExtKeyUsageServerAuth { |
| return nil, fmt.Errorf("bad extended key usage") |
| } |
| case x509.ExtKeyUsageClientAuth: |
| if cert.ExtKeyUsage[0] != x509.ExtKeyUsageClientAuth { |
| return nil, fmt.Errorf("bad extended key usage") |
| } |
| case x509.ExtKeyUsageCodeSigning: |
| if cert.ExtKeyUsage[0] != x509.ExtKeyUsageCodeSigning { |
| return nil, fmt.Errorf("bad extended key usage") |
| } |
| } |
| |
| // TODO: We incremented 20->25 due to CircleCI execution |
| // being slow and pausing this test. We might consider recording the |
| // actual issuance time of the cert and calculating the expected |
| // validity period +/- fuzz, but that'd require recording and passing |
| // through more information. |
| if math.Abs(float64(time.Now().Add(validity).Unix()-cert.NotAfter.Unix())) > 25 { |
| return nil, fmt.Errorf("certificate validity end: %s; expected within 25 seconds of %s", cert.NotAfter.Format(time.RFC3339), time.Now().Add(validity).Format(time.RFC3339)) |
| } |
| |
| return parsedCertBundle, nil |
| } |
| |
| func generateURLSteps(t *testing.T, caCert, caKey string, intdata, reqdata map[string]interface{}) []logicaltest.TestStep { |
| expected := certutil.URLEntries{ |
| IssuingCertificates: []string{ |
| "http://example.com/ca1", |
| "http://example.com/ca2", |
| }, |
| CRLDistributionPoints: []string{ |
| "http://example.com/crl1", |
| "http://example.com/crl2", |
| }, |
| OCSPServers: []string{ |
| "http://example.com/ocsp1", |
| "http://example.com/ocsp2", |
| }, |
| } |
| csrTemplate := x509.CertificateRequest{ |
| Subject: pkix.Name{ |
| CommonName: "my@example.com", |
| }, |
| } |
| |
| priv1024, _ := rsa.GenerateKey(rand.Reader, 1024) |
| csr1024, _ := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, priv1024) |
| csrPem1024 := strings.TrimSpace(string(pem.EncodeToMemory(&pem.Block{ |
| Type: "CERTIFICATE REQUEST", |
| Bytes: csr1024, |
| }))) |
| |
| priv2048, _ := rsa.GenerateKey(rand.Reader, 2048) |
| csr2048, _ := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, priv2048) |
| csrPem2048 := strings.TrimSpace(string(pem.EncodeToMemory(&pem.Block{ |
| Type: "CERTIFICATE REQUEST", |
| Bytes: csr2048, |
| }))) |
| |
| ret := []logicaltest.TestStep{ |
| { |
| Operation: logical.UpdateOperation, |
| Path: "root/generate/exported", |
| Data: map[string]interface{}{ |
| "common_name": "Root Cert", |
| "ttl": "180h", |
| }, |
| Check: func(resp *logical.Response) error { |
| if resp.Secret != nil && resp.Secret.LeaseID != "" { |
| return fmt.Errorf("root returned with a lease") |
| } |
| return nil |
| }, |
| }, |
| |
| { |
| Operation: logical.UpdateOperation, |
| Path: "config/urls", |
| Data: map[string]interface{}{ |
| "issuing_certificates": strings.Join(expected.IssuingCertificates, ","), |
| "crl_distribution_points": strings.Join(expected.CRLDistributionPoints, ","), |
| "ocsp_servers": strings.Join(expected.OCSPServers, ","), |
| }, |
| }, |
| |
| { |
| Operation: logical.ReadOperation, |
| Path: "config/urls", |
| Check: func(resp *logical.Response) error { |
| if resp.Data == nil { |
| return fmt.Errorf("no data returned") |
| } |
| var entries certutil.URLEntries |
| err := mapstructure.Decode(resp.Data, &entries) |
| if err != nil { |
| return err |
| } |
| if !reflect.DeepEqual(entries, expected) { |
| return fmt.Errorf("expected urls\n%#v\ndoes not match provided\n%#v\n", expected, entries) |
| } |
| |
| return nil |
| }, |
| }, |
| |
| { |
| Operation: logical.UpdateOperation, |
| Path: "root/sign-intermediate", |
| Data: map[string]interface{}{ |
| "common_name": "intermediate.cert.com", |
| "csr": csrPem1024, |
| "format": "der", |
| }, |
| ErrorOk: true, |
| Check: func(resp *logical.Response) error { |
| if !resp.IsError() { |
| return fmt.Errorf("expected an error response but did not get one") |
| } |
| if !strings.Contains(resp.Data["error"].(string), "2048") { |
| return fmt.Errorf("received an error but not about a 1024-bit key, error was: %s", resp.Data["error"].(string)) |
| } |
| |
| return nil |
| }, |
| }, |
| |
| { |
| Operation: logical.UpdateOperation, |
| Path: "root/sign-intermediate", |
| Data: map[string]interface{}{ |
| "common_name": "intermediate.cert.com", |
| "csr": csrPem2048, |
| "signature_bits": 512, |
| "format": "der", |
| "not_before_duration": "2h", |
| // Let's Encrypt -- R3 SKID |
| "skid": "14:2E:B3:17:B7:58:56:CB:AE:50:09:40:E6:1F:AF:9D:8B:14:C2:C6", |
| }, |
| Check: func(resp *logical.Response) error { |
| certString := resp.Data["certificate"].(string) |
| if certString == "" { |
| return fmt.Errorf("no certificate returned") |
| } |
| if resp.Secret != nil && resp.Secret.LeaseID != "" { |
| return fmt.Errorf("signed intermediate returned with a lease") |
| } |
| certBytes, _ := base64.StdEncoding.DecodeString(certString) |
| certs, err := x509.ParseCertificates(certBytes) |
| if err != nil { |
| return fmt.Errorf("returned cert cannot be parsed: %w", err) |
| } |
| if len(certs) != 1 { |
| return fmt.Errorf("unexpected returned length of certificates: %d", len(certs)) |
| } |
| cert := certs[0] |
| |
| skid, _ := hex.DecodeString("142EB317B75856CBAE500940E61FAF9D8B14C2C6") |
| |
| switch { |
| case !reflect.DeepEqual(expected.IssuingCertificates, cert.IssuingCertificateURL): |
| return fmt.Errorf("IssuingCertificateURL:\nexpected\n%#v\ngot\n%#v\n", expected.IssuingCertificates, cert.IssuingCertificateURL) |
| case !reflect.DeepEqual(expected.CRLDistributionPoints, cert.CRLDistributionPoints): |
| return fmt.Errorf("CRLDistributionPoints:\nexpected\n%#v\ngot\n%#v\n", expected.CRLDistributionPoints, cert.CRLDistributionPoints) |
| case !reflect.DeepEqual(expected.OCSPServers, cert.OCSPServer): |
| return fmt.Errorf("OCSPServer:\nexpected\n%#v\ngot\n%#v\n", expected.OCSPServers, cert.OCSPServer) |
| case !reflect.DeepEqual([]string{"intermediate.cert.com"}, cert.DNSNames): |
| return fmt.Errorf("DNSNames\nexpected\n%#v\ngot\n%#v\n", []string{"intermediate.cert.com"}, cert.DNSNames) |
| case !reflect.DeepEqual(x509.SHA512WithRSA, cert.SignatureAlgorithm): |
| return fmt.Errorf("Signature Algorithm:\nexpected\n%#v\ngot\n%#v\n", x509.SHA512WithRSA, cert.SignatureAlgorithm) |
| case !reflect.DeepEqual(skid, cert.SubjectKeyId): |
| return fmt.Errorf("SKID:\nexpected\n%#v\ngot\n%#v\n", skid, cert.SubjectKeyId) |
| } |
| |
| if math.Abs(float64(time.Now().Add(-2*time.Hour).Unix()-cert.NotBefore.Unix())) > 10 { |
| t.Fatalf("root/sign-intermediate did not properly set validity period (notBefore): was %v vs expected %v", cert.NotBefore, time.Now().Add(-2*time.Hour)) |
| } |
| |
| return nil |
| }, |
| }, |
| |
| // Same as above but exclude adding to sans |
| { |
| Operation: logical.UpdateOperation, |
| Path: "root/sign-intermediate", |
| Data: map[string]interface{}{ |
| "common_name": "intermediate.cert.com", |
| "csr": csrPem2048, |
| "format": "der", |
| "exclude_cn_from_sans": true, |
| }, |
| Check: func(resp *logical.Response) error { |
| certString := resp.Data["certificate"].(string) |
| if certString == "" { |
| return fmt.Errorf("no certificate returned") |
| } |
| if resp.Secret != nil && resp.Secret.LeaseID != "" { |
| return fmt.Errorf("signed intermediate returned with a lease") |
| } |
| certBytes, _ := base64.StdEncoding.DecodeString(certString) |
| certs, err := x509.ParseCertificates(certBytes) |
| if err != nil { |
| return fmt.Errorf("returned cert cannot be parsed: %w", err) |
| } |
| if len(certs) != 1 { |
| return fmt.Errorf("unexpected returned length of certificates: %d", len(certs)) |
| } |
| cert := certs[0] |
| |
| switch { |
| case !reflect.DeepEqual(expected.IssuingCertificates, cert.IssuingCertificateURL): |
| return fmt.Errorf("expected\n%#v\ngot\n%#v\n", expected.IssuingCertificates, cert.IssuingCertificateURL) |
| case !reflect.DeepEqual(expected.CRLDistributionPoints, cert.CRLDistributionPoints): |
| return fmt.Errorf("expected\n%#v\ngot\n%#v\n", expected.CRLDistributionPoints, cert.CRLDistributionPoints) |
| case !reflect.DeepEqual(expected.OCSPServers, cert.OCSPServer): |
| return fmt.Errorf("expected\n%#v\ngot\n%#v\n", expected.OCSPServers, cert.OCSPServer) |
| case !reflect.DeepEqual([]string(nil), cert.DNSNames): |
| return fmt.Errorf("expected\n%#v\ngot\n%#v\n", []string(nil), cert.DNSNames) |
| } |
| |
| return nil |
| }, |
| }, |
| } |
| return ret |
| } |
| |
| func generateCSR(t *testing.T, csrTemplate *x509.CertificateRequest, keyType string, keyBits int) (interface{}, []byte, string) { |
| t.Helper() |
| |
| var priv interface{} |
| var err error |
| switch keyType { |
| case "rsa": |
| priv, err = rsa.GenerateKey(rand.Reader, keyBits) |
| case "ec": |
| switch keyBits { |
| case 224: |
| priv, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader) |
| case 256: |
| priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) |
| case 384: |
| priv, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) |
| case 521: |
| priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader) |
| default: |
| t.Fatalf("Got unknown ec< key bits: %v", keyBits) |
| } |
| case "ed25519": |
| _, priv, err = ed25519.GenerateKey(rand.Reader) |
| } |
| |
| if err != nil { |
| t.Fatalf("Got error generating private key for CSR: %v", err) |
| } |
| |
| csr, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, priv) |
| if err != nil { |
| t.Fatalf("Got error generating CSR: %v", err) |
| } |
| |
| csrPem := strings.TrimSpace(string(pem.EncodeToMemory(&pem.Block{ |
| Type: "CERTIFICATE REQUEST", |
| Bytes: csr, |
| }))) |
| |
| return priv, csr, csrPem |
| } |
| |
| func generateCSRSteps(t *testing.T, caCert, caKey string, intdata, reqdata map[string]interface{}) []logicaltest.TestStep { |
| csrTemplate, csrPem := generateTestCsr(t, certutil.RSAPrivateKey, 2048) |
| |
| ret := []logicaltest.TestStep{ |
| { |
| Operation: logical.UpdateOperation, |
| Path: "root/generate/exported", |
| Data: map[string]interface{}{ |
| "common_name": "Root Cert", |
| "ttl": "180h", |
| "max_path_length": 0, |
| }, |
| }, |
| |
| { |
| Operation: logical.UpdateOperation, |
| Path: "root/sign-intermediate", |
| Data: map[string]interface{}{ |
| "use_csr_values": true, |
| "csr": csrPem, |
| "format": "der", |
| }, |
| ErrorOk: true, |
| }, |
| |
| { |
| Operation: logical.DeleteOperation, |
| Path: "root", |
| }, |
| |
| { |
| Operation: logical.UpdateOperation, |
| Path: "root/generate/exported", |
| Data: map[string]interface{}{ |
| "common_name": "Root Cert", |
| "ttl": "180h", |
| "max_path_length": 1, |
| }, |
| }, |
| |
| { |
| Operation: logical.UpdateOperation, |
| Path: "root/sign-intermediate", |
| Data: map[string]interface{}{ |
| "use_csr_values": true, |
| "csr": csrPem, |
| "format": "der", |
| }, |
| Check: func(resp *logical.Response) error { |
| certString := resp.Data["certificate"].(string) |
| if certString == "" { |
| return fmt.Errorf("no certificate returned") |
| } |
| certBytes, _ := base64.StdEncoding.DecodeString(certString) |
| certs, err := x509.ParseCertificates(certBytes) |
| if err != nil { |
| return fmt.Errorf("returned cert cannot be parsed: %w", err) |
| } |
| if len(certs) != 1 { |
| return fmt.Errorf("unexpected returned length of certificates: %d", len(certs)) |
| } |
| cert := certs[0] |
| |
| if cert.MaxPathLen != 0 { |
| return fmt.Errorf("max path length of %d does not match the requested of 3", cert.MaxPathLen) |
| } |
| if !cert.MaxPathLenZero { |
| return fmt.Errorf("max path length zero is not set") |
| } |
| |
| // We need to set these as they are filled in with unparsed values in the final cert |
| csrTemplate.Subject.Names = cert.Subject.Names |
| csrTemplate.Subject.ExtraNames = cert.Subject.ExtraNames |
| |
| switch { |
| case !reflect.DeepEqual(cert.Subject, csrTemplate.Subject): |
| return fmt.Errorf("cert subject\n%#v\ndoes not match csr subject\n%#v\n", cert.Subject, csrTemplate.Subject) |
| case !reflect.DeepEqual(cert.DNSNames, csrTemplate.DNSNames): |
| return fmt.Errorf("cert dns names\n%#v\ndoes not match csr dns names\n%#v\n", cert.DNSNames, csrTemplate.DNSNames) |
| case !reflect.DeepEqual(cert.EmailAddresses, csrTemplate.EmailAddresses): |
| return fmt.Errorf("cert email addresses\n%#v\ndoes not match csr email addresses\n%#v\n", cert.EmailAddresses, csrTemplate.EmailAddresses) |
| case !reflect.DeepEqual(cert.IPAddresses, csrTemplate.IPAddresses): |
| return fmt.Errorf("cert ip addresses\n%#v\ndoes not match csr ip addresses\n%#v\n", cert.IPAddresses, csrTemplate.IPAddresses) |
| } |
| return nil |
| }, |
| }, |
| } |
| return ret |
| } |
| |
| func generateTestCsr(t *testing.T, keyType certutil.PrivateKeyType, keyBits int) (x509.CertificateRequest, string) { |
| t.Helper() |
| |
| csrTemplate := x509.CertificateRequest{ |
| Subject: pkix.Name{ |
| Country: []string{"MyCountry"}, |
| PostalCode: []string{"MyPostalCode"}, |
| SerialNumber: "MySerialNumber", |
| CommonName: "my@example.com", |
| }, |
| DNSNames: []string{ |
| "name1.example.com", |
| "name2.example.com", |
| "name3.example.com", |
| }, |
| EmailAddresses: []string{ |
| "name1@example.com", |
| "name2@example.com", |
| "name3@example.com", |
| }, |
| IPAddresses: []net.IP{ |
| net.ParseIP("::ff:1:2:3:4"), |
| net.ParseIP("::ff:5:6:7:8"), |
| }, |
| } |
| |
| _, _, csrPem := generateCSR(t, &csrTemplate, string(keyType), keyBits) |
| return csrTemplate, csrPem |
| } |
| |
| // Generates steps to test out various role permutations |
| func generateRoleSteps(t *testing.T, useCSRs bool) []logicaltest.TestStep { |
| roleVals := roleEntry{ |
| MaxTTL: 12 * time.Hour, |
| KeyType: "rsa", |
| KeyBits: 2048, |
| RequireCN: true, |
| AllowWildcardCertificates: new(bool), |
| } |
| *roleVals.AllowWildcardCertificates = true |
| |
| issueVals := certutil.IssueData{} |
| ret := []logicaltest.TestStep{} |
| |
| roleTestStep := logicaltest.TestStep{ |
| Operation: logical.UpdateOperation, |
| Path: "roles/test", |
| } |
| var issueTestStep logicaltest.TestStep |
| if useCSRs { |
| issueTestStep = logicaltest.TestStep{ |
| Operation: logical.UpdateOperation, |
| Path: "sign/test", |
| } |
| } else { |
| issueTestStep = logicaltest.TestStep{ |
| Operation: logical.UpdateOperation, |
| Path: "issue/test", |
| } |
| } |
| |
| generatedRSAKeys := map[int]crypto.Signer{} |
| generatedECKeys := map[int]crypto.Signer{} |
| generatedEdKeys := map[int]crypto.Signer{} |
| /* |
| // For the number of tests being run, a seed of 1 has been tested |
| // to hit all of the various values below. However, for normal |
| // testing we use a randomized time for maximum fuzziness. |
| */ |
| var seed int64 = 1 |
| fixedSeed := os.Getenv("VAULT_PKITESTS_FIXED_SEED") |
| if len(fixedSeed) == 0 { |
| seed = time.Now().UnixNano() |
| } else { |
| var err error |
| seed, err = strconv.ParseInt(fixedSeed, 10, 64) |
| if err != nil { |
| t.Fatalf("error parsing fixed seed of %s: %v", fixedSeed, err) |
| } |
| } |
| mathRand := mathrand.New(mathrand.NewSource(seed)) |
| // t.Logf("seed under test: %v", seed) |
| |
| // Used by tests not toggling common names to turn off the behavior of random key bit fuzziness |
| keybitSizeRandOff := false |
| |
| genericErrorOkCheck := func(resp *logical.Response) error { |
| if resp.IsError() { |
| return nil |
| } |
| return fmt.Errorf("expected an error, but did not seem to get one") |
| } |
| |
| // Adds tests with the currently configured issue/role information |
| addTests := func(testCheck logicaltest.TestCheckFunc) { |
| stepCount++ |
| // t.Logf("test step %d\nrole vals: %#v\n", stepCount, roleVals) |
| stepCount++ |
| // t.Logf("test step %d\nissue vals: %#v\n", stepCount, issueTestStep) |
| roleTestStep.Data = roleVals.ToResponseData() |
| roleTestStep.Data["generate_lease"] = false |
| ret = append(ret, roleTestStep) |
| issueTestStep.Data = structs.New(issueVals).Map() |
| switch { |
| case issueTestStep.ErrorOk: |
| issueTestStep.Check = genericErrorOkCheck |
| case testCheck != nil: |
| issueTestStep.Check = testCheck |
| default: |
| issueTestStep.Check = nil |
| } |
| ret = append(ret, issueTestStep) |
| } |
| |
| getCountryCheck := func(role roleEntry) logicaltest.TestCheckFunc { |
| var certBundle certutil.CertBundle |
| return func(resp *logical.Response) error { |
| err := mapstructure.Decode(resp.Data, &certBundle) |
| if err != nil { |
| return err |
| } |
| parsedCertBundle, err := certBundle.ToParsedCertBundle() |
| if err != nil { |
| return fmt.Errorf("error checking generated certificate: %s", err) |
| } |
| cert := parsedCertBundle.Certificate |
| |
| expected := strutil.RemoveDuplicates(role.Country, true) |
| if !reflect.DeepEqual(cert.Subject.Country, expected) { |
| return fmt.Errorf("error: returned certificate has Country of %s but %s was specified in the role", cert.Subject.Country, expected) |
| } |
| return nil |
| } |
| } |
| |
| getOuCheck := func(role roleEntry) logicaltest.TestCheckFunc { |
| var certBundle certutil.CertBundle |
| return func(resp *logical.Response) error { |
| err := mapstructure.Decode(resp.Data, &certBundle) |
| if err != nil { |
| return err |
| } |
| parsedCertBundle, err := certBundle.ToParsedCertBundle() |
| if err != nil { |
| return fmt.Errorf("error checking generated certificate: %s", err) |
| } |
| cert := parsedCertBundle.Certificate |
| |
| expected := strutil.RemoveDuplicatesStable(role.OU, true) |
| if !reflect.DeepEqual(cert.Subject.OrganizationalUnit, expected) { |
| return fmt.Errorf("error: returned certificate has OU of %s but %s was specified in the role", cert.Subject.OrganizationalUnit, expected) |
| } |
| return nil |
| } |
| } |
| |
| getOrganizationCheck := func(role roleEntry) logicaltest.TestCheckFunc { |
| var certBundle certutil.CertBundle |
| return func(resp *logical.Response) error { |
| err := mapstructure.Decode(resp.Data, &certBundle) |
| if err != nil { |
| return err |
| } |
| parsedCertBundle, err := certBundle.ToParsedCertBundle() |
| if err != nil { |
| return fmt.Errorf("error checking generated certificate: %s", err) |
| } |
| cert := parsedCertBundle.Certificate |
| |
| expected := strutil.RemoveDuplicates(role.Organization, true) |
| if !reflect.DeepEqual(cert.Subject.Organization, expected) { |
| return fmt.Errorf("error: returned certificate has Organization of %s but %s was specified in the role", cert.Subject.Organization, expected) |
| } |
| return nil |
| } |
| } |
| |
| getLocalityCheck := func(role roleEntry) logicaltest.TestCheckFunc { |
| var certBundle certutil.CertBundle |
| return func(resp *logical.Response) error { |
| err := mapstructure.Decode(resp.Data, &certBundle) |
| if err != nil { |
| return err |
| } |
| parsedCertBundle, err := certBundle.ToParsedCertBundle() |
| if err != nil { |
| return fmt.Errorf("error checking generated certificate: %s", err) |
| } |
| cert := parsedCertBundle.Certificate |
| |
| expected := strutil.RemoveDuplicates(role.Locality, true) |
| if !reflect.DeepEqual(cert.Subject.Locality, expected) { |
| return fmt.Errorf("error: returned certificate has Locality of %s but %s was specified in the role", cert.Subject.Locality, expected) |
| } |
| return nil |
| } |
| } |
| |
| getProvinceCheck := func(role roleEntry) logicaltest.TestCheckFunc { |
| var certBundle certutil.CertBundle |
| return func(resp *logical.Response) error { |
| err := mapstructure.Decode(resp.Data, &certBundle) |
| if err != nil { |
| return err |
| } |
| parsedCertBundle, err := certBundle.ToParsedCertBundle() |
| if err != nil { |
| return fmt.Errorf("error checking generated certificate: %s", err) |
| } |
| cert := parsedCertBundle.Certificate |
| |
| expected := strutil.RemoveDuplicates(role.Province, true) |
| if !reflect.DeepEqual(cert.Subject.Province, expected) { |
| return fmt.Errorf("error: returned certificate has Province of %s but %s was specified in the role", cert.Subject.Province, expected) |
| } |
| return nil |
| } |
| } |
| |
| getStreetAddressCheck := func(role roleEntry) logicaltest.TestCheckFunc { |
| var certBundle certutil.CertBundle |
| return func(resp *logical.Response) error { |
| err := mapstructure.Decode(resp.Data, &certBundle) |
| if err != nil { |
| return err |
| } |
| parsedCertBundle, err := certBundle.ToParsedCertBundle() |
| if err != nil { |
| return fmt.Errorf("error checking generated certificate: %s", err) |
| } |
| cert := parsedCertBundle.Certificate |
| |
| expected := strutil.RemoveDuplicates(role.StreetAddress, true) |
| if !reflect.DeepEqual(cert.Subject.StreetAddress, expected) { |
| return fmt.Errorf("error: returned certificate has StreetAddress of %s but %s was specified in the role", cert.Subject.StreetAddress, expected) |
| } |
| return nil |
| } |
| } |
| |
| getPostalCodeCheck := func(role roleEntry) logicaltest.TestCheckFunc { |
| var certBundle certutil.CertBundle |
| return func(resp *logical.Response) error { |
| err := mapstructure.Decode(resp.Data, &certBundle) |
| if err != nil { |
| return err |
| } |
| parsedCertBundle, err := certBundle.ToParsedCertBundle() |
| if err != nil { |
| return fmt.Errorf("error checking generated certificate: %s", err) |
| } |
| cert := parsedCertBundle.Certificate |
| |
| expected := strutil.RemoveDuplicates(role.PostalCode, true) |
| if !reflect.DeepEqual(cert.Subject.PostalCode, expected) { |
| return fmt.Errorf("error: returned certificate has PostalCode of %s but %s was specified in the role", cert.Subject.PostalCode, expected) |
| } |
| return nil |
| } |
| } |
| |
| getNotBeforeCheck := func(role roleEntry) logicaltest.TestCheckFunc { |
| var certBundle certutil.CertBundle |
| return func(resp *logical.Response) error { |
| err := mapstructure.Decode(resp.Data, &certBundle) |
| if err != nil { |
| return err |
| } |
| parsedCertBundle, err := certBundle.ToParsedCertBundle() |
| if err != nil { |
| return fmt.Errorf("error checking generated certificate: %s", err) |
| } |
| cert := parsedCertBundle.Certificate |
| |
| actualDiff := time.Since(cert.NotBefore) |
| certRoleDiff := (role.NotBeforeDuration - actualDiff).Truncate(time.Second) |
| // These times get truncated, so give a 1 second buffer on each side |
| if certRoleDiff >= -1*time.Second && certRoleDiff <= 1*time.Second { |
| return nil |
| } |
| return fmt.Errorf("validity period out of range diff: %v", certRoleDiff) |
| } |
| } |
| |
| // Returns a TestCheckFunc that performs various validity checks on the |
| // returned certificate information, mostly within checkCertsAndPrivateKey |
| getCnCheck := func(name string, role roleEntry, key crypto.Signer, usage x509.KeyUsage, extUsage x509.ExtKeyUsage, validity time.Duration) logicaltest.TestCheckFunc { |
| var certBundle certutil.CertBundle |
| return func(resp *logical.Response) error { |
| err := mapstructure.Decode(resp.Data, &certBundle) |
| if err != nil { |
| return err |
| } |
| parsedCertBundle, err := checkCertsAndPrivateKey(role.KeyType, key, usage, extUsage, validity, &certBundle) |
| if err != nil { |
| return fmt.Errorf("error checking generated certificate: %s", err) |
| } |
| cert := parsedCertBundle.Certificate |
| if cert.Subject.CommonName != name { |
| return fmt.Errorf("error: returned certificate has CN of %s but %s was requested", cert.Subject.CommonName, name) |
| } |
| if strings.Contains(cert.Subject.CommonName, "@") { |
| if len(cert.DNSNames) != 0 || len(cert.EmailAddresses) != 1 { |
| return fmt.Errorf("error: found more than one DNS SAN or not one Email SAN but only one was requested, cert.DNSNames = %#v, cert.EmailAddresses = %#v", cert.DNSNames, cert.EmailAddresses) |
| } |
| } else { |
| if len(cert.DNSNames) != 1 || len(cert.EmailAddresses) != 0 { |
| return fmt.Errorf("error: found more than one Email SAN or not one DNS SAN but only one was requested, cert.DNSNames = %#v, cert.EmailAddresses = %#v", cert.DNSNames, cert.EmailAddresses) |
| } |
| } |
| var retName string |
| if len(cert.DNSNames) > 0 { |
| retName = cert.DNSNames[0] |
| } |
| if len(cert.EmailAddresses) > 0 { |
| retName = cert.EmailAddresses[0] |
| } |
| if retName != name { |
| // Check IDNA |
| p := idna.New( |
| idna.StrictDomainName(true), |
| idna.VerifyDNSLength(true), |
| ) |
| converted, err := p.ToUnicode(retName) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if converted != name { |
| return fmt.Errorf("error: returned certificate has a DNS SAN of %s (from idna: %s) but %s was requested", retName, converted, name) |
| } |
| } |
| return nil |
| } |
| } |
| |
| type csrPlan struct { |
| errorOk bool |
| roleKeyBits int |
| cert string |
| privKey crypto.Signer |
| } |
| |
| getCsr := func(keyType string, keyBits int, csrTemplate *x509.CertificateRequest) (*pem.Block, crypto.Signer) { |
| var privKey crypto.Signer |
| var ok bool |
| switch keyType { |
| case "rsa": |
| privKey, ok = generatedRSAKeys[keyBits] |
| if !ok { |
| privKey, _ = rsa.GenerateKey(rand.Reader, keyBits) |
| generatedRSAKeys[keyBits] = privKey |
| } |
| |
| case "ec": |
| var curve elliptic.Curve |
| |
| switch keyBits { |
| case 224: |
| curve = elliptic.P224() |
| case 256: |
| curve = elliptic.P256() |
| case 384: |
| curve = elliptic.P384() |
| case 521: |
| curve = elliptic.P521() |
| } |
| |
| privKey, ok = generatedECKeys[keyBits] |
| if !ok { |
| privKey, _ = ecdsa.GenerateKey(curve, rand.Reader) |
| generatedECKeys[keyBits] = privKey |
| } |
| |
| case "ed25519": |
| privKey, ok = generatedEdKeys[keyBits] |
| if !ok { |
| _, privKey, _ = ed25519.GenerateKey(rand.Reader) |
| generatedEdKeys[keyBits] = privKey |
| } |
| |
| default: |
| panic("invalid key type: " + keyType) |
| } |
| |
| csr, err := x509.CreateCertificateRequest(rand.Reader, csrTemplate, privKey) |
| if err != nil { |
| t.Fatalf("Error creating certificate request: %s", err) |
| } |
| block := pem.Block{ |
| Type: "CERTIFICATE REQUEST", |
| Bytes: csr, |
| } |
| return &block, privKey |
| } |
| |
| getRandCsr := func(keyType string, errorOk bool, csrTemplate *x509.CertificateRequest) csrPlan { |
| rsaKeyBits := []int{2048, 3072, 4096} |
| ecKeyBits := []int{224, 256, 384, 521} |
| plan := csrPlan{errorOk: errorOk} |
| |
| var testBitSize int |
| switch keyType { |
| case "rsa": |
| plan.roleKeyBits = rsaKeyBits[mathRand.Int()%len(rsaKeyBits)] |
| testBitSize = plan.roleKeyBits |
| |
| // If we don't expect an error already, randomly choose a |
| // key size and expect an error if it's less than the role |
| // setting |
| if !keybitSizeRandOff && !errorOk { |
| testBitSize = rsaKeyBits[mathRand.Int()%len(rsaKeyBits)] |
| } |
| |
| if testBitSize < plan.roleKeyBits { |
| plan.errorOk = true |
| } |
| |
| case "ec": |
| plan.roleKeyBits = ecKeyBits[mathRand.Int()%len(ecKeyBits)] |
| testBitSize = plan.roleKeyBits |
| |
| // If we don't expect an error already, randomly choose a |
| // key size and expect an error if it's less than the role |
| // setting |
| if !keybitSizeRandOff && !errorOk { |
| testBitSize = ecKeyBits[mathRand.Int()%len(ecKeyBits)] |
| } |
| |
| if testBitSize < plan.roleKeyBits { |
| plan.errorOk = true |
| } |
| |
| default: |
| panic("invalid key type: " + keyType) |
| } |
| if len(os.Getenv("VAULT_VERBOSE_PKITESTS")) > 0 { |
| t.Logf("roleKeyBits=%d testBitSize=%d errorOk=%v", plan.roleKeyBits, testBitSize, plan.errorOk) |
| } |
| |
| block, privKey := getCsr(keyType, testBitSize, csrTemplate) |
| plan.cert = strings.TrimSpace(string(pem.EncodeToMemory(block))) |
| plan.privKey = privKey |
| return plan |
| } |
| |
| // Common names to test with the various role flags toggled |
| var commonNames struct { |
| Localhost bool `structs:"localhost"` |
| BareDomain bool `structs:"example.com"` |
| SecondDomain bool `structs:"foobar.com"` |
| SubDomain bool `structs:"foo.example.com"` |
| Wildcard bool `structs:"*.example.com"` |
| SubSubdomain bool `structs:"foo.bar.example.com"` |
| SubSubdomainWildcard bool `structs:"*.bar.example.com"` |
| GlobDomain bool `structs:"fooexample.com"` |
| IDN bool `structs:"daɪˈɛrɨsɨs"` |
| AnyHost bool `structs:"porkslap.beer"` |
| } |
| |
| // Adds a series of tests based on the current selection of |
| // allowed common names; contains some (seeded) randomness |
| // |
| // This allows for a variety of common names to be tested in various |
| // combinations with allowed toggles of the role |
| addCnTests := func() { |
| cnMap := structs.New(commonNames).Map() |
| for name, allowedInt := range cnMap { |
| roleVals.KeyType = "rsa" |
| roleVals.KeyBits = 2048 |
| if mathRand.Int()%3 == 1 { |
| roleVals.KeyType = "ec" |
| roleVals.KeyBits = 224 |
| } |
| |
| roleVals.ServerFlag = false |
| roleVals.ClientFlag = false |
| roleVals.CodeSigningFlag = false |
| roleVals.EmailProtectionFlag = false |
| |
| var usage []string |
| if mathRand.Int()%2 == 1 { |
| usage = append(usage, "DigitalSignature") |
| } |
| if mathRand.Int()%2 == 1 { |
| usage = append(usage, "ContentCoMmitment") |
| } |
| if mathRand.Int()%2 == 1 { |
| usage = append(usage, "KeyEncipherment") |
| } |
| if mathRand.Int()%2 == 1 { |
| usage = append(usage, "DataEncipherment") |
| } |
| if mathRand.Int()%2 == 1 { |
| usage = append(usage, "KeyAgreemEnt") |
| } |
| if mathRand.Int()%2 == 1 { |
| usage = append(usage, "CertSign") |
| } |
| if mathRand.Int()%2 == 1 { |
| usage = append(usage, "CRLSign") |
| } |
| if mathRand.Int()%2 == 1 { |
| usage = append(usage, "EncipherOnly") |
| } |
| if mathRand.Int()%2 == 1 { |
| usage = append(usage, "DecipherOnly") |
| } |
| |
| roleVals.KeyUsage = usage |
| parsedKeyUsage := parseKeyUsages(roleVals.KeyUsage) |
| if parsedKeyUsage == 0 && len(usage) != 0 { |
| panic("parsed key usages was zero") |
| } |
| |
| var extUsage x509.ExtKeyUsage |
| i := mathRand.Int() % 4 |
| switch { |
| case i == 0: |
| // Punt on this for now since I'm not clear the actual proper |
| // way to format these |
| if name != "daɪˈɛrɨsɨs" { |
| extUsage = x509.ExtKeyUsageEmailProtection |
| roleVals.EmailProtectionFlag = true |
| break |
| } |
| fallthrough |
| case i == 1: |
| extUsage = x509.ExtKeyUsageServerAuth |
| roleVals.ServerFlag = true |
| case i == 2: |
| extUsage = x509.ExtKeyUsageClientAuth |
| roleVals.ClientFlag = true |
| default: |
| extUsage = x509.ExtKeyUsageCodeSigning |
| roleVals.CodeSigningFlag = true |
| } |
| |
| allowed := allowedInt.(bool) |
| issueVals.CommonName = name |
| if roleVals.EmailProtectionFlag { |
| if !strings.HasPrefix(name, "*") { |
| issueVals.CommonName = "user@" + issueVals.CommonName |
| } |
| } |
| |
| issueTestStep.ErrorOk = !allowed |
| |
| validity := roleVals.MaxTTL |
| |
| if useCSRs { |
| templ := &x509.CertificateRequest{ |
| Subject: pkix.Name{ |
| CommonName: issueVals.CommonName, |
| }, |
| } |
| plan := getRandCsr(roleVals.KeyType, issueTestStep.ErrorOk, templ) |
| issueVals.CSR = plan.cert |
| roleVals.KeyBits = plan.roleKeyBits |
| issueTestStep.ErrorOk = plan.errorOk |
| |
| addTests(getCnCheck(issueVals.CommonName, roleVals, plan.privKey, x509.KeyUsage(parsedKeyUsage), extUsage, validity)) |
| } else { |
| addTests(getCnCheck(issueVals.CommonName, roleVals, nil, x509.KeyUsage(parsedKeyUsage), extUsage, validity)) |
| } |
| } |
| } |
| |
| funcs := []interface{}{ |
| addCnTests, getCnCheck, getCountryCheck, getLocalityCheck, getNotBeforeCheck, |
| getOrganizationCheck, getOuCheck, getPostalCodeCheck, getRandCsr, getStreetAddressCheck, |
| getProvinceCheck, |
| } |
| if len(os.Getenv("VAULT_VERBOSE_PKITESTS")) > 0 { |
| t.Logf("funcs=%d", len(funcs)) |
| } |
| |
| // Common Name tests |
| { |
| // common_name not provided |
| issueVals.CommonName = "" |
| issueTestStep.ErrorOk = true |
| addTests(nil) |
| |
| // Nothing is allowed |
| addCnTests() |
| |
| roleVals.AllowLocalhost = true |
| commonNames.Localhost = true |
| addCnTests() |
| |
| roleVals.AllowedDomains = []string{"foobar.com"} |
| addCnTests() |
| |
| roleVals.AllowedDomains = []string{"example.com"} |
| roleVals.AllowSubdomains = true |
| commonNames.SubDomain = true |
| commonNames.Wildcard = true |
| commonNames.SubSubdomain = true |
| commonNames.SubSubdomainWildcard = true |
| addCnTests() |
| |
| roleVals.AllowedDomains = []string{"foobar.com", "example.com"} |
| commonNames.SecondDomain = true |
| roleVals.AllowBareDomains = true |
| commonNames.BareDomain = true |
| addCnTests() |
| |
| roleVals.AllowedDomains = []string{"foobar.com", "*example.com"} |
| roleVals.AllowGlobDomains = true |
| commonNames.GlobDomain = true |
| addCnTests() |
| |
| roleVals.AllowAnyName = true |
| roleVals.EnforceHostnames = true |
| commonNames.AnyHost = true |
| commonNames.IDN = true |
| addCnTests() |
| |
| roleVals.EnforceHostnames = false |
| addCnTests() |
| |
| // Ensure that we end up with acceptable key sizes since they won't be |
| // toggled any longer |
| keybitSizeRandOff = true |
| addCnTests() |
| } |
| // Country tests |
| { |
| roleVals.Country = []string{"foo"} |
| addTests(getCountryCheck(roleVals)) |
| |
| roleVals.Country = []string{"foo", "bar"} |
| addTests(getCountryCheck(roleVals)) |
| } |
| // OU tests |
| { |
| roleVals.OU = []string{"foo"} |
| addTests(getOuCheck(roleVals)) |
| |
| roleVals.OU = []string{"bar", "foo"} |
| addTests(getOuCheck(roleVals)) |
| } |
| // Organization tests |
| { |
| roleVals.Organization = []string{"system:masters"} |
| addTests(getOrganizationCheck(roleVals)) |
| |
| roleVals.Organization = []string{"foo", "bar"} |
| addTests(getOrganizationCheck(roleVals)) |
| } |
| // Locality tests |
| { |
| roleVals.Locality = []string{"foo"} |
| addTests(getLocalityCheck(roleVals)) |
| |
| roleVals.Locality = []string{"foo", "bar"} |
| addTests(getLocalityCheck(roleVals)) |
| } |
| // Province tests |
| { |
| roleVals.Province = []string{"foo"} |
| addTests(getProvinceCheck(roleVals)) |
| |
| roleVals.Province = []string{"foo", "bar"} |
| addTests(getProvinceCheck(roleVals)) |
| } |
| // StreetAddress tests |
| { |
| roleVals.StreetAddress = []string{"123 foo street"} |
| addTests(getStreetAddressCheck(roleVals)) |
| |
| roleVals.StreetAddress = []string{"123 foo street", "456 bar avenue"} |
| addTests(getStreetAddressCheck(roleVals)) |
| } |
| // PostalCode tests |
| { |
| roleVals.PostalCode = []string{"f00"} |
| addTests(getPostalCodeCheck(roleVals)) |
| |
| roleVals.PostalCode = []string{"f00", "b4r"} |
| addTests(getPostalCodeCheck(roleVals)) |
| } |
| // NotBefore tests |
| { |
| roleVals.NotBeforeDuration = 10 * time.Second |
| addTests(getNotBeforeCheck(roleVals)) |
| |
| roleVals.NotBeforeDuration = 30 * time.Second |
| addTests(getNotBeforeCheck(roleVals)) |
| |
| roleVals.NotBeforeDuration = 0 |
| } |
| |
| // IP SAN tests |
| { |
| getIpCheck := func(expectedIp ...net.IP) logicaltest.TestCheckFunc { |
| return func(resp *logical.Response) error { |
| var certBundle certutil.CertBundle |
| err := mapstructure.Decode(resp.Data, &certBundle) |
| if err != nil { |
| return err |
| } |
| parsedCertBundle, err := certBundle.ToParsedCertBundle() |
| if err != nil { |
| return fmt.Errorf("error parsing cert bundle: %s", err) |
| } |
| cert := parsedCertBundle.Certificate |
| var expected []net.IP |
| expected = append(expected, expectedIp...) |
| if diff := deep.Equal(cert.IPAddresses, expected); len(diff) > 0 { |
| return fmt.Errorf("wrong SAN IPs, diff: %v", diff) |
| } |
| return nil |
| } |
| } |
| addIPSANTests := func(useCSRs, useCSRSANs, allowIPSANs, errorOk bool, ipSANs string, csrIPSANs []net.IP, check logicaltest.TestCheckFunc) { |
| if useCSRs { |
| csrTemplate := &x509.CertificateRequest{ |
| Subject: pkix.Name{ |
| CommonName: issueVals.CommonName, |
| }, |
| IPAddresses: csrIPSANs, |
| } |
| block, _ := getCsr(roleVals.KeyType, roleVals.KeyBits, csrTemplate) |
| issueVals.CSR = strings.TrimSpace(string(pem.EncodeToMemory(block))) |
| } |
| oldRoleVals, oldIssueVals, oldIssueTestStep := roleVals, issueVals, issueTestStep |
| roleVals.UseCSRSANs = useCSRSANs |
| roleVals.AllowIPSANs = allowIPSANs |
| issueVals.CommonName = "someone@example.com" |
| issueVals.IPSANs = ipSANs |
| issueTestStep.ErrorOk = errorOk |
| addTests(check) |
| roleVals, issueVals, issueTestStep = oldRoleVals, oldIssueVals, oldIssueTestStep |
| } |
| roleVals.AllowAnyName = true |
| roleVals.EnforceHostnames = true |
| roleVals.AllowLocalhost = true |
| roleVals.UseCSRCommonName = true |
| commonNames.Localhost = true |
| |
| netip1, netip2 := net.IP{127, 0, 0, 1}, net.IP{170, 171, 172, 173} |
| textip1, textip3 := "127.0.0.1", "::1" |
| |
| // IPSANs not allowed and not provided, should not be an error. |
| addIPSANTests(useCSRs, false, false, false, "", nil, getIpCheck()) |
| |
| // IPSANs not allowed, valid IPSANs provided, should be an error. |
| addIPSANTests(useCSRs, false, false, true, textip1+","+textip3, nil, nil) |
| |
| // IPSANs allowed, bogus IPSANs provided, should be an error. |
| addIPSANTests(useCSRs, false, true, true, "foobar", nil, nil) |
| |
| // Given IPSANs as API argument and useCSRSANs false, CSR arg ignored. |
| addIPSANTests(useCSRs, false, true, false, textip1, |
| []net.IP{netip2}, getIpCheck(netip1)) |
| |
| if useCSRs { |
| // IPSANs not allowed, valid IPSANs provided via CSR, should be an error. |
| addIPSANTests(useCSRs, true, false, true, "", []net.IP{netip1}, nil) |
| |
| // Given IPSANs as both API and CSR arguments and useCSRSANs=true, API arg ignored. |
| addIPSANTests(useCSRs, true, true, false, textip3, |
| []net.IP{netip1, netip2}, getIpCheck(netip1, netip2)) |
| } |
| } |
| |
| { |
| getOtherCheck := func(expectedOthers ...otherNameUtf8) logicaltest.TestCheckFunc { |
| return func(resp *logical.Response) error { |
| var certBundle certutil.CertBundle |
| err := mapstructure.Decode(resp.Data, &certBundle) |
| if err != nil { |
| return err |
| } |
| parsedCertBundle, err := certBundle.ToParsedCertBundle() |
| if err != nil { |
| return fmt.Errorf("error parsing cert bundle: %s", err) |
| } |
| cert := parsedCertBundle.Certificate |
| foundOthers, err := getOtherSANsFromX509Extensions(cert.Extensions) |
| if err != nil { |
| return err |
| } |
| var expected []otherNameUtf8 |
| expected = append(expected, expectedOthers...) |
| if diff := deep.Equal(foundOthers, expected); len(diff) > 0 { |
| return fmt.Errorf("wrong SAN IPs, diff: %v", diff) |
| } |
| return nil |
| } |
| } |
| |
| addOtherSANTests := func(useCSRs, useCSRSANs bool, allowedOtherSANs []string, errorOk bool, otherSANs []string, csrOtherSANs []otherNameUtf8, check logicaltest.TestCheckFunc) { |
| otherSansMap := func(os []otherNameUtf8) map[string][]string { |
| ret := make(map[string][]string) |
| for _, o := range os { |
| ret[o.oid] = append(ret[o.oid], o.value) |
| } |
| return ret |
| } |
| if useCSRs { |
| csrTemplate := &x509.CertificateRequest{ |
| Subject: pkix.Name{ |
| CommonName: issueVals.CommonName, |
| }, |
| } |
| if err := handleOtherCSRSANs(csrTemplate, otherSansMap(csrOtherSANs)); err != nil { |
| t.Fatal(err) |
| } |
| block, _ := getCsr(roleVals.KeyType, roleVals.KeyBits, csrTemplate) |
| issueVals.CSR = strings.TrimSpace(string(pem.EncodeToMemory(block))) |
| } |
| oldRoleVals, oldIssueVals, oldIssueTestStep := roleVals, issueVals, issueTestStep |
| roleVals.UseCSRSANs = useCSRSANs |
| roleVals.AllowedOtherSANs = allowedOtherSANs |
| issueVals.CommonName = "someone@example.com" |
| issueVals.OtherSANs = strings.Join(otherSANs, ",") |
| issueTestStep.ErrorOk = errorOk |
| addTests(check) |
| roleVals, issueVals, issueTestStep = oldRoleVals, oldIssueVals, oldIssueTestStep |
| } |
| roleVals.AllowAnyName = true |
| roleVals.EnforceHostnames = true |
| roleVals.AllowLocalhost = true |
| roleVals.UseCSRCommonName = true |
| commonNames.Localhost = true |
| |
| newOtherNameUtf8 := func(s string) (ret otherNameUtf8) { |
| pieces := strings.Split(s, ";") |
| if len(pieces) == 2 { |
| piecesRest := strings.Split(pieces[1], ":") |
| if len(piecesRest) == 2 { |
| switch strings.ToUpper(piecesRest[0]) { |
| case "UTF-8", "UTF8": |
| return otherNameUtf8{oid: pieces[0], value: piecesRest[1]} |
| } |
| } |
| } |
| t.Fatalf("error parsing otherName: %q", s) |
| return |
| } |
| oid1 := "1.3.6.1.4.1.311.20.2.3" |
| oth1str := oid1 + ";utf8:devops@nope.com" |
| oth1 := newOtherNameUtf8(oth1str) |
| oth2 := otherNameUtf8{oid1, "me@example.com"} |
| // allowNone, allowAll := []string{}, []string{oid1 + ";UTF-8:*"} |
| allowNone, allowAll := []string{}, []string{"*"} |
| |
| // OtherSANs not allowed and not provided, should not be an error. |
| addOtherSANTests(useCSRs, false, allowNone, false, nil, nil, getOtherCheck()) |
| |
| // OtherSANs not allowed, valid OtherSANs provided, should be an error. |
| addOtherSANTests(useCSRs, false, allowNone, true, []string{oth1str}, nil, nil) |
| |
| // OtherSANs allowed, bogus OtherSANs provided, should be an error. |
| addOtherSANTests(useCSRs, false, allowAll, true, []string{"foobar"}, nil, nil) |
| |
| // Given OtherSANs as API argument and useCSRSANs false, CSR arg ignored. |
| addOtherSANTests(useCSRs, false, allowAll, false, []string{oth1str}, |
| []otherNameUtf8{oth2}, getOtherCheck(oth1)) |
| |
| if useCSRs { |
| // OtherSANs not allowed, valid OtherSANs provided via CSR, should be an error. |
| addOtherSANTests(useCSRs, true, allowNone, true, nil, []otherNameUtf8{oth1}, nil) |
| |
| // Given OtherSANs as both API and CSR arguments and useCSRSANs=true, API arg ignored. |
| addOtherSANTests(useCSRs, false, allowAll, false, []string{oth2.String()}, |
| []otherNameUtf8{oth1}, getOtherCheck(oth2)) |
| } |
| } |
| |
| // Lease tests |
| { |
| roleTestStep.ErrorOk = true |
| roleVals.Lease = "" |
| roleVals.MaxTTL = 0 |
| addTests(nil) |
| |
| roleVals.Lease = "12h" |
| roleVals.MaxTTL = 6 * time.Hour |
| addTests(nil) |
| |
| roleTestStep.ErrorOk = false |
| roleVals.TTL = 0 |
| roleVals.MaxTTL = 12 * time.Hour |
| } |
| |
| // Listing test |
| ret = append(ret, logicaltest.TestStep{ |
| Operation: logical.ListOperation, |
| Path: "roles/", |
| Check: func(resp *logical.Response) error { |
| if resp.Data == nil { |
| return fmt.Errorf("nil data") |
| } |
| |
| keysRaw, ok := resp.Data["keys"] |
| if !ok { |
| return fmt.Errorf("no keys found") |
| } |
| |
| keys, ok := keysRaw.([]string) |
| if !ok { |
| return fmt.Errorf("could not convert keys to a string list") |
| } |
| |
| if len(keys) != 1 { |
| return fmt.Errorf("unexpected keys length of %d", len(keys)) |
| } |
| |
| if keys[0] != "test" { |
| return fmt.Errorf("unexpected key value of %s", keys[0]) |
| } |
| |
| return nil |
| }, |
| }) |
| |
| return ret |
| } |
| |
| func TestRolesAltIssuer(t *testing.T) { |
| t.Parallel() |
| b, s := CreateBackendWithStorage(t) |
| |
| // Create two issuers. |
| resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ |
| "common_name": "root a - example.com", |
| "issuer_name": "root-a", |
| "key_type": "ec", |
| }) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| rootAPem := resp.Data["certificate"].(string) |
| rootACert := parseCert(t, rootAPem) |
| |
| resp, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{ |
| "common_name": "root b - example.com", |
| "issuer_name": "root-b", |
| "key_type": "ec", |
| }) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| rootBPem := resp.Data["certificate"].(string) |
| rootBCert := parseCert(t, rootBPem) |
| |
| // Create three roles: one with no assignment, one with explicit root-a, |
| // one with explicit root-b. |
| _, err = CBWrite(b, s, "roles/use-default", map[string]interface{}{ |
| "allow_any_name": true, |
| "enforce_hostnames": false, |
| "key_type": "ec", |
| }) |
| require.NoError(t, err) |
| |
| _, err = CBWrite(b, s, "roles/use-root-a", map[string]interface{}{ |
| "allow_any_name": true, |
| "enforce_hostnames": false, |
| "key_type": "ec", |
| "issuer_ref": "root-a", |
| }) |
| require.NoError(t, err) |
| |
| _, err = CBWrite(b, s, "roles/use-root-b", map[string]interface{}{ |
| "allow_any_name": true, |
| "enforce_hostnames": false, |
| "issuer_ref": "root-b", |
| }) |
| require.NoError(t, err) |
| |
| // Now issue certs against these roles. |
| resp, err = CBWrite(b, s, "issue/use-default", map[string]interface{}{ |
| "common_name": "testing", |
| "ttl": "5s", |
| }) |
| require.NoError(t, err) |
| leafPem := resp.Data["certificate"].(string) |
| leafCert := parseCert(t, leafPem) |
| err = leafCert.CheckSignatureFrom(rootACert) |
| require.NoError(t, err, "should be signed by root-a but wasn't") |
| |
| resp, err = CBWrite(b, s, "issue/use-root-a", map[string]interface{}{ |
| "common_name": "testing", |
| "ttl": "5s", |
| }) |
| require.NoError(t, err) |
| leafPem = resp.Data["certificate"].(string) |
| leafCert = parseCert(t, leafPem) |
| err = leafCert.CheckSignatureFrom(rootACert) |
| require.NoError(t, err, "should be signed by root-a but wasn't") |
| |
| resp, err = CBWrite(b, s, "issue/use-root-b", map[string]interface{}{ |
| "common_name": "testing", |
| "ttl": "5s", |
| }) |
| require.NoError(t, err) |
| leafPem = resp.Data["certificate"].(string) |
| leafCert = parseCert(t, leafPem) |
| err = leafCert.CheckSignatureFrom(rootBCert) |
| require.NoError(t, err, "should be signed by root-b but wasn't") |
| |
| // Update the default issuer to be root B and make sure that the |
| // use-default role updates. |
| _, err = CBWrite(b, s, "config/issuers", map[string]interface{}{ |
| "default": "root-b", |
| }) |
| require.NoError(t, err) |
| |
| resp, err = CBWrite(b, s, "issue/use-default", map[string]interface{}{ |
| "common_name": "testing", |
| "ttl": "5s", |
| }) |
| require.NoError(t, err) |
| leafPem = resp.Data["certificate"].(string) |
| leafCert = parseCert(t, leafPem) |
| err = leafCert.CheckSignatureFrom(rootBCert) |
| require.NoError(t, err, "should be signed by root-b but wasn't") |
| } |
| |
| func TestBackend_PathFetchValidRaw(t *testing.T) { |
| t.Parallel() |
| b, storage := CreateBackendWithStorage(t) |
| |
| resp, err := b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "root/generate/internal", |
| Storage: storage, |
| Data: map[string]interface{}{ |
| "common_name": "test.com", |
| "ttl": "6h", |
| }, |
| MountPoint: "pki/", |
| }) |
| require.NoError(t, err) |
| if resp != nil && resp.IsError() { |
| t.Fatalf("failed to generate root, %#v", resp) |
| } |
| rootCaAsPem := resp.Data["certificate"].(string) |
| |
| // Chain should contain the root. |
| resp, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.ReadOperation, |
| Path: "ca_chain", |
| Storage: storage, |
| Data: map[string]interface{}{}, |
| MountPoint: "pki/", |
| }) |
| require.NoError(t, err) |
| if resp != nil && resp.IsError() { |
| t.Fatalf("failed read ca_chain, %#v", resp) |
| } |
| if strings.Count(string(resp.Data[logical.HTTPRawBody].([]byte)), rootCaAsPem) != 1 { |
| t.Fatalf("expected raw chain to contain the root cert") |
| } |
| |
| // The ca/pem should return us the actual CA... |
| resp, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.ReadOperation, |
| Path: "ca/pem", |
| Storage: storage, |
| Data: map[string]interface{}{}, |
| MountPoint: "pki/", |
| }) |
| schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("ca/pem"), logical.ReadOperation), resp, true) |
| require.NoError(t, err) |
| if resp != nil && resp.IsError() { |
| t.Fatalf("failed read ca/pem, %#v", resp) |
| } |
| // check the raw cert matches the response body |
| if !bytes.Equal(resp.Data[logical.HTTPRawBody].([]byte), []byte(rootCaAsPem)) { |
| t.Fatalf("failed to get raw cert") |
| } |
| |
| _, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "roles/example", |
| Storage: storage, |
| Data: map[string]interface{}{ |
| "allowed_domains": "example.com", |
| "allow_subdomains": "true", |
| "max_ttl": "1h", |
| "no_store": "false", |
| }, |
| MountPoint: "pki/", |
| }) |
| require.NoError(t, err, "error setting up pki role: %v", err) |
| |
| // Now issue a short-lived certificate from our pki-external. |
| resp, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "issue/example", |
| Storage: storage, |
| Data: map[string]interface{}{ |
| "common_name": "test.example.com", |
| "ttl": "5m", |
| }, |
| MountPoint: "pki/", |
| }) |
| require.NoError(t, err, "error issuing certificate: %v", err) |
| require.NotNil(t, resp, "got nil response from issuing request") |
| |
| issueCrtAsPem := resp.Data["certificate"].(string) |
| issuedCrt := parseCert(t, issueCrtAsPem) |
| expectedSerial := serialFromCert(issuedCrt) |
| expectedCert := []byte(issueCrtAsPem) |
| |
| // get der cert |
| resp, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.ReadOperation, |
| Path: fmt.Sprintf("cert/%s/raw", expectedSerial), |
| Storage: storage, |
| }) |
| if resp != nil && resp.IsError() { |
| t.Fatalf("failed to get raw cert, %#v", resp) |
| } |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // check the raw cert matches the response body |
| rawBody := resp.Data[logical.HTTPRawBody].([]byte) |
| bodyAsPem := []byte(strings.TrimSpace(string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rawBody})))) |
| if !bytes.Equal(bodyAsPem, expectedCert) { |
| t.Fatalf("failed to get raw cert for serial number: %s", expectedSerial) |
| } |
| if resp.Data[logical.HTTPContentType] != "application/pkix-cert" { |
| t.Fatalf("failed to get raw cert content-type") |
| } |
| |
| // get pem |
| resp, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.ReadOperation, |
| Path: fmt.Sprintf("cert/%s/raw/pem", expectedSerial), |
| Storage: storage, |
| }) |
| if resp != nil && resp.IsError() { |
| t.Fatalf("failed to get raw, %#v", resp) |
| } |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // check the pem cert matches the response body |
| if !bytes.Equal(resp.Data[logical.HTTPRawBody].([]byte), expectedCert) { |
| t.Fatalf("failed to get pem cert") |
| } |
| if resp.Data[logical.HTTPContentType] != "application/pem-certificate-chain" { |
| t.Fatalf("failed to get raw cert content-type") |
| } |
| } |
| |
| func TestBackend_PathFetchCertList(t *testing.T) { |
| t.Parallel() |
| // create the backend |
| b, storage := CreateBackendWithStorage(t) |
| |
| // generate root |
| rootData := map[string]interface{}{ |
| "common_name": "test.com", |
| "ttl": "6h", |
| } |
| |
| resp, err := b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "root/generate/internal", |
| Storage: storage, |
| Data: rootData, |
| MountPoint: "pki/", |
| }) |
| |
| if resp != nil && resp.IsError() { |
| t.Fatalf("failed to generate root, %#v", resp) |
| } |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // config urls |
| urlsData := map[string]interface{}{ |
| "issuing_certificates": "http://127.0.0.1:8200/v1/pki/ca", |
| "crl_distribution_points": "http://127.0.0.1:8200/v1/pki/crl", |
| } |
| |
| resp, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "config/urls", |
| Storage: storage, |
| Data: urlsData, |
| MountPoint: "pki/", |
| }) |
| schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("config/urls"), logical.UpdateOperation), resp, true) |
| |
| resp, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.ReadOperation, |
| Path: "config/urls", |
| Storage: storage, |
| MountPoint: "pki/", |
| }) |
| schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("config/urls"), logical.ReadOperation), resp, true) |
| |
| if resp != nil && resp.IsError() { |
| t.Fatalf("failed to config urls, %#v", resp) |
| } |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // create a role entry |
| roleData := map[string]interface{}{ |
| "allowed_domains": "test.com", |
| "allow_subdomains": "true", |
| "max_ttl": "4h", |
| } |
| |
| resp, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "roles/test-example", |
| Storage: storage, |
| Data: roleData, |
| MountPoint: "pki/", |
| }) |
| if resp != nil && resp.IsError() { |
| t.Fatalf("failed to create a role, %#v", resp) |
| } |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // issue some certs |
| i := 1 |
| for i < 10 { |
| certData := map[string]interface{}{ |
| "common_name": "example.test.com", |
| } |
| resp, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "issue/test-example", |
| Storage: storage, |
| Data: certData, |
| MountPoint: "pki/", |
| }) |
| if resp != nil && resp.IsError() { |
| t.Fatalf("failed to issue a cert, %#v", resp) |
| } |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| i = i + 1 |
| } |
| |
| // list certs |
| resp, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.ListOperation, |
| Path: "certs", |
| Storage: storage, |
| MountPoint: "pki/", |
| }) |
| if resp != nil && resp.IsError() { |
| t.Fatalf("failed to list certs, %#v", resp) |
| } |
| if err != nil { |
| t.Fatal(err) |
| } |
| // check that the root and 9 additional certs are all listed |
| if len(resp.Data["keys"].([]string)) != 10 { |
| t.Fatalf("failed to list all 10 certs") |
| } |
| |
| // list certs/ |
| resp, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.ListOperation, |
| Path: "certs/", |
| Storage: storage, |
| MountPoint: "pki/", |
| }) |
| if resp != nil && resp.IsError() { |
| t.Fatalf("failed to list certs, %#v", resp) |
| } |
| if err != nil { |
| t.Fatal(err) |
| } |
| // check that the root and 9 additional certs are all listed |
| if len(resp.Data["keys"].([]string)) != 10 { |
| t.Fatalf("failed to list all 10 certs") |
| } |
| } |
| |
| func TestBackend_SignVerbatim(t *testing.T) { |
| t.Parallel() |
| testCases := []struct { |
| testName string |
| keyType string |
| }{ |
| {testName: "RSA", keyType: "rsa"}, |
| {testName: "ED25519", keyType: "ed25519"}, |
| {testName: "EC", keyType: "ec"}, |
| {testName: "Any", keyType: "any"}, |
| } |
| for _, tc := range testCases { |
| tc := tc |
| t.Run(tc.testName, func(t *testing.T) { |
| runTestSignVerbatim(t, tc.keyType) |
| }) |
| } |
| } |
| |
| func runTestSignVerbatim(t *testing.T, keyType string) { |
| // create the backend |
| b, storage := CreateBackendWithStorage(t) |
| |
| // generate root |
| rootData := map[string]interface{}{ |
| "common_name": "test.com", |
| "not_after": "9999-12-31T23:59:59Z", |
| } |
| |
| resp, err := b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "root/generate/internal", |
| Storage: storage, |
| Data: rootData, |
| MountPoint: "pki/", |
| }) |
| if resp != nil && resp.IsError() { |
| t.Fatalf("failed to generate root, %#v", *resp) |
| } |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // create a CSR and key |
| key, err := rsa.GenerateKey(rand.Reader, 2048) |
| if err != nil { |
| t.Fatal(err) |
| } |
| csrReq := &x509.CertificateRequest{ |
| Subject: pkix.Name{ |
| CommonName: "foo.bar.com", |
| }, |
| // Check that otherName extensions are not duplicated (see hashicorp/vault#16700). |
| // If these extensions are duplicated, sign-verbatim will fail when parsing the signed certificate on Go 1.19+ (see golang/go#50988). |
| // On older versions of Go this test will fail due to an explicit check for duplicate otherNames later in this test. |
| ExtraExtensions: []pkix.Extension{ |
| { |
| Id: oidExtensionSubjectAltName, |
| Critical: false, |
| Value: []byte{0x30, 0x26, 0xA0, 0x24, 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x14, 0x02, 0x03, 0xA0, 0x16, 0x0C, 0x14, 0x75, 0x73, 0x65, 0x72, 0x6E, 0x61, 0x6D, 0x65, 0x40, 0x65, 0x78, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x63, 0x6F, 0x6D}, |
| }, |
| }, |
| } |
| csr, err := x509.CreateCertificateRequest(rand.Reader, csrReq, key) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if len(csr) == 0 { |
| t.Fatal("generated csr is empty") |
| } |
| pemCSR := strings.TrimSpace(string(pem.EncodeToMemory(&pem.Block{ |
| Type: "CERTIFICATE REQUEST", |
| Bytes: csr, |
| }))) |
| if len(pemCSR) == 0 { |
| t.Fatal("pem csr is empty") |
| } |
| |
| signVerbatimData := map[string]interface{}{ |
| "csr": pemCSR, |
| } |
| if keyType == "rsa" { |
| signVerbatimData["signature_bits"] = 512 |
| } |
| resp, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "sign-verbatim", |
| Storage: storage, |
| Data: signVerbatimData, |
| MountPoint: "pki/", |
| }) |
| schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("sign-verbatim"), logical.UpdateOperation), resp, true) |
| |
| if resp != nil && resp.IsError() { |
| t.Fatalf("failed to sign-verbatim basic CSR: %#v", *resp) |
| } |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp.Secret != nil { |
| t.Fatal("secret is not nil") |
| } |
| |
| // create a role entry; we use this to check that sign-verbatim when used with a role is still honoring TTLs |
| roleData := map[string]interface{}{ |
| "ttl": "4h", |
| "max_ttl": "8h", |
| "key_type": keyType, |
| "not_before_duration": "2h", |
| } |
| resp, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "roles/test", |
| Storage: storage, |
| Data: roleData, |
| MountPoint: "pki/", |
| }) |
| if resp != nil && resp.IsError() { |
| t.Fatalf("failed to create a role, %#v", *resp) |
| } |
| if err != nil { |
| t.Fatal(err) |
| } |
| resp, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "sign-verbatim/test", |
| Storage: storage, |
| Data: map[string]interface{}{ |
| "csr": pemCSR, |
| "ttl": "5h", |
| }, |
| MountPoint: "pki/", |
| }) |
| if resp != nil && resp.IsError() { |
| t.Fatalf("failed to sign-verbatim ttl'd CSR: %#v", *resp) |
| } |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp.Secret != nil { |
| t.Fatal("got a lease when we should not have") |
| } |
| resp, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "sign-verbatim/test", |
| Storage: storage, |
| Data: map[string]interface{}{ |
| "csr": pemCSR, |
| "ttl": "12h", |
| }, |
| MountPoint: "pki/", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp != nil && resp.IsError() { |
| t.Fatalf(resp.Error().Error()) |
| } |
| if resp.Data == nil || resp.Data["certificate"] == nil { |
| t.Fatal("did not get expected data") |
| } |
| certString := resp.Data["certificate"].(string) |
| block, _ := pem.Decode([]byte(certString)) |
| if block == nil { |
| t.Fatal("nil pem block") |
| } |
| certs, err := x509.ParseCertificates(block.Bytes) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if len(certs) != 1 { |
| t.Fatalf("expected a single cert, got %d", len(certs)) |
| } |
| cert := certs[0] |
| if math.Abs(float64(time.Now().Add(12*time.Hour).Unix()-cert.NotAfter.Unix())) < 10 { |
| t.Fatalf("sign-verbatim did not properly cap validity period (notAfter) on signed CSR: was %v vs requested %v but should've been %v", cert.NotAfter, time.Now().Add(12*time.Hour), time.Now().Add(8*time.Hour)) |
| } |
| if math.Abs(float64(time.Now().Add(-2*time.Hour).Unix()-cert.NotBefore.Unix())) > 10 { |
| t.Fatalf("sign-verbatim did not properly cap validity period (notBefore) on signed CSR: was %v vs expected %v", cert.NotBefore, time.Now().Add(-2*time.Hour)) |
| } |
| |
| // Now check signing a certificate using the not_after input using the Y10K value |
| resp, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "sign-verbatim/test", |
| Storage: storage, |
| Data: map[string]interface{}{ |
| "csr": pemCSR, |
| "not_after": "9999-12-31T23:59:59Z", |
| }, |
| MountPoint: "pki/", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp != nil && resp.IsError() { |
| t.Fatalf(resp.Error().Error()) |
| } |
| if resp.Data == nil || resp.Data["certificate"] == nil { |
| t.Fatal("did not get expected data") |
| } |
| certString = resp.Data["certificate"].(string) |
| block, _ = pem.Decode([]byte(certString)) |
| if block == nil { |
| t.Fatal("nil pem block") |
| } |
| certs, err = x509.ParseCertificates(block.Bytes) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if len(certs) != 1 { |
| t.Fatalf("expected a single cert, got %d", len(certs)) |
| } |
| cert = certs[0] |
| |
| // Fallback check for duplicate otherName, necessary on Go versions before 1.19. |
| // We assume that there is only one SAN in the original CSR and that it is an otherName. |
| san_count := 0 |
| for _, ext := range cert.Extensions { |
| if ext.Id.Equal(oidExtensionSubjectAltName) { |
| san_count += 1 |
| } |
| } |
| if san_count != 1 { |
| t.Fatalf("expected one SAN extension, got %d", san_count) |
| } |
| |
| notAfter := cert.NotAfter.Format(time.RFC3339) |
| if notAfter != "9999-12-31T23:59:59Z" { |
| t.Fatal(fmt.Errorf("not after from certificate is not matching with input parameter")) |
| } |
| |
| // now check that if we set generate-lease it takes it from the role and the TTLs match |
| roleData = map[string]interface{}{ |
| "ttl": "4h", |
| "max_ttl": "8h", |
| "generate_lease": true, |
| "key_type": keyType, |
| } |
| resp, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "roles/test", |
| Storage: storage, |
| Data: roleData, |
| MountPoint: "pki/", |
| }) |
| if resp != nil && resp.IsError() { |
| t.Fatalf("failed to create a role, %#v", *resp) |
| } |
| if err != nil { |
| t.Fatal(err) |
| } |
| resp, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "sign-verbatim/test", |
| Storage: storage, |
| Data: map[string]interface{}{ |
| "csr": pemCSR, |
| "ttl": "5h", |
| }, |
| MountPoint: "pki/", |
| }) |
| if resp != nil && resp.IsError() { |
| t.Fatalf("failed to sign-verbatim role-leased CSR: %#v", *resp) |
| } |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp.Secret == nil { |
| t.Fatalf("secret is nil, response is %#v", *resp) |
| } |
| if math.Abs(float64(resp.Secret.TTL-(5*time.Hour))) > float64(5*time.Hour) { |
| t.Fatalf("ttl not default; wanted %v, got %v", b.System().DefaultLeaseTTL(), resp.Secret.TTL) |
| } |
| } |
| |
| func TestBackend_Root_Idempotency(t *testing.T) { |
| t.Parallel() |
| b, s := CreateBackendWithStorage(t) |
| |
| // This is a change within 1.11, we are no longer idempotent across generate/internal calls. |
| resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ |
| "common_name": "myvault.com", |
| }) |
| require.NoError(t, err) |
| require.NotNil(t, resp, "expected ca info") |
| keyId1 := resp.Data["key_id"] |
| issuerId1 := resp.Data["issuer_id"] |
| cert := parseCert(t, resp.Data["certificate"].(string)) |
| certSkid := certutil.GetHexFormatted(cert.SubjectKeyId, ":") |
| |
| // -> Validate the SKID matches between the root cert and the key |
| resp, err = CBRead(b, s, "key/"+keyId1.(keyID).String()) |
| require.NoError(t, err) |
| require.NotNil(t, resp, "expected a response") |
| require.Equal(t, resp.Data["subject_key_id"], certSkid) |
| |
| resp, err = CBRead(b, s, "cert/ca_chain") |
| require.NoError(t, err, "error reading ca_chain: %v", err) |
| |
| r1Data := resp.Data |
| |
| // Calling generate/internal should generate a new CA as well. |
| resp, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{ |
| "common_name": "myvault.com", |
| }) |
| require.NoError(t, err) |
| require.NotNil(t, resp, "expected ca info") |
| keyId2 := resp.Data["key_id"] |
| issuerId2 := resp.Data["issuer_id"] |
| cert = parseCert(t, resp.Data["certificate"].(string)) |
| certSkid = certutil.GetHexFormatted(cert.SubjectKeyId, ":") |
| |
| // -> Validate the SKID matches between the root cert and the key |
| resp, err = CBRead(b, s, "key/"+keyId2.(keyID).String()) |
| require.NoError(t, err) |
| require.NotNil(t, resp, "expected a response") |
| require.Equal(t, resp.Data["subject_key_id"], certSkid) |
| |
| // Make sure that we actually generated different issuer and key values |
| require.NotEqual(t, keyId1, keyId2) |
| require.NotEqual(t, issuerId1, issuerId2) |
| |
| // Now because the issued CA's have no links, the call to ca_chain should return the same data (ca chain from default) |
| resp, err = CBRead(b, s, "cert/ca_chain") |
| require.NoError(t, err, "error reading ca_chain: %v", err) |
| schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("cert/ca_chain"), logical.ReadOperation), resp, true) |
| |
| r2Data := resp.Data |
| if !reflect.DeepEqual(r1Data, r2Data) { |
| t.Fatal("got different ca certs") |
| } |
| |
| // Now let's validate that the import bundle is idempotent. |
| pemBundleRootCA := rootCACertPEM + "\n" + rootCAKeyPEM |
| resp, err = CBWrite(b, s, "config/ca", map[string]interface{}{ |
| "pem_bundle": pemBundleRootCA, |
| }) |
| schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("config/ca"), logical.UpdateOperation), resp, true) |
| |
| require.NoError(t, err) |
| require.NotNil(t, resp, "expected ca info") |
| firstMapping := resp.Data["mapping"].(map[string]string) |
| firstImportedKeys := resp.Data["imported_keys"].([]string) |
| firstImportedIssuers := resp.Data["imported_issuers"].([]string) |
| firstExistingKeys := resp.Data["existing_keys"].([]string) |
| firstExistingIssuers := resp.Data["existing_issuers"].([]string) |
| |
| require.NotContains(t, firstImportedKeys, keyId1) |
| require.NotContains(t, firstImportedKeys, keyId2) |
| require.NotContains(t, firstImportedIssuers, issuerId1) |
| require.NotContains(t, firstImportedIssuers, issuerId2) |
| require.Empty(t, firstExistingKeys) |
| require.Empty(t, firstExistingIssuers) |
| require.NotEmpty(t, firstMapping) |
| require.Equal(t, 1, len(firstMapping)) |
| |
| var issuerId3 string |
| var keyId3 string |
| for i, k := range firstMapping { |
| issuerId3 = i |
| keyId3 = k |
| } |
| |
| // Performing this again should result in no key/issuer ids being imported/generated. |
| resp, err = CBWrite(b, s, "config/ca", map[string]interface{}{ |
| "pem_bundle": pemBundleRootCA, |
| }) |
| require.NoError(t, err) |
| require.NotNil(t, resp, "expected ca info") |
| secondMapping := resp.Data["mapping"].(map[string]string) |
| secondImportedKeys := resp.Data["imported_keys"] |
| secondImportedIssuers := resp.Data["imported_issuers"] |
| secondExistingKeys := resp.Data["existing_keys"] |
| secondExistingIssuers := resp.Data["existing_issuers"] |
| |
| require.Empty(t, secondImportedKeys) |
| require.Empty(t, secondImportedIssuers) |
| require.Contains(t, secondExistingKeys, keyId3) |
| require.Contains(t, secondExistingIssuers, issuerId3) |
| require.Equal(t, 1, len(secondMapping)) |
| |
| resp, err = CBDelete(b, s, "root") |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| require.Equal(t, 1, len(resp.Warnings)) |
| |
| // Make sure we can delete twice... |
| resp, err = CBDelete(b, s, "root") |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| require.Equal(t, 1, len(resp.Warnings)) |
| |
| _, err = CBRead(b, s, "cert/ca_chain") |
| require.Error(t, err, "expected an error fetching deleted ca_chain") |
| |
| // We should be able to import the same ca bundle as before and get a different key/issuer ids |
| resp, err = CBWrite(b, s, "config/ca", map[string]interface{}{ |
| "pem_bundle": pemBundleRootCA, |
| }) |
| require.NoError(t, err) |
| require.NotNil(t, resp, "expected ca info") |
| postDeleteImportedKeys := resp.Data["imported_keys"] |
| postDeleteImportedIssuers := resp.Data["imported_issuers"] |
| |
| // Make sure that we actually generated different issuer and key values, then the previous import |
| require.NotNil(t, postDeleteImportedKeys) |
| require.NotNil(t, postDeleteImportedIssuers) |
| require.NotEqual(t, postDeleteImportedKeys, firstImportedKeys) |
| require.NotEqual(t, postDeleteImportedIssuers, firstImportedIssuers) |
| |
| resp, err = CBRead(b, s, "cert/ca_chain") |
| require.NoError(t, err) |
| |
| caChainPostDelete := resp.Data |
| if reflect.DeepEqual(r1Data, caChainPostDelete) { |
| t.Fatal("ca certs from ca_chain were the same post delete, should have changed.") |
| } |
| } |
| |
| func TestBackend_SignIntermediate_AllowedPastCAValidity(t *testing.T) { |
| t.Parallel() |
| b_root, s_root := CreateBackendWithStorage(t) |
| b_int, s_int := CreateBackendWithStorage(t) |
| var err error |
| |
| // Direct issuing from root |
| _, err = CBWrite(b_root, s_root, "root/generate/internal", map[string]interface{}{ |
| "ttl": "40h", |
| "common_name": "myvault.com", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| _, err = CBWrite(b_root, s_root, "roles/test", map[string]interface{}{ |
| "allow_bare_domains": true, |
| "allow_subdomains": true, |
| "allow_any_name": true, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| resp, err := CBWrite(b_int, s_int, "intermediate/generate/internal", map[string]interface{}{ |
| "common_name": "myint.com", |
| }) |
| schema.ValidateResponse(t, schema.GetResponseSchema(t, b_root.Route("intermediate/generate/internal"), logical.UpdateOperation), resp, true) |
| require.Contains(t, resp.Data, "key_id") |
| intKeyId := resp.Data["key_id"].(keyID) |
| csr := resp.Data["csr"] |
| |
| resp, err = CBRead(b_int, s_int, "key/"+intKeyId.String()) |
| require.NoError(t, err) |
| require.NotNil(t, resp, "expected a response") |
| intSkid := resp.Data["subject_key_id"].(string) |
| |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| _, err = CBWrite(b_root, s_root, "sign/test", map[string]interface{}{ |
| "common_name": "myint.com", |
| "csr": csr, |
| "ttl": "60h", |
| }) |
| require.ErrorContains(t, err, "that is beyond the expiration of the CA certificate") |
| |
| _, err = CBWrite(b_root, s_root, "sign-verbatim/test", map[string]interface{}{ |
| "common_name": "myint.com", |
| "other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:caadmin@example.com", |
| "csr": csr, |
| "ttl": "60h", |
| }) |
| require.ErrorContains(t, err, "that is beyond the expiration of the CA certificate") |
| |
| resp, err = CBWrite(b_root, s_root, "root/sign-intermediate", map[string]interface{}{ |
| "common_name": "myint.com", |
| "other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:caadmin@example.com", |
| "csr": csr, |
| "ttl": "60h", |
| }) |
| if err != nil { |
| t.Fatalf("got error: %v", err) |
| } |
| if resp == nil { |
| t.Fatal("got nil response") |
| } |
| if len(resp.Warnings) == 0 { |
| t.Fatalf("expected warnings, got %#v", *resp) |
| } |
| |
| cert := parseCert(t, resp.Data["certificate"].(string)) |
| certSkid := certutil.GetHexFormatted(cert.SubjectKeyId, ":") |
| require.Equal(t, intSkid, certSkid) |
| } |
| |
| func TestBackend_ConsulSignLeafWithLegacyRole(t *testing.T) { |
| t.Parallel() |
| // create the backend |
| b, s := CreateBackendWithStorage(t) |
| |
| // generate root |
| data, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ |
| "ttl": "40h", |
| "common_name": "myvault.com", |
| }) |
| require.NoError(t, err, "failed generating internal root cert") |
| rootCaPem := data.Data["certificate"].(string) |
| |
| // Create a signing role like Consul did with the default args prior to Vault 1.10 |
| _, err = CBWrite(b, s, "roles/test", map[string]interface{}{ |
| "allow_any_name": true, |
| "allowed_serial_numbers": []string{"MySerialNumber"}, |
| "key_type": "any", |
| "key_bits": "2048", |
| "signature_bits": "256", |
| }) |
| require.NoError(t, err, "failed creating legacy role") |
| |
| _, csrPem := generateTestCsr(t, certutil.ECPrivateKey, 256) |
| data, err = CBWrite(b, s, "sign/test", map[string]interface{}{ |
| "csr": csrPem, |
| }) |
| require.NoError(t, err, "failed signing csr") |
| certAsPem := data.Data["certificate"].(string) |
| |
| signedCert := parseCert(t, certAsPem) |
| rootCert := parseCert(t, rootCaPem) |
| requireSignedBy(t, signedCert, rootCert) |
| } |
| |
| func TestBackend_SignSelfIssued(t *testing.T) { |
| t.Parallel() |
| // create the backend |
| b, storage := CreateBackendWithStorage(t) |
| |
| // generate root |
| rootData := map[string]interface{}{ |
| "common_name": "test.com", |
| "ttl": "172800", |
| } |
| |
| resp, err := b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "root/generate/internal", |
| Storage: storage, |
| Data: rootData, |
| MountPoint: "pki/", |
| }) |
| if resp != nil && resp.IsError() { |
| t.Fatalf("failed to generate root, %#v", *resp) |
| } |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| key, err := rsa.GenerateKey(rand.Reader, 2048) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| template := &x509.Certificate{ |
| Subject: pkix.Name{ |
| CommonName: "foo.bar.com", |
| }, |
| SerialNumber: big.NewInt(1234), |
| IsCA: false, |
| BasicConstraintsValid: true, |
| } |
| |
| ss, _ := getSelfSigned(t, template, template, key) |
| resp, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "root/sign-self-issued", |
| Storage: storage, |
| Data: map[string]interface{}{ |
| "certificate": ss, |
| }, |
| MountPoint: "pki/", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("got nil response") |
| } |
| if !resp.IsError() { |
| t.Fatalf("expected error due to non-CA; got: %#v", *resp) |
| } |
| |
| // Set CA to true, but leave issuer alone |
| template.IsCA = true |
| |
| issuer := &x509.Certificate{ |
| Subject: pkix.Name{ |
| CommonName: "bar.foo.com", |
| }, |
| SerialNumber: big.NewInt(2345), |
| IsCA: true, |
| BasicConstraintsValid: true, |
| } |
| ss, ssCert := getSelfSigned(t, template, issuer, key) |
| resp, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "root/sign-self-issued", |
| Storage: storage, |
| Data: map[string]interface{}{ |
| "certificate": ss, |
| }, |
| MountPoint: "pki/", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("got nil response") |
| } |
| if !resp.IsError() { |
| t.Fatalf("expected error due to different issuer; cert info is\nIssuer\n%#v\nSubject\n%#v\n", ssCert.Issuer, ssCert.Subject) |
| } |
| |
| ss, _ = getSelfSigned(t, template, template, key) |
| resp, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "root/sign-self-issued", |
| Storage: storage, |
| Data: map[string]interface{}{ |
| "certificate": ss, |
| }, |
| MountPoint: "pki/", |
| }) |
| schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("root/sign-self-issued"), logical.UpdateOperation), resp, true) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("got nil response") |
| } |
| if resp.IsError() { |
| t.Fatalf("error in response: %s", resp.Error().Error()) |
| } |
| |
| newCertString := resp.Data["certificate"].(string) |
| block, _ := pem.Decode([]byte(newCertString)) |
| newCert, err := x509.ParseCertificate(block.Bytes) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| sc := b.makeStorageContext(context.Background(), storage) |
| signingBundle, err := sc.fetchCAInfo(defaultRef, ReadOnlyUsage) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if reflect.DeepEqual(newCert.Subject, newCert.Issuer) { |
| t.Fatal("expected different subject/issuer") |
| } |
| if !reflect.DeepEqual(newCert.Issuer, signingBundle.Certificate.Subject) { |
| t.Fatalf("expected matching issuer/CA subject\n\nIssuer:\n%#v\nSubject:\n%#v\n", newCert.Issuer, signingBundle.Certificate.Subject) |
| } |
| if bytes.Equal(newCert.AuthorityKeyId, newCert.SubjectKeyId) { |
| t.Fatal("expected different authority/subject") |
| } |
| if !bytes.Equal(newCert.AuthorityKeyId, signingBundle.Certificate.SubjectKeyId) { |
| t.Fatal("expected authority on new cert to be same as signing subject") |
| } |
| if newCert.Subject.CommonName != "foo.bar.com" { |
| t.Fatalf("unexpected common name on new cert: %s", newCert.Subject.CommonName) |
| } |
| } |
| |
| // TestBackend_SignSelfIssued_DifferentTypes tests the functionality of the |
| // require_matching_certificate_algorithms flag. |
| func TestBackend_SignSelfIssued_DifferentTypes(t *testing.T) { |
| t.Parallel() |
| // create the backend |
| b, storage := CreateBackendWithStorage(t) |
| |
| // generate root |
| rootData := map[string]interface{}{ |
| "common_name": "test.com", |
| "ttl": "172800", |
| "key_type": "ec", |
| "key_bits": "521", |
| } |
| |
| resp, err := b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "root/generate/internal", |
| Storage: storage, |
| Data: rootData, |
| MountPoint: "pki/", |
| }) |
| if resp != nil && resp.IsError() { |
| t.Fatalf("failed to generate root, %#v", *resp) |
| } |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| key, err := rsa.GenerateKey(rand.Reader, 2048) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| template := &x509.Certificate{ |
| Subject: pkix.Name{ |
| CommonName: "foo.bar.com", |
| }, |
| SerialNumber: big.NewInt(1234), |
| IsCA: true, |
| BasicConstraintsValid: true, |
| } |
| |
| // Tests absent the flag |
| ss, _ := getSelfSigned(t, template, template, key) |
| resp, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "root/sign-self-issued", |
| Storage: storage, |
| Data: map[string]interface{}{ |
| "certificate": ss, |
| }, |
| MountPoint: "pki/", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("got nil response") |
| } |
| |
| // Set CA to true, but leave issuer alone |
| template.IsCA = true |
| |
| // Tests with flag present but false |
| ss, _ = getSelfSigned(t, template, template, key) |
| resp, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "root/sign-self-issued", |
| Storage: storage, |
| Data: map[string]interface{}{ |
| "certificate": ss, |
| "require_matching_certificate_algorithms": false, |
| }, |
| MountPoint: "pki/", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("got nil response") |
| } |
| |
| // Test with flag present and true |
| ss, _ = getSelfSigned(t, template, template, key) |
| _, err = b.HandleRequest(context.Background(), &logical.Request{ |
| Operation: logical.UpdateOperation, |
| Path: "root/sign-self-issued", |
| Storage: storage, |
| Data: map[string]interface{}{ |
| "certificate": ss, |
| "require_matching_certificate_algorithms": true, |
| }, |
| MountPoint: "pki/", |
| }) |
| if err == nil { |
| t.Fatal("expected error due to mismatched algorithms") |
| } |
| } |
| |
| // This is a really tricky test because the Go stdlib asn1 package is incapable |
| // of doing the right thing with custom OID SANs (see comments in the package, |
| // it's readily admitted that it's too magic) but that means that any |
| // validation logic written for this test isn't being independently verified, |
| // as in, if cryptobytes is used to decode it to make the test work, that |
| // doesn't mean we're encoding and decoding correctly, only that we made the |
| // test pass. Instead, when run verbosely it will first perform a bunch of |
| // checks to verify that the OID SAN logic doesn't screw up other SANs, then |
| // will spit out the PEM. This can be validated independently. |
| // |
| // You want the hex dump of the octet string corresponding to the X509v3 |
| // Subject Alternative Name. There's a nice online utility at |
| // https://lapo.it/asn1js that can be used to view the structure of an |
| // openssl-generated other SAN at |
| // https://lapo.it/asn1js/#3022A020060A2B060104018237140203A0120C106465766F7073406C6F63616C686F7374 |
| // (openssl asn1parse can also be used with -strparse using an offset of the |
| // hex blob for the subject alternative names extension). |
| // |
| // The structure output from here should match that precisely (even if the OID |
| // itself doesn't) in the second test. |
| // |
| // The test that encodes two should have them be in separate elements in the |
| // top-level sequence; see |
| // https://lapo.it/asn1js/#3046A020060A2B060104018237140203A0120C106465766F7073406C6F63616C686F7374A022060A2B060104018237140204A0140C12322D6465766F7073406C6F63616C686F7374 for an openssl-generated example. |
| // |
| // The good news is that it's valid to simply copy and paste the PEM output from |
| // here into the form at that site as it will do the right thing so it's pretty |
| // easy to validate. |
| func TestBackend_OID_SANs(t *testing.T) { |
| t.Parallel() |
| b, s := CreateBackendWithStorage(t) |
| |
| var err error |
| var resp *logical.Response |
| var certStr string |
| var block *pem.Block |
| var cert *x509.Certificate |
| |
| _, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{ |
| "ttl": "40h", |
| "common_name": "myvault.com", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| _, err = CBWrite(b, s, "roles/test", map[string]interface{}{ |
| "allowed_domains": []string{"foobar.com", "zipzap.com"}, |
| "allow_bare_domains": true, |
| "allow_subdomains": true, |
| "allow_ip_sans": true, |
| "allowed_other_sans": "1.3.6.1.4.1.311.20.2.3;UTF8:devops@*,1.3.6.1.4.1.311.20.2.4;utf8:d*e@foobar.com", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Get a baseline before adding OID SANs. In the next sections we'll verify |
| // that the SANs are all added even as the OID SAN inclusion forces other |
| // adding logic (custom rather than built-in Golang logic) |
| resp, err = CBWrite(b, s, "issue/test", map[string]interface{}{ |
| "common_name": "foobar.com", |
| "ip_sans": "1.2.3.4", |
| "alt_names": "foobar.com,foo.foobar.com,bar.foobar.com", |
| "ttl": "1h", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| certStr = resp.Data["certificate"].(string) |
| block, _ = pem.Decode([]byte(certStr)) |
| cert, err = x509.ParseCertificate(block.Bytes) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if cert.IPAddresses[0].String() != "1.2.3.4" { |
| t.Fatalf("unexpected IP SAN %q", cert.IPAddresses[0].String()) |
| } |
| if len(cert.DNSNames) != 3 || |
| cert.DNSNames[0] != "bar.foobar.com" || |
| cert.DNSNames[1] != "foo.foobar.com" || |
| cert.DNSNames[2] != "foobar.com" { |
| t.Fatalf("unexpected DNS SANs %v", cert.DNSNames) |
| } |
| |
| // First test some bad stuff that shouldn't work |
| _, err = CBWrite(b, s, "issue/test", map[string]interface{}{ |
| "common_name": "foobar.com", |
| "ip_sans": "1.2.3.4", |
| "alt_names": "foo.foobar.com,bar.foobar.com", |
| "ttl": "1h", |
| // Not a valid value for the first possibility |
| "other_sans": "1.3.6.1.4.1.311.20.2.3;UTF8:devop@nope.com", |
| }) |
| if err == nil { |
| t.Fatal("expected error") |
| } |
| |
| _, err = CBWrite(b, s, "issue/test", map[string]interface{}{ |
| "common_name": "foobar.com", |
| "ip_sans": "1.2.3.4", |
| "alt_names": "foo.foobar.com,bar.foobar.com", |
| "ttl": "1h", |
| // Not a valid OID for the first possibility |
| "other_sans": "1.3.6.1.4.1.311.20.2.5;UTF8:devops@nope.com", |
| }) |
| if err == nil { |
| t.Fatal("expected error") |
| } |
| |
| _, err = CBWrite(b, s, "issue/test", map[string]interface{}{ |
| "common_name": "foobar.com", |
| "ip_sans": "1.2.3.4", |
| "alt_names": "foo.foobar.com,bar.foobar.com", |
| "ttl": "1h", |
| // Not a valid name for the second possibility |
| "other_sans": "1.3.6.1.4.1.311.20.2.4;UTF8:d34g@foobar.com", |
| }) |
| if err == nil { |
| t.Fatal("expected error") |
| } |
| |
| _, err = CBWrite(b, s, "issue/test", map[string]interface{}{ |
| "common_name": "foobar.com", |
| "ip_sans": "1.2.3.4", |
| "alt_names": "foo.foobar.com,bar.foobar.com", |
| "ttl": "1h", |
| // Not a valid OID for the second possibility |
| "other_sans": "1.3.6.1.4.1.311.20.2.5;UTF8:d34e@foobar.com", |
| }) |
| if err == nil { |
| t.Fatal("expected error") |
| } |
| |
| _, err = CBWrite(b, s, "issue/test", map[string]interface{}{ |
| "common_name": "foobar.com", |
| "ip_sans": "1.2.3.4", |
| "alt_names": "foo.foobar.com,bar.foobar.com", |
| "ttl": "1h", |
| // Not a valid type |
| "other_sans": "1.3.6.1.4.1.311.20.2.5;UTF2:d34e@foobar.com", |
| }) |
| if err == nil { |
| t.Fatal("expected error") |
| } |
| |
| // Valid for first possibility |
| resp, err = CBWrite(b, s, "issue/test", map[string]interface{}{ |
| "common_name": "foobar.com", |
| "ip_sans": "1.2.3.4", |
| "alt_names": "foo.foobar.com,bar.foobar.com", |
| "ttl": "1h", |
| "other_sans": "1.3.6.1.4.1.311.20.2.3;utf8:devops@nope.com", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| certStr = resp.Data["certificate"].(string) |
| block, _ = pem.Decode([]byte(certStr)) |
| cert, err = x509.ParseCertificate(block.Bytes) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if cert.IPAddresses[0].String() != "1.2.3.4" { |
| t.Fatalf("unexpected IP SAN %q", cert.IPAddresses[0].String()) |
| } |
| if len(cert.DNSNames) != 3 || |
| cert.DNSNames[0] != "bar.foobar.com" || |
| cert.DNSNames[1] != "foo.foobar.com" || |
| cert.DNSNames[2] != "foobar.com" { |
| t.Fatalf("unexpected DNS SANs %v", cert.DNSNames) |
| } |
| if len(os.Getenv("VAULT_VERBOSE_PKITESTS")) > 0 { |
| t.Logf("certificate 1 to check:\n%s", certStr) |
| } |
| |
| // Valid for second possibility |
| resp, err = CBWrite(b, s, "issue/test", map[string]interface{}{ |
| "common_name": "foobar.com", |
| "ip_sans": "1.2.3.4", |
| "alt_names": "foo.foobar.com,bar.foobar.com", |
| "ttl": "1h", |
| "other_sans": "1.3.6.1.4.1.311.20.2.4;UTF8:d234e@foobar.com", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| certStr = resp.Data["certificate"].(string) |
| block, _ = pem.Decode([]byte(certStr)) |
| cert, err = x509.ParseCertificate(block.Bytes) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if cert.IPAddresses[0].String() != "1.2.3.4" { |
| t.Fatalf("unexpected IP SAN %q", cert.IPAddresses[0].String()) |
| } |
| if len(cert.DNSNames) != 3 || |
| cert.DNSNames[0] != "bar.foobar.com" || |
| cert.DNSNames[1] != "foo.foobar.com" || |
| cert.DNSNames[2] != "foobar.com" { |
| t.Fatalf("unexpected DNS SANs %v", cert.DNSNames) |
| } |
| if len(os.Getenv("VAULT_VERBOSE_PKITESTS")) > 0 { |
| t.Logf("certificate 2 to check:\n%s", certStr) |
| } |
| |
| // Valid for both |
| oid1, type1, val1 := "1.3.6.1.4.1.311.20.2.3", "utf8", "devops@nope.com" |
| oid2, type2, val2 := "1.3.6.1.4.1.311.20.2.4", "utf-8", "d234e@foobar.com" |
| otherNames := []string{ |
| fmt.Sprintf("%s;%s:%s", oid1, type1, val1), |
| fmt.Sprintf("%s;%s:%s", oid2, type2, val2), |
| } |
| resp, err = CBWrite(b, s, "issue/test", map[string]interface{}{ |
| "common_name": "foobar.com", |
| "ip_sans": "1.2.3.4", |
| "alt_names": "foo.foobar.com,bar.foobar.com", |
| "ttl": "1h", |
| "other_sans": strings.Join(otherNames, ","), |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| certStr = resp.Data["certificate"].(string) |
| block, _ = pem.Decode([]byte(certStr)) |
| cert, err = x509.ParseCertificate(block.Bytes) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if cert.IPAddresses[0].String() != "1.2.3.4" { |
| t.Fatalf("unexpected IP SAN %q", cert.IPAddresses[0].String()) |
| } |
| if len(cert.DNSNames) != 3 || |
| cert.DNSNames[0] != "bar.foobar.com" || |
| cert.DNSNames[1] != "foo.foobar.com" || |
| cert.DNSNames[2] != "foobar.com" { |
| t.Fatalf("unexpected DNS SANs %v", cert.DNSNames) |
| } |
| expectedOtherNames := []otherNameUtf8{{oid1, val1}, {oid2, val2}} |
| foundOtherNames, err := getOtherSANsFromX509Extensions(cert.Extensions) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if diff := deep.Equal(expectedOtherNames, foundOtherNames); len(diff) != 0 { |
| t.Errorf("unexpected otherNames: %v", diff) |
| } |
| if len(os.Getenv("VAULT_VERBOSE_PKITESTS")) > 0 { |
| t.Logf("certificate 3 to check:\n%s", certStr) |
| } |
| } |
| |
| func TestBackend_AllowedSerialNumbers(t *testing.T) { |
| t.Parallel() |
| b, s := CreateBackendWithStorage(t) |
| |
| var err error |
| var resp *logical.Response |
| var certStr string |
| var block *pem.Block |
| var cert *x509.Certificate |
| |
| _, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{ |
| "ttl": "40h", |
| "common_name": "myvault.com", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // First test that Serial Numbers are not allowed |
| _, err = CBWrite(b, s, "roles/test", map[string]interface{}{ |
| "allow_any_name": true, |
| "enforce_hostnames": false, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| _, err = CBWrite(b, s, "issue/test", map[string]interface{}{ |
| "common_name": "foobar", |
| "ttl": "1h", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| _, err = CBWrite(b, s, "issue/test", map[string]interface{}{ |
| "common_name": "foobar", |
| "ttl": "1h", |
| "serial_number": "foobar", |
| }) |
| if err == nil { |
| t.Fatal("expected error") |
| } |
| |
| // Update the role to allow serial numbers |
| _, err = CBWrite(b, s, "roles/test", map[string]interface{}{ |
| "allow_any_name": true, |
| "enforce_hostnames": false, |
| "allowed_serial_numbers": "f00*,b4r*", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| _, err = CBWrite(b, s, "issue/test", map[string]interface{}{ |
| "common_name": "foobar", |
| "ttl": "1h", |
| // Not a valid serial number |
| "serial_number": "foobar", |
| }) |
| if err == nil { |
| t.Fatal("expected error") |
| } |
| |
| // Valid for first possibility |
| resp, err = CBWrite(b, s, "issue/test", map[string]interface{}{ |
| "common_name": "foobar", |
| "serial_number": "f00bar", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| certStr = resp.Data["certificate"].(string) |
| block, _ = pem.Decode([]byte(certStr)) |
| cert, err = x509.ParseCertificate(block.Bytes) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if cert.Subject.SerialNumber != "f00bar" { |
| t.Fatalf("unexpected Subject SerialNumber %s", cert.Subject.SerialNumber) |
| } |
| if len(os.Getenv("VAULT_VERBOSE_PKITESTS")) > 0 { |
| t.Logf("certificate 1 to check:\n%s", certStr) |
| } |
| |
| // Valid for second possibility |
| resp, err = CBWrite(b, s, "issue/test", map[string]interface{}{ |
| "common_name": "foobar", |
| "serial_number": "b4rf00", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| certStr = resp.Data["certificate"].(string) |
| block, _ = pem.Decode([]byte(certStr)) |
| cert, err = x509.ParseCertificate(block.Bytes) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if cert.Subject.SerialNumber != "b4rf00" { |
| t.Fatalf("unexpected Subject SerialNumber %s", cert.Subject.SerialNumber) |
| } |
| if len(os.Getenv("VAULT_VERBOSE_PKITESTS")) > 0 { |
| t.Logf("certificate 2 to check:\n%s", certStr) |
| } |
| } |
| |
| func TestBackend_URI_SANs(t *testing.T) { |
| t.Parallel() |
| b, s := CreateBackendWithStorage(t) |
| |
| var err error |
| |
| _, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{ |
| "ttl": "40h", |
| "common_name": "myvault.com", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| _, err = CBWrite(b, s, "roles/test", map[string]interface{}{ |
| "allowed_domains": []string{"foobar.com", "zipzap.com"}, |
| "allow_bare_domains": true, |
| "allow_subdomains": true, |
| "allow_ip_sans": true, |
| "allowed_uri_sans": []string{"http://someuri/abc", "spiffe://host.com/*"}, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // First test some bad stuff that shouldn't work |
| _, err = CBWrite(b, s, "issue/test", map[string]interface{}{ |
| "common_name": "foobar.com", |
| "ip_sans": "1.2.3.4", |
| "alt_names": "foo.foobar.com,bar.foobar.com", |
| "ttl": "1h", |
| "uri_sans": "http://www.mydomain.com/zxf", |
| }) |
| if err == nil { |
| t.Fatal("expected error") |
| } |
| |
| // Test valid single entry |
| _, err = CBWrite(b, s, "issue/test", map[string]interface{}{ |
| "common_name": "foobar.com", |
| "ip_sans": "1.2.3.4", |
| "alt_names": "foo.foobar.com,bar.foobar.com", |
| "ttl": "1h", |
| "uri_sans": "http://someuri/abc", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Test globed entry |
| _, err = CBWrite(b, s, "issue/test", map[string]interface{}{ |
| "common_name": "foobar.com", |
| "ip_sans": "1.2.3.4", |
| "alt_names": "foo.foobar.com,bar.foobar.com", |
| "ttl": "1h", |
| "uri_sans": "spiffe://host.com/something", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Test multiple entries |
| resp, err := CBWrite(b, s, "issue/test", map[string]interface{}{ |
| "common_name": "foobar.com", |
| "ip_sans": "1.2.3.4", |
| "alt_names": "foo.foobar.com,bar.foobar.com", |
| "ttl": "1h", |
| "uri_sans": "spiffe://host.com/something,http://someuri/abc", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| certStr := resp.Data["certificate"].(string) |
| block, _ := pem.Decode([]byte(certStr)) |
| cert, err := x509.ParseCertificate(block.Bytes) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| URI0, _ := url.Parse("spiffe://host.com/something") |
| URI1, _ := url.Parse("http://someuri/abc") |
| |
| if len(cert.URIs) != 2 { |
| t.Fatalf("expected 2 valid URIs SANs %v", cert.URIs) |
| } |
| |
| if cert.URIs[0].String() != URI0.String() || cert.URIs[1].String() != URI1.String() { |
| t.Fatalf( |
| "expected URIs SANs %v to equal provided values spiffe://host.com/something, http://someuri/abc", |
| cert.URIs) |
| } |
| } |
| |
| func TestBackend_AllowedURISANsTemplate(t *testing.T) { |
| t.Parallel() |
| coreConfig := &vault.CoreConfig{ |
| CredentialBackends: map[string]logical.Factory{ |
| "userpass": userpass.Factory, |
| }, |
| LogicalBackends: map[string]logical.Factory{ |
| "pki": Factory, |
| }, |
| } |
| cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ |
| HandlerFunc: vaulthttp.Handler, |
| }) |
| cluster.Start() |
| defer cluster.Cleanup() |
| client := cluster.Cores[0].Client |
| |
| // Write test policy for userpass auth method. |
| err := client.Sys().PutPolicy("test", ` |
| path "pki/*" { |
| capabilities = ["update"] |
| }`) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Enable userpass auth method. |
| if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Configure test role for userpass. |
| if _, err := client.Logical().Write("auth/userpass/users/userpassname", map[string]interface{}{ |
| "password": "test", |
| "policies": "test", |
| }); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Login userpass for test role and keep client token. |
| secret, err := client.Logical().Write("auth/userpass/login/userpassname", map[string]interface{}{ |
| "password": "test", |
| }) |
| if err != nil || secret == nil { |
| t.Fatal(err) |
| } |
| userpassToken := secret.Auth.ClientToken |
| |
| // Get auth accessor for identity template. |
| auths, err := client.Sys().ListAuth() |
| if err != nil { |
| t.Fatal(err) |
| } |
| userpassAccessor := auths["userpass/"].Accessor |
| |
| // Mount PKI. |
| err = client.Sys().Mount("pki", &api.MountInput{ |
| Type: "pki", |
| Config: api.MountConfigInput{ |
| DefaultLeaseTTL: "16h", |
| MaxLeaseTTL: "60h", |
| }, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Generate internal CA. |
| _, err = client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ |
| "ttl": "40h", |
| "common_name": "myvault.com", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Write role PKI. |
| _, err = client.Logical().Write("pki/roles/test", map[string]interface{}{ |
| "allowed_uri_sans": []string{ |
| "spiffe://domain/{{identity.entity.aliases." + userpassAccessor + ".name}}", |
| "spiffe://domain/{{identity.entity.aliases." + userpassAccessor + ".name}}/*", "spiffe://domain/foo", |
| }, |
| "allowed_uri_sans_template": true, |
| "require_cn": false, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Issue certificate with identity templating |
| client.SetToken(userpassToken) |
| _, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"uri_sans": "spiffe://domain/userpassname, spiffe://domain/foo"}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Issue certificate with identity templating and glob |
| client.SetToken(userpassToken) |
| _, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"uri_sans": "spiffe://domain/userpassname/bar"}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Issue certificate with non-matching identity template parameter |
| client.SetToken(userpassToken) |
| _, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"uri_sans": "spiffe://domain/unknownuser"}) |
| if err == nil { |
| t.Fatal(err) |
| } |
| |
| // Set allowed_uri_sans_template to false. |
| _, err = client.Logical().Write("pki/roles/test", map[string]interface{}{ |
| "allowed_uri_sans_template": false, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Issue certificate with userpassToken. |
| _, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"uri_sans": "spiffe://domain/users/userpassname"}) |
| if err == nil { |
| t.Fatal("expected error") |
| } |
| } |
| |
| func TestBackend_AllowedDomainsTemplate(t *testing.T) { |
| t.Parallel() |
| coreConfig := &vault.CoreConfig{ |
| CredentialBackends: map[string]logical.Factory{ |
| "userpass": userpass.Factory, |
| }, |
| LogicalBackends: map[string]logical.Factory{ |
| "pki": Factory, |
| }, |
| } |
| cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ |
| HandlerFunc: vaulthttp.Handler, |
| }) |
| cluster.Start() |
| defer cluster.Cleanup() |
| client := cluster.Cores[0].Client |
| |
| // Write test policy for userpass auth method. |
| err := client.Sys().PutPolicy("test", ` |
| path "pki/*" { |
| capabilities = ["update"] |
| }`) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Enable userpass auth method. |
| if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Configure test role for userpass. |
| if _, err := client.Logical().Write("auth/userpass/users/userpassname", map[string]interface{}{ |
| "password": "test", |
| "policies": "test", |
| }); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Login userpass for test role and set client token |
| userpassAuth, err := auth.NewUserpassAuth("userpassname", &auth.Password{FromString: "test"}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Get auth accessor for identity template. |
| auths, err := client.Sys().ListAuth() |
| if err != nil { |
| t.Fatal(err) |
| } |
| userpassAccessor := auths["userpass/"].Accessor |
| |
| // Mount PKI. |
| err = client.Sys().Mount("pki", &api.MountInput{ |
| Type: "pki", |
| Config: api.MountConfigInput{ |
| DefaultLeaseTTL: "16h", |
| MaxLeaseTTL: "60h", |
| }, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Generate internal CA. |
| _, err = client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ |
| "ttl": "40h", |
| "common_name": "myvault.com", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Write role PKI. |
| _, err = client.Logical().Write("pki/roles/test", map[string]interface{}{ |
| "allowed_domains": []string{ |
| "foobar.com", "zipzap.com", "{{identity.entity.aliases." + userpassAccessor + ".name}}", |
| "foo.{{identity.entity.aliases." + userpassAccessor + ".name}}.example.com", |
| }, |
| "allowed_domains_template": true, |
| "allow_bare_domains": true, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Issue certificate with userpassToken. |
| secret, err := client.Auth().Login(context.TODO(), userpassAuth) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if err != nil || secret == nil { |
| t.Fatal(err) |
| } |
| _, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"common_name": "userpassname"}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Issue certificate for foobar.com to verify allowed_domain_template doesn't break plain domains. |
| _, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"common_name": "foobar.com"}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Issue certificate for unknown userpassname. |
| _, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"common_name": "unknownuserpassname"}) |
| if err == nil { |
| t.Fatal("expected error") |
| } |
| |
| // Issue certificate for foo.userpassname.domain. |
| _, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"common_name": "foo.userpassname.example.com"}) |
| if err != nil { |
| t.Fatal("expected error") |
| } |
| |
| // Set allowed_domains_template to false. |
| _, err = client.Logical().Write("pki/roles/test", map[string]interface{}{ |
| "allowed_domains_template": false, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Issue certificate with userpassToken. |
| _, err = client.Logical().Write("pki/issue/test", map[string]interface{}{"common_name": "userpassname"}) |
| if err == nil { |
| t.Fatal("expected error") |
| } |
| } |
| |
| func TestReadWriteDeleteRoles(t *testing.T) { |
| t.Parallel() |
| ctx := context.Background() |
| coreConfig := &vault.CoreConfig{ |
| CredentialBackends: map[string]logical.Factory{ |
| "userpass": userpass.Factory, |
| }, |
| LogicalBackends: map[string]logical.Factory{ |
| "pki": Factory, |
| }, |
| } |
| cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ |
| HandlerFunc: vaulthttp.Handler, |
| }) |
| cluster.Start() |
| defer cluster.Cleanup() |
| client := cluster.Cores[0].Client |
| |
| // Mount PKI. |
| err := client.Sys().MountWithContext(ctx, "pki", &api.MountInput{ |
| Type: "pki", |
| Config: api.MountConfigInput{ |
| DefaultLeaseTTL: "16h", |
| MaxLeaseTTL: "60h", |
| }, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| resp, err := client.Logical().ReadWithContext(ctx, "pki/roles/test") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| if resp != nil { |
| t.Fatalf("response should have been emtpy but was:\n%#v", resp) |
| } |
| |
| // Write role PKI. |
| _, err = client.Logical().WriteWithContext(ctx, "pki/roles/test", map[string]interface{}{}) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Read the role. |
| resp, err = client.Logical().ReadWithContext(ctx, "pki/roles/test") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| if resp.Data == nil { |
| t.Fatal("default data within response was nil when it should have contained data") |
| } |
| |
| // Validate that we have not changed any defaults unknowingly |
| expectedData := map[string]interface{}{ |
| "key_type": "rsa", |
| "use_csr_sans": true, |
| "client_flag": true, |
| "allowed_serial_numbers": []interface{}{}, |
| "generate_lease": false, |
| "signature_bits": json.Number("256"), |
| "use_pss": false, |
| "allowed_domains": []interface{}{}, |
| "allowed_uri_sans_template": false, |
| "enforce_hostnames": true, |
| "policy_identifiers": []interface{}{}, |
| "require_cn": true, |
| "allowed_domains_template": false, |
| "allow_token_displayname": false, |
| "country": []interface{}{}, |
| "not_after": "", |
| "postal_code": []interface{}{}, |
| "use_csr_common_name": true, |
| "allow_localhost": true, |
| "allow_subdomains": false, |
| "allow_wildcard_certificates": true, |
| "allowed_other_sans": []interface{}{}, |
| "allowed_uri_sans": []interface{}{}, |
| "basic_constraints_valid_for_non_ca": false, |
| "key_usage": []interface{}{"DigitalSignature", "KeyAgreement", "KeyEncipherment"}, |
| "not_before_duration": json.Number("30"), |
| "allow_glob_domains": false, |
| "ttl": json.Number("0"), |
| "ou": []interface{}{}, |
| "email_protection_flag": false, |
| "locality": []interface{}{}, |
| "server_flag": true, |
| "allow_bare_domains": false, |
| "allow_ip_sans": true, |
| "ext_key_usage_oids": []interface{}{}, |
| "allow_any_name": false, |
| "ext_key_usage": []interface{}{}, |
| "key_bits": json.Number("2048"), |
| "max_ttl": json.Number("0"), |
| "no_store": false, |
| "organization": []interface{}{}, |
| "province": []interface{}{}, |
| "street_address": []interface{}{}, |
| "code_signing_flag": false, |
| "issuer_ref": "default", |
| "cn_validations": []interface{}{"email", "hostname"}, |
| "allowed_user_ids": []interface{}{}, |
| } |
| |
| if diff := deep.Equal(expectedData, resp.Data); len(diff) > 0 { |
| t.Fatalf("pki role default values have changed, diff: %v", diff) |
| } |
| |
| _, err = client.Logical().DeleteWithContext(ctx, "pki/roles/test") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| resp, err = client.Logical().ReadWithContext(ctx, "pki/roles/test") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| if resp != nil { |
| t.Fatalf("response should have been empty but was:\n%#v", resp) |
| } |
| } |
| |
| func setCerts() { |
| cak, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) |
| if err != nil { |
| panic(err) |
| } |
| marshaledKey, err := x509.MarshalECPrivateKey(cak) |
| if err != nil { |
| panic(err) |
| } |
| keyPEMBlock := &pem.Block{ |
| Type: "EC PRIVATE KEY", |
| Bytes: marshaledKey, |
| } |
| ecCAKey = strings.TrimSpace(string(pem.EncodeToMemory(keyPEMBlock))) |
| if err != nil { |
| panic(err) |
| } |
| subjKeyID, err := certutil.GetSubjKeyID(cak) |
| if err != nil { |
| panic(err) |
| } |
| caCertTemplate := &x509.Certificate{ |
| Subject: pkix.Name{ |
| CommonName: "root.localhost", |
| }, |
| SubjectKeyId: subjKeyID, |
| DNSNames: []string{"root.localhost"}, |
| KeyUsage: x509.KeyUsage(x509.KeyUsageCertSign | x509.KeyUsageCRLSign), |
| SerialNumber: big.NewInt(mathrand.Int63()), |
| NotAfter: time.Now().Add(262980 * time.Hour), |
| BasicConstraintsValid: true, |
| IsCA: true, |
| } |
| caBytes, err := x509.CreateCertificate(rand.Reader, caCertTemplate, caCertTemplate, cak.Public(), cak) |
| if err != nil { |
| panic(err) |
| } |
| caCertPEMBlock := &pem.Block{ |
| Type: "CERTIFICATE", |
| Bytes: caBytes, |
| } |
| ecCACert = strings.TrimSpace(string(pem.EncodeToMemory(caCertPEMBlock))) |
| |
| rak, err := rsa.GenerateKey(rand.Reader, 2048) |
| if err != nil { |
| panic(err) |
| } |
| marshaledKey = x509.MarshalPKCS1PrivateKey(rak) |
| keyPEMBlock = &pem.Block{ |
| Type: "RSA PRIVATE KEY", |
| Bytes: marshaledKey, |
| } |
| rsaCAKey = strings.TrimSpace(string(pem.EncodeToMemory(keyPEMBlock))) |
| if err != nil { |
| panic(err) |
| } |
| _, err = certutil.GetSubjKeyID(rak) |
| if err != nil { |
| panic(err) |
| } |
| caBytes, err = x509.CreateCertificate(rand.Reader, caCertTemplate, caCertTemplate, rak.Public(), rak) |
| if err != nil { |
| panic(err) |
| } |
| caCertPEMBlock = &pem.Block{ |
| Type: "CERTIFICATE", |
| Bytes: caBytes, |
| } |
| rsaCACert = strings.TrimSpace(string(pem.EncodeToMemory(caCertPEMBlock))) |
| |
| _, edk, err := ed25519.GenerateKey(rand.Reader) |
| if err != nil { |
| panic(err) |
| } |
| marshaledKey, err = x509.MarshalPKCS8PrivateKey(edk) |
| if err != nil { |
| panic(err) |
| } |
| keyPEMBlock = &pem.Block{ |
| Type: "PRIVATE KEY", |
| Bytes: marshaledKey, |
| } |
| edCAKey = strings.TrimSpace(string(pem.EncodeToMemory(keyPEMBlock))) |
| if err != nil { |
| panic(err) |
| } |
| _, err = certutil.GetSubjKeyID(edk) |
| if err != nil { |
| panic(err) |
| } |
| caBytes, err = x509.CreateCertificate(rand.Reader, caCertTemplate, caCertTemplate, edk.Public(), edk) |
| if err != nil { |
| panic(err) |
| } |
| caCertPEMBlock = &pem.Block{ |
| Type: "CERTIFICATE", |
| Bytes: caBytes, |
| } |
| edCACert = strings.TrimSpace(string(pem.EncodeToMemory(caCertPEMBlock))) |
| } |
| |
| func TestBackend_RevokePlusTidy_Intermediate(t *testing.T) { |
| // Use a ridiculously long time to minimize the chance |
| // that we have to deal with more than one interval. |
| // InMemSink rounds down to an interval boundary rather than |
| // starting one at the time of initialization. |
| // |
| // This test is not parallelizable. |
| inmemSink := metrics.NewInmemSink( |
| 1000000*time.Hour, |
| 2000000*time.Hour) |
| |
| metricsConf := metrics.DefaultConfig("") |
| metricsConf.EnableHostname = false |
| metricsConf.EnableHostnameLabel = false |
| metricsConf.EnableServiceLabel = false |
| metricsConf.EnableTypePrefix = false |
| |
| _, err := metrics.NewGlobal(metricsConf, inmemSink) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Enable PKI secret engine |
| coreConfig := &vault.CoreConfig{ |
| LogicalBackends: map[string]logical.Factory{ |
| "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 |
| |
| // 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 up Metric Configuration, then restart to enable it |
| _, err = client.Logical().Write("pki/config/auto-tidy", map[string]interface{}{ |
| "maintain_stored_certificate_counts": true, |
| "publish_stored_certificate_count_metrics": true, |
| }) |
| _, err = client.Logical().Write("/sys/plugins/reload/backend", map[string]interface{}{ |
| "mounts": "pki/", |
| }) |
| |
| // Check the metrics initialized in order to calculate backendUUID for /pki |
| // BackendUUID not consistent during tests with UUID from /sys/mounts/pki |
| metricsSuffix := "total_certificates_stored" |
| backendUUID := "" |
| mostRecentInterval := inmemSink.Data()[len(inmemSink.Data())-1] |
| for _, existingGauge := range mostRecentInterval.Gauges { |
| if strings.HasSuffix(existingGauge.Name, metricsSuffix) { |
| expandedGaugeName := existingGauge.Name |
| backendUUID = strings.Split(expandedGaugeName, ".")[2] |
| break |
| } |
| } |
| if backendUUID == "" { |
| t.Fatalf("No Gauge Found ending with %s", metricsSuffix) |
| } |
| |
| // 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) |
| } |
| // Set up Metric Configuration, then restart to enable it |
| _, err = client.Logical().Write("pki2/config/auto-tidy", map[string]interface{}{ |
| "maintain_stored_certificate_counts": true, |
| "publish_stored_certificate_count_metrics": true, |
| }) |
| _, err = client.Logical().Write("/sys/plugins/reload/backend", map[string]interface{}{ |
| "mounts": "pki2/", |
| }) |
| |
| // 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, |
| "ttl": "10s", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| intermediateCertSerial := secret.Data["serial_number"].(string) |
| intermediateCASerialColon := strings.ReplaceAll(strings.ToLower(intermediateCertSerial), ":", "-") |
| |
| // Get the intermediate cert after signing |
| secret, err = client.Logical().Read("pki/cert/" + intermediateCASerialColon) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| if secret == nil || len(secret.Data) == 0 || len(secret.Data["certificate"].(string)) == 0 { |
| t.Fatal("expected certificate information from read operation") |
| } |
| |
| // Issue a revoke on on /pki |
| _, err = client.Logical().Write("pki/revoke", map[string]interface{}{ |
| "serial_number": intermediateCertSerial, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Check the cert-count metrics |
| expectedCertCountGaugeMetrics := map[string]float32{ |
| "secrets.pki." + backendUUID + ".total_revoked_certificates_stored": 1, |
| "secrets.pki." + backendUUID + ".total_certificates_stored": 1, |
| } |
| mostRecentInterval = inmemSink.Data()[len(inmemSink.Data())-1] |
| for gauge, value := range expectedCertCountGaugeMetrics { |
| if _, ok := mostRecentInterval.Gauges[gauge]; !ok { |
| t.Fatalf("Expected metrics to include a value for gauge %s", gauge) |
| } |
| if value != mostRecentInterval.Gauges[gauge].Value { |
| t.Fatalf("Expected value metric %s to be %f but got %f", gauge, value, mostRecentInterval.Gauges[gauge].Value) |
| } |
| } |
| |
| // Revoke adds a fixed 2s buffer, so we sleep for a bit longer to ensure |
| // the revocation time is past the current time. |
| time.Sleep(3 * time.Second) |
| |
| // Issue a tidy on /pki |
| _, err = client.Logical().Write("pki/tidy", map[string]interface{}{ |
| "tidy_cert_store": true, |
| "tidy_revoked_certs": true, |
| "safety_buffer": "1s", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Sleep a bit to make sure we're past the safety buffer |
| time.Sleep(2 * time.Second) |
| |
| // Get CRL and ensure the tidied cert is still in the list after the tidy |
| // operation since it's not past the NotAfter (ttl) value yet. |
| crl := getParsedCrl(t, client, "pki") |
| |
| revokedCerts := crl.TBSCertList.RevokedCertificates |
| if len(revokedCerts) == 0 { |
| t.Fatal("expected CRL to be non-empty") |
| } |
| |
| sn := certutil.GetHexFormatted(revokedCerts[0].SerialNumber.Bytes(), ":") |
| if sn != intermediateCertSerial { |
| t.Fatalf("expected: %v, got: %v", intermediateCertSerial, sn) |
| } |
| |
| // Wait for cert to expire |
| time.Sleep(10 * time.Second) |
| |
| // Issue a tidy on /pki |
| _, err = client.Logical().Write("pki/tidy", map[string]interface{}{ |
| "tidy_cert_store": true, |
| "tidy_revoked_certs": true, |
| "safety_buffer": "1s", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Sleep a bit to make sure we're past the safety buffer |
| time.Sleep(2 * time.Second) |
| |
| // Issue a tidy-status on /pki |
| { |
| tidyStatus, err := client.Logical().Read("pki/tidy-status") |
| if err != nil { |
| t.Fatal(err) |
| } |
| expectedData := map[string]interface{}{ |
| "safety_buffer": json.Number("1"), |
| "issuer_safety_buffer": json.Number("31536000"), |
| "revocation_queue_safety_buffer": json.Number("172800"), |
| "tidy_cert_store": true, |
| "tidy_revoked_certs": true, |
| "tidy_revoked_cert_issuer_associations": false, |
| "tidy_expired_issuers": false, |
| "tidy_move_legacy_ca_bundle": false, |
| "tidy_revocation_queue": false, |
| "tidy_cross_cluster_revoked_certs": false, |
| "pause_duration": "0s", |
| "state": "Finished", |
| "error": nil, |
| "time_started": nil, |
| "time_finished": nil, |
| "last_auto_tidy_finished": nil, |
| "message": nil, |
| "cert_store_deleted_count": json.Number("1"), |
| "revoked_cert_deleted_count": json.Number("1"), |
| "missing_issuer_cert_count": json.Number("0"), |
| "current_cert_store_count": json.Number("0"), |
| "current_revoked_cert_count": json.Number("0"), |
| "revocation_queue_deleted_count": json.Number("0"), |
| "cross_revoked_cert_deleted_count": json.Number("0"), |
| "internal_backend_uuid": backendUUID, |
| "tidy_acme": false, |
| "acme_account_safety_buffer": json.Number("2592000"), |
| "acme_orders_deleted_count": json.Number("0"), |
| "acme_account_revoked_count": json.Number("0"), |
| "acme_account_deleted_count": json.Number("0"), |
| "total_acme_account_count": json.Number("0"), |
| } |
| // Let's copy the times from the response so that we can use deep.Equal() |
| timeStarted, ok := tidyStatus.Data["time_started"] |
| if !ok || timeStarted == "" { |
| t.Fatal("Expected tidy status response to include a value for time_started") |
| } |
| expectedData["time_started"] = timeStarted |
| timeFinished, ok := tidyStatus.Data["time_finished"] |
| if !ok || timeFinished == "" { |
| t.Fatal("Expected tidy status response to include a value for time_finished") |
| } |
| expectedData["time_finished"] = timeFinished |
| expectedData["last_auto_tidy_finished"] = tidyStatus.Data["last_auto_tidy_finished"] |
| |
| if diff := deep.Equal(expectedData, tidyStatus.Data); diff != nil { |
| t.Fatal(diff) |
| } |
| } |
| // Check the tidy metrics |
| { |
| // Map of gauges to expected value |
| expectedGauges := map[string]float32{ |
| "secrets.pki.tidy.cert_store_current_entry": 0, |
| "secrets.pki.tidy.cert_store_total_entries": 1, |
| "secrets.pki.tidy.revoked_cert_current_entry": 0, |
| "secrets.pki.tidy.revoked_cert_total_entries": 1, |
| "secrets.pki.tidy.start_time_epoch": 0, |
| "secrets.pki." + backendUUID + ".total_certificates_stored": 0, |
| "secrets.pki." + backendUUID + ".total_revoked_certificates_stored": 0, |
| "secrets.pki.tidy.cert_store_total_entries_remaining": 0, |
| "secrets.pki.tidy.revoked_cert_total_entries_remaining": 0, |
| } |
| // Map of counters to the sum of the metrics for that counter |
| expectedCounters := map[string]float64{ |
| "secrets.pki.tidy.cert_store_deleted_count": 1, |
| "secrets.pki.tidy.revoked_cert_deleted_count": 1, |
| "secrets.pki.tidy.success": 2, |
| // Note that "secrets.pki.tidy.failure" won't be in the captured metrics |
| } |
| |
| // If the metrics span more than one interval, skip the checks |
| intervals := inmemSink.Data() |
| if len(intervals) == 1 { |
| interval := inmemSink.Data()[0] |
| |
| for gauge, value := range expectedGauges { |
| if _, ok := interval.Gauges[gauge]; !ok { |
| t.Fatalf("Expected metrics to include a value for gauge %s", gauge) |
| } |
| if value != interval.Gauges[gauge].Value { |
| t.Fatalf("Expected value metric %s to be %f but got %f", gauge, value, interval.Gauges[gauge].Value) |
| } |
| |
| } |
| for counter, value := range expectedCounters { |
| if _, ok := interval.Counters[counter]; !ok { |
| t.Fatalf("Expected metrics to include a value for couter %s", counter) |
| } |
| if value != interval.Counters[counter].Sum { |
| t.Fatalf("Expected the sum of metric %s to be %f but got %f", counter, value, interval.Counters[counter].Sum) |
| } |
| } |
| |
| tidyDuration, ok := interval.Samples["secrets.pki.tidy.duration"] |
| if !ok { |
| t.Fatal("Expected metrics to include a value for sample secrets.pki.tidy.duration") |
| } |
| if tidyDuration.Count <= 0 { |
| t.Fatalf("Expected metrics to have count > 0 for sample secrets.pki.tidy.duration, but got %d", tidyDuration.Count) |
| } |
| } |
| } |
| |
| crl = getParsedCrl(t, client, "pki") |
| |
| revokedCerts = crl.TBSCertList.RevokedCertificates |
| if len(revokedCerts) != 0 { |
| t.Fatal("expected CRL to be empty") |
| } |
| } |
| |
| func TestBackend_Root_FullCAChain(t *testing.T) { |
| t.Parallel() |
| testCases := []struct { |
| testName string |
| keyType string |
| }{ |
| {testName: "RSA", keyType: "rsa"}, |
| {testName: "ED25519", keyType: "ed25519"}, |
| {testName: "EC", keyType: "ec"}, |
| } |
| for _, tc := range testCases { |
| tc := tc |
| t.Run(tc.testName, func(t *testing.T) { |
| runFullCAChainTest(t, tc.keyType) |
| }) |
| } |
| } |
| |
| func runFullCAChainTest(t *testing.T, keyType string) { |
| // Generate a root CA at /pki-root |
| b_root, s_root := CreateBackendWithStorage(t) |
| |
| var err error |
| |
| resp, err := CBWrite(b_root, s_root, "root/generate/exported", map[string]interface{}{ |
| "common_name": "root myvault.com", |
| "key_type": keyType, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("expected ca info") |
| } |
| rootData := resp.Data |
| rootCert := rootData["certificate"].(string) |
| |
| // Validate that root's /cert/ca-chain now contains the certificate. |
| resp, err = CBRead(b_root, s_root, "cert/ca_chain") |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("expected intermediate chain information") |
| } |
| |
| fullChain := resp.Data["ca_chain"].(string) |
| requireCertInCaChainString(t, fullChain, rootCert, "expected root cert within root cert/ca_chain") |
| |
| // Make sure when we issue a leaf certificate we get the full chain back. |
| _, err = CBWrite(b_root, s_root, "roles/example", map[string]interface{}{ |
| "allowed_domains": "example.com", |
| "allow_subdomains": "true", |
| "max_ttl": "1h", |
| }) |
| require.NoError(t, err, "error setting up pki root role: %v", err) |
| |
| resp, err = CBWrite(b_root, s_root, "issue/example", map[string]interface{}{ |
| "common_name": "test.example.com", |
| "ttl": "5m", |
| }) |
| require.NoError(t, err, "error issuing certificate from pki root: %v", err) |
| fullChainArray := resp.Data["ca_chain"].([]string) |
| requireCertInCaChainArray(t, fullChainArray, rootCert, "expected root cert within root issuance pki-root/issue/example") |
| |
| // Now generate an intermediate at /pki-intermediate, signed by the root. |
| b_int, s_int := CreateBackendWithStorage(t) |
| |
| resp, err = CBWrite(b_int, s_int, "intermediate/generate/exported", map[string]interface{}{ |
| "common_name": "intermediate myvault.com", |
| "key_type": keyType, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("expected intermediate CSR info") |
| } |
| intermediateData := resp.Data |
| intermediateKey := intermediateData["private_key"].(string) |
| |
| resp, err = CBWrite(b_root, s_root, "root/sign-intermediate", map[string]interface{}{ |
| "csr": intermediateData["csr"], |
| "format": "pem", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("expected signed intermediate info") |
| } |
| intermediateSignedData := resp.Data |
| intermediateCert := intermediateSignedData["certificate"].(string) |
| |
| rootCaCert := parseCert(t, rootCert) |
| intermediaryCaCert := parseCert(t, intermediateCert) |
| requireSignedBy(t, intermediaryCaCert, rootCaCert) |
| intermediateCaChain := intermediateSignedData["ca_chain"].([]string) |
| |
| require.Equal(t, parseCert(t, intermediateCaChain[0]), intermediaryCaCert, "intermediate signed cert should have been part of ca_chain") |
| require.Equal(t, parseCert(t, intermediateCaChain[1]), rootCaCert, "root cert should have been part of ca_chain") |
| |
| _, err = CBWrite(b_int, s_int, "intermediate/set-signed", map[string]interface{}{ |
| "certificate": intermediateCert + "\n" + rootCert + "\n", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Validate that intermediate's ca_chain field now includes the full |
| // chain. |
| resp, err = CBRead(b_int, s_int, "cert/ca_chain") |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("expected intermediate chain information") |
| } |
| |
| // Verify we have a proper CRL now |
| crl := getParsedCrlFromBackend(t, b_int, s_int, "crl") |
| require.Equal(t, 0, len(crl.TBSCertList.RevokedCertificates)) |
| |
| fullChain = resp.Data["ca_chain"].(string) |
| requireCertInCaChainString(t, fullChain, intermediateCert, "expected full chain to contain intermediate certificate from pki-intermediate/cert/ca_chain") |
| requireCertInCaChainString(t, fullChain, rootCert, "expected full chain to contain root certificate from pki-intermediate/cert/ca_chain") |
| |
| // Make sure when we issue a leaf certificate we get the full chain back. |
| _, err = CBWrite(b_int, s_int, "roles/example", map[string]interface{}{ |
| "allowed_domains": "example.com", |
| "allow_subdomains": "true", |
| "max_ttl": "1h", |
| }) |
| require.NoError(t, err, "error setting up pki intermediate role: %v", err) |
| |
| resp, err = CBWrite(b_int, s_int, "issue/example", map[string]interface{}{ |
| "common_name": "test.example.com", |
| "ttl": "5m", |
| }) |
| require.NoError(t, err, "error issuing certificate from pki intermediate: %v", err) |
| fullChainArray = resp.Data["ca_chain"].([]string) |
| requireCertInCaChainArray(t, fullChainArray, intermediateCert, "expected full chain to contain intermediate certificate from pki-intermediate/issue/example") |
| requireCertInCaChainArray(t, fullChainArray, rootCert, "expected full chain to contain root certificate from pki-intermediate/issue/example") |
| |
| // Finally, import this signing cert chain into a new mount to ensure |
| // "external" CAs behave as expected. |
| b_ext, s_ext := CreateBackendWithStorage(t) |
| |
| _, err = CBWrite(b_ext, s_ext, "config/ca", map[string]interface{}{ |
| "pem_bundle": intermediateKey + "\n" + intermediateCert + "\n" + rootCert + "\n", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Validate the external chain information was loaded correctly. |
| resp, err = CBRead(b_ext, s_ext, "cert/ca_chain") |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("expected intermediate chain information") |
| } |
| |
| fullChain = resp.Data["ca_chain"].(string) |
| if strings.Count(fullChain, intermediateCert) != 1 { |
| t.Fatalf("expected full chain to contain intermediate certificate; got %v occurrences", strings.Count(fullChain, intermediateCert)) |
| } |
| if strings.Count(fullChain, rootCert) != 1 { |
| t.Fatalf("expected full chain to contain root certificate; got %v occurrences", strings.Count(fullChain, rootCert)) |
| } |
| |
| // Now issue a short-lived certificate from our pki-external. |
| _, err = CBWrite(b_ext, s_ext, "roles/example", map[string]interface{}{ |
| "allowed_domains": "example.com", |
| "allow_subdomains": "true", |
| "max_ttl": "1h", |
| }) |
| require.NoError(t, err, "error setting up pki role: %v", err) |
| |
| resp, err = CBWrite(b_ext, s_ext, "issue/example", map[string]interface{}{ |
| "common_name": "test.example.com", |
| "ttl": "5m", |
| }) |
| require.NoError(t, err, "error issuing certificate: %v", err) |
| require.NotNil(t, resp, "got nil response from issuing request") |
| issueCrtAsPem := resp.Data["certificate"].(string) |
| issuedCrt := parseCert(t, issueCrtAsPem) |
| |
| // Verify that the certificates are signed by the intermediary CA key... |
| requireSignedBy(t, issuedCrt, intermediaryCaCert) |
| |
| // Test that we can request that the root ca certificate not appear in the ca_chain field |
| resp, err = CBWrite(b_ext, s_ext, "issue/example", map[string]interface{}{ |
| "common_name": "test.example.com", |
| "ttl": "5m", |
| "remove_roots_from_chain": "true", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "error issuing certificate when removing self signed") |
| fullChain = strings.Join(resp.Data["ca_chain"].([]string), "\n") |
| if strings.Count(fullChain, intermediateCert) != 1 { |
| t.Fatalf("expected full chain to contain intermediate certificate; got %v occurrences", strings.Count(fullChain, intermediateCert)) |
| } |
| if strings.Count(fullChain, rootCert) != 0 { |
| t.Fatalf("expected full chain to NOT contain root certificate; got %v occurrences", strings.Count(fullChain, rootCert)) |
| } |
| } |
| |
| func requireCertInCaChainArray(t *testing.T, chain []string, cert string, msgAndArgs ...interface{}) { |
| var fullChain string |
| for _, caCert := range chain { |
| fullChain = fullChain + "\n" + caCert |
| } |
| |
| requireCertInCaChainString(t, fullChain, cert, msgAndArgs) |
| } |
| |
| func requireCertInCaChainString(t *testing.T, chain string, cert string, msgAndArgs ...interface{}) { |
| count := strings.Count(chain, cert) |
| if count != 1 { |
| failMsg := fmt.Sprintf("Found %d occurrances of the cert in the provided chain", count) |
| require.FailNow(t, failMsg, msgAndArgs...) |
| } |
| } |
| |
| type MultiBool int |
| |
| const ( |
| MFalse MultiBool = iota |
| MTrue MultiBool = iota |
| MAny MultiBool = iota |
| ) |
| |
| func (o MultiBool) ToValues() []bool { |
| if o == MTrue { |
| return []bool{true} |
| } |
| |
| if o == MFalse { |
| return []bool{false} |
| } |
| |
| if o == MAny { |
| return []bool{true, false} |
| } |
| |
| return []bool{} |
| } |
| |
| type IssuanceRegression struct { |
| AllowedDomains []string |
| AllowBareDomains MultiBool |
| AllowGlobDomains MultiBool |
| AllowSubdomains MultiBool |
| AllowLocalhost MultiBool |
| AllowWildcardCertificates MultiBool |
| CNValidations []string |
| CommonName string |
| Issued bool |
| } |
| |
| func RoleIssuanceRegressionHelper(t *testing.T, b *backend, s logical.Storage, index int, test IssuanceRegression) int { |
| tested := 0 |
| for _, AllowBareDomains := range test.AllowBareDomains.ToValues() { |
| for _, AllowGlobDomains := range test.AllowGlobDomains.ToValues() { |
| for _, AllowSubdomains := range test.AllowSubdomains.ToValues() { |
| for _, AllowLocalhost := range test.AllowLocalhost.ToValues() { |
| for _, AllowWildcardCertificates := range test.AllowWildcardCertificates.ToValues() { |
| role := fmt.Sprintf("issuance-regression-%d-bare-%v-glob-%v-subdomains-%v-localhost-%v-wildcard-%v", index, AllowBareDomains, AllowGlobDomains, AllowSubdomains, AllowLocalhost, AllowWildcardCertificates) |
| _, err := CBWrite(b, s, "roles/"+role, map[string]interface{}{ |
| "allowed_domains": test.AllowedDomains, |
| "allow_bare_domains": AllowBareDomains, |
| "allow_glob_domains": AllowGlobDomains, |
| "allow_subdomains": AllowSubdomains, |
| "allow_localhost": AllowLocalhost, |
| "allow_wildcard_certificates": AllowWildcardCertificates, |
| "cn_validations": test.CNValidations, |
| // TODO: test across this vector as well. Currently certain wildcard |
| // matching is broken with it enabled (such as x*x.foo). |
| "enforce_hostnames": false, |
| "key_type": "ec", |
| "key_bits": 256, |
| "no_store": true, |
| // With the CN Validations field, ensure we prevent CN from appearing |
| // in SANs. |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| resp, err := CBWrite(b, s, "issue/"+role, map[string]interface{}{ |
| "common_name": test.CommonName, |
| "exclude_cn_from_sans": true, |
| }) |
| |
| haveErr := err != nil || resp == nil |
| expectErr := !test.Issued |
| |
| if haveErr != expectErr { |
| t.Fatalf("issuance regression test [%d] failed: haveErr: %v, expectErr: %v, err: %v, resp: %v, test case: %v, role: %v", index, haveErr, expectErr, err, resp, test, role) |
| } |
| |
| tested += 1 |
| } |
| } |
| } |
| } |
| } |
| |
| return tested |
| } |
| |
| func TestBackend_Roles_IssuanceRegression(t *testing.T) { |
| t.Parallel() |
| // Regression testing of role's issuance policy. |
| testCases := []IssuanceRegression{ |
| // allowed, bare, glob, subdomains, localhost, wildcards, cn, issued |
| |
| // === Globs not allowed but used === // |
| // Allowed contains globs, but globbing not allowed, resulting in all |
| // issuances failing. Note that tests against issuing a wildcard with |
| // a bare domain will be covered later. |
| /* 0 */ {[]string{"*.*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "baz.fud.bar.foo", false}, |
| /* 1 */ {[]string{"*.*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "*.fud.bar.foo", false}, |
| /* 2 */ {[]string{"*.*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "fud.bar.foo", false}, |
| /* 3 */ {[]string{"*.*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "*.bar.foo", false}, |
| /* 4 */ {[]string{"*.*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "bar.foo", false}, |
| /* 5 */ {[]string{"*.*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "*.foo", false}, |
| /* 6 */ {[]string{"*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "foo", false}, |
| /* 7 */ {[]string{"*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "baz.fud.bar.foo", false}, |
| /* 8 */ {[]string{"*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "*.fud.bar.foo", false}, |
| /* 9 */ {[]string{"*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "fud.bar.foo", false}, |
| /* 10 */ {[]string{"*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "*.bar.foo", false}, |
| /* 11 */ {[]string{"*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "bar.foo", false}, |
| /* 12 */ {[]string{"*.foo"}, MAny, MFalse, MAny, MAny, MAny, nil, "foo", false}, |
| |
| // === Localhost sanity === // |
| // Localhost forbidden, not matching allowed domains -> not issued |
| /* 13 */ {[]string{"*.*.foo"}, MAny, MAny, MAny, MFalse, MAny, nil, "localhost", false}, |
| // Localhost allowed, not matching allowed domains -> issued |
| /* 14 */ {[]string{"*.*.foo"}, MAny, MAny, MAny, MTrue, MAny, nil, "localhost", true}, |
| // Localhost allowed via allowed domains (and bare allowed), not by AllowLocalhost -> issued |
| /* 15 */ {[]string{"localhost"}, MTrue, MAny, MAny, MFalse, MAny, nil, "localhost", true}, |
| // Localhost allowed via allowed domains (and bare not allowed), not by AllowLocalhost -> not issued |
| /* 16 */ {[]string{"localhost"}, MFalse, MAny, MAny, MFalse, MAny, nil, "localhost", false}, |
| // Localhost allowed via allowed domains (but bare not allowed), and by AllowLocalhost -> issued |
| /* 17 */ {[]string{"localhost"}, MFalse, MAny, MAny, MTrue, MAny, nil, "localhost", true}, |
| |
| // === Bare wildcard issuance == // |
| // allowed_domains contains one or more wildcards and bare domains allowed, |
| // resulting in the cert being issued. |
| /* 18 */ {[]string{"*.foo"}, MTrue, MAny, MAny, MAny, MTrue, nil, "*.foo", true}, |
| /* 19 */ {[]string{"*.*.foo"}, MTrue, MAny, MAny, MAny, MAny, nil, "*.*.foo", false}, // Does not conform to RFC 6125 |
| |
| // === Double Leading Glob Testing === // |
| // Allowed contains globs, but glob allowed so certain matches work. |
| // The value of bare and localhost does not impact these results. |
| /* 20 */ {[]string{"*.*.foo"}, MAny, MTrue, MFalse, MAny, MAny, nil, "baz.fud.bar.foo", true}, // glob domains allow infinite subdomains |
| /* 21 */ {[]string{"*.*.foo"}, MAny, MTrue, MFalse, MAny, MTrue, nil, "*.fud.bar.foo", true}, // glob domain allows wildcard of subdomains |
| /* 22 */ {[]string{"*.*.foo"}, MAny, MTrue, MFalse, MAny, MAny, nil, "fud.bar.foo", true}, |
| /* 23 */ {[]string{"*.*.foo"}, MAny, MTrue, MFalse, MAny, MTrue, nil, "*.bar.foo", true}, // Regression fix: Vault#13530 |
| /* 24 */ {[]string{"*.*.foo"}, MAny, MTrue, MFalse, MAny, MAny, nil, "bar.foo", false}, |
| /* 25 */ {[]string{"*.*.foo"}, MAny, MTrue, MFalse, MAny, MAny, nil, "*.foo", false}, |
| /* 26 */ {[]string{"*.*.foo"}, MAny, MTrue, MFalse, MAny, MAny, nil, "foo", false}, |
| |
| // Allowed contains globs, but glob and subdomain both work, so we expect |
| // wildcard issuance to work as well. The value of bare and localhost does |
| // not impact these results. |
| /* 27 */ {[]string{"*.*.foo"}, MAny, MTrue, MTrue, MAny, MAny, nil, "baz.fud.bar.foo", true}, |
| /* 28 */ {[]string{"*.*.foo"}, MAny, MTrue, MTrue, MAny, MTrue, nil, "*.fud.bar.foo", true}, |
| /* 29 */ {[]string{"*.*.foo"}, MAny, MTrue, MTrue, MAny, MAny, nil, "fud.bar.foo", true}, |
| /* 30 */ {[]string{"*.*.foo"}, MAny, MTrue, MTrue, MAny, MTrue, nil, "*.bar.foo", true}, // Regression fix: Vault#13530 |
| /* 31 */ {[]string{"*.*.foo"}, MAny, MTrue, MTrue, MAny, MAny, nil, "bar.foo", false}, |
| /* 32 */ {[]string{"*.*.foo"}, MAny, MTrue, MTrue, MAny, MAny, nil, "*.foo", false}, |
| /* 33 */ {[]string{"*.*.foo"}, MAny, MTrue, MTrue, MAny, MAny, nil, "foo", false}, |
| |
| // === Single Leading Glob Testing === // |
| // Allowed contains globs, but glob allowed so certain matches work. |
| // The value of bare and localhost does not impact these results. |
| /* 34 */ {[]string{"*.foo"}, MAny, MTrue, MFalse, MAny, MAny, nil, "baz.fud.bar.foo", true}, // glob domains allow infinite subdomains |
| /* 35 */ {[]string{"*.foo"}, MAny, MTrue, MFalse, MAny, MTrue, nil, "*.fud.bar.foo", true}, // glob domain allows wildcard of subdomains |
| /* 36 */ {[]string{"*.foo"}, MAny, MTrue, MFalse, MAny, MAny, nil, "fud.bar.foo", true}, // glob domains allow infinite subdomains |
| /* 37 */ {[]string{"*.foo"}, MAny, MTrue, MFalse, MAny, MTrue, nil, "*.bar.foo", true}, // glob domain allows wildcards of subdomains |
| /* 38 */ {[]string{"*.foo"}, MAny, MTrue, MFalse, MAny, MAny, nil, "bar.foo", true}, |
| /* 39 */ {[]string{"*.foo"}, MAny, MTrue, MFalse, MAny, MAny, nil, "foo", false}, |
| |
| // Allowed contains globs, but glob and subdomain both work, so we expect |
| // wildcard issuance to work as well. The value of bare and localhost does |
| // not impact these results. |
| /* 40 */ {[]string{"*.foo"}, MAny, MTrue, MTrue, MAny, MAny, nil, "baz.fud.bar.foo", true}, |
| /* 41 */ {[]string{"*.foo"}, MAny, MTrue, MTrue, MAny, MTrue, nil, "*.fud.bar.foo", true}, |
| /* 42 */ {[]string{"*.foo"}, MAny, MTrue, MTrue, MAny, MAny, nil, "fud.bar.foo", true}, |
| /* 43 */ {[]string{"*.foo"}, MAny, MTrue, MTrue, MAny, MTrue, nil, "*.bar.foo", true}, |
| /* 44 */ {[]string{"*.foo"}, MAny, MTrue, MTrue, MAny, MAny, nil, "bar.foo", true}, |
| /* 45 */ {[]string{"*.foo"}, MAny, MTrue, MTrue, MAny, MAny, nil, "foo", false}, |
| |
| // === Only base domain name === // |
| // Allowed contains only domain components, but subdomains not allowed. This |
| // results in most issuances failing unless we allow bare domains, in which |
| // case only the final issuance for "foo" will succeed. |
| /* 46 */ {[]string{"foo"}, MAny, MAny, MFalse, MAny, MAny, nil, "baz.fud.bar.foo", false}, |
| /* 47 */ {[]string{"foo"}, MAny, MAny, MFalse, MAny, MAny, nil, "*.fud.bar.foo", false}, |
| /* 48 */ {[]string{"foo"}, MAny, MAny, MFalse, MAny, MAny, nil, "fud.bar.foo", false}, |
| /* 49 */ {[]string{"foo"}, MAny, MAny, MFalse, MAny, MAny, nil, "*.bar.foo", false}, |
| /* 50 */ {[]string{"foo"}, MAny, MAny, MFalse, MAny, MAny, nil, "bar.foo", false}, |
| /* 51 */ {[]string{"foo"}, MAny, MAny, MFalse, MAny, MAny, nil, "*.foo", false}, |
| /* 52 */ {[]string{"foo"}, MFalse, MAny, MFalse, MAny, MAny, nil, "foo", false}, |
| /* 53 */ {[]string{"foo"}, MTrue, MAny, MFalse, MAny, MAny, nil, "foo", true}, |
| |
| // Allowed contains only domain components, and subdomains are now allowed. |
| // This results in most issuances succeeding, with the exception of the |
| // base foo, which is still governed by base's value. |
| /* 54 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MAny, nil, "baz.fud.bar.foo", true}, |
| /* 55 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MTrue, nil, "*.fud.bar.foo", true}, |
| /* 56 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MAny, nil, "fud.bar.foo", true}, |
| /* 57 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MTrue, nil, "*.bar.foo", true}, |
| /* 58 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MAny, nil, "bar.foo", true}, |
| /* 59 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MTrue, nil, "*.foo", true}, |
| /* 60 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MTrue, nil, "x*x.foo", true}, // internal wildcards should be allowed per RFC 6125/6.4.3 |
| /* 61 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MTrue, nil, "*x.foo", true}, // prefix wildcards should be allowed per RFC 6125/6.4.3 |
| /* 62 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MTrue, nil, "x*.foo", true}, // suffix wildcards should be allowed per RFC 6125/6.4.3 |
| /* 63 */ {[]string{"foo"}, MFalse, MAny, MTrue, MAny, MAny, nil, "foo", false}, |
| /* 64 */ {[]string{"foo"}, MTrue, MAny, MTrue, MAny, MAny, nil, "foo", true}, |
| |
| // === Internal Glob Matching === // |
| // Basic glob matching requirements |
| /* 65 */ {[]string{"x*x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "xerox.foo", true}, |
| /* 66 */ {[]string{"x*x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "xylophone.files.pyrex.foo", true}, // globs can match across subdomains |
| /* 67 */ {[]string{"x*x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "xercex.bar.foo", false}, // x.foo isn't matched |
| /* 68 */ {[]string{"x*x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "bar.foo", false}, // x*x isn't matched. |
| /* 69 */ {[]string{"x*x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "*.foo", false}, // unrelated wildcard |
| /* 70 */ {[]string{"x*x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "*.x*x.foo", false}, // Does not conform to RFC 6125 |
| /* 71 */ {[]string{"x*x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "*.xyx.foo", false}, // Globs and Subdomains do not layer per docs. |
| |
| // Various requirements around x*x.foo wildcard matching. |
| /* 72 */ {[]string{"x*x.foo"}, MFalse, MFalse, MAny, MAny, MAny, nil, "x*x.foo", false}, // base disabled, shouldn't match wildcard |
| /* 73 */ {[]string{"x*x.foo"}, MFalse, MTrue, MAny, MAny, MTrue, nil, "x*x.foo", true}, // base disallowed, but globbing allowed and should match |
| /* 74 */ {[]string{"x*x.foo"}, MTrue, MAny, MAny, MAny, MTrue, nil, "x*x.foo", true}, // base allowed, should match wildcard |
| |
| // Basic glob matching requirements with internal dots. |
| /* 75 */ {[]string{"x.*.x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "xerox.foo", false}, // missing dots |
| /* 76 */ {[]string{"x.*.x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "x.ero.x.foo", true}, |
| /* 77 */ {[]string{"x.*.x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "xylophone.files.pyrex.foo", false}, // missing dots |
| /* 78 */ {[]string{"x.*.x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "x.ylophone.files.pyre.x.foo", true}, // globs can match across subdomains |
| /* 79 */ {[]string{"x.*.x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "xercex.bar.foo", false}, // x.foo isn't matched |
| /* 80 */ {[]string{"x.*.x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "bar.foo", false}, // x.*.x isn't matched. |
| /* 81 */ {[]string{"x.*.x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "*.foo", false}, // unrelated wildcard |
| /* 82 */ {[]string{"x.*.x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "*.x.*.x.foo", false}, // Does not conform to RFC 6125 |
| /* 83 */ {[]string{"x.*.x.foo"}, MAny, MTrue, MAny, MAny, MAny, nil, "*.x.y.x.foo", false}, // Globs and Subdomains do not layer per docs. |
| |
| // === Wildcard restriction testing === // |
| /* 84 */ {[]string{"*.foo"}, MAny, MTrue, MFalse, MAny, MFalse, nil, "*.fud.bar.foo", false}, // glob domain allows wildcard of subdomains |
| /* 85 */ {[]string{"*.foo"}, MAny, MTrue, MFalse, MAny, MFalse, nil, "*.bar.foo", false}, // glob domain allows wildcards of subdomains |
| /* 86 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MFalse, nil, "*.fud.bar.foo", false}, |
| /* 87 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MFalse, nil, "*.bar.foo", false}, |
| /* 88 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MFalse, nil, "*.foo", false}, |
| /* 89 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MFalse, nil, "x*x.foo", false}, |
| /* 90 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MFalse, nil, "*x.foo", false}, |
| /* 91 */ {[]string{"foo"}, MAny, MAny, MTrue, MAny, MFalse, nil, "x*.foo", false}, |
| /* 92 */ {[]string{"x*x.foo"}, MTrue, MAny, MAny, MAny, MFalse, nil, "x*x.foo", false}, |
| /* 93 */ {[]string{"*.foo"}, MFalse, MFalse, MAny, MAny, MAny, nil, "*.foo", false}, // Bare and globs forbidden despite (potentially) allowing wildcards. |
| /* 94 */ {[]string{"x.*.x.foo"}, MAny, MAny, MAny, MAny, MAny, nil, "x.*.x.foo", false}, // Does not conform to RFC 6125 |
| |
| // === CN validation allowances === // |
| /* 95 */ {[]string{"foo"}, MAny, MAny, MAny, MAny, MAny, []string{"disabled"}, "*.fud.bar.foo", true}, |
| /* 96 */ {[]string{"foo"}, MAny, MAny, MAny, MAny, MAny, []string{"disabled"}, "*.fud.*.foo", true}, |
| /* 97 */ {[]string{"foo"}, MAny, MAny, MAny, MAny, MAny, []string{"disabled"}, "*.bar.*.bar", true}, |
| /* 98 */ {[]string{"foo"}, MAny, MAny, MAny, MAny, MAny, []string{"disabled"}, "foo@foo", true}, |
| /* 99 */ {[]string{"foo"}, MAny, MAny, MAny, MAny, MAny, []string{"disabled"}, "foo@foo@foo", true}, |
| /* 100 */ {[]string{"foo"}, MAny, MAny, MAny, MAny, MAny, []string{"disabled"}, "bar@bar@bar", true}, |
| /* 101 */ {[]string{"foo"}, MTrue, MTrue, MTrue, MTrue, MTrue, []string{"email"}, "bar@bar@bar", false}, |
| /* 102 */ {[]string{"foo"}, MTrue, MTrue, MTrue, MTrue, MTrue, []string{"email"}, "bar@bar", false}, |
| /* 103 */ {[]string{"foo"}, MTrue, MTrue, MTrue, MTrue, MTrue, []string{"email"}, "bar@foo", true}, |
| /* 104 */ {[]string{"foo"}, MTrue, MTrue, MTrue, MTrue, MTrue, []string{"hostname"}, "bar@foo", false}, |
| /* 105 */ {[]string{"foo"}, MTrue, MTrue, MTrue, MTrue, MTrue, []string{"hostname"}, "bar@bar", false}, |
| /* 106 */ {[]string{"foo"}, MTrue, MTrue, MTrue, MTrue, MTrue, []string{"hostname"}, "bar.foo", true}, |
| /* 107 */ {[]string{"foo"}, MTrue, MTrue, MTrue, MTrue, MTrue, []string{"hostname"}, "bar.bar", false}, |
| /* 108 */ {[]string{"foo"}, MTrue, MTrue, MTrue, MTrue, MTrue, []string{"email"}, "bar.foo", false}, |
| /* 109 */ {[]string{"foo"}, MTrue, MTrue, MTrue, MTrue, MTrue, []string{"email"}, "bar.bar", false}, |
| } |
| |
| if len(testCases) != 110 { |
| t.Fatalf("misnumbered test case entries will make it hard to find bugs: %v", len(testCases)) |
| } |
| |
| b, s := CreateBackendWithStorage(t) |
| |
| // We need a RSA key so all signature sizes are valid with it. |
| resp, err := CBWrite(b, s, "root/generate/exported", map[string]interface{}{ |
| "common_name": "myvault.com", |
| "ttl": "128h", |
| "key_type": "rsa", |
| "key_bits": 2048, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("expected ca info") |
| } |
| |
| tested := 0 |
| for index, test := range testCases { |
| tested += RoleIssuanceRegressionHelper(t, b, s, index, test) |
| } |
| |
| t.Logf("Issuance regression expanded matrix test scenarios: %d", tested) |
| } |
| |
| type KeySizeRegression struct { |
| // Values reused for both Role and CA configuration. |
| RoleKeyType string |
| RoleKeyBits []int |
| |
| // Signature Bits presently is only specified on the role. |
| RoleSignatureBits []int |
| RoleUsePSS bool |
| |
| // These are tuples; must be of the same length. |
| TestKeyTypes []string |
| TestKeyBits []int |
| |
| // All of the above key types/sizes must pass or fail together. |
| ExpectError bool |
| } |
| |
| func (k KeySizeRegression) KeyTypeValues() []string { |
| if k.RoleKeyType == "any" { |
| return []string{"rsa", "ec", "ed25519"} |
| } |
| |
| return []string{k.RoleKeyType} |
| } |
| |
| func RoleKeySizeRegressionHelper(t *testing.T, b *backend, s logical.Storage, index int, test KeySizeRegression) int { |
| tested := 0 |
| |
| for _, caKeyType := range test.KeyTypeValues() { |
| for _, caKeyBits := range test.RoleKeyBits { |
| // Generate a new CA key. |
| resp, err := CBWrite(b, s, "root/generate/exported", map[string]interface{}{ |
| "common_name": "myvault.com", |
| "ttl": "128h", |
| "key_type": caKeyType, |
| "key_bits": caKeyBits, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("expected ca info") |
| } |
| |
| for _, roleKeyBits := range test.RoleKeyBits { |
| for _, roleSignatureBits := range test.RoleSignatureBits { |
| role := fmt.Sprintf("key-size-regression-%d-keytype-%v-keybits-%d-signature-bits-%d", index, test.RoleKeyType, roleKeyBits, roleSignatureBits) |
| _, err := CBWrite(b, s, "roles/"+role, map[string]interface{}{ |
| "key_type": test.RoleKeyType, |
| "key_bits": roleKeyBits, |
| "signature_bits": roleSignatureBits, |
| "use_pss": test.RoleUsePSS, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| for index, keyType := range test.TestKeyTypes { |
| keyBits := test.TestKeyBits[index] |
| |
| _, _, csrPem := generateCSR(t, &x509.CertificateRequest{ |
| Subject: pkix.Name{ |
| CommonName: "localhost", |
| }, |
| }, keyType, keyBits) |
| |
| resp, err = CBWrite(b, s, "sign/"+role, map[string]interface{}{ |
| "common_name": "localhost", |
| "csr": csrPem, |
| }) |
| |
| haveErr := err != nil || resp == nil |
| |
| if haveErr != test.ExpectError { |
| t.Fatalf("key size regression test [%d] failed: haveErr: %v, expectErr: %v, err: %v, resp: %v, test case: %v, caKeyType: %v, caKeyBits: %v, role: %v, keyType: %v, keyBits: %v", index, haveErr, test.ExpectError, err, resp, test, caKeyType, caKeyBits, role, keyType, keyBits) |
| } |
| |
| if resp != nil && test.RoleUsePSS && caKeyType == "rsa" { |
| leafCert := parseCert(t, resp.Data["certificate"].(string)) |
| switch leafCert.SignatureAlgorithm { |
| case x509.SHA256WithRSAPSS, x509.SHA384WithRSAPSS, x509.SHA512WithRSAPSS: |
| default: |
| t.Fatalf("key size regression test [%d] failed on role %v: unexpected signature algorithm; expected RSA-type CA to sign a leaf cert with PSS algorithm; got %v", index, role, leafCert.SignatureAlgorithm.String()) |
| } |
| } |
| |
| tested += 1 |
| } |
| } |
| } |
| |
| _, err = CBDelete(b, s, "root") |
| if err != nil { |
| t.Fatal(err) |
| } |
| } |
| } |
| |
| return tested |
| } |
| |
| func TestBackend_Roles_KeySizeRegression(t *testing.T) { |
| t.Parallel() |
| // Regression testing of role's issuance policy. |
| testCases := []KeySizeRegression{ |
| // RSA with default parameters should fail to issue smaller RSA keys |
| // and any size ECDSA/Ed25519 keys. |
| /* 0 */ {"rsa", []int{0, 2048}, []int{0, 256, 384, 512}, false, []string{"rsa", "ec", "ec", "ec", "ec", "ed25519"}, []int{1024, 224, 256, 384, 521, 0}, true}, |
| // But it should work to issue larger RSA keys. |
| /* 1 */ {"rsa", []int{0, 2048}, []int{0, 256, 384, 512}, false, []string{"rsa", "rsa"}, []int{2048, 3072}, false}, |
| |
| // EC with default parameters should fail to issue smaller EC keys |
| // and any size RSA/Ed25519 keys. |
| /* 2 */ {"ec", []int{0}, []int{0}, false, []string{"rsa", "ec", "ed25519"}, []int{2048, 224, 0}, true}, |
| // But it should work to issue larger EC keys. Note that we should be |
| // independent of signature bits as that's computed from the issuer |
| // type (for EC based issuers). |
| /* 3 */ {"ec", []int{224}, []int{0, 256, 384, 521}, false, []string{"ec", "ec", "ec", "ec"}, []int{224, 256, 384, 521}, false}, |
| /* 4 */ {"ec", []int{0, 256}, []int{0, 256, 384, 521}, false, []string{"ec", "ec", "ec"}, []int{256, 384, 521}, false}, |
| /* 5 */ {"ec", []int{384}, []int{0, 256, 384, 521}, false, []string{"ec", "ec"}, []int{384, 521}, false}, |
| /* 6 */ {"ec", []int{521}, []int{0, 256, 384, 512}, false, []string{"ec"}, []int{521}, false}, |
| |
| // Ed25519 should reject RSA and EC keys. |
| /* 7 */ {"ed25519", []int{0}, []int{0}, false, []string{"rsa", "ec", "ec"}, []int{2048, 256, 521}, true}, |
| // But it should work to issue Ed25519 keys. |
| /* 8 */ {"ed25519", []int{0}, []int{0}, false, []string{"ed25519"}, []int{0}, false}, |
| |
| // Any key type should reject insecure RSA key sizes. |
| /* 9 */ {"any", []int{0}, []int{0, 256, 384, 512}, false, []string{"rsa", "rsa"}, []int{512, 1024}, true}, |
| // But work for everything else. |
| /* 10 */ {"any", []int{0}, []int{0, 256, 384, 512}, false, []string{"rsa", "rsa", "ec", "ec", "ec", "ec", "ed25519"}, []int{2048, 3072, 224, 256, 384, 521, 0}, false}, |
| |
| // RSA with larger than default key size should reject smaller ones. |
| /* 11 */ {"rsa", []int{3072}, []int{0, 256, 384, 512}, false, []string{"rsa"}, []int{2048}, true}, |
| |
| // We should be able to sign with PSS with any CA key type. |
| /* 12 */ {"rsa", []int{0}, []int{0, 256, 384, 512}, true, []string{"rsa"}, []int{2048}, false}, |
| /* 13 */ {"ec", []int{0}, []int{0}, true, []string{"ec"}, []int{256}, false}, |
| /* 14 */ {"ed25519", []int{0}, []int{0}, true, []string{"ed25519"}, []int{0}, false}, |
| } |
| |
| if len(testCases) != 15 { |
| t.Fatalf("misnumbered test case entries will make it hard to find bugs: %v", len(testCases)) |
| } |
| |
| b, s := CreateBackendWithStorage(t) |
| |
| tested := 0 |
| for index, test := range testCases { |
| tested += RoleKeySizeRegressionHelper(t, b, s, index, test) |
| } |
| |
| t.Logf("Key size regression expanded matrix test scenarios: %d", tested) |
| } |
| |
| func TestRootWithExistingKey(t *testing.T) { |
| t.Parallel() |
| b, s := CreateBackendWithStorage(t) |
| var err error |
| |
| // Fail requests if type is existing, and we specify the key_type param |
| _, err = CBWrite(b, s, "root/generate/existing", map[string]interface{}{ |
| "common_name": "root myvault.com", |
| "key_type": "rsa", |
| }) |
| require.Error(t, err) |
| require.Contains(t, err.Error(), "key_type nor key_bits arguments can be set in this mode") |
| |
| // Fail requests if type is existing, and we specify the key_bits param |
| _, err = CBWrite(b, s, "root/generate/existing", map[string]interface{}{ |
| "common_name": "root myvault.com", |
| "key_bits": "2048", |
| }) |
| require.Error(t, err) |
| require.Contains(t, err.Error(), "key_type nor key_bits arguments can be set in this mode") |
| |
| // Fail if the specified key does not exist. |
| _, err = CBWrite(b, s, "issuers/generate/root/existing", map[string]interface{}{ |
| "common_name": "root myvault.com", |
| "issuer_name": "my-issuer1", |
| "key_ref": "my-key1", |
| }) |
| require.Error(t, err) |
| require.Contains(t, err.Error(), "unable to find PKI key for reference: my-key1") |
| |
| // Fail if the specified key name is default. |
| _, err = CBWrite(b, s, "issuers/generate/root/internal", map[string]interface{}{ |
| "common_name": "root myvault.com", |
| "issuer_name": "my-issuer1", |
| "key_name": "Default", |
| }) |
| require.Error(t, err) |
| require.Contains(t, err.Error(), "reserved keyword 'default' can not be used as key name") |
| |
| // Fail if the specified issuer name is default. |
| _, err = CBWrite(b, s, "issuers/generate/root/internal", map[string]interface{}{ |
| "common_name": "root myvault.com", |
| "issuer_name": "DEFAULT", |
| }) |
| require.Error(t, err) |
| require.Contains(t, err.Error(), "reserved keyword 'default' can not be used as issuer name") |
| |
| // Create the first CA |
| resp, err := CBWrite(b, s, "issuers/generate/root/internal", map[string]interface{}{ |
| "common_name": "root myvault.com", |
| "key_type": "rsa", |
| "issuer_name": "my-issuer1", |
| }) |
| schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("issuers/generate/root/internal"), logical.UpdateOperation), resp, true) |
| require.NoError(t, err) |
| require.NotNil(t, resp.Data["certificate"]) |
| myIssuerId1 := resp.Data["issuer_id"] |
| myKeyId1 := resp.Data["key_id"] |
| require.NotEmpty(t, myIssuerId1) |
| require.NotEmpty(t, myKeyId1) |
| |
| // Fetch the parsed CRL; it should be empty as we've not revoked anything |
| parsedCrl := getParsedCrlFromBackend(t, b, s, "issuer/my-issuer1/crl/der") |
| require.Equal(t, len(parsedCrl.TBSCertList.RevokedCertificates), 0, "should have no revoked certificates") |
| |
| // Fail if the specified issuer name is re-used. |
| _, err = CBWrite(b, s, "issuers/generate/root/internal", map[string]interface{}{ |
| "common_name": "root myvault.com", |
| "issuer_name": "my-issuer1", |
| }) |
| require.Error(t, err) |
| require.Contains(t, err.Error(), "issuer name already in use") |
| |
| // Create the second CA |
| resp, err = CBWrite(b, s, "issuers/generate/root/internal", map[string]interface{}{ |
| "common_name": "root myvault.com", |
| "key_type": "rsa", |
| "issuer_name": "my-issuer2", |
| "key_name": "root-key2", |
| }) |
| require.NoError(t, err) |
| require.NotNil(t, resp.Data["certificate"]) |
| myIssuerId2 := resp.Data["issuer_id"] |
| myKeyId2 := resp.Data["key_id"] |
| require.NotEmpty(t, myIssuerId2) |
| require.NotEmpty(t, myKeyId2) |
| |
| // Fetch the parsed CRL; it should be empty as we've not revoked anything |
| parsedCrl = getParsedCrlFromBackend(t, b, s, "issuer/my-issuer2/crl/der") |
| require.Equal(t, len(parsedCrl.TBSCertList.RevokedCertificates), 0, "should have no revoked certificates") |
| |
| // Fail if the specified key name is re-used. |
| _, err = CBWrite(b, s, "issuers/generate/root/internal", map[string]interface{}{ |
| "common_name": "root myvault.com", |
| "issuer_name": "my-issuer3", |
| "key_name": "root-key2", |
| }) |
| require.Error(t, err) |
| require.Contains(t, err.Error(), "key name already in use") |
| |
| // Create a third CA re-using key from CA 1 |
| resp, err = CBWrite(b, s, "issuers/generate/root/existing", map[string]interface{}{ |
| "common_name": "root myvault.com", |
| "issuer_name": "my-issuer3", |
| "key_ref": myKeyId1, |
| }) |
| require.NoError(t, err) |
| require.NotNil(t, resp.Data["certificate"]) |
| myIssuerId3 := resp.Data["issuer_id"] |
| myKeyId3 := resp.Data["key_id"] |
| require.NotEmpty(t, myIssuerId3) |
| require.NotEmpty(t, myKeyId3) |
| |
| // Fetch the parsed CRL; it should be empty as we've not revoking anything. |
| parsedCrl = getParsedCrlFromBackend(t, b, s, "issuer/my-issuer3/crl/der") |
| require.Equal(t, len(parsedCrl.TBSCertList.RevokedCertificates), 0, "should have no revoked certificates") |
| // Signatures should be the same since this is just a reissued cert. We |
| // use signature as a proxy for "these two CRLs are equal". |
| firstCrl := getParsedCrlFromBackend(t, b, s, "issuer/my-issuer1/crl/der") |
| require.Equal(t, parsedCrl.SignatureValue, firstCrl.SignatureValue) |
| |
| require.NotEqual(t, myIssuerId1, myIssuerId2) |
| require.NotEqual(t, myIssuerId1, myIssuerId3) |
| require.NotEqual(t, myKeyId1, myKeyId2) |
| require.Equal(t, myKeyId1, myKeyId3) |
| |
| resp, err = CBList(b, s, "issuers") |
| require.NoError(t, err) |
| require.Equal(t, 3, len(resp.Data["keys"].([]string))) |
| require.Contains(t, resp.Data["keys"], string(myIssuerId1.(issuerID))) |
| require.Contains(t, resp.Data["keys"], string(myIssuerId2.(issuerID))) |
| require.Contains(t, resp.Data["keys"], string(myIssuerId3.(issuerID))) |
| } |
| |
| func TestIntermediateWithExistingKey(t *testing.T) { |
| t.Parallel() |
| b, s := CreateBackendWithStorage(t) |
| |
| var err error |
| |
| // Fail requests if type is existing, and we specify the key_type param |
| _, err = CBWrite(b, s, "intermediate/generate/existing", map[string]interface{}{ |
| "common_name": "root myvault.com", |
| "key_type": "rsa", |
| }) |
| require.Error(t, err) |
| require.Contains(t, err.Error(), "key_type nor key_bits arguments can be set in this mode") |
| |
| // Fail requests if type is existing, and we specify the key_bits param |
| _, err = CBWrite(b, s, "intermediate/generate/existing", map[string]interface{}{ |
| "common_name": "root myvault.com", |
| "key_bits": "2048", |
| }) |
| require.Error(t, err) |
| require.Contains(t, err.Error(), "key_type nor key_bits arguments can be set in this mode") |
| |
| // Fail if the specified key does not exist. |
| _, err = CBWrite(b, s, "issuers/generate/intermediate/existing", map[string]interface{}{ |
| "common_name": "root myvault.com", |
| "key_ref": "my-key1", |
| }) |
| require.Error(t, err) |
| require.Contains(t, err.Error(), "unable to find PKI key for reference: my-key1") |
| |
| // Create the first intermediate CA |
| resp, err := CBWrite(b, s, "issuers/generate/intermediate/internal", map[string]interface{}{ |
| "common_name": "root myvault.com", |
| "key_type": "rsa", |
| }) |
| schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("issuers/generate/intermediate/internal"), logical.UpdateOperation), resp, true) |
| require.NoError(t, err) |
| // csr1 := resp.Data["csr"] |
| myKeyId1 := resp.Data["key_id"] |
| require.NotEmpty(t, myKeyId1) |
| |
| // Create the second intermediate CA |
| resp, err = CBWrite(b, s, "issuers/generate/intermediate/internal", map[string]interface{}{ |
| "common_name": "root myvault.com", |
| "key_type": "rsa", |
| "key_name": "interkey1", |
| }) |
| require.NoError(t, err) |
| // csr2 := resp.Data["csr"] |
| myKeyId2 := resp.Data["key_id"] |
| require.NotEmpty(t, myKeyId2) |
| |
| // Create a third intermediate CA re-using key from intermediate CA 1 |
| resp, err = CBWrite(b, s, "issuers/generate/intermediate/existing", map[string]interface{}{ |
| "common_name": "root myvault.com", |
| "key_ref": myKeyId1, |
| }) |
| require.NoError(t, err) |
| // csr3 := resp.Data["csr"] |
| myKeyId3 := resp.Data["key_id"] |
| require.NotEmpty(t, myKeyId3) |
| |
| require.NotEqual(t, myKeyId1, myKeyId2) |
| require.Equal(t, myKeyId1, myKeyId3, "our new ca did not seem to reuse the key as we expected.") |
| } |
| |
| func TestIssuanceTTLs(t *testing.T) { |
| t.Parallel() |
| b, s := CreateBackendWithStorage(t) |
| |
| resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ |
| "common_name": "root example.com", |
| "issuer_name": "root", |
| "ttl": "10s", |
| "key_type": "ec", |
| }) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| rootCert := parseCert(t, resp.Data["certificate"].(string)) |
| |
| _, err = CBWrite(b, s, "roles/local-testing", map[string]interface{}{ |
| "allow_any_name": true, |
| "enforce_hostnames": false, |
| "key_type": "ec", |
| }) |
| require.NoError(t, err) |
| |
| _, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{ |
| "common_name": "testing", |
| "ttl": "1s", |
| }) |
| require.NoError(t, err, "expected issuance to succeed due to shorter ttl than cert ttl") |
| |
| _, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{ |
| "common_name": "testing", |
| }) |
| require.Error(t, err, "expected issuance to fail due to longer default ttl than cert ttl") |
| |
| resp, err = CBPatch(b, s, "issuer/root", map[string]interface{}{ |
| "leaf_not_after_behavior": "permit", |
| }) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| require.NotNil(t, resp.Data) |
| require.Equal(t, resp.Data["leaf_not_after_behavior"], "permit") |
| |
| _, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{ |
| "common_name": "testing", |
| }) |
| require.NoError(t, err, "expected issuance to succeed due to permitted longer TTL") |
| |
| resp, err = CBWrite(b, s, "issuer/root", map[string]interface{}{ |
| "issuer_name": "root", |
| "leaf_not_after_behavior": "truncate", |
| }) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| require.NotNil(t, resp.Data) |
| require.Equal(t, resp.Data["leaf_not_after_behavior"], "truncate") |
| |
| _, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{ |
| "common_name": "testing", |
| }) |
| require.NoError(t, err, "expected issuance to succeed due to truncated ttl") |
| |
| // Sleep until the parent cert expires and the clock rolls over |
| // to the next second. |
| time.Sleep(time.Until(rootCert.NotAfter) + (1500 * time.Millisecond)) |
| |
| resp, err = CBWrite(b, s, "issuer/root", map[string]interface{}{ |
| "issuer_name": "root", |
| "leaf_not_after_behavior": "err", |
| }) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| |
| // Even 1s ttl should now fail. |
| _, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{ |
| "common_name": "testing", |
| "ttl": "1s", |
| }) |
| require.Error(t, err, "expected issuance to fail due to longer default ttl than cert ttl") |
| } |
| |
| func TestSealWrappedStorageConfigured(t *testing.T) { |
| t.Parallel() |
| b, _ := CreateBackendWithStorage(t) |
| wrappedEntries := b.Backend.PathsSpecial.SealWrapStorage |
| |
| // Make sure our legacy bundle is within the list |
| // NOTE: do not convert these test values to constants, we should always have these paths within seal wrap config |
| require.Contains(t, wrappedEntries, "config/ca_bundle", "Legacy bundle missing from seal wrap") |
| // The trailing / is important as it treats the entire folder requiring seal wrapping, not just config/key |
| require.Contains(t, wrappedEntries, "config/key/", "key prefix with trailing / missing from seal wrap.") |
| } |
| |
| func TestBackend_ConfigCA_WithECParams(t *testing.T) { |
| t.Parallel() |
| b, s := CreateBackendWithStorage(t) |
| |
| // Generated key with OpenSSL: |
| // $ openssl ecparam -out p256.key -name prime256v1 -genkey |
| // |
| // Regression test for https://github.com/hashicorp/vault/issues/16667 |
| resp, err := CBWrite(b, s, "config/ca", map[string]interface{}{ |
| "pem_bundle": ` |
| -----BEGIN EC PARAMETERS----- |
| BggqhkjOPQMBBw== |
| -----END EC PARAMETERS----- |
| -----BEGIN EC PRIVATE KEY----- |
| MHcCAQEEINzXthCZdhyV7+wIEBl/ty+ctNsUS99ykTeax6EbYZtvoAoGCCqGSM49 |
| AwEHoUQDQgAE57NX8bR/nDoW8yRgLswoXBQcjHrdyfuHS0gPwki6BNnfunUzryVb |
| 8f22/JWj6fsEF6AOADZlrswKIbR2Es9e/w== |
| -----END EC PRIVATE KEY----- |
| `, |
| }) |
| require.NoError(t, err) |
| require.NotNil(t, resp, "expected ca info") |
| importedKeys := resp.Data["imported_keys"].([]string) |
| importedIssuers := resp.Data["imported_issuers"].([]string) |
| |
| require.Equal(t, len(importedKeys), 1) |
| require.Equal(t, len(importedIssuers), 0) |
| } |
| |
| func TestPerIssuerAIA(t *testing.T) { |
| t.Parallel() |
| b, s := CreateBackendWithStorage(t) |
| |
| // Generating a root without anything should not have AIAs. |
| resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ |
| "common_name": "root example.com", |
| "issuer_name": "root", |
| "key_type": "ec", |
| }) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| rootCert := parseCert(t, resp.Data["certificate"].(string)) |
| require.Empty(t, rootCert.OCSPServer) |
| require.Empty(t, rootCert.IssuingCertificateURL) |
| require.Empty(t, rootCert.CRLDistributionPoints) |
| |
| // Set some local URLs on the issuer. |
| resp, err = CBWrite(b, s, "issuer/default", map[string]interface{}{ |
| "issuing_certificates": []string{"https://google.com"}, |
| }) |
| schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("issuer/default"), logical.UpdateOperation), resp, true) |
| |
| require.NoError(t, err) |
| |
| _, err = CBWrite(b, s, "roles/testing", map[string]interface{}{ |
| "allow_any_name": true, |
| "ttl": "85s", |
| "key_type": "ec", |
| }) |
| require.NoError(t, err) |
| |
| // Issue something with this re-configured issuer. |
| resp, err = CBWrite(b, s, "issuer/default/issue/testing", map[string]interface{}{ |
| "common_name": "localhost.com", |
| }) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| leafCert := parseCert(t, resp.Data["certificate"].(string)) |
| require.Empty(t, leafCert.OCSPServer) |
| require.Equal(t, leafCert.IssuingCertificateURL, []string{"https://google.com"}) |
| require.Empty(t, leafCert.CRLDistributionPoints) |
| |
| // Set global URLs and ensure they don't appear on this issuer's leaf. |
| _, err = CBWrite(b, s, "config/urls", map[string]interface{}{ |
| "issuing_certificates": []string{"https://example.com/ca", "https://backup.example.com/ca"}, |
| "crl_distribution_points": []string{"https://example.com/crl", "https://backup.example.com/crl"}, |
| "ocsp_servers": []string{"https://example.com/ocsp", "https://backup.example.com/ocsp"}, |
| }) |
| require.NoError(t, err) |
| resp, err = CBWrite(b, s, "issuer/default/issue/testing", map[string]interface{}{ |
| "common_name": "localhost.com", |
| }) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| leafCert = parseCert(t, resp.Data["certificate"].(string)) |
| require.Empty(t, leafCert.OCSPServer) |
| require.Equal(t, leafCert.IssuingCertificateURL, []string{"https://google.com"}) |
| require.Empty(t, leafCert.CRLDistributionPoints) |
| |
| // Now come back and remove the local modifications and ensure we get |
| // the defaults again. |
| _, err = CBPatch(b, s, "issuer/default", map[string]interface{}{ |
| "issuing_certificates": []string{}, |
| }) |
| require.NoError(t, err) |
| resp, err = CBWrite(b, s, "issuer/default/issue/testing", map[string]interface{}{ |
| "common_name": "localhost.com", |
| }) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| leafCert = parseCert(t, resp.Data["certificate"].(string)) |
| require.Equal(t, leafCert.IssuingCertificateURL, []string{"https://example.com/ca", "https://backup.example.com/ca"}) |
| require.Equal(t, leafCert.OCSPServer, []string{"https://example.com/ocsp", "https://backup.example.com/ocsp"}) |
| require.Equal(t, leafCert.CRLDistributionPoints, []string{"https://example.com/crl", "https://backup.example.com/crl"}) |
| |
| // Validate that we can set an issuer name and remove it. |
| _, err = CBPatch(b, s, "issuer/default", map[string]interface{}{ |
| "issuer_name": "my-issuer", |
| }) |
| require.NoError(t, err) |
| _, err = CBPatch(b, s, "issuer/default", map[string]interface{}{ |
| "issuer_name": "", |
| }) |
| require.NoError(t, err) |
| } |
| |
| func TestIssuersWithoutCRLBits(t *testing.T) { |
| t.Parallel() |
| b, s := CreateBackendWithStorage(t) |
| |
| // Importing a root without CRL signing bits should work fine. |
| customBundleWithoutCRLBits := ` |
| -----BEGIN CERTIFICATE----- |
| MIIDGTCCAgGgAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhyb290 |
| LW5ldzAeFw0yMjA4MjQxMjEzNTVaFw0yMzA5MDMxMjEzNTVaMBMxETAPBgNVBAMM |
| CHJvb3QtbmV3MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAojTA/Mx7 |
| LVW/Zgn/N4BqZbaF82MrTIBFug3ob7mqycNRlWp4/PH8v37+jYn8e691HUsKjden |
| rDTrO06kiQKiJinAzmlLJvgcazE3aXoh7wSzVG9lFHYvljEmVj+yDbkeaqaCktup |
| skuNjxCoN9BLmKzZIwVCHn92ZHlhN6LI7CNaU3SDJdu7VftWF9Ugzt9FIvI+6Gcn |
| /WNE9FWvZ9o7035rZ+1vvTn7/tgxrj2k3XvD51Kq4tsSbqjnSf3QieXT6E6uvtUE |
| TbPp3xjBElgBCKmeogR1l28rs1aujqqwzZ0B/zOeF8ptaH0aZOIBsVDJR8yTwHzq |
| s34hNdNfKLHzOwIDAQABo3gwdjAdBgNVHQ4EFgQUF4djNmx+1+uJINhZ82pN+7jz |
| H8EwHwYDVR0jBBgwFoAUF4djNmx+1+uJINhZ82pN+7jzH8EwDwYDVR0TAQH/BAUw |
| AwEB/zAOBgNVHQ8BAf8EBAMCAoQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDQYJKoZI |
| hvcNAQELBQADggEBAICQovBz4KLWlLmXeZ2Vf6WfQYyGNgGyJa10XNXtWQ5dM2NU |
| OLAit4x1c2dz+aFocc8ZsX/ikYi/bruT2rsGWqMAGC4at3U4GuaYGO5a6XzMKIDC |
| nxIlbiO+Pn6Xum7fAqUri7+ZNf/Cygmc5sByi3MAAIkszeObUDZFTJL7gEOuXIMT |
| rKIXCINq/U+qc7m9AQ8vKhF1Ddj+dLGLzNQ5j3cKfilPs/wRaYqbMQvnmarX+5Cs |
| k1UL6kWSQsiP3+UWaBlcWkmD6oZ3fIG7c0aMxf7RISq1eTAM9XjH3vMxWQJlS5q3 |
| 2weJ2LYoPe/DwX5CijR0IezapBCrin1BscJMLFQ= |
| -----END CERTIFICATE----- |
| -----BEGIN PRIVATE KEY----- |
| MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCiNMD8zHstVb9m |
| Cf83gGpltoXzYytMgEW6DehvuarJw1GVanj88fy/fv6Nifx7r3UdSwqN16esNOs7 |
| TqSJAqImKcDOaUsm+BxrMTdpeiHvBLNUb2UUdi+WMSZWP7INuR5qpoKS26myS42P |
| EKg30EuYrNkjBUIef3ZkeWE3osjsI1pTdIMl27tV+1YX1SDO30Ui8j7oZyf9Y0T0 |
| Va9n2jvTfmtn7W+9Ofv+2DGuPaTde8PnUqri2xJuqOdJ/dCJ5dPoTq6+1QRNs+nf |
| GMESWAEIqZ6iBHWXbyuzVq6OqrDNnQH/M54Xym1ofRpk4gGxUMlHzJPAfOqzfiE1 |
| 018osfM7AgMBAAECggEAAVd6kZZaN69IZITIc1vHRYa2rlZpKS2JP7c8Vd3Z/4Fz |
| ZZvnJ7LgVAmUYg5WPZ2sOqBNLfKVN/oke5Q0dALgdxYl7dWQIhPjHeRFbZFtjqEV |
| OXZGBniamMO/HSKGWGrqFf7BM/H7AhClUwQgjnzVSz+B+LJJidM+SVys3n1xuDmC |
| EP+iOda+bAHqHv/7oCELQKhLmCvPc9v2fDy+180ttdo8EHuxwVnKiyR/ryKFhSyx |
| K1wgAPQ9jO+V+GESL90rqpX/r501REsIOOpm4orueelHTD4+dnHxvUPqJ++9aYGX |
| 79qBNPPUhxrQI1yoHxwW0cTxW5EqkZ9bT2lSd5rjcQKBgQDNyPBpidkHPrYemQDT |
| RldtS6FiW/jc1It/CRbjU4A6Gi7s3Cda43pEUObKNLeXMyLQaMf4GbDPDX+eh7B8 |
| RkUq0Q/N0H4bn1hbxYSUdgv0j/6czpMo6rLcJHGwOTSpHGsNsxSLL7xlpgzuzqrG |
| FzEgjMA1aD3w8B9+/77AoSLoMQKBgQDJyYMw82+euLYRbR5Wc/SbrWfh2n1Mr2BG |
| pp1ZNYorXE5CL4ScdLcgH1q/b8r5XGwmhMcpeA+geAAaKmk1CGG+gPLoq20c9Q1Y |
| Ykq9tUVJasIkelvbb/SPxyjkJdBwylzcPP14IJBsqQM0be+yVqLJJVHSaoKhXZcl |
| IW2xgCpjKwKBgFpeX5U5P+F6nKebMU2WmlYY3GpBUWxIummzKCX0SV86mFjT5UR4 |
| mPzfOjqaI/V2M1eqbAZ74bVLjDumAs7QXReMb5BGetrOgxLqDmrT3DQt9/YMkXtq |
| ddlO984XkRSisjB18BOfhvBsl0lX4I7VKHHO3amWeX0RNgOjc7VMDfRBAoGAWAQH |
| r1BfvZHACLXZ58fISCdJCqCsysgsbGS8eW77B5LJp+DmLQBT6DUE9j+i/0Wq/ton |
| rRTrbAkrsj4RicpQKDJCwe4UN+9DlOu6wijRQgbJC/Q7IOoieJxcX7eGxcve2UnZ |
| HY7GsD7AYRwa02UquCYJHIjM1enmxZFhMW1AD+UCgYEAm4jdNz5e4QjA4AkNF+cB |
| ZenrAZ0q3NbTyiSsJEAtRe/c5fNFpmXo3mqgCannarREQYYDF0+jpSoTUY8XAc4q |
| wL7EZNzwxITLqBnnHQbdLdAvYxB43kvWTy+JRK8qY9LAMCCFeDoYwXkWV4Wkx/b0 |
| TgM7RZnmEjNdeaa4M52o7VY= |
| -----END PRIVATE KEY----- |
| ` |
| resp, err := CBWrite(b, s, "issuers/import/bundle", map[string]interface{}{ |
| "pem_bundle": customBundleWithoutCRLBits, |
| }) |
| schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("issuers/import/bundle"), logical.UpdateOperation), resp, true) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| require.NotEmpty(t, resp.Data) |
| require.NotEmpty(t, resp.Data["imported_issuers"]) |
| require.NotEmpty(t, resp.Data["imported_keys"]) |
| require.NotEmpty(t, resp.Data["mapping"]) |
| |
| // Shouldn't have crl-signing on the newly imported issuer's usage. |
| resp, err = CBRead(b, s, "issuer/default") |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| require.NotEmpty(t, resp.Data) |
| require.NotEmpty(t, resp.Data["usage"]) |
| require.NotContains(t, resp.Data["usage"], "crl-signing") |
| |
| // Modifying to set CRL should fail. |
| resp, err = CBPatch(b, s, "issuer/default", map[string]interface{}{ |
| "usage": "issuing-certificates,crl-signing", |
| }) |
| require.Error(t, err) |
| require.True(t, resp.IsError()) |
| |
| // Modifying to set issuing-certificates and ocsp-signing should succeed. |
| resp, err = CBPatch(b, s, "issuer/default", map[string]interface{}{ |
| "usage": "issuing-certificates,ocsp-signing", |
| }) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| require.NotEmpty(t, resp.Data) |
| require.NotEmpty(t, resp.Data["usage"]) |
| require.NotContains(t, resp.Data["usage"], "crl-signing") |
| } |
| |
| func TestBackend_IfModifiedSinceHeaders(t *testing.T) { |
| t.Parallel() |
| coreConfig := &vault.CoreConfig{ |
| LogicalBackends: map[string]logical.Factory{ |
| "pki": Factory, |
| }, |
| } |
| cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ |
| HandlerFunc: vaulthttp.Handler, |
| RequestResponseCallback: schema.ResponseValidatingCallback(t), |
| }) |
| cluster.Start() |
| defer cluster.Cleanup() |
| client := cluster.Cores[0].Client |
| |
| // Mount PKI. |
| err := client.Sys().Mount("pki", &api.MountInput{ |
| Type: "pki", |
| Config: api.MountConfigInput{ |
| DefaultLeaseTTL: "16h", |
| MaxLeaseTTL: "60h", |
| // Required to allow the header to be passed through. |
| PassthroughRequestHeaders: []string{"if-modified-since"}, |
| AllowedResponseHeaders: []string{"Last-Modified"}, |
| }, |
| }) |
| require.NoError(t, err) |
| |
| // Get a time before CA generation. Subtract two seconds to ensure |
| // the value in the seconds field is different than the time the CA |
| // is actually generated at. |
| beforeOldCAGeneration := time.Now().Add(-2 * time.Second) |
| |
| // Generate an internal CA. This one is the default. |
| resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ |
| "ttl": "40h", |
| "common_name": "Root X1", |
| "key_type": "ec", |
| "issuer_name": "old-root", |
| }) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| require.NotNil(t, resp.Data) |
| require.NotEmpty(t, resp.Data["certificate"]) |
| |
| // CA is generated, but give a grace window. |
| afterOldCAGeneration := time.Now().Add(2 * time.Second) |
| |
| // When you _save_ headers, client returns a copy. But when you go to |
| // reset them, it doesn't create a new copy (and instead directly |
| // assigns). This means we have to continually refresh our view of the |
| // last headers, otherwise the headers added after the last set operation |
| // leak into this copy... Yuck! |
| lastHeaders := client.Headers() |
| for _, path := range []string{"pki/cert/ca", "pki/cert/crl", "pki/issuer/default/json", "pki/issuer/old-root/json", "pki/issuer/old-root/crl", "pki/cert/delta-crl", "pki/issuer/old-root/crl/delta"} { |
| t.Logf("path: %v", path) |
| field := "certificate" |
| if strings.HasPrefix(path, "pki/issuer") && strings.Contains(path, "/crl") { |
| field = "crl" |
| } |
| |
| // Reading the CA should work, without a header. |
| resp, err := client.Logical().Read(path) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| require.NotNil(t, resp.Data) |
| require.NotEmpty(t, resp.Data[field]) |
| |
| // Ensure that the CA is returned correctly if we give it the old time. |
| client.AddHeader("If-Modified-Since", beforeOldCAGeneration.Format(time.RFC1123)) |
| resp, err = client.Logical().Read(path) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| require.NotNil(t, resp.Data) |
| require.NotEmpty(t, resp.Data[field]) |
| client.SetHeaders(lastHeaders) |
| lastHeaders = client.Headers() |
| |
| // Ensure that the CA is elided if we give it the present time (plus a |
| // grace window). |
| client.AddHeader("If-Modified-Since", afterOldCAGeneration.Format(time.RFC1123)) |
| t.Logf("headers: %v", client.Headers()) |
| resp, err = client.Logical().Read(path) |
| require.NoError(t, err) |
| require.Nil(t, resp) |
| client.SetHeaders(lastHeaders) |
| lastHeaders = client.Headers() |
| } |
| |
| // Wait three seconds. This ensures we have adequate grace period |
| // to distinguish the two cases, even with grace periods. |
| time.Sleep(3 * time.Second) |
| |
| // Generating a second root. This one isn't the default. |
| beforeNewCAGeneration := time.Now().Add(-2 * time.Second) |
| |
| // Generate an internal CA. This one is the default. |
| _, err = client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ |
| "ttl": "40h", |
| "common_name": "Root X1", |
| "key_type": "ec", |
| "issuer_name": "new-root", |
| }) |
| require.NoError(t, err) |
| |
| // As above. |
| afterNewCAGeneration := time.Now().Add(2 * time.Second) |
| |
| // New root isn't the default, so it has fewer paths. |
| for _, path := range []string{"pki/issuer/new-root/json", "pki/issuer/new-root/crl", "pki/issuer/new-root/crl/delta"} { |
| t.Logf("path: %v", path) |
| field := "certificate" |
| if strings.HasPrefix(path, "pki/issuer") && strings.Contains(path, "/crl") { |
| field = "crl" |
| } |
| |
| // Reading the CA should work, without a header. |
| resp, err := client.Logical().Read(path) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| require.NotNil(t, resp.Data) |
| require.NotEmpty(t, resp.Data[field]) |
| |
| // Ensure that the CA is returned correctly if we give it the old time. |
| client.AddHeader("If-Modified-Since", beforeNewCAGeneration.Format(time.RFC1123)) |
| resp, err = client.Logical().Read(path) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| require.NotNil(t, resp.Data) |
| require.NotEmpty(t, resp.Data[field]) |
| client.SetHeaders(lastHeaders) |
| lastHeaders = client.Headers() |
| |
| // Ensure that the CA is elided if we give it the present time (plus a |
| // grace window). |
| client.AddHeader("If-Modified-Since", afterNewCAGeneration.Format(time.RFC1123)) |
| t.Logf("headers: %v", client.Headers()) |
| resp, err = client.Logical().Read(path) |
| require.NoError(t, err) |
| require.Nil(t, resp) |
| client.SetHeaders(lastHeaders) |
| lastHeaders = client.Headers() |
| } |
| |
| // Wait three seconds. This ensures we have adequate grace period |
| // to distinguish the two cases, even with grace periods. |
| time.Sleep(3 * time.Second) |
| |
| // Now swap the default issuers around. |
| _, err = client.Logical().Write("pki/config/issuers", map[string]interface{}{ |
| "default": "new-root", |
| }) |
| require.NoError(t, err) |
| |
| // Reading both with the last modified date should return new values. |
| for _, path := range []string{"pki/cert/ca", "pki/cert/crl", "pki/issuer/default/json", "pki/issuer/old-root/json", "pki/issuer/new-root/json", "pki/issuer/old-root/crl", "pki/issuer/new-root/crl", "pki/cert/delta-crl", "pki/issuer/old-root/crl/delta", "pki/issuer/new-root/crl/delta"} { |
| t.Logf("path: %v", path) |
| field := "certificate" |
| if strings.HasPrefix(path, "pki/issuer") && strings.Contains(path, "/crl") { |
| field = "crl" |
| } |
| |
| // Ensure that the CA is returned correctly if we give it the old time. |
| client.AddHeader("If-Modified-Since", afterOldCAGeneration.Format(time.RFC1123)) |
| resp, err = client.Logical().Read(path) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| require.NotNil(t, resp.Data) |
| require.NotEmpty(t, resp.Data[field]) |
| client.SetHeaders(lastHeaders) |
| lastHeaders = client.Headers() |
| |
| // Ensure that the CA is returned correctly if we give it the old time. |
| client.AddHeader("If-Modified-Since", afterNewCAGeneration.Format(time.RFC1123)) |
| resp, err = client.Logical().Read(path) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| require.NotNil(t, resp.Data) |
| require.NotEmpty(t, resp.Data[field]) |
| client.SetHeaders(lastHeaders) |
| lastHeaders = client.Headers() |
| } |
| |
| // Wait for things to settle, record the present time, and wait for the |
| // clock to definitely tick over again. |
| time.Sleep(2 * time.Second) |
| preRevocationTimestamp := time.Now() |
| time.Sleep(2 * time.Second) |
| |
| // The above tests should say everything is cached. |
| for _, path := range []string{"pki/cert/ca", "pki/cert/crl", "pki/issuer/default/json", "pki/issuer/old-root/json", "pki/issuer/new-root/json", "pki/issuer/old-root/crl", "pki/issuer/new-root/crl", "pki/cert/delta-crl", "pki/issuer/old-root/crl/delta", "pki/issuer/new-root/crl/delta"} { |
| t.Logf("path: %v", path) |
| |
| // Ensure that the CA is returned correctly if we give it the new time. |
| client.AddHeader("If-Modified-Since", preRevocationTimestamp.Format(time.RFC1123)) |
| resp, err = client.Logical().Read(path) |
| require.NoError(t, err) |
| require.Nil(t, resp) |
| client.SetHeaders(lastHeaders) |
| lastHeaders = client.Headers() |
| } |
| |
| // We could generate some leaves and verify the revocation updates the |
| // CRL. But, revoking the issuer behaves the same, so let's do that |
| // instead. |
| _, err = client.Logical().Write("pki/issuer/old-root/revoke", map[string]interface{}{}) |
| require.NoError(t, err) |
| |
| // CA should still be valid. |
| for _, path := range []string{"pki/cert/ca", "pki/issuer/default/json", "pki/issuer/old-root/json", "pki/issuer/new-root/json"} { |
| t.Logf("path: %v", path) |
| |
| // Ensure that the CA is returned correctly if we give it the old time. |
| client.AddHeader("If-Modified-Since", preRevocationTimestamp.Format(time.RFC1123)) |
| resp, err = client.Logical().Read(path) |
| require.NoError(t, err) |
| require.Nil(t, resp) |
| client.SetHeaders(lastHeaders) |
| lastHeaders = client.Headers() |
| } |
| |
| // CRL should be invalidated |
| for _, path := range []string{"pki/cert/crl", "pki/issuer/old-root/crl", "pki/issuer/new-root/crl", "pki/cert/delta-crl", "pki/issuer/old-root/crl/delta", "pki/issuer/new-root/crl/delta"} { |
| t.Logf("path: %v", path) |
| field := "certificate" |
| if strings.HasPrefix(path, "pki/issuer") && strings.Contains(path, "/crl") { |
| field = "crl" |
| } |
| |
| client.AddHeader("If-Modified-Since", preRevocationTimestamp.Format(time.RFC1123)) |
| resp, err = client.Logical().Read(path) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| require.NotNil(t, resp.Data) |
| require.NotEmpty(t, resp.Data[field]) |
| client.SetHeaders(lastHeaders) |
| lastHeaders = client.Headers() |
| } |
| |
| // If we send some time in the future, everything should be cached again! |
| futureTime := time.Now().Add(30 * time.Second) |
| for _, path := range []string{"pki/cert/ca", "pki/cert/crl", "pki/issuer/default/json", "pki/issuer/old-root/json", "pki/issuer/new-root/json", "pki/issuer/old-root/crl", "pki/issuer/new-root/crl", "pki/cert/delta-crl", "pki/issuer/old-root/crl/delta", "pki/issuer/new-root/crl/delta"} { |
| t.Logf("path: %v", path) |
| |
| // Ensure that the CA is returned correctly if we give it the new time. |
| client.AddHeader("If-Modified-Since", futureTime.Format(time.RFC1123)) |
| resp, err = client.Logical().Read(path) |
| require.NoError(t, err) |
| require.Nil(t, resp) |
| client.SetHeaders(lastHeaders) |
| lastHeaders = client.Headers() |
| } |
| |
| beforeThreeWaySwap := time.Now().Add(-2 * time.Second) |
| |
| // Now, do a three-way swap of names (old->tmp; new->old; tmp->new). This |
| // should result in all names/CRLs being invalidated. |
| _, err = client.Logical().JSONMergePatch(ctx, "pki/issuer/old-root", map[string]interface{}{ |
| "issuer_name": "tmp-root", |
| }) |
| require.NoError(t, err) |
| _, err = client.Logical().JSONMergePatch(ctx, "pki/issuer/new-root", map[string]interface{}{ |
| "issuer_name": "old-root", |
| }) |
| require.NoError(t, err) |
| _, err = client.Logical().JSONMergePatch(ctx, "pki/issuer/tmp-root", map[string]interface{}{ |
| "issuer_name": "new-root", |
| }) |
| require.NoError(t, err) |
| |
| afterThreeWaySwap := time.Now().Add(2 * time.Second) |
| |
| for _, path := range []string{"pki/cert/ca", "pki/cert/crl", "pki/issuer/default/json", "pki/issuer/old-root/json", "pki/issuer/new-root/json", "pki/issuer/old-root/crl", "pki/issuer/new-root/crl", "pki/cert/delta-crl", "pki/issuer/old-root/crl/delta", "pki/issuer/new-root/crl/delta"} { |
| t.Logf("path: %v", path) |
| field := "certificate" |
| if strings.HasPrefix(path, "pki/issuer") && strings.Contains(path, "/crl") { |
| field = "crl" |
| } |
| |
| // Ensure that the CA is returned if we give it the pre-update time. |
| client.AddHeader("If-Modified-Since", beforeThreeWaySwap.Format(time.RFC1123)) |
| resp, err = client.Logical().Read(path) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| require.NotNil(t, resp.Data) |
| require.NotEmpty(t, resp.Data[field]) |
| client.SetHeaders(lastHeaders) |
| lastHeaders = client.Headers() |
| |
| // Ensure that the CA is elided correctly if we give it the after time. |
| client.AddHeader("If-Modified-Since", afterThreeWaySwap.Format(time.RFC1123)) |
| resp, err = client.Logical().Read(path) |
| require.NoError(t, err) |
| require.Nil(t, resp) |
| client.SetHeaders(lastHeaders) |
| lastHeaders = client.Headers() |
| } |
| |
| // Finally, rebuild the delta CRL and ensure that only that is |
| // invalidated. We first need to enable it though, and wait for |
| // all CRLs to rebuild. |
| _, err = client.Logical().Write("pki/config/crl", map[string]interface{}{ |
| "auto_rebuild": true, |
| "enable_delta": true, |
| }) |
| require.NoError(t, err) |
| time.Sleep(4 * time.Second) |
| beforeDeltaRotation := time.Now().Add(-2 * time.Second) |
| |
| resp, err = client.Logical().Read("pki/crl/rotate-delta") |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| require.NotNil(t, resp.Data) |
| require.Equal(t, resp.Data["success"], true) |
| |
| afterDeltaRotation := time.Now().Add(2 * time.Second) |
| |
| for _, path := range []string{"pki/cert/ca", "pki/cert/crl", "pki/issuer/default/json", "pki/issuer/old-root/json", "pki/issuer/new-root/json", "pki/issuer/old-root/crl", "pki/issuer/new-root/crl"} { |
| t.Logf("path: %v", path) |
| |
| for _, when := range []time.Time{beforeDeltaRotation, afterDeltaRotation} { |
| client.AddHeader("If-Modified-Since", when.Format(time.RFC1123)) |
| resp, err = client.Logical().Read(path) |
| require.NoError(t, err) |
| require.Nil(t, resp) |
| client.SetHeaders(lastHeaders) |
| lastHeaders = client.Headers() |
| } |
| } |
| |
| for _, path := range []string{"pki/cert/delta-crl", "pki/issuer/old-root/crl/delta", "pki/issuer/new-root/crl/delta"} { |
| t.Logf("path: %v", path) |
| field := "certificate" |
| if strings.HasPrefix(path, "pki/issuer") && strings.Contains(path, "/crl") { |
| field = "crl" |
| } |
| |
| // Ensure that the CRL is present if we give it the pre-update time. |
| client.AddHeader("If-Modified-Since", beforeDeltaRotation.Format(time.RFC1123)) |
| resp, err = client.Logical().Read(path) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| require.NotNil(t, resp.Data) |
| require.NotEmpty(t, resp.Data[field]) |
| client.SetHeaders(lastHeaders) |
| lastHeaders = client.Headers() |
| |
| client.AddHeader("If-Modified-Since", afterDeltaRotation.Format(time.RFC1123)) |
| resp, err = client.Logical().Read(path) |
| require.NoError(t, err) |
| require.Nil(t, resp) |
| client.SetHeaders(lastHeaders) |
| lastHeaders = client.Headers() |
| } |
| } |
| |
| func TestBackend_InitializeCertificateCounts(t *testing.T) { |
| t.Parallel() |
| b, s := CreateBackendWithStorage(t) |
| ctx := context.Background() |
| |
| // Set up an Issuer and Role |
| // We need a root certificate to write/revoke certificates with |
| resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ |
| "common_name": "myvault.com", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if resp == nil { |
| t.Fatal("expected ca info") |
| } |
| |
| // Create a role |
| _, err = CBWrite(b, s, "roles/example", map[string]interface{}{ |
| "allowed_domains": "myvault.com", |
| "allow_bare_domains": true, |
| "allow_subdomains": true, |
| "max_ttl": "2h", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Put certificates A, B, C, D, E in backend |
| var certificates []string = []string{"a", "b", "c", "d", "e"} |
| serials := make([]string, 5) |
| for i, cn := range certificates { |
| resp, err = CBWrite(b, s, "issue/example", map[string]interface{}{ |
| "common_name": cn + ".myvault.com", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| serials[i] = resp.Data["serial_number"].(string) |
| } |
| |
| // Turn on certificate counting: |
| CBWrite(b, s, "config/auto-tidy", map[string]interface{}{ |
| "maintain_stored_certificate_counts": true, |
| "publish_stored_certificate_count_metrics": false, |
| }) |
| // Assert initialize from clean is correct: |
| b.initializeStoredCertificateCounts(ctx) |
| |
| // Revoke certificates A + B |
| revocations := serials[0:2] |
| for _, key := range revocations { |
| resp, err = CBWrite(b, s, "revoke", map[string]interface{}{ |
| "serial_number": key, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| if b.certCount.Load() != 6 { |
| t.Fatalf("Failed to count six certificates root,A,B,C,D,E, instead counted %d certs", b.certCount.Load()) |
| } |
| if b.revokedCertCount.Load() != 2 { |
| t.Fatalf("Failed to count two revoked certificates A+B, instead counted %d certs", b.revokedCertCount.Load()) |
| } |
| |
| // Simulates listing while initialize in progress, by "restarting it" |
| b.certCount.Store(0) |
| b.revokedCertCount.Store(0) |
| b.certsCounted.Store(false) |
| |
| // Revoke certificates C, D |
| dirtyRevocations := serials[2:4] |
| for _, key := range dirtyRevocations { |
| resp, err = CBWrite(b, s, "revoke", map[string]interface{}{ |
| "serial_number": key, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| // Put certificates F, G in the backend |
| dirtyCertificates := []string{"f", "g"} |
| for _, cn := range dirtyCertificates { |
| resp, err = CBWrite(b, s, "issue/example", map[string]interface{}{ |
| "common_name": cn + ".myvault.com", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| // Run initialize |
| b.initializeStoredCertificateCounts(ctx) |
| |
| // Test certificate count |
| if b.certCount.Load() != 8 { |
| t.Fatalf("Failed to initialize count of certificates root, A,B,C,D,E,F,G counted %d certs", b.certCount.Load()) |
| } |
| |
| if b.revokedCertCount.Load() != 4 { |
| t.Fatalf("Failed to count revoked certificates A,B,C,D counted %d certs", b.revokedCertCount.Load()) |
| } |
| |
| return |
| } |
| |
| // Verify that our default values are consistent when creating an issuer and when we do an |
| // empty POST update to it. This will hopefully identify if we have different default values |
| // for fields across the two APIs. |
| func TestBackend_VerifyIssuerUpdateDefaultsMatchCreation(t *testing.T) { |
| t.Parallel() |
| b, s := CreateBackendWithStorage(t) |
| |
| resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ |
| "common_name": "myvault.com", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed generating root issuer") |
| |
| resp, err = CBRead(b, s, "issuer/default") |
| requireSuccessNonNilResponse(t, resp, err, "failed reading default issuer") |
| preUpdateValues := resp.Data |
| |
| // This field gets reset during issuer update to the empty string |
| // (meaning Go will auto-detect the rev-sig-algo). |
| preUpdateValues["revocation_signature_algorithm"] = "" |
| |
| resp, err = CBWrite(b, s, "issuer/default", map[string]interface{}{}) |
| requireSuccessNonNilResponse(t, resp, err, "failed updating default issuer with no values") |
| |
| resp, err = CBRead(b, s, "issuer/default") |
| requireSuccessNonNilResponse(t, resp, err, "failed reading default issuer") |
| postUpdateValues := resp.Data |
| |
| require.Equal(t, preUpdateValues, postUpdateValues, |
| "A value was updated based on the empty update of an issuer, "+ |
| "most likely we have a different set of field parameters across create and update of issuers.") |
| } |
| |
| func TestBackend_VerifyPSSKeysIssuersFailImport(t *testing.T) { |
| t.Parallel() |
| b, s := CreateBackendWithStorage(t) |
| |
| // PKCS8 parsing fails on this key due to rsaPSS OID |
| rsaOIDKey := ` |
| -----BEGIN PRIVATE KEY----- |
| MIIEugIBADALBgkqhkiG9w0BAQoEggSmMIIEogIBAAKCAQEAtN0/NPuJHLuyEdBr |
| tUikXoXOV741XZcNvLAIVBIqDA0ege2gXt9A15FGUI4X3u6kT16Fl6MRdtUZ/qNS |
| Vs15nK9A1PI/AVekMgTVFTnoCzs550CKN8iRk9Om+lwHimpyXxKkFW69v8fsXwKE |
| Bsz69jjT7HV9VZQ7fQhmE79brAMuwKP1fUQKdHq5OBKtQ7Cl3Gmipp0izCsVuQIE |
| kBHvT3UUgyaSp2n+FONpOiyuBoYUH5tVEv9sZzBqSsrYBJYF+GvfnFy9AcTdqRe2 |
| VX2SjjWjDF84T30OBA798gIFIPwu9R4OjWOlPeh2bo2kGeo3AITjwFZ28m7kS7kc |
| OtvHpwIDAQABAoIBAFQxmjbj0RQbG+3HBBzD0CBgUYnu9ZC3vKFVoMriGci6YrVB |
| FSKU8u5mpkDhpKMWnE6GRdItCvgyg4NSLAZUaIRT4O5ARqwtTDYsobTb2/U+gNnx |
| 5WXKbFpQcK6jIK+ClfNEDjYb8yDPxG0GEsfHrBvqoFy25L1t37N4sWwH7HjJyZIe |
| Hbqx4NVDur9qgqaUwkfSeufn4ycHqFtkzKNzCUarDkST9cxE6/1AKfhl09PPuMEa |
| lAY2JLiEplQL5sh9cxG5FObJbutJo5EIhR2OdM0VcPf0MTD9LXKRoGR3SNlG7IlS |
| llJzBjlh4J1ByMX32btKMHzEvlhyrMI90E1SEGECgYEAx1yDQWe4/b1MBqCxA3d0 |
| 20dDmUHSRQFhkd/Mzkl5dPzRkG42W3ryNbMKdeuL0ZgK9AhfaLCjcj1i+44O7dHb |
| qBTVwfRrer2uoQVCqqJ6z8PGxPJJxTaqh9QuJxkoQ0i43ZNPcjc2M2sWLn+lkkdE |
| MaGMiyrmjIQEC6tmgCtZ1VUCgYEA6D9xoT9VuAnQjDvW2tO5N2U2H/8ZyRd1pC3z |
| H1CzjwShhxsP4YOUaVdw59K95JL4SMxSmpRrhthlW3cRaiT/exBcXLEvz0Qu0OhW |
| a6155ZFjK3UaLDKlwvmtuoAsuAFqX084LO0B1oxvUJESgyPncQ36fv2lZGV7A66z |
| Uo+BKQsCgYB2yGBMMAjA5nDN4iCV+C7gF+3m+pjWFKSVzcqxfoWndptGeuRYTUDT |
| TgIFkHqWPwkHrZVrQxOflYPMbi/m8wr1crSKA5+mWi4aMpAuKvERqYxc/B+IKbIh |
| jAKTuSGMNWAwZP0JCGx65mso+VUleuDe0Wpz4PPM9TuT2GQSKcI0oQKBgHAHcouC |
| npmo+lU65DgoWzaydrpWdpy+2Tt6AsW/Su4ZIMWoMy/oJaXuzQK2cG0ay/NpxArW |
| v0uLhNDrDZZzBF3blYIM4nALhr205UMJqjwntnuXACoDwFvdzoShIXEdFa+l6gYZ |
| yYIxudxWLmTd491wDb5GIgrcvMsY8V1I5dfjAoGAM9g2LtdqgPgK33dCDtZpBm8m |
| y4ri9PqHxnpps9WJ1dO6MW/YbW+a7vbsmNczdJ6XNLEfy2NWho1dw3xe7ztFVDjF |
| cWNUzs1+/6aFsi41UX7EFn3zAFhQUPxT59hXspuWuKbRAWc5fMnxbCfI/Cr8wTLJ |
| E/0kiZ4swUMyI4tYSbM= |
| -----END PRIVATE KEY----- |
| ` |
| _, err := CBWrite(b, s, "issuers/import/bundle", map[string]interface{}{ |
| "pem_bundle": rsaOIDKey, |
| }) |
| require.Error(t, err, "expected error importing PKCS8 rsaPSS OID key") |
| |
| _, err = CBWrite(b, s, "keys/import", map[string]interface{}{ |
| "key": rsaOIDKey, |
| }) |
| require.Error(t, err, "expected error importing PKCS8 rsaPSS OID key") |
| |
| // Importing a cert with rsaPSS OID should also fail |
| rsaOIDCert := ` |
| -----BEGIN CERTIFICATE----- |
| MIIDfjCCAjGgAwIBAgIBATBCBgkqhkiG9w0BAQowNaAPMA0GCWCGSAFlAwQCAQUA |
| oRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQCAQUAogQCAgDeMBMxETAPBgNVBAMM |
| CHJvb3Qtb2xkMB4XDTIyMDkxNjE0MDEwM1oXDTIzMDkyNjE0MDEwM1owEzERMA8G |
| A1UEAwwIcm9vdC1vbGQwggEgMAsGCSqGSIb3DQEBCgOCAQ8AMIIBCgKCAQEAtN0/ |
| NPuJHLuyEdBrtUikXoXOV741XZcNvLAIVBIqDA0ege2gXt9A15FGUI4X3u6kT16F |
| l6MRdtUZ/qNSVs15nK9A1PI/AVekMgTVFTnoCzs550CKN8iRk9Om+lwHimpyXxKk |
| FW69v8fsXwKEBsz69jjT7HV9VZQ7fQhmE79brAMuwKP1fUQKdHq5OBKtQ7Cl3Gmi |
| pp0izCsVuQIEkBHvT3UUgyaSp2n+FONpOiyuBoYUH5tVEv9sZzBqSsrYBJYF+Gvf |
| nFy9AcTdqRe2VX2SjjWjDF84T30OBA798gIFIPwu9R4OjWOlPeh2bo2kGeo3AITj |
| wFZ28m7kS7kcOtvHpwIDAQABo3UwczAdBgNVHQ4EFgQUVGkTAUJ8inxIVGBlfxf4 |
| cDhRSnowHwYDVR0jBBgwFoAUVGkTAUJ8inxIVGBlfxf4cDhRSnowDAYDVR0TBAUw |
| AwEB/zAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwEwQgYJKoZI |
| hvcNAQEKMDWgDzANBglghkgBZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglghkgB |
| ZQMEAgEFAKIEAgIA3gOCAQEAQZ3iQ3NjvS4FYJ5WG41huZI0dkvNFNan+ZYWlYHJ |
| MIQhbFogb/UQB0rlsuldG0+HF1RDXoYNuThfzt5hiBWYEtMBNurezvnOn4DF0hrl |
| Uk3sBVnvTalVXg+UVjqh9hBGB75JYJl6a5Oa2Zrq++4qGNwjd0FqgnoXzqS5UGuB |
| TJL8nlnXPuOIK3VHoXEy7l9GtvEzKcys0xa7g1PYpaJ5D2kpbBJmuQGmU6CDcbP+ |
| m0hI4QDfVfHtnBp2VMCvhj0yzowtwF4BFIhv4EXZBU10mzxVj0zyKKft9++X8auH |
| nebuK22ZwzbPe4NhOvAdfNDElkrrtGvTnzkDB7ezPYjelA== |
| -----END CERTIFICATE----- |
| ` |
| _, err = CBWrite(b, s, "issuers/import/bundle", map[string]interface{}{ |
| "pem_bundle": rsaOIDCert, |
| }) |
| require.Error(t, err, "expected error importing PKCS8 rsaPSS OID cert") |
| |
| _, err = CBWrite(b, s, "issuers/import/bundle", map[string]interface{}{ |
| "pem_bundle": rsaOIDKey + "\n" + rsaOIDCert, |
| }) |
| require.Error(t, err, "expected error importing PKCS8 rsaPSS OID key+cert") |
| |
| _, err = CBWrite(b, s, "issuers/import/bundle", map[string]interface{}{ |
| "pem_bundle": rsaOIDCert + "\n" + rsaOIDKey, |
| }) |
| require.Error(t, err, "expected error importing PKCS8 rsaPSS OID cert+key") |
| |
| // After all these errors, we should have zero issuers and keys. |
| resp, err := CBList(b, s, "issuers") |
| require.NoError(t, err) |
| require.Equal(t, nil, resp.Data["keys"]) |
| |
| resp, err = CBList(b, s, "keys") |
| require.NoError(t, err) |
| require.Equal(t, nil, resp.Data["keys"]) |
| |
| // If we create a new PSS root, we should be able to issue an intermediate |
| // under it. |
| resp, err = CBWrite(b, s, "root/generate/exported", map[string]interface{}{ |
| "use_pss": "true", |
| "common_name": "root x1 - pss", |
| "key_type": "ec", |
| }) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| require.NotNil(t, resp.Data) |
| require.NotEmpty(t, resp.Data["certificate"]) |
| require.NotEmpty(t, resp.Data["private_key"]) |
| |
| resp, err = CBWrite(b, s, "intermediate/generate/exported", map[string]interface{}{ |
| "use_pss": "true", |
| "common_name": "int x1 - pss", |
| "key_type": "ec", |
| }) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| require.NotNil(t, resp.Data) |
| require.NotEmpty(t, resp.Data["csr"]) |
| require.NotEmpty(t, resp.Data["private_key"]) |
| |
| resp, err = CBWrite(b, s, "issuer/default/sign-intermediate", map[string]interface{}{ |
| "use_pss": "true", |
| "common_name": "int x1 - pss", |
| "csr": resp.Data["csr"].(string), |
| }) |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| require.NotNil(t, resp.Data) |
| require.NotEmpty(t, resp.Data["certificate"]) |
| |
| resp, err = CBWrite(b, s, "issuers/import/bundle", map[string]interface{}{ |
| "pem_bundle": resp.Data["certificate"].(string), |
| }) |
| require.NoError(t, err) |
| |
| // Finally, if we were to take an rsaPSS OID'd CSR and use it against this |
| // mount, it will fail. |
| _, err = CBWrite(b, s, "roles/testing", map[string]interface{}{ |
| "allow_any_name": true, |
| "ttl": "85s", |
| "key_type": "any", |
| }) |
| require.NoError(t, err) |
| |
| // Issuing a leaf from a CSR with rsaPSS OID should fail... |
| rsaOIDCSR := `-----BEGIN CERTIFICATE REQUEST----- |
| MIICkTCCAUQCAQAwGTEXMBUGA1UEAwwOcmFuY2hlci5teS5vcmcwggEgMAsGCSqG |
| SIb3DQEBCgOCAQ8AMIIBCgKCAQEAtzHuGEUK55lXI08yp9DXoye9yCZbkJZO+Hej |
| 1TWGEkbX4hzauRJeNp2+wn8xU5y8ITjWSIXEVDHeezosLCSy0Y2QT7/V45zWPUYY |
| ld0oUnPiwsb9CPFlBRFnX3dO9SS5MONIrNCJGKXmLdF3lgSl8zPT6J/hWM+JBjHO |
| hBzK6L8IYwmcEujrQfnOnOztzgMEBJtWG8rnI8roz1adpczTddDKGymh2QevjhlL |
| X9CLeYSSQZInOMsgaDYl98Hn00K5x0CBp8ADzzXtaPSQ9nsnihN8VvZ/wHw6YbBS |
| BSHa6OD+MrYnw3Sao6/YgBRNT2glIX85uro4ARW9zGB9/748dwIDAQABoAAwQgYJ |
| KoZIhvcNAQEKMDWgDzANBglghkgBZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglg |
| hkgBZQMEAgEFAKIEAgIA3gOCAQEARGAa0HiwzWCpvAdLOVc4/srEyOYFZPLbtv+Y |
| ezZIaUBNaWhOvkunqpa48avmcbGlji7r6fxJ5sT28lHt7ODWcJfn1XPAnqesXErm |
| EBuOIhCv6WiwVyGeTVynuHYkHyw3rIL/zU7N8+zIFV2G2M1UAv5D/eyh/74cr9Of |
| +nvm9jAbkHix8UwOBCFY2LLNl6bXvbIeJEdDOEtA9UmDXs8QGBg4lngyqcE2Z7rz |
| +5N/x4guMk2FqblbFGiCc5fLB0Gp6lFFOqhX9Q8nLJ6HteV42xGJUUtsFpppNCRm |
| 82dGIH2PTbXZ0k7iAAwLaPjzOv1v58Wq90o35d4iEsOfJ8v98Q== |
| -----END CERTIFICATE REQUEST-----` |
| |
| _, err = CBWrite(b, s, "issuer/default/sign/testing", map[string]interface{}{ |
| "common_name": "example.com", |
| "csr": rsaOIDCSR, |
| }) |
| require.Error(t, err) |
| |
| _, err = CBWrite(b, s, "issuer/default/sign-verbatim", map[string]interface{}{ |
| "common_name": "example.com", |
| "use_pss": true, |
| "csr": rsaOIDCSR, |
| }) |
| require.Error(t, err) |
| |
| _, err = CBWrite(b, s, "issuer/default/sign-intermediate", map[string]interface{}{ |
| "common_name": "faulty x1 - pss", |
| "use_pss": true, |
| "csr": rsaOIDCSR, |
| }) |
| require.Error(t, err) |
| |
| // Vault has a weird API for signing self-signed certificates. Ensure |
| // that doesn't accept rsaPSS OID'd certificates either. |
| _, err = CBWrite(b, s, "issuer/default/sign-self-issued", map[string]interface{}{ |
| "use_pss": true, |
| "certificate": rsaOIDCert, |
| }) |
| require.Error(t, err) |
| |
| // Issuing a regular leaf should succeed. |
| _, err = CBWrite(b, s, "roles/testing", map[string]interface{}{ |
| "allow_any_name": true, |
| "ttl": "85s", |
| "key_type": "rsa", |
| "use_pss": "true", |
| }) |
| require.NoError(t, err) |
| |
| resp, err = CBWrite(b, s, "issuer/default/issue/testing", map[string]interface{}{ |
| "common_name": "example.com", |
| "use_pss": "true", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed to issue PSS leaf") |
| } |
| |
| func TestPKI_EmptyCRLConfigUpgraded(t *testing.T) { |
| t.Parallel() |
| b, s := CreateBackendWithStorage(t) |
| |
| // Write an empty CRLConfig into storage. |
| crlConfigEntry, err := logical.StorageEntryJSON("config/crl", &crlConfig{}) |
| require.NoError(t, err) |
| err = s.Put(ctx, crlConfigEntry) |
| require.NoError(t, err) |
| |
| resp, err := CBRead(b, s, "config/crl") |
| require.NoError(t, err) |
| require.NotNil(t, resp) |
| require.NotNil(t, resp.Data) |
| require.Equal(t, resp.Data["expiry"], defaultCrlConfig.Expiry) |
| require.Equal(t, resp.Data["disable"], defaultCrlConfig.Disable) |
| require.Equal(t, resp.Data["ocsp_disable"], defaultCrlConfig.OcspDisable) |
| require.Equal(t, resp.Data["auto_rebuild"], defaultCrlConfig.AutoRebuild) |
| require.Equal(t, resp.Data["auto_rebuild_grace_period"], defaultCrlConfig.AutoRebuildGracePeriod) |
| require.Equal(t, resp.Data["enable_delta"], defaultCrlConfig.EnableDelta) |
| require.Equal(t, resp.Data["delta_rebuild_interval"], defaultCrlConfig.DeltaRebuildInterval) |
| } |
| |
| func TestPKI_ListRevokedCerts(t *testing.T) { |
| t.Parallel() |
| b, s := CreateBackendWithStorage(t) |
| |
| // Test empty cluster |
| resp, err := CBList(b, s, "certs/revoked") |
| schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("certs/revoked"), logical.ListOperation), resp, true) |
| requireSuccessNonNilResponse(t, resp, err, "failed listing empty cluster") |
| require.Empty(t, resp.Data, "response map contained data that we did not expect") |
| |
| // Set up a mount that we can revoke under (We will create 3 leaf certs, 2 of which will be revoked) |
| resp, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{ |
| "common_name": "test.com", |
| "key_type": "ec", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "error generating root CA") |
| requireFieldsSetInResp(t, resp, "serial_number") |
| issuerSerial := resp.Data["serial_number"] |
| |
| resp, err = CBWrite(b, s, "roles/test", map[string]interface{}{ |
| "allowed_domains": "test.com", |
| "allow_subdomains": "true", |
| "max_ttl": "1h", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "error setting up pki role") |
| |
| resp, err = CBWrite(b, s, "issue/test", map[string]interface{}{ |
| "common_name": "test1.test.com", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "error issuing cert 1") |
| requireFieldsSetInResp(t, resp, "serial_number") |
| serial1 := resp.Data["serial_number"] |
| |
| resp, err = CBWrite(b, s, "issue/test", map[string]interface{}{ |
| "common_name": "test2.test.com", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "error issuing cert 2") |
| requireFieldsSetInResp(t, resp, "serial_number") |
| serial2 := resp.Data["serial_number"] |
| |
| resp, err = CBWrite(b, s, "issue/test", map[string]interface{}{ |
| "common_name": "test3.test.com", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "error issuing cert 2") |
| requireFieldsSetInResp(t, resp, "serial_number") |
| serial3 := resp.Data["serial_number"] |
| |
| resp, err = CBWrite(b, s, "revoke", map[string]interface{}{"serial_number": serial1}) |
| requireSuccessNonNilResponse(t, resp, err, "error revoking cert 1") |
| |
| resp, err = CBWrite(b, s, "revoke", map[string]interface{}{"serial_number": serial2}) |
| requireSuccessNonNilResponse(t, resp, err, "error revoking cert 2") |
| |
| // Test that we get back the expected revoked serial numbers. |
| resp, err = CBList(b, s, "certs/revoked") |
| requireSuccessNonNilResponse(t, resp, err, "failed listing revoked certs") |
| requireFieldsSetInResp(t, resp, "keys") |
| revokedKeys := resp.Data["keys"].([]string) |
| |
| require.Contains(t, revokedKeys, serial1) |
| require.Contains(t, revokedKeys, serial2) |
| require.Equal(t, 2, len(revokedKeys), "Expected 2 revoked entries got %d: %v", len(revokedKeys), revokedKeys) |
| |
| // Test that listing our certs returns a different response |
| resp, err = CBList(b, s, "certs") |
| requireSuccessNonNilResponse(t, resp, err, "failed listing written certs") |
| requireFieldsSetInResp(t, resp, "keys") |
| certKeys := resp.Data["keys"].([]string) |
| |
| require.Contains(t, certKeys, serial1) |
| require.Contains(t, certKeys, serial2) |
| require.Contains(t, certKeys, serial3) |
| require.Contains(t, certKeys, issuerSerial) |
| require.Equal(t, 4, len(certKeys), "Expected 4 cert entries got %d: %v", len(certKeys), certKeys) |
| } |
| |
| func TestPKI_TemplatedAIAs(t *testing.T) { |
| t.Parallel() |
| b, s := CreateBackendWithStorage(t) |
| |
| // Setting templated AIAs should succeed. |
| resp, err := CBWrite(b, s, "config/cluster", map[string]interface{}{ |
| "path": "http://localhost:8200/v1/pki", |
| "aia_path": "http://localhost:8200/cdn/pki", |
| }) |
| require.NoError(t, err) |
| schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("config/cluster"), logical.UpdateOperation), resp, true) |
| |
| resp, err = CBRead(b, s, "config/cluster") |
| require.NoError(t, err) |
| schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("config/cluster"), logical.ReadOperation), resp, true) |
| |
| aiaData := map[string]interface{}{ |
| "crl_distribution_points": "{{cluster_path}}/issuer/{{issuer_id}}/crl/der", |
| "issuing_certificates": "{{cluster_aia_path}}/issuer/{{issuer_id}}/der", |
| "ocsp_servers": "{{cluster_path}}/ocsp", |
| "enable_templating": true, |
| } |
| _, err = CBWrite(b, s, "config/urls", aiaData) |
| require.NoError(t, err) |
| |
| // Root generation should succeed, but without AIA info. |
| rootData := map[string]interface{}{ |
| "common_name": "Long-Lived Root X1", |
| "issuer_name": "long-root-x1", |
| "key_type": "ec", |
| } |
| resp, err = CBWrite(b, s, "root/generate/internal", rootData) |
| require.NoError(t, err) |
| _, err = CBDelete(b, s, "root") |
| require.NoError(t, err) |
| |
| // Clearing the config and regenerating the root should still succeed. |
| _, err = CBWrite(b, s, "config/urls", map[string]interface{}{ |
| "crl_distribution_points": "{{cluster_path}}/issuer/my-root-id/crl/der", |
| "issuing_certificates": "{{cluster_aia_path}}/issuer/my-root-id/der", |
| "ocsp_servers": "{{cluster_path}}/ocsp", |
| "enable_templating": true, |
| }) |
| require.NoError(t, err) |
| resp, err = CBWrite(b, s, "root/generate/internal", rootData) |
| requireSuccessNonNilResponse(t, resp, err) |
| issuerId := string(resp.Data["issuer_id"].(issuerID)) |
| |
| // Now write the original AIA config and sign a leaf. |
| _, err = CBWrite(b, s, "config/urls", aiaData) |
| require.NoError(t, err) |
| _, err = CBWrite(b, s, "roles/testing", map[string]interface{}{ |
| "allow_any_name": "true", |
| "key_type": "ec", |
| "ttl": "50m", |
| }) |
| require.NoError(t, err) |
| resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ |
| "common_name": "example.com", |
| }) |
| requireSuccessNonNilResponse(t, resp, err) |
| |
| // Validate the AIA info is correctly templated. |
| cert := parseCert(t, resp.Data["certificate"].(string)) |
| require.Equal(t, cert.OCSPServer, []string{"http://localhost:8200/v1/pki/ocsp"}) |
| require.Equal(t, cert.IssuingCertificateURL, []string{"http://localhost:8200/cdn/pki/issuer/" + issuerId + "/der"}) |
| require.Equal(t, cert.CRLDistributionPoints, []string{"http://localhost:8200/v1/pki/issuer/" + issuerId + "/crl/der"}) |
| |
| // Modify our issuer to set custom AIAs: these URLs are bad. |
| _, err = CBPatch(b, s, "issuer/default", map[string]interface{}{ |
| "enable_aia_url_templating": "false", |
| "crl_distribution_points": "a", |
| "issuing_certificates": "b", |
| "ocsp_servers": "c", |
| }) |
| require.Error(t, err) |
| |
| // These URLs are good. |
| _, err = CBPatch(b, s, "issuer/default", map[string]interface{}{ |
| "enable_aia_url_templating": "false", |
| "crl_distribution_points": "http://localhost/a", |
| "issuing_certificates": "http://localhost/b", |
| "ocsp_servers": "http://localhost/c", |
| }) |
| |
| resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ |
| "common_name": "example.com", |
| }) |
| requireSuccessNonNilResponse(t, resp, err) |
| |
| // Validate the AIA info is correctly templated. |
| cert = parseCert(t, resp.Data["certificate"].(string)) |
| require.Equal(t, cert.OCSPServer, []string{"http://localhost/c"}) |
| require.Equal(t, cert.IssuingCertificateURL, []string{"http://localhost/b"}) |
| require.Equal(t, cert.CRLDistributionPoints, []string{"http://localhost/a"}) |
| |
| // These URLs are bad, but will fail at issuance time due to AIA templating. |
| resp, err = CBPatch(b, s, "issuer/default", map[string]interface{}{ |
| "enable_aia_url_templating": "true", |
| "crl_distribution_points": "a", |
| "issuing_certificates": "b", |
| "ocsp_servers": "c", |
| }) |
| requireSuccessNonNilResponse(t, resp, err) |
| require.NotEmpty(t, resp.Warnings) |
| _, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ |
| "common_name": "example.com", |
| }) |
| require.Error(t, err) |
| } |
| |
| func requireSubjectUserIDAttr(t *testing.T, cert string, target string) { |
| xCert := parseCert(t, cert) |
| |
| for _, attr := range xCert.Subject.Names { |
| var userID string |
| if attr.Type.Equal(certutil.SubjectPilotUserIDAttributeOID) { |
| if target == "" { |
| t.Fatalf("expected no UserID (OID: %v) subject attributes in cert:\n%v", certutil.SubjectPilotUserIDAttributeOID, cert) |
| } |
| |
| switch aValue := attr.Value.(type) { |
| case string: |
| userID = aValue |
| case []byte: |
| userID = string(aValue) |
| default: |
| t.Fatalf("unknown type for UserID attribute: %v\nCert: %v", attr, cert) |
| } |
| |
| if userID == target { |
| return |
| } |
| } |
| } |
| |
| if target != "" { |
| t.Fatalf("failed to find UserID (OID: %v) matching %v in cert:\n%v", certutil.SubjectPilotUserIDAttributeOID, target, cert) |
| } |
| } |
| |
| func TestUserIDsInLeafCerts(t *testing.T) { |
| t.Parallel() |
| b, s := CreateBackendWithStorage(t) |
| |
| // 1. Setup root issuer. |
| resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ |
| "common_name": "Vault Root CA", |
| "key_type": "ec", |
| "ttl": "7200h", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed generating root issuer") |
| |
| // 2. Allow no user IDs. |
| resp, err = CBWrite(b, s, "roles/testing", map[string]interface{}{ |
| "allowed_user_ids": "", |
| "key_type": "ec", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed setting up role") |
| |
| // - Issue cert without user IDs should work. |
| resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ |
| "common_name": "localhost", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") |
| requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "") |
| |
| // - Issue cert with user ID should fail. |
| resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ |
| "common_name": "localhost", |
| "user_ids": "humanoid", |
| }) |
| require.Error(t, err) |
| require.True(t, resp.IsError()) |
| |
| // 3. Allow any user IDs. |
| resp, err = CBWrite(b, s, "roles/testing", map[string]interface{}{ |
| "allowed_user_ids": "*", |
| "key_type": "ec", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed setting up role") |
| |
| // - Issue cert without user IDs. |
| resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ |
| "common_name": "localhost", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") |
| requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "") |
| |
| // - Issue cert with one user ID. |
| resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ |
| "common_name": "localhost", |
| "user_ids": "humanoid", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") |
| requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "humanoid") |
| |
| // - Issue cert with two user IDs. |
| resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ |
| "common_name": "localhost", |
| "user_ids": "humanoid,robot", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") |
| requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "humanoid") |
| requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "robot") |
| |
| // 4. Allow one specific user ID. |
| resp, err = CBWrite(b, s, "roles/testing", map[string]interface{}{ |
| "allowed_user_ids": "humanoid", |
| "key_type": "ec", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed setting up role") |
| |
| // - Issue cert without user IDs. |
| resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ |
| "common_name": "localhost", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") |
| requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "") |
| |
| // - Issue cert with approved ID. |
| resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ |
| "common_name": "localhost", |
| "user_ids": "humanoid", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") |
| requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "humanoid") |
| |
| // - Issue cert with non-approved user ID should fail. |
| resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ |
| "common_name": "localhost", |
| "user_ids": "robot", |
| }) |
| require.Error(t, err) |
| require.True(t, resp.IsError()) |
| |
| // - Issue cert with one approved and one non-approved should also fail. |
| resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ |
| "common_name": "localhost", |
| "user_ids": "humanoid,robot", |
| }) |
| require.Error(t, err) |
| require.True(t, resp.IsError()) |
| |
| // 5. Allow two specific user IDs. |
| resp, err = CBWrite(b, s, "roles/testing", map[string]interface{}{ |
| "allowed_user_ids": "humanoid,robot", |
| "key_type": "ec", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed setting up role") |
| |
| // - Issue cert without user IDs. |
| resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ |
| "common_name": "localhost", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") |
| requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "") |
| |
| // - Issue cert with one approved ID. |
| resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ |
| "common_name": "localhost", |
| "user_ids": "humanoid", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") |
| requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "humanoid") |
| |
| // - Issue cert with other user ID. |
| resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ |
| "common_name": "localhost", |
| "user_ids": "robot", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") |
| requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "robot") |
| |
| // - Issue cert with unknown user ID will fail. |
| resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ |
| "common_name": "localhost", |
| "user_ids": "robot2", |
| }) |
| require.Error(t, err) |
| require.True(t, resp.IsError()) |
| |
| // - Issue cert with both should succeed. |
| resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ |
| "common_name": "localhost", |
| "user_ids": "humanoid,robot", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") |
| requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "humanoid") |
| requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "robot") |
| |
| // 6. Use a glob. |
| resp, err = CBWrite(b, s, "roles/testing", map[string]interface{}{ |
| "allowed_user_ids": "human*", |
| "key_type": "ec", |
| "use_csr_sans": true, // setup for further testing. |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed setting up role") |
| |
| // - Issue cert without user IDs. |
| resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ |
| "common_name": "localhost", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") |
| requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "") |
| |
| // - Issue cert with approved ID. |
| resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ |
| "common_name": "localhost", |
| "user_ids": "humanoid", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") |
| requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "humanoid") |
| |
| // - Issue cert with another approved ID. |
| resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ |
| "common_name": "localhost", |
| "user_ids": "human", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") |
| requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "human") |
| |
| // - Issue cert with literal glob. |
| resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ |
| "common_name": "localhost", |
| "user_ids": "human*", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") |
| requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "human*") |
| |
| // - Still no robotic certs are allowed; will fail. |
| resp, err = CBWrite(b, s, "issue/testing", map[string]interface{}{ |
| "common_name": "localhost", |
| "user_ids": "robot", |
| }) |
| require.Error(t, err) |
| require.True(t, resp.IsError()) |
| |
| // Create a CSR and validate it works with both sign/ and sign-verbatim. |
| csrTemplate := x509.CertificateRequest{ |
| Subject: pkix.Name{ |
| CommonName: "localhost", |
| ExtraNames: []pkix.AttributeTypeAndValue{ |
| { |
| Type: certutil.SubjectPilotUserIDAttributeOID, |
| Value: "humanoid", |
| }, |
| }, |
| }, |
| } |
| _, _, csrPem := generateCSR(t, &csrTemplate, "ec", 256) |
| |
| // Should work with role-based signing. |
| resp, err = CBWrite(b, s, "sign/testing", map[string]interface{}{ |
| "csr": csrPem, |
| }) |
| schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("sign/testing"), logical.UpdateOperation), resp, true) |
| requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") |
| requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "humanoid") |
| |
| // - Definitely will work with sign-verbatim. |
| resp, err = CBWrite(b, s, "sign-verbatim", map[string]interface{}{ |
| "csr": csrPem, |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed issuing leaf cert") |
| requireSubjectUserIDAttr(t, resp.Data["certificate"].(string), "humanoid") |
| } |
| |
| // TestStandby_Operations test proper forwarding for PKI requests from a standby node to the |
| // active node within a cluster. |
| func TestStandby_Operations(t *testing.T) { |
| conf, opts := teststorage.ClusterSetup(&vault.CoreConfig{ |
| LogicalBackends: map[string]logical.Factory{ |
| "pki": Factory, |
| }, |
| }, nil, teststorage.InmemBackendSetup) |
| cluster := vault.NewTestCluster(t, conf, opts) |
| cluster.Start() |
| defer cluster.Cleanup() |
| |
| testhelpers.WaitForActiveNodeAndStandbys(t, cluster) |
| standbyCores := testhelpers.DeriveStandbyCores(t, cluster) |
| require.Greater(t, len(standbyCores), 0, "Need at least one standby core.") |
| client := standbyCores[0].Client |
| |
| mountPKIEndpoint(t, client, "pki") |
| |
| _, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ |
| "key_type": "ec", |
| "common_name": "root-ca.com", |
| "ttl": "600h", |
| }) |
| require.NoError(t, err, "error setting up pki role: %v", err) |
| |
| _, err = client.Logical().Write("pki/roles/example", map[string]interface{}{ |
| "allowed_domains": "example.com", |
| "allow_subdomains": "true", |
| "no_store": "false", // make sure we store this cert |
| "ttl": "5h", |
| "key_type": "ec", |
| }) |
| require.NoError(t, err, "error setting up pki role: %v", err) |
| |
| resp, err := client.Logical().Write("pki/issue/example", map[string]interface{}{ |
| "common_name": "test.example.com", |
| }) |
| require.NoError(t, err, "error issuing certificate: %v", err) |
| require.NotNil(t, resp, "got nil response from issuing request") |
| serialOfCert := resp.Data["serial_number"].(string) |
| |
| resp, err = client.Logical().Write("pki/revoke", map[string]interface{}{ |
| "serial_number": serialOfCert, |
| }) |
| require.NoError(t, err, "error revoking certificate: %v", err) |
| require.NotNil(t, resp, "got nil response from revoke request") |
| } |
| |
| type pathAuthCheckerFunc func(t *testing.T, client *api.Client, path string, token string) |
| |
| func isPermDenied(err error) bool { |
| return err != nil && strings.Contains(err.Error(), "permission denied") |
| } |
| |
| func isUnsupportedPathOperation(err error) bool { |
| return err != nil && (strings.Contains(err.Error(), "unsupported path") || strings.Contains(err.Error(), "unsupported operation")) |
| } |
| |
| func isDeniedOp(err error) bool { |
| return isPermDenied(err) || isUnsupportedPathOperation(err) |
| } |
| |
| func pathShouldBeAuthed(t *testing.T, client *api.Client, path string, token string) { |
| client.SetToken("") |
| resp, err := client.Logical().ReadWithContext(ctx, path) |
| if err == nil || !isPermDenied(err) { |
| t.Fatalf("expected failure to read %v while unauthed: %v / %v", path, err, resp) |
| } |
| resp, err = client.Logical().ListWithContext(ctx, path) |
| if err == nil || !isPermDenied(err) { |
| t.Fatalf("expected failure to list %v while unauthed: %v / %v", path, err, resp) |
| } |
| resp, err = client.Logical().WriteWithContext(ctx, path, map[string]interface{}{}) |
| if err == nil || !isPermDenied(err) { |
| t.Fatalf("expected failure to write %v while unauthed: %v / %v", path, err, resp) |
| } |
| resp, err = client.Logical().DeleteWithContext(ctx, path) |
| if err == nil || !isPermDenied(err) { |
| t.Fatalf("expected failure to delete %v while unauthed: %v / %v", path, err, resp) |
| } |
| resp, err = client.Logical().JSONMergePatch(ctx, path, map[string]interface{}{}) |
| if err == nil || !isPermDenied(err) { |
| t.Fatalf("expected failure to patch %v while unauthed: %v / %v", path, err, resp) |
| } |
| } |
| |
| func pathShouldBeUnauthedReadList(t *testing.T, client *api.Client, path string, token string) { |
| // Should be able to read both with and without a token. |
| client.SetToken("") |
| resp, err := client.Logical().ReadWithContext(ctx, path) |
| if err != nil && isPermDenied(err) { |
| // Read will sometimes return permission denied, when the handler |
| // does not support the given operation. Retry with the token. |
| client.SetToken(token) |
| resp2, err2 := client.Logical().ReadWithContext(ctx, path) |
| if err2 != nil && !isUnsupportedPathOperation(err2) { |
| t.Fatalf("unexpected failure to read %v while unauthed: %v / %v\nWhile authed: %v / %v", path, err, resp, err2, resp2) |
| } |
| client.SetToken("") |
| } |
| resp, err = client.Logical().ListWithContext(ctx, path) |
| if err != nil && isPermDenied(err) { |
| // List will sometimes return permission denied, when the handler |
| // does not support the given operation. Retry with the token. |
| client.SetToken(token) |
| resp2, err2 := client.Logical().ListWithContext(ctx, path) |
| if err2 != nil && !isUnsupportedPathOperation(err2) { |
| t.Fatalf("unexpected failure to list %v while unauthed: %v / %v\nWhile authed: %v / %v", path, err, resp, err2, resp2) |
| } |
| client.SetToken("") |
| } |
| |
| // These should all be denied. |
| resp, err = client.Logical().WriteWithContext(ctx, path, map[string]interface{}{}) |
| if err == nil || !isDeniedOp(err) { |
| if !strings.Contains(path, "ocsp") || !strings.Contains(err.Error(), "Code: 40") { |
| t.Fatalf("unexpected failure during write on read-only path %v while unauthed: %v / %v", path, err, resp) |
| } |
| } |
| resp, err = client.Logical().DeleteWithContext(ctx, path) |
| if err == nil || !isDeniedOp(err) { |
| t.Fatalf("unexpected failure during delete on read-only path %v while unauthed: %v / %v", path, err, resp) |
| } |
| resp, err = client.Logical().JSONMergePatch(ctx, path, map[string]interface{}{}) |
| if err == nil || !isDeniedOp(err) { |
| t.Fatalf("unexpected failure during patch on read-only path %v while unauthed: %v / %v", path, err, resp) |
| } |
| |
| // Retrying with token should allow read/list, but not modification still. |
| client.SetToken(token) |
| resp, err = client.Logical().ReadWithContext(ctx, path) |
| if err != nil && isPermDenied(err) { |
| t.Fatalf("unexpected failure to read %v while authed: %v / %v", path, err, resp) |
| } |
| resp, err = client.Logical().ListWithContext(ctx, path) |
| if err != nil && isPermDenied(err) { |
| t.Fatalf("unexpected failure to list %v while authed: %v / %v", path, err, resp) |
| } |
| |
| // Should all be denied. |
| resp, err = client.Logical().WriteWithContext(ctx, path, map[string]interface{}{}) |
| if err == nil || !isDeniedOp(err) { |
| if !strings.Contains(path, "ocsp") || !strings.Contains(err.Error(), "Code: 40") { |
| t.Fatalf("unexpected failure during write on read-only path %v while authed: %v / %v", path, err, resp) |
| } |
| } |
| resp, err = client.Logical().DeleteWithContext(ctx, path) |
| if err == nil || !isDeniedOp(err) { |
| t.Fatalf("unexpected failure during delete on read-only path %v while authed: %v / %v", path, err, resp) |
| } |
| resp, err = client.Logical().JSONMergePatch(ctx, path, map[string]interface{}{}) |
| if err == nil || !isDeniedOp(err) { |
| t.Fatalf("unexpected failure during patch on read-only path %v while authed: %v / %v", path, err, resp) |
| } |
| } |
| |
| func pathShouldBeUnauthedWriteOnly(t *testing.T, client *api.Client, path string, token string) { |
| client.SetToken("") |
| resp, err := client.Logical().WriteWithContext(ctx, path, map[string]interface{}{}) |
| if err != nil && isPermDenied(err) { |
| t.Fatalf("unexpected failure to write %v while unauthed: %v / %v", path, err, resp) |
| } |
| |
| // These should all be denied. However, on OSS, we might end up with |
| // a regular 404, which looks like err == resp == nil; hence we only |
| // fail when there's a non-nil response and/or a non-nil err. |
| resp, err = client.Logical().ReadWithContext(ctx, path) |
| if (err == nil && resp != nil) || (err != nil && !isDeniedOp(err)) { |
| t.Fatalf("unexpected failure during read on write-only path %v while unauthed: %v / %v", path, err, resp) |
| } |
| resp, err = client.Logical().ListWithContext(ctx, path) |
| if (err == nil && resp != nil) || (err != nil && !isDeniedOp(err)) { |
| t.Fatalf("unexpected failure during list on write-only path %v while unauthed: %v / %v", path, err, resp) |
| } |
| resp, err = client.Logical().DeleteWithContext(ctx, path) |
| if (err == nil && resp != nil) || (err != nil && !isDeniedOp(err)) { |
| t.Fatalf("unexpected failure during delete on write-only path %v while unauthed: %v / %v", path, err, resp) |
| } |
| resp, err = client.Logical().JSONMergePatch(ctx, path, map[string]interface{}{}) |
| if (err == nil && resp != nil) || (err != nil && !isDeniedOp(err)) { |
| t.Fatalf("unexpected failure during patch on write-only path %v while unauthed: %v / %v", path, err, resp) |
| } |
| |
| // Retrying with token should allow writing, but nothing else. |
| client.SetToken(token) |
| resp, err = client.Logical().WriteWithContext(ctx, path, map[string]interface{}{}) |
| if err != nil && isPermDenied(err) { |
| t.Fatalf("unexpected failure to write %v while unauthed: %v / %v", path, err, resp) |
| } |
| |
| // These should all be denied. |
| resp, err = client.Logical().ReadWithContext(ctx, path) |
| if (err == nil && resp != nil) || (err != nil && !isDeniedOp(err)) { |
| t.Fatalf("unexpected failure during read on write-only path %v while authed: %v / %v", path, err, resp) |
| } |
| resp, err = client.Logical().ListWithContext(ctx, path) |
| if (err == nil && resp != nil) || (err != nil && !isDeniedOp(err)) { |
| if resp != nil || err != nil { |
| t.Fatalf("unexpected failure during list on write-only path %v while authed: %v / %v", path, err, resp) |
| } |
| } |
| resp, err = client.Logical().DeleteWithContext(ctx, path) |
| if (err == nil && resp != nil) || (err != nil && !isDeniedOp(err)) { |
| t.Fatalf("unexpected failure during delete on write-only path %v while authed: %v / %v", path, err, resp) |
| } |
| resp, err = client.Logical().JSONMergePatch(ctx, path, map[string]interface{}{}) |
| if (err == nil && resp != nil) || (err != nil && !isDeniedOp(err)) { |
| t.Fatalf("unexpected failure during patch on write-only path %v while authed: %v / %v", path, err, resp) |
| } |
| } |
| |
| type pathAuthChecker int |
| |
| const ( |
| shouldBeAuthed pathAuthChecker = iota |
| shouldBeUnauthedReadList |
| shouldBeUnauthedWriteOnly |
| ) |
| |
| var pathAuthChckerMap = map[pathAuthChecker]pathAuthCheckerFunc{ |
| shouldBeAuthed: pathShouldBeAuthed, |
| shouldBeUnauthedReadList: pathShouldBeUnauthedReadList, |
| shouldBeUnauthedWriteOnly: pathShouldBeUnauthedWriteOnly, |
| } |
| |
| func TestProperAuthing(t *testing.T) { |
| t.Parallel() |
| ctx := context.Background() |
| coreConfig := &vault.CoreConfig{ |
| LogicalBackends: map[string]logical.Factory{ |
| "pki": Factory, |
| }, |
| } |
| cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ |
| HandlerFunc: vaulthttp.Handler, |
| }) |
| cluster.Start() |
| defer cluster.Cleanup() |
| client := cluster.Cores[0].Client |
| token := client.Token() |
| |
| // Mount PKI. |
| err := client.Sys().MountWithContext(ctx, "pki", &api.MountInput{ |
| Type: "pki", |
| Config: api.MountConfigInput{ |
| DefaultLeaseTTL: "16h", |
| MaxLeaseTTL: "60h", |
| }, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Setup basic configuration. |
| _, err = client.Logical().WriteWithContext(ctx, "pki/root/generate/internal", map[string]interface{}{ |
| "ttl": "40h", |
| "common_name": "myvault.com", |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| _, err = client.Logical().WriteWithContext(ctx, "pki/roles/test", map[string]interface{}{ |
| "allow_localhost": true, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| resp, err := client.Logical().WriteWithContext(ctx, "pki/issue/test", map[string]interface{}{ |
| "common_name": "localhost", |
| }) |
| if err != nil || resp == nil { |
| t.Fatal(err) |
| } |
| serial := resp.Data["serial_number"].(string) |
| eabKid := "13b80844-e60d-42d2-b7e9-152a8e834b90" |
| paths := map[string]pathAuthChecker{ |
| "ca_chain": shouldBeUnauthedReadList, |
| "cert/ca_chain": shouldBeUnauthedReadList, |
| "ca": shouldBeUnauthedReadList, |
| "ca/pem": shouldBeUnauthedReadList, |
| "cert/" + serial: shouldBeUnauthedReadList, |
| "cert/" + serial + "/raw": shouldBeUnauthedReadList, |
| "cert/" + serial + "/raw/pem": shouldBeUnauthedReadList, |
| "cert/crl": shouldBeUnauthedReadList, |
| "cert/crl/raw": shouldBeUnauthedReadList, |
| "cert/crl/raw/pem": shouldBeUnauthedReadList, |
| "cert/delta-crl": shouldBeUnauthedReadList, |
| "cert/delta-crl/raw": shouldBeUnauthedReadList, |
| "cert/delta-crl/raw/pem": shouldBeUnauthedReadList, |
| "cert/unified-crl": shouldBeUnauthedReadList, |
| "cert/unified-crl/raw": shouldBeUnauthedReadList, |
| "cert/unified-crl/raw/pem": shouldBeUnauthedReadList, |
| "cert/unified-delta-crl": shouldBeUnauthedReadList, |
| "cert/unified-delta-crl/raw": shouldBeUnauthedReadList, |
| "cert/unified-delta-crl/raw/pem": shouldBeUnauthedReadList, |
| "certs": shouldBeAuthed, |
| "certs/revoked": shouldBeAuthed, |
| "certs/revocation-queue": shouldBeAuthed, |
| "certs/revocation-queue/": shouldBeAuthed, |
| "certs/unified-revoked": shouldBeAuthed, |
| "certs/unified-revoked/": shouldBeAuthed, |
| "config/acme": shouldBeAuthed, |
| "config/auto-tidy": shouldBeAuthed, |
| "config/ca": shouldBeAuthed, |
| "config/cluster": shouldBeAuthed, |
| "config/crl": shouldBeAuthed, |
| "config/issuers": shouldBeAuthed, |
| "config/keys": shouldBeAuthed, |
| "config/urls": shouldBeAuthed, |
| "crl": shouldBeUnauthedReadList, |
| "crl/pem": shouldBeUnauthedReadList, |
| "crl/delta": shouldBeUnauthedReadList, |
| "crl/delta/pem": shouldBeUnauthedReadList, |
| "crl/rotate": shouldBeAuthed, |
| "crl/rotate-delta": shouldBeAuthed, |
| "intermediate/cross-sign": shouldBeAuthed, |
| "intermediate/generate/exported": shouldBeAuthed, |
| "intermediate/generate/internal": shouldBeAuthed, |
| "intermediate/generate/existing": shouldBeAuthed, |
| "intermediate/generate/kms": shouldBeAuthed, |
| "intermediate/set-signed": shouldBeAuthed, |
| "issue/test": shouldBeAuthed, |
| "issuer/default": shouldBeAuthed, |
| "issuer/default/der": shouldBeUnauthedReadList, |
| "issuer/default/json": shouldBeUnauthedReadList, |
| "issuer/default/pem": shouldBeUnauthedReadList, |
| "issuer/default/crl": shouldBeUnauthedReadList, |
| "issuer/default/crl/pem": shouldBeUnauthedReadList, |
| "issuer/default/crl/der": shouldBeUnauthedReadList, |
| "issuer/default/crl/delta": shouldBeUnauthedReadList, |
| "issuer/default/crl/delta/der": shouldBeUnauthedReadList, |
| "issuer/default/crl/delta/pem": shouldBeUnauthedReadList, |
| "issuer/default/unified-crl": shouldBeUnauthedReadList, |
| "issuer/default/unified-crl/pem": shouldBeUnauthedReadList, |
| "issuer/default/unified-crl/der": shouldBeUnauthedReadList, |
| "issuer/default/unified-crl/delta": shouldBeUnauthedReadList, |
| "issuer/default/unified-crl/delta/der": shouldBeUnauthedReadList, |
| "issuer/default/unified-crl/delta/pem": shouldBeUnauthedReadList, |
| "issuer/default/issue/test": shouldBeAuthed, |
| "issuer/default/resign-crls": shouldBeAuthed, |
| "issuer/default/revoke": shouldBeAuthed, |
| "issuer/default/sign-intermediate": shouldBeAuthed, |
| "issuer/default/sign-revocation-list": shouldBeAuthed, |
| "issuer/default/sign-self-issued": shouldBeAuthed, |
| "issuer/default/sign-verbatim": shouldBeAuthed, |
| "issuer/default/sign-verbatim/test": shouldBeAuthed, |
| "issuer/default/sign/test": shouldBeAuthed, |
| "issuers": shouldBeUnauthedReadList, |
| "issuers/generate/intermediate/exported": shouldBeAuthed, |
| "issuers/generate/intermediate/internal": shouldBeAuthed, |
| "issuers/generate/intermediate/existing": shouldBeAuthed, |
| "issuers/generate/intermediate/kms": shouldBeAuthed, |
| "issuers/generate/root/exported": shouldBeAuthed, |
| "issuers/generate/root/internal": shouldBeAuthed, |
| "issuers/generate/root/existing": shouldBeAuthed, |
| "issuers/generate/root/kms": shouldBeAuthed, |
| "issuers/import/cert": shouldBeAuthed, |
| "issuers/import/bundle": shouldBeAuthed, |
| "key/default": shouldBeAuthed, |
| "keys": shouldBeAuthed, |
| "keys/generate/internal": shouldBeAuthed, |
| "keys/generate/exported": shouldBeAuthed, |
| "keys/generate/kms": shouldBeAuthed, |
| "keys/import": shouldBeAuthed, |
| "ocsp": shouldBeUnauthedWriteOnly, |
| "ocsp/dGVzdAo=": shouldBeUnauthedReadList, |
| "revoke": shouldBeAuthed, |
| "revoke-with-key": shouldBeAuthed, |
| "roles/test": shouldBeAuthed, |
| "roles": shouldBeAuthed, |
| "root": shouldBeAuthed, |
| "root/generate/exported": shouldBeAuthed, |
| "root/generate/internal": shouldBeAuthed, |
| "root/generate/existing": shouldBeAuthed, |
| "root/generate/kms": shouldBeAuthed, |
| "root/replace": shouldBeAuthed, |
| "root/rotate/internal": shouldBeAuthed, |
| "root/rotate/exported": shouldBeAuthed, |
| "root/rotate/existing": shouldBeAuthed, |
| "root/rotate/kms": shouldBeAuthed, |
| "root/sign-intermediate": shouldBeAuthed, |
| "root/sign-self-issued": shouldBeAuthed, |
| "sign-verbatim": shouldBeAuthed, |
| "sign-verbatim/test": shouldBeAuthed, |
| "sign/test": shouldBeAuthed, |
| "tidy": shouldBeAuthed, |
| "tidy-cancel": shouldBeAuthed, |
| "tidy-status": shouldBeAuthed, |
| "unified-crl": shouldBeUnauthedReadList, |
| "unified-crl/pem": shouldBeUnauthedReadList, |
| "unified-crl/delta": shouldBeUnauthedReadList, |
| "unified-crl/delta/pem": shouldBeUnauthedReadList, |
| "unified-ocsp": shouldBeUnauthedWriteOnly, |
| "unified-ocsp/dGVzdAo=": shouldBeUnauthedReadList, |
| "eab": shouldBeAuthed, |
| "eab/" + eabKid: shouldBeAuthed, |
| } |
| |
| // Add ACME based paths to the test suite |
| for _, acmePrefix := range []string{"", "issuer/default/", "roles/test/", "issuer/default/roles/test/"} { |
| paths[acmePrefix+"acme/directory"] = shouldBeUnauthedReadList |
| paths[acmePrefix+"acme/new-nonce"] = shouldBeUnauthedReadList |
| paths[acmePrefix+"acme/new-account"] = shouldBeUnauthedWriteOnly |
| paths[acmePrefix+"acme/revoke-cert"] = shouldBeUnauthedWriteOnly |
| paths[acmePrefix+"acme/new-order"] = shouldBeUnauthedWriteOnly |
| paths[acmePrefix+"acme/orders"] = shouldBeUnauthedWriteOnly |
| paths[acmePrefix+"acme/account/hrKmDYTvicHoHGVN2-3uzZV_BPGdE0W_dNaqYTtYqeo="] = shouldBeUnauthedWriteOnly |
| paths[acmePrefix+"acme/authorization/29da8c38-7a09-465e-b9a6-3d76802b1afd"] = shouldBeUnauthedWriteOnly |
| paths[acmePrefix+"acme/challenge/29da8c38-7a09-465e-b9a6-3d76802b1afd/http-01"] = shouldBeUnauthedWriteOnly |
| paths[acmePrefix+"acme/order/13b80844-e60d-42d2-b7e9-152a8e834b90"] = shouldBeUnauthedWriteOnly |
| paths[acmePrefix+"acme/order/13b80844-e60d-42d2-b7e9-152a8e834b90/finalize"] = shouldBeUnauthedWriteOnly |
| paths[acmePrefix+"acme/order/13b80844-e60d-42d2-b7e9-152a8e834b90/cert"] = shouldBeUnauthedWriteOnly |
| |
| // Make sure this new-eab path is auth'd |
| paths[acmePrefix+"acme/new-eab"] = shouldBeAuthed |
| } |
| |
| for path, checkerType := range paths { |
| checker := pathAuthChckerMap[checkerType] |
| checker(t, client, "pki/"+path, token) |
| } |
| |
| client.SetToken(token) |
| openAPIResp, err := client.Logical().ReadWithContext(ctx, "sys/internal/specs/openapi") |
| if err != nil { |
| t.Fatalf("failed to get openapi data: %v", err) |
| } |
| |
| validatedPath := false |
| for openapi_path, raw_data := range openAPIResp.Data["paths"].(map[string]interface{}) { |
| if !strings.HasPrefix(openapi_path, "/pki/") { |
| t.Logf("Skipping path: %v", openapi_path) |
| continue |
| } |
| |
| t.Logf("Validating path: %v", openapi_path) |
| validatedPath = true |
| // Substitute values in from our testing map. |
| raw_path := openapi_path[5:] |
| if strings.Contains(raw_path, "roles/") && strings.Contains(raw_path, "{name}") { |
| raw_path = strings.ReplaceAll(raw_path, "{name}", "test") |
| } |
| if strings.Contains(raw_path, "{role}") { |
| raw_path = strings.ReplaceAll(raw_path, "{role}", "test") |
| } |
| if strings.Contains(raw_path, "ocsp/") && strings.Contains(raw_path, "{req}") { |
| raw_path = strings.ReplaceAll(raw_path, "{req}", "dGVzdAo=") |
| } |
| if strings.Contains(raw_path, "{issuer_ref}") { |
| raw_path = strings.ReplaceAll(raw_path, "{issuer_ref}", "default") |
| } |
| if strings.Contains(raw_path, "{key_ref}") { |
| raw_path = strings.ReplaceAll(raw_path, "{key_ref}", "default") |
| } |
| if strings.Contains(raw_path, "{exported}") { |
| raw_path = strings.ReplaceAll(raw_path, "{exported}", "internal") |
| } |
| if strings.Contains(raw_path, "{serial}") { |
| raw_path = strings.ReplaceAll(raw_path, "{serial}", serial) |
| } |
| if strings.Contains(raw_path, "acme/account/") && strings.Contains(raw_path, "{kid}") { |
| raw_path = strings.ReplaceAll(raw_path, "{kid}", "hrKmDYTvicHoHGVN2-3uzZV_BPGdE0W_dNaqYTtYqeo=") |
| } |
| if strings.Contains(raw_path, "acme/") && strings.Contains(raw_path, "{auth_id}") { |
| raw_path = strings.ReplaceAll(raw_path, "{auth_id}", "29da8c38-7a09-465e-b9a6-3d76802b1afd") |
| } |
| if strings.Contains(raw_path, "acme/") && strings.Contains(raw_path, "{challenge_type}") { |
| raw_path = strings.ReplaceAll(raw_path, "{challenge_type}", "http-01") |
| } |
| if strings.Contains(raw_path, "acme/") && strings.Contains(raw_path, "{order_id}") { |
| raw_path = strings.ReplaceAll(raw_path, "{order_id}", "13b80844-e60d-42d2-b7e9-152a8e834b90") |
| } |
| if strings.Contains(raw_path, "eab") && strings.Contains(raw_path, "{key_id}") { |
| raw_path = strings.ReplaceAll(raw_path, "{key_id}", eabKid) |
| } |
| |
| handler, present := paths[raw_path] |
| if !present { |
| t.Fatalf("OpenAPI reports PKI mount contains %v->%v but was not tested to be authed or authed.", openapi_path, raw_path) |
| } |
| |
| openapi_data := raw_data.(map[string]interface{}) |
| hasList := false |
| rawGetData, hasGet := openapi_data["get"] |
| if hasGet { |
| getData := rawGetData.(map[string]interface{}) |
| getParams, paramsPresent := getData["parameters"].(map[string]interface{}) |
| if getParams != nil && paramsPresent { |
| if _, hasList = getParams["list"]; hasList { |
| // LIST is exclusive from GET on the same endpoint usually. |
| hasGet = false |
| } |
| } |
| } |
| _, hasPost := openapi_data["post"] |
| _, hasDelete := openapi_data["delete"] |
| |
| if handler == shouldBeUnauthedReadList { |
| if hasPost || hasDelete { |
| t.Fatalf("Unauthed read-only endpoints should not have POST/DELETE capabilities: %v->%v", openapi_path, raw_path) |
| } |
| } else if handler == shouldBeUnauthedWriteOnly { |
| if hasGet || hasList { |
| t.Fatalf("Unauthed write-only endpoints should not have GET/LIST capabilities: %v->%v", openapi_path, raw_path) |
| } |
| } |
| } |
| |
| if !validatedPath { |
| t.Fatalf("Expected to have validated at least one path.") |
| } |
| } |
| |
| func TestPatchIssuer(t *testing.T) { |
| t.Parallel() |
| |
| type TestCase struct { |
| Field string |
| Before interface{} |
| Patched interface{} |
| } |
| testCases := []TestCase{ |
| { |
| Field: "issuer_name", |
| Before: "root", |
| Patched: "root-new", |
| }, |
| { |
| Field: "leaf_not_after_behavior", |
| Before: "err", |
| Patched: "permit", |
| }, |
| { |
| Field: "usage", |
| Before: "crl-signing,issuing-certificates,ocsp-signing,read-only", |
| Patched: "issuing-certificates,read-only", |
| }, |
| { |
| Field: "revocation_signature_algorithm", |
| Before: "ECDSAWithSHA256", |
| Patched: "ECDSAWithSHA384", |
| }, |
| { |
| Field: "issuing_certificates", |
| Before: []string{"http://localhost/v1/pki-1/ca"}, |
| Patched: []string{"http://localhost/v1/pki/ca"}, |
| }, |
| { |
| Field: "crl_distribution_points", |
| Before: []string{"http://localhost/v1/pki-1/crl"}, |
| Patched: []string{"http://localhost/v1/pki/crl"}, |
| }, |
| { |
| Field: "ocsp_servers", |
| Before: []string{"http://localhost/v1/pki-1/ocsp"}, |
| Patched: []string{"http://localhost/v1/pki/ocsp"}, |
| }, |
| { |
| Field: "enable_aia_url_templating", |
| Before: false, |
| Patched: true, |
| }, |
| { |
| Field: "manual_chain", |
| Before: []string(nil), |
| Patched: []string{"self"}, |
| }, |
| } |
| |
| for index, testCase := range testCases { |
| t.Logf("index: %v / tc: %v", index, testCase) |
| |
| b, s := CreateBackendWithStorage(t) |
| |
| // 1. Setup root issuer. |
| resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ |
| "common_name": "Vault Root CA", |
| "key_type": "ec", |
| "ttl": "7200h", |
| "issuer_name": "root", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed generating root issuer") |
| id := string(resp.Data["issuer_id"].(issuerID)) |
| |
| // 2. Enable Cluster paths |
| resp, err = CBWrite(b, s, "config/urls", map[string]interface{}{ |
| "path": "https://localhost/v1/pki", |
| "aia_path": "http://localhost/v1/pki", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed updating AIA config") |
| |
| // 3. Add AIA information |
| resp, err = CBPatch(b, s, "issuer/default", map[string]interface{}{ |
| "issuing_certificates": "http://localhost/v1/pki-1/ca", |
| "crl_distribution_points": "http://localhost/v1/pki-1/crl", |
| "ocsp_servers": "http://localhost/v1/pki-1/ocsp", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed setting up issuer") |
| |
| // 4. Read the issuer before. |
| resp, err = CBRead(b, s, "issuer/default") |
| requireSuccessNonNilResponse(t, resp, err, "failed reading root issuer before") |
| require.Equal(t, testCase.Before, resp.Data[testCase.Field], "bad expectations") |
| |
| // 5. Perform modification. |
| resp, err = CBPatch(b, s, "issuer/default", map[string]interface{}{ |
| testCase.Field: testCase.Patched, |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "failed patching root issuer") |
| |
| if testCase.Field != "manual_chain" { |
| require.Equal(t, testCase.Patched, resp.Data[testCase.Field], "failed persisting value") |
| } else { |
| // self->id |
| require.Equal(t, []string{id}, resp.Data[testCase.Field], "failed persisting value") |
| } |
| |
| // 6. Ensure it stuck |
| resp, err = CBRead(b, s, "issuer/default") |
| requireSuccessNonNilResponse(t, resp, err, "failed reading root issuer after") |
| |
| if testCase.Field != "manual_chain" { |
| require.Equal(t, testCase.Patched, resp.Data[testCase.Field]) |
| } else { |
| // self->id |
| require.Equal(t, []string{id}, resp.Data[testCase.Field], "failed persisting value") |
| } |
| } |
| } |
| |
| func TestGenerateRootCAWithAIA(t *testing.T) { |
| // Generate a root CA at /pki-root |
| b_root, s_root := CreateBackendWithStorage(t) |
| |
| // Setup templated AIA information |
| _, err := CBWrite(b_root, s_root, "config/cluster", map[string]interface{}{ |
| "path": "https://localhost:8200", |
| "aia_path": "https://localhost:8200", |
| }) |
| require.NoError(t, err, "failed to write AIA settings") |
| |
| _, err = CBWrite(b_root, s_root, "config/urls", map[string]interface{}{ |
| "crl_distribution_points": "{{cluster_path}}/issuer/{{issuer_id}}/crl/der", |
| "issuing_certificates": "{{cluster_aia_path}}/issuer/{{issuer_id}}/der", |
| "ocsp_servers": "{{cluster_path}}/ocsp", |
| "enable_templating": true, |
| }) |
| require.NoError(t, err, "failed to write AIA settings") |
| |
| // Write a root issuer, this should succeed. |
| resp, err := CBWrite(b_root, s_root, "root/generate/exported", map[string]interface{}{ |
| "common_name": "root myvault.com", |
| "key_type": "ec", |
| }) |
| requireSuccessNonNilResponse(t, resp, err, "expected root generation to succeed") |
| } |
| |
| var ( |
| initTest sync.Once |
| rsaCAKey string |
| rsaCACert string |
| ecCAKey string |
| ecCACert string |
| edCAKey string |
| edCACert string |
| ) |