feat: enhance options and fix revoke plus more tests

This commit is contained in:
Ahmet DEMIR 2022-01-27 11:14:19 +01:00
parent 8ef3abf6d9
commit b49ac2501b
No known key found for this signature in database
GPG key ID: 7F0E92AFAC67CDD5
2 changed files with 467 additions and 29 deletions

View file

@ -26,7 +26,7 @@ func init() {
type VaultOptions struct { type VaultOptions struct {
PKI string `json:"pki,omitempty"` PKI string `json:"pki,omitempty"`
PKIRole string `json:"pkiRole,omitempty"` PKIRoleDefault string `json:"PKIRoleDefault,omitempty"`
PKIRoleRSA string `json:"pkiRoleRSA,omitempty"` PKIRoleRSA string `json:"pkiRoleRSA,omitempty"`
PKIRoleEC string `json:"pkiRoleEC,omitempty"` PKIRoleEC string `json:"pkiRoleEC,omitempty"`
PKIRoleEd25519 string `json:"PKIRoleEd25519,omitempty"` PKIRoleEd25519 string `json:"PKIRoleEd25519,omitempty"`
@ -43,45 +43,38 @@ type VaultCAS struct {
fingerprint string fingerprint string
} }
func loadOptions(config json.RawMessage) (vc VaultOptions, err error) { func loadOptions(config json.RawMessage) (*VaultOptions, error) {
err = json.Unmarshal(config, &vc) var vc *VaultOptions
err := json.Unmarshal(config, &vc)
if err != nil { if err != nil {
return vc, errors.Wrap(err, "error decoding vaultCAS config") return nil, errors.Wrap(err, "error decoding vaultCAS config")
} }
if vc.PKI == "" { if vc.PKI == "" {
vc.PKI = "pki" // use default pki vault name vc.PKI = "pki" // use default pki vault name
} }
// pkirole or per key type must be defined if vc.PKIRoleDefault == "" {
if vc.PKIRole == "" && vc.PKIRoleRSA == "" && vc.PKIRoleEC == "" && vc.PKIRoleEd25519 == "" { vc.PKIRoleDefault = "default" // use default pki role name
return vc, errors.New("vaultCAS config options must define `pkiRole`")
} }
// if pkirole is empty all others keys must be set if vc.PKIRoleRSA == "" {
if vc.PKIRole == "" && (vc.PKIRoleRSA == "" || vc.PKIRoleEC == "" || vc.PKIRoleEd25519 == "") { vc.PKIRoleRSA = vc.PKIRoleDefault
return vc, errors.New("vaultCAS config options must include a `pkiRole` or `pkiRoleRSA`, `pkiRoleEC` and `PKIRoleEd25519`")
} }
if vc.PKIRoleEC == "" {
// if pkirole is not empty, use it as default for unset keys vc.PKIRoleEC = vc.PKIRoleDefault
if vc.PKIRole != "" { }
if vc.PKIRoleRSA == "" { if vc.PKIRoleEd25519 == "" {
vc.PKIRoleRSA = vc.PKIRole vc.PKIRoleEd25519 = vc.PKIRoleDefault
}
if vc.PKIRoleEC == "" {
vc.PKIRoleEC = vc.PKIRole
}
if vc.PKIRoleEd25519 == "" {
vc.PKIRoleEd25519 = vc.PKIRole
}
} }
if vc.RoleID == "" { 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 == "" { 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 == "" { if vc.PKI == "" {
@ -237,7 +230,7 @@ func New(ctx context.Context, opts apiv1.Options) (*VaultCAS, error) {
return &VaultCAS{ return &VaultCAS{
client: client, client: client,
config: vc, config: *vc,
fingerprint: opts.CertificateAuthorityFingerprint, fingerprint: opts.CertificateAuthorityFingerprint,
}, nil }, nil
} }
@ -326,7 +319,11 @@ func (v *VaultCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv
serialNumberDash := certutil.GetHexFormatted(serialNumber, "-") 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 { if err != nil {
return nil, errors.Wrap(err, "unable to revoke certificate") return nil, errors.Wrap(err, "unable to revoke certificate")
} }

View file

@ -1,6 +1,7 @@
package vaultcas package vaultcas
import ( import (
"bytes"
"context" "context"
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
@ -60,7 +61,17 @@ pV/v53oR/ewbtrkHZQkN/amFMLagITAfBgkqhkiG9w0BCQ4xEjAQMA4GA1UdEQQH
MAWCA09LUDAFBgMrZXADQQDJi47MAgl/WKAz+V/kDu1k/zbKk1nrHHAUonbofHUW MAWCA09LUDAFBgMrZXADQQDJi47MAgl/WKAz+V/kDu1k/zbKk1nrHHAUonbofHUW
M6ihSD43+awq3BPeyPbToeH5orSH9l3MuTfbxPb5BVEH M6ihSD43+awq3BPeyPbToeH5orSH9l3MuTfbxPb5BVEH
-----END CERTIFICATE REQUEST-----` -----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 { 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}} cert := map[string]interface{}{"data": map[string]interface{}{"certificate": testCertificateSigned}}
writeJSON(w, cert) writeJSON(w, cert)
return 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: default:
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, `{"error":"not found"}`) fmt.Fprintf(w, `{"error":"not found"}`)
@ -151,7 +186,7 @@ func TestNew_register(t *testing.T) {
CertificateAuthorityFingerprint: testRootFingerprint, CertificateAuthorityFingerprint: testRootFingerprint,
Config: json.RawMessage(`{ Config: json.RawMessage(`{
"PKI": "pki", "PKI": "pki",
"PKIRole": "pki-role", "PKIRoleDefault": "pki-role",
"RoleID": "roleID", "RoleID": "roleID",
"SecretID": {"FromString": "secretID"}, "SecretID": {"FromString": "secretID"},
"IsWrappingToken": false "IsWrappingToken": false
@ -169,7 +204,7 @@ func TestVaultCAS_CreateCertificate(t *testing.T) {
options := VaultOptions{ options := VaultOptions{
PKI: "pki", PKI: "pki",
PKIRole: "role", PKIRoleDefault: "role",
PKIRoleRSA: "rsa", PKIRoleRSA: "rsa",
PKIRoleEC: "ec", PKIRoleEC: "ec",
PKIRoleEd25519: "ed25519", PKIRoleEd25519: "ed25519",
@ -216,6 +251,14 @@ func TestVaultCAS_CreateCertificate(t *testing.T) {
Certificate: mustParseCertificate(t, testCertificateSigned), Certificate: mustParseCertificate(t, testCertificateSigned),
CertificateChain: []*x509.Certificate{}, CertificateChain: []*x509.Certificate{},
}, false}, }, 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 { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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)
}
})
}
}