From b49ac2501bafd77e9ae983f5d608beafdad32904 Mon Sep 17 00:00:00 2001 From: Ahmet DEMIR Date: Thu, 27 Jan 2022 11:14:19 +0100 Subject: [PATCH] feat: enhance options and fix revoke plus more tests --- cas/vaultcas/vaultcas.go | 49 ++-- cas/vaultcas/vaultcas_test.go | 447 +++++++++++++++++++++++++++++++++- 2 files changed, 467 insertions(+), 29 deletions(-) diff --git a/cas/vaultcas/vaultcas.go b/cas/vaultcas/vaultcas.go index fce1bdaf..e503ab28 100644 --- a/cas/vaultcas/vaultcas.go +++ b/cas/vaultcas/vaultcas.go @@ -26,7 +26,7 @@ func init() { type VaultOptions struct { PKI string `json:"pki,omitempty"` - PKIRole string `json:"pkiRole,omitempty"` + PKIRoleDefault string `json:"PKIRoleDefault,omitempty"` PKIRoleRSA string `json:"pkiRoleRSA,omitempty"` PKIRoleEC string `json:"pkiRoleEC,omitempty"` PKIRoleEd25519 string `json:"PKIRoleEd25519,omitempty"` @@ -43,45 +43,38 @@ type VaultCAS struct { fingerprint string } -func loadOptions(config json.RawMessage) (vc VaultOptions, err error) { - err = json.Unmarshal(config, &vc) +func loadOptions(config json.RawMessage) (*VaultOptions, error) { + var vc *VaultOptions + + err := json.Unmarshal(config, &vc) if err != nil { - return vc, errors.Wrap(err, "error decoding vaultCAS config") + return nil, errors.Wrap(err, "error decoding vaultCAS config") } if vc.PKI == "" { vc.PKI = "pki" // use default pki vault name } - // pkirole or per key type must be defined - if vc.PKIRole == "" && vc.PKIRoleRSA == "" && vc.PKIRoleEC == "" && vc.PKIRoleEd25519 == "" { - return vc, errors.New("vaultCAS config options must define `pkiRole`") + if vc.PKIRoleDefault == "" { + vc.PKIRoleDefault = "default" // use default pki role name } - // if pkirole is empty all others keys must be set - if vc.PKIRole == "" && (vc.PKIRoleRSA == "" || vc.PKIRoleEC == "" || vc.PKIRoleEd25519 == "") { - return vc, errors.New("vaultCAS config options must include a `pkiRole` or `pkiRoleRSA`, `pkiRoleEC` and `PKIRoleEd25519`") + if vc.PKIRoleRSA == "" { + vc.PKIRoleRSA = vc.PKIRoleDefault } - - // if pkirole is not empty, use it as default for unset keys - if vc.PKIRole != "" { - if vc.PKIRoleRSA == "" { - vc.PKIRoleRSA = vc.PKIRole - } - if vc.PKIRoleEC == "" { - vc.PKIRoleEC = vc.PKIRole - } - if vc.PKIRoleEd25519 == "" { - vc.PKIRoleEd25519 = vc.PKIRole - } + if vc.PKIRoleEC == "" { + vc.PKIRoleEC = vc.PKIRoleDefault + } + if vc.PKIRoleEd25519 == "" { + vc.PKIRoleEd25519 = vc.PKIRoleDefault } if vc.RoleID == "" { - return vc, errors.New("vaultCAS config options must define `roleID`") + return nil, errors.New("vaultCAS config options must define `roleID`") } if vc.SecretID.FromEnv == "" && vc.SecretID.FromFile == "" && vc.SecretID.FromString == "" { - return vc, errors.New("vaultCAS config options must define `secretID` object with one of `FromEnv`, `FromFile` or `FromString`") + return nil, errors.New("vaultCAS config options must define `secretID` object with one of `FromEnv`, `FromFile` or `FromString`") } if vc.PKI == "" { @@ -237,7 +230,7 @@ func New(ctx context.Context, opts apiv1.Options) (*VaultCAS, error) { return &VaultCAS{ client: client, - config: vc, + config: *vc, fingerprint: opts.CertificateAuthorityFingerprint, }, nil } @@ -326,7 +319,11 @@ func (v *VaultCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv serialNumberDash := certutil.GetHexFormatted(serialNumber, "-") - _, err := v.client.Logical().Write(v.config.PKI+"/revoke/"+serialNumberDash, nil) + y := map[string]interface{}{ + "serial_number": serialNumberDash, + } + + _, err := v.client.Logical().Write(v.config.PKI+"/revoke/", y) if err != nil { return nil, errors.Wrap(err, "unable to revoke certificate") } diff --git a/cas/vaultcas/vaultcas_test.go b/cas/vaultcas/vaultcas_test.go index 73725b43..3bf06347 100644 --- a/cas/vaultcas/vaultcas_test.go +++ b/cas/vaultcas/vaultcas_test.go @@ -1,6 +1,7 @@ package vaultcas import ( + "bytes" "context" "crypto/x509" "encoding/json" @@ -60,7 +61,17 @@ pV/v53oR/ewbtrkHZQkN/amFMLagITAfBgkqhkiG9w0BCQ4xEjAQMA4GA1UdEQQH MAWCA09LUDAFBgMrZXADQQDJi47MAgl/WKAz+V/kDu1k/zbKk1nrHHAUonbofHUW M6ihSD43+awq3BPeyPbToeH5orSH9l3MuTfbxPb5BVEH -----END CERTIFICATE REQUEST-----` - testRootFingerprint = `e7678acf0d8de731262bce2fe792c48f19547285f5976805125a40867c77464e` + testRootCertificate = `-----BEGIN CERTIFICATE----- +MIIBeDCCAR+gAwIBAgIQcXWWjtSZ/PAyH8D1Ou4L9jAKBggqhkjOPQQDAjAbMRkw +FwYDVQQDExBDbG91ZENBUyBSb290IENBMB4XDTIwMTAyNzIyNTM1NFoXDTMwMTAy +NzIyNTM1NFowGzEZMBcGA1UEAxMQQ2xvdWRDQVMgUm9vdCBDQTBZMBMGByqGSM49 +AgEGCCqGSM49AwEHA0IABIySHA4b78Yu4LuGhZIlv/PhNwXz4ZoV1OUZQ0LrK3vj +B13O12DLZC5uj1z3kxdQzXUttSbtRv49clMpBiTpsZKjRTBDMA4GA1UdDwEB/wQE +AwIBBjASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBSZ+t9RMHbFTl5BatM3 +5bJlHPOu3DAKBggqhkjOPQQDAgNHADBEAiASah6gg0tVM3WI0meCQ4SEKk7Mjhbv ++SmhuZHWV1QlXQIgRXNyWcpVUrAoG6Uy1KQg07LDpF5dFeK9InrDxSJAkVo= +-----END CERTIFICATE-----` + testRootFingerprint = `62e816cbac5c501b7705e18415503852798dfbcd67062f06bcb4af67c290e3c8` ) func mustParseCertificate(t *testing.T, pemCert string) *x509.Certificate { @@ -112,6 +123,30 @@ func testCAHelper(t *testing.T) (*url.URL, *vault.Client) { cert := map[string]interface{}{"data": map[string]interface{}{"certificate": testCertificateSigned}} writeJSON(w, cert) return + case r.RequestURI == "/v1/pki/cert/ca": + w.WriteHeader(http.StatusOK) + cert := map[string]interface{}{"data": map[string]interface{}{"certificate": testRootCertificate}} + writeJSON(w, cert) + return + case r.RequestURI == "/v1/pki/revoke": + buf := new(bytes.Buffer) + buf.ReadFrom(r.Body) + m := make(map[string]string) + json.Unmarshal(buf.Bytes(), &m) + switch { + case m["serial_number"] == "1c-71-6e-18-cc-f4-70-29-5f-75-ee-64-a8-fe-69-ad": + w.WriteHeader(http.StatusOK) + return + case m["serial_number"] == "01-e2-40": + w.WriteHeader(http.StatusOK) + return + // both + case m["serial_number"] == "01-34-3e": + w.WriteHeader(http.StatusOK) + return + default: + w.WriteHeader(http.StatusNotFound) + } default: w.WriteHeader(http.StatusNotFound) fmt.Fprintf(w, `{"error":"not found"}`) @@ -151,7 +186,7 @@ func TestNew_register(t *testing.T) { CertificateAuthorityFingerprint: testRootFingerprint, Config: json.RawMessage(`{ "PKI": "pki", - "PKIRole": "pki-role", + "PKIRoleDefault": "pki-role", "RoleID": "roleID", "SecretID": {"FromString": "secretID"}, "IsWrappingToken": false @@ -169,7 +204,7 @@ func TestVaultCAS_CreateCertificate(t *testing.T) { options := VaultOptions{ PKI: "pki", - PKIRole: "role", + PKIRoleDefault: "role", PKIRoleRSA: "rsa", PKIRoleEC: "ec", PKIRoleEd25519: "ed25519", @@ -216,6 +251,14 @@ func TestVaultCAS_CreateCertificate(t *testing.T) { Certificate: mustParseCertificate(t, testCertificateSigned), CertificateChain: []*x509.Certificate{}, }, false}, + {"fail CSR", fields{client, options}, args{&apiv1.CreateCertificateRequest{ + CSR: nil, + Lifetime: time.Hour, + }}, nil, true}, + {"fail lifetime", fields{client, options}, args{&apiv1.CreateCertificateRequest{ + CSR: mustParseCertificateRequest(t, testCertificateCsrEc), + Lifetime: 0, + }}, nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -234,3 +277,401 @@ func TestVaultCAS_CreateCertificate(t *testing.T) { }) } } + +func TestVaultCAS_GetCertificateAuthority(t *testing.T) { + caURL, client := testCAHelper(t) + + type fields struct { + client *vault.Client + options VaultOptions + fingerprint string + } + + type args struct { + req *apiv1.GetCertificateAuthorityRequest + } + + options := VaultOptions{ + PKI: "pki", + } + + rootCert, _ := parseCertificate(testRootCertificate) + + tests := []struct { + name string + fields fields + args args + want *apiv1.GetCertificateAuthorityResponse + wantErr bool + }{ + {"ok", fields{client, options, testRootFingerprint}, args{&apiv1.GetCertificateAuthorityRequest{ + Name: caURL.String(), + }}, &apiv1.GetCertificateAuthorityResponse{ + RootCertificate: rootCert, + }, false}, + {"fail fingerprint", fields{client, options, "fail"}, args{&apiv1.GetCertificateAuthorityRequest{ + Name: caURL.String(), + }}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &VaultCAS{ + client: tt.fields.client, + fingerprint: tt.fields.fingerprint, + config: tt.fields.options, + } + got, err := s.GetCertificateAuthority(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("VaultCAS.GetCertificateAuthority() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("VaultCAS.GetCertificateAuthority() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestVaultCAS_RevokeCertificate(t *testing.T) { + _, client := testCAHelper(t) + + options := VaultOptions{ + PKI: "pki", + PKIRoleDefault: "role", + PKIRoleRSA: "rsa", + PKIRoleEC: "ec", + PKIRoleEd25519: "ed25519", + RoleID: "roleID", + SecretID: auth.SecretID{FromString: "secretID"}, + AppRole: "approle", + IsWrappingToken: false, + } + + type fields struct { + client *vault.Client + options VaultOptions + } + + type args struct { + req *apiv1.RevokeCertificateRequest + } + + testCrt, _ := parseCertificate(testCertificateSigned) + + tests := []struct { + name string + fields fields + args args + want *apiv1.RevokeCertificateResponse + wantErr bool + }{ + {"ok serial number", fields{client, options}, args{&apiv1.RevokeCertificateRequest{ + SerialNumber: "123456", + Certificate: nil, + }}, &apiv1.RevokeCertificateResponse{}, false}, + {"ok certificate", fields{client, options}, args{&apiv1.RevokeCertificateRequest{ + SerialNumber: "", + Certificate: testCrt, + }}, &apiv1.RevokeCertificateResponse{ + Certificate: testCrt, + }, false}, + {"ok both", fields{client, options}, args{&apiv1.RevokeCertificateRequest{ + SerialNumber: "78910", + Certificate: testCrt, + }}, &apiv1.RevokeCertificateResponse{ + Certificate: testCrt, + }, false}, + {"fail serial string", fields{client, options}, args{&apiv1.RevokeCertificateRequest{ + SerialNumber: "fail", + Certificate: nil, + }}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &VaultCAS{ + client: tt.fields.client, + config: tt.fields.options, + } + got, err := s.RevokeCertificate(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("VaultCAS.RevokeCertificate() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("VaultCAS.RevokeCertificate() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestVaultCAS_RenewCertificate(t *testing.T) { + _, client := testCAHelper(t) + + options := VaultOptions{ + PKI: "pki", + PKIRoleDefault: "role", + PKIRoleRSA: "rsa", + PKIRoleEC: "ec", + PKIRoleEd25519: "ed25519", + RoleID: "roleID", + SecretID: auth.SecretID{FromString: "secretID"}, + AppRole: "approle", + IsWrappingToken: false, + } + + type fields struct { + client *vault.Client + options VaultOptions + } + + type args struct { + req *apiv1.RenewCertificateRequest + } + + tests := []struct { + name string + fields fields + args args + want *apiv1.RenewCertificateResponse + wantErr bool + }{ + {"not implemented", fields{client, options}, args{&apiv1.RenewCertificateRequest{ + CSR: mustParseCertificateRequest(t, testCertificateCsrEc), + Lifetime: time.Hour, + }}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &VaultCAS{ + client: tt.fields.client, + config: tt.fields.options, + } + got, err := s.RenewCertificate(tt.args.req) + if (err != nil) != tt.wantErr { + t.Errorf("VaultCAS.RenewCertificate() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("VaultCAS.RenewCertificate() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestVaultCAS_loadOptions(t *testing.T) { + tests := []struct { + name string + raw string + want *VaultOptions + wantErr bool + }{ + { + "ok mandatory with SecretID FromString", + `{"RoleID": "roleID", "SecretID": {"FromString": "secretID"}}`, + &VaultOptions{ + PKI: "pki", + PKIRoleDefault: "default", + PKIRoleRSA: "default", + PKIRoleEC: "default", + PKIRoleEd25519: "default", + RoleID: "roleID", + SecretID: auth.SecretID{FromString: "secretID"}, + AppRole: "auth/approle", + IsWrappingToken: false, + }, + false, + }, + { + "ok mandatory with SecretID FromFile", + `{"RoleID": "roleID", "SecretID": {"FromFile": "secretID"}}`, + &VaultOptions{ + PKI: "pki", + PKIRoleDefault: "default", + PKIRoleRSA: "default", + PKIRoleEC: "default", + PKIRoleEd25519: "default", + RoleID: "roleID", + SecretID: auth.SecretID{FromFile: "secretID"}, + AppRole: "auth/approle", + IsWrappingToken: false, + }, + false, + }, + { + "ok mandatory with SecretID FromEnv", + `{"RoleID": "roleID", "SecretID": {"FromEnv": "secretID"}}`, + &VaultOptions{ + PKI: "pki", + PKIRoleDefault: "default", + PKIRoleRSA: "default", + PKIRoleEC: "default", + PKIRoleEd25519: "default", + RoleID: "roleID", + SecretID: auth.SecretID{FromEnv: "secretID"}, + AppRole: "auth/approle", + IsWrappingToken: false, + }, + false, + }, + { + "ok mandatory PKIRole PKIRoleEd25519", + `{"PKIRoleDefault": "role", "PKIRoleEd25519": "ed25519" , "RoleID": "roleID", "SecretID": {"FromEnv": "secretID"}}`, + &VaultOptions{ + PKI: "pki", + PKIRoleDefault: "role", + PKIRoleRSA: "role", + PKIRoleEC: "role", + PKIRoleEd25519: "ed25519", + RoleID: "roleID", + SecretID: auth.SecretID{FromEnv: "secretID"}, + AppRole: "auth/approle", + IsWrappingToken: false, + }, + false, + }, + { + "ok mandatory PKIRole PKIRoleEC", + `{"PKIRoleDefault": "role", "PKIRoleEC": "ec" , "RoleID": "roleID", "SecretID": {"FromEnv": "secretID"}}`, + &VaultOptions{ + PKI: "pki", + PKIRoleDefault: "role", + PKIRoleRSA: "role", + PKIRoleEC: "ec", + PKIRoleEd25519: "role", + RoleID: "roleID", + SecretID: auth.SecretID{FromEnv: "secretID"}, + AppRole: "auth/approle", + IsWrappingToken: false, + }, + false, + }, + { + "ok mandatory PKIRole PKIRoleRSA", + `{"PKIRoleDefault": "role", "PKIRoleRSA": "rsa" , "RoleID": "roleID", "SecretID": {"FromEnv": "secretID"}}`, + &VaultOptions{ + PKI: "pki", + PKIRoleDefault: "role", + PKIRoleRSA: "rsa", + PKIRoleEC: "role", + PKIRoleEd25519: "role", + RoleID: "roleID", + SecretID: auth.SecretID{FromEnv: "secretID"}, + AppRole: "auth/approle", + IsWrappingToken: false, + }, + false, + }, + { + "ok mandatory PKIRoleRSA PKIRoleEC PKIRoleEd25519", + `{"PKIRoleRSA": "rsa", "PKIRoleEC": "ec", "PKIRoleEd25519": "ed25519", "RoleID": "roleID", "SecretID": {"FromEnv": "secretID"}}`, + &VaultOptions{ + PKI: "pki", + PKIRoleDefault: "default", + PKIRoleRSA: "rsa", + PKIRoleEC: "ec", + PKIRoleEd25519: "ed25519", + RoleID: "roleID", + SecretID: auth.SecretID{FromEnv: "secretID"}, + AppRole: "auth/approle", + IsWrappingToken: false, + }, + false, + }, + { + "ok mandatory PKIRoleRSA PKIRoleEC PKIRoleEd25519 with useless PKIRoleDefault", + `{"PKIRoleDefault": "role", "PKIRoleRSA": "rsa", "PKIRoleEC": "ec", "PKIRoleEd25519": "ed25519", "RoleID": "roleID", "SecretID": {"FromEnv": "secretID"}}`, + &VaultOptions{ + PKI: "pki", + PKIRoleDefault: "role", + PKIRoleRSA: "rsa", + PKIRoleEC: "ec", + PKIRoleEd25519: "ed25519", + RoleID: "roleID", + SecretID: auth.SecretID{FromEnv: "secretID"}, + AppRole: "auth/approle", + IsWrappingToken: false, + }, + false, + }, + { + "ok mandatory with AppRole", + `{"AppRole": "test", "RoleID": "roleID", "SecretID": {"FromString": "secretID"}}`, + &VaultOptions{ + PKI: "pki", + PKIRoleDefault: "default", + PKIRoleRSA: "default", + PKIRoleEC: "default", + PKIRoleEd25519: "default", + RoleID: "roleID", + SecretID: auth.SecretID{FromString: "secretID"}, + AppRole: "test", + IsWrappingToken: false, + }, + false, + }, + { + "ok mandatory with IsWrappingToken", + `{"IsWrappingToken": true, "RoleID": "roleID", "SecretID": {"FromString": "secretID"}}`, + &VaultOptions{ + PKI: "pki", + PKIRoleDefault: "default", + PKIRoleRSA: "default", + PKIRoleEC: "default", + PKIRoleEd25519: "default", + RoleID: "roleID", + SecretID: auth.SecretID{FromString: "secretID"}, + AppRole: "auth/approle", + IsWrappingToken: true, + }, + false, + }, + { + "fail with SecretID FromFail", + `{"RoleID": "roleID", "SecretID": {"FromFail": "secretID"}}`, + nil, + true, + }, + { + "fail with SecretID empty FromEnv", + `{"RoleID": "roleID", "SecretID": {"FromEnv": ""}}`, + nil, + true, + }, + { + "fail with SecretID empty FromFile", + `{"RoleID": "roleID", "SecretID": {"FromFile": ""}}`, + nil, + true, + }, + { + "fail with SecretID empty FromString", + `{"RoleID": "roleID", "SecretID": {"FromString": ""}}`, + nil, + true, + }, + { + "fail mandatory with SecretID FromFail", + `{"RoleID": "roleID", "SecretID": {"FromFail": "secretID"}}`, + nil, + true, + }, + { + "fail missing RoleID", + `{"SecretID": {"FromString": "secretID"}}`, + nil, + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := loadOptions(json.RawMessage(tt.raw)) + if (err != nil) != tt.wantErr { + t.Errorf("VaultCAS.loadOptions() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("VaultCAS.loadOptions() = %v, want %v", got, tt.want) + } + }) + } +}