package cloudcas import ( "bytes" "context" "crypto/rand" "crypto/x509" "encoding/asn1" "io" "os" "reflect" "testing" "time" "github.com/google/uuid" gax "github.com/googleapis/gax-go/v2" "github.com/pkg/errors" "github.com/smallstep/certificates/cas/apiv1" pb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1beta1" ) var ( errTest = errors.New("test error") testAuthorityName = "projects/test-project/locations/us-west1/certificateAuthorities/test-ca" testCertificateName = "projects/test-project/locations/us-west1/certificateAuthorities/test-ca/certificates/test-certificate" testRootCertificate = `-----BEGIN CERTIFICATE----- MIIBhjCCAS2gAwIBAgIQLbKTuXau4+t3KFbGpJJAADAKBggqhkjOPQQDAjAiMSAw HgYDVQQDExdHb29nbGUgQ0FTIFRlc3QgUm9vdCBDQTAeFw0yMDA5MTQyMjQ4NDla Fw0zMDA5MTIyMjQ4NDlaMCIxIDAeBgNVBAMTF0dvb2dsZSBDQVMgVGVzdCBSb290 IENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYKGgQ3/0D7+oBTc0CXoYfSC6 M8hOqLsmzBapPZSYpfwjgEsjdNU84jdrYmW1zF1+p+MrL4c7qJv9NLo/picCuqNF MEMwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYE FFVn9V7Qymd7cUJh9KAhnUDAQL5YMAoGCCqGSM49BAMCA0cAMEQCIA4LzttYoT3u 8TYgSrvFT+Z+cklfi4UrPBU6aSbcUaW2AiAPfaqbyccQT3CxMVyHg+xZZjAirZp8 lAeA/T4FxAonHA== -----END CERTIFICATE-----` testIntermediateCertificate = `-----BEGIN CERTIFICATE----- MIIBsDCCAVagAwIBAgIQOb91kHxWKVzSJ9ESW1ViVzAKBggqhkjOPQQDAjAiMSAw HgYDVQQDExdHb29nbGUgQ0FTIFRlc3QgUm9vdCBDQTAeFw0yMDA5MTQyMjQ4NDla Fw0zMDA5MTIyMjQ4NDlaMCoxKDAmBgNVBAMTH0dvb2dsZSBDQVMgVGVzdCBJbnRl cm1lZGlhdGUgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASUHN1cNyId4Ei/ 4MxD5VrZFc51P50caMUdDZVrPveidChBYCU/9IM6vnRlZHx2HLjQ0qAvqHwY3rT0 xc7n+PfCo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADAd BgNVHQ4EFgQUSDlasiw0pRKyS7llhL0ZuVFNa9UwHwYDVR0jBBgwFoAUVWf1XtDK Z3txQmH0oCGdQMBAvlgwCgYIKoZIzj0EAwIDSAAwRQIgMmsLcoC4KriXw+s+cZx2 bJMf6Mx/WESj31buJJhpzY0CIQCBUa/JtvS3nyce/4DF5tK2v49/NWHREgqAaZ57 DcYyHQ== -----END CERTIFICATE-----` testLeafCertificate = `-----BEGIN CERTIFICATE----- MIIB1jCCAX2gAwIBAgIQQfOn+COMeuD8VYF1TiDkEzAKBggqhkjOPQQDAjAqMSgw JgYDVQQDEx9Hb29nbGUgQ0FTIFRlc3QgSW50ZXJtZWRpYXRlIENBMB4XDTIwMDkx NDIyNTE1NVoXDTMwMDkxMjIyNTE1MlowHTEbMBkGA1UEAxMSdGVzdC5zbWFsbHN0 ZXAuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAdUSRBrpgHFilN4eaGlN nX2+xfjXa1Iwk2/+AensjFTXJi1UAIB0e+4pqi7Sen5E2QVBhntEHCrA3xOf7czg P6OBkTCBjjAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG AQUFBwMCMB0GA1UdDgQWBBSYPbu4Tmm7Zze/hCePeZH1Avoj+jAfBgNVHSMEGDAW gBRIOVqyLDSlErJLuWWEvRm5UU1r1TAdBgNVHREEFjAUghJ0ZXN0LnNtYWxsc3Rl cC5jb20wCgYIKoZIzj0EAwIDRwAwRAIgY+nTc+RHn31/BOhht4JpxCmJPHxqFT3S ojnictBudV0CIB87ipY5HV3c8FLVEzTA0wFwdDZvQraQYsthwbg2kQFb -----END CERTIFICATE-----` testSignedCertificate = `-----BEGIN CERTIFICATE----- MIIB/DCCAaKgAwIBAgIQHHFuGMz0cClfde5kqP5prTAKBggqhkjOPQQDAjAqMSgw JgYDVQQDEx9Hb29nbGUgQ0FTIFRlc3QgSW50ZXJtZWRpYXRlIENBMB4XDTIwMDkx NTAwMDQ0M1oXDTMwMDkxMzAwMDQ0MFowHTEbMBkGA1UEAxMSdGVzdC5zbWFsbHN0 ZXAuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMqNCiXMvbn74LsHzRv+8 17m9vEzH6RHrg3m82e0uEc36+fZWV/zJ9SKuONmnl5VP79LsjL5SVH0RDj73U2XO DKOBtjCBszAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG AQUFBwMCMB0GA1UdDgQWBBRTA2cTs7PCNjnps/+T0dS8diqv0DAfBgNVHSMEGDAW gBRIOVqyLDSlErJLuWWEvRm5UU1r1TBCBgwrBgEEAYKkZMYoQAIEMjAwEwhjbG91 ZGNhcxMkZDhkMThhNjgtNTI5Ni00YWYzLWFlNGItMmY4NzdkYTNmYmQ5MAoGCCqG SM49BAMCA0gAMEUCIGxl+pqJ50WYWUqK2l4V1FHoXSi0Nht5kwTxFxnWZu1xAiEA zemu3bhWLFaGg3s8i+HTEhw4RqkHP74vF7AVYp88bAw= -----END CERTIFICATE-----` ) type testClient struct { credentialsFile string certificate *pb.Certificate err error } func newTestClient(credentialsFile string) (CertificateAuthorityClient, error) { if credentialsFile == "testdata/error.json" { return nil, errTest } return &testClient{ credentialsFile: credentialsFile, }, nil } func okTestClient() *testClient { return &testClient{ credentialsFile: "testdata/credentials.json", certificate: &pb.Certificate{ Name: testCertificateName, PemCertificate: testSignedCertificate, PemCertificateChain: []string{testIntermediateCertificate, testRootCertificate}, }, } } func failTestClient() *testClient { return &testClient{ credentialsFile: "testdata/credentials.json", err: errTest, } } func badTestClient() *testClient { return &testClient{ credentialsFile: "testdata/credentials.json", certificate: &pb.Certificate{ Name: testCertificateName, PemCertificate: "not a pem cert", PemCertificateChain: []string{testIntermediateCertificate, testRootCertificate}, }, } } func setTeeReader(t *testing.T, w *bytes.Buffer) { t.Helper() reader := rand.Reader t.Cleanup(func() { rand.Reader = reader }) rand.Reader = io.TeeReader(reader, w) } func (c *testClient) CreateCertificate(ctx context.Context, req *pb.CreateCertificateRequest, opts ...gax.CallOption) (*pb.Certificate, error) { return c.certificate, c.err } func (c *testClient) RevokeCertificate(ctx context.Context, req *pb.RevokeCertificateRequest, opts ...gax.CallOption) (*pb.Certificate, error) { return c.certificate, c.err } func mustParseCertificate(t *testing.T, pemCert string) *x509.Certificate { t.Helper() crt, err := parseCertificate(pemCert) if err != nil { t.Fatal(err) } return crt } func TestNew(t *testing.T) { tmp := newCertificateAuthorityClient newCertificateAuthorityClient = func(ctx context.Context, credentialsFile string) (CertificateAuthorityClient, error) { return newTestClient(credentialsFile) } t.Cleanup(func() { newCertificateAuthorityClient = tmp }) type args struct { ctx context.Context opts apiv1.Options } tests := []struct { name string args args want *CloudCAS wantErr bool }{ {"ok", args{context.Background(), apiv1.Options{ Certificateauthority: testAuthorityName, }}, &CloudCAS{ client: &testClient{}, certificateAuthority: testAuthorityName, }, false}, {"ok with credentials", args{context.Background(), apiv1.Options{ Certificateauthority: testAuthorityName, CredentialsFile: "testdata/credentials.json", }}, &CloudCAS{ client: &testClient{credentialsFile: "testdata/credentials.json"}, certificateAuthority: testAuthorityName, }, false}, {"fail certificate authority", args{context.Background(), apiv1.Options{}}, nil, true}, {"fail with credentials", args{context.Background(), apiv1.Options{ Certificateauthority: testAuthorityName, CredentialsFile: "testdata/error.json", }}, nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := New(tt.args.ctx, tt.args.opts) if (err != nil) != tt.wantErr { t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("New() = %v, want %v", got, tt.want) } }) } } func TestNew_real(t *testing.T) { if v, ok := os.LookupEnv("GOOGLE_APPLICATION_CREDENTIALS"); ok { os.Unsetenv("GOOGLE_APPLICATION_CREDENTIALS") t.Cleanup(func() { os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", v) }) } type args struct { ctx context.Context opts apiv1.Options } tests := []struct { name string args args wantErr bool }{ {"fail default credentials", args{context.Background(), apiv1.Options{Certificateauthority: testAuthorityName}}, true}, {"fail certificate authority", args{context.Background(), apiv1.Options{}}, true}, {"fail with credentials", args{context.Background(), apiv1.Options{ Certificateauthority: testAuthorityName, CredentialsFile: "testdata/missing.json", }}, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := New(tt.args.ctx, tt.args.opts) if (err != nil) != tt.wantErr { t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestCloudCAS_CreateCertificate(t *testing.T) { type fields struct { client CertificateAuthorityClient certificateAuthority string } type args struct { req *apiv1.CreateCertificateRequest } tests := []struct { name string fields fields args args want *apiv1.CreateCertificateResponse wantErr bool }{ {"ok", fields{okTestClient(), testCertificateName}, args{&apiv1.CreateCertificateRequest{ Template: mustParseCertificate(t, testLeafCertificate), Lifetime: 24 * time.Hour, }}, &apiv1.CreateCertificateResponse{ Certificate: mustParseCertificate(t, testSignedCertificate), CertificateChain: []*x509.Certificate{mustParseCertificate(t, testIntermediateCertificate)}, }, false}, {"fail Template", fields{okTestClient(), testCertificateName}, args{&apiv1.CreateCertificateRequest{ Lifetime: 24 * time.Hour, }}, nil, true}, {"fail Lifetime", fields{okTestClient(), testCertificateName}, args{&apiv1.CreateCertificateRequest{ Template: mustParseCertificate(t, testLeafCertificate), }}, nil, true}, {"fail CreateCertificate", fields{failTestClient(), testCertificateName}, args{&apiv1.CreateCertificateRequest{ Template: mustParseCertificate(t, testLeafCertificate), Lifetime: 24 * time.Hour, }}, nil, true}, {"fail Certificate", fields{badTestClient(), testCertificateName}, args{&apiv1.CreateCertificateRequest{ Template: mustParseCertificate(t, testLeafCertificate), Lifetime: 24 * time.Hour, }}, nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &CloudCAS{ client: tt.fields.client, certificateAuthority: tt.fields.certificateAuthority, } got, err := c.CreateCertificate(tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("CloudCAS.CreateCertificate() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("CloudCAS.CreateCertificate() = %v, want %v", got, tt.want) } }) } } func TestCloudCAS_createCertificate(t *testing.T) { leaf := mustParseCertificate(t, testLeafCertificate) signed := mustParseCertificate(t, testSignedCertificate) chain := []*x509.Certificate{mustParseCertificate(t, testIntermediateCertificate)} type fields struct { client CertificateAuthorityClient certificateAuthority string } type args struct { tpl *x509.Certificate lifetime time.Duration requestID string } tests := []struct { name string fields fields args args want *x509.Certificate want1 []*x509.Certificate wantErr bool }{ {"ok", fields{okTestClient(), testAuthorityName}, args{leaf, 24 * time.Hour, "request-id"}, signed, chain, false}, {"fail CertificateConfig", fields{okTestClient(), testAuthorityName}, args{&x509.Certificate{}, 24 * time.Hour, "request-id"}, nil, nil, true}, {"fail CreateCertificate", fields{failTestClient(), testAuthorityName}, args{leaf, 24 * time.Hour, "request-id"}, nil, nil, true}, {"fail ParseCertificates", fields{badTestClient(), testAuthorityName}, args{leaf, 24 * time.Hour, "request-id"}, nil, nil, true}, {"fail create id", fields{okTestClient(), testAuthorityName}, args{leaf, 24 * time.Hour, "request-id"}, nil, nil, true}, } // Pre-calulate rand.Random buf := new(bytes.Buffer) setTeeReader(t, buf) for i := 0; i < len(tests)-1; i++ { _, err := uuid.NewRandomFromReader(rand.Reader) if err != nil { t.Fatal(err) } } rand.Reader = buf for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &CloudCAS{ client: tt.fields.client, certificateAuthority: tt.fields.certificateAuthority, } got, got1, err := c.createCertificate(tt.args.tpl, tt.args.lifetime, tt.args.requestID) if (err != nil) != tt.wantErr { t.Errorf("CloudCAS.createCertificate() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("CloudCAS.createCertificate() got = %v, want %v", got, tt.want) } if !reflect.DeepEqual(got1, tt.want1) { t.Errorf("CloudCAS.createCertificate() got1 = %v, want %v", got1, tt.want1) } }) } } func TestCloudCAS_RenewCertificate(t *testing.T) { type fields struct { client CertificateAuthorityClient certificateAuthority string } type args struct { req *apiv1.RenewCertificateRequest } tests := []struct { name string fields fields args args want *apiv1.RenewCertificateResponse wantErr bool }{ {"ok", fields{okTestClient(), testCertificateName}, args{&apiv1.RenewCertificateRequest{ Template: mustParseCertificate(t, testLeafCertificate), Lifetime: 24 * time.Hour, }}, &apiv1.RenewCertificateResponse{ Certificate: mustParseCertificate(t, testSignedCertificate), CertificateChain: []*x509.Certificate{mustParseCertificate(t, testIntermediateCertificate)}, }, false}, {"fail Template", fields{okTestClient(), testCertificateName}, args{&apiv1.RenewCertificateRequest{ Lifetime: 24 * time.Hour, }}, nil, true}, {"fail Lifetime", fields{okTestClient(), testCertificateName}, args{&apiv1.RenewCertificateRequest{ Template: mustParseCertificate(t, testLeafCertificate), }}, nil, true}, {"fail CreateCertificate", fields{failTestClient(), testCertificateName}, args{&apiv1.RenewCertificateRequest{ Template: mustParseCertificate(t, testLeafCertificate), Lifetime: 24 * time.Hour, }}, nil, true}, {"fail Certificate", fields{badTestClient(), testCertificateName}, args{&apiv1.RenewCertificateRequest{ Template: mustParseCertificate(t, testLeafCertificate), Lifetime: 24 * time.Hour, }}, nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &CloudCAS{ client: tt.fields.client, certificateAuthority: tt.fields.certificateAuthority, } got, err := c.RenewCertificate(tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("CloudCAS.RenewCertificate() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("CloudCAS.RenewCertificate() = %v, want %v", got, tt.want) } }) } } func TestCloudCAS_RevokeCertificate(t *testing.T) { badExtensionCert := mustParseCertificate(t, testSignedCertificate) for i, ext := range badExtensionCert.Extensions { if ext.Id.Equal(asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 37476, 9000, 64, 2}) { badExtensionCert.Extensions[i].Value = []byte("bad-data") } } type fields struct { client CertificateAuthorityClient certificateAuthority string } type args struct { req *apiv1.RevokeCertificateRequest } tests := []struct { name string fields fields args args want *apiv1.RevokeCertificateResponse wantErr bool }{ {"ok", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{ Certificate: mustParseCertificate(t, testSignedCertificate), ReasonCode: 1, }}, &apiv1.RevokeCertificateResponse{ Certificate: mustParseCertificate(t, testSignedCertificate), CertificateChain: []*x509.Certificate{mustParseCertificate(t, testIntermediateCertificate)}, }, false}, {"fail Extension", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{ Certificate: mustParseCertificate(t, testLeafCertificate), ReasonCode: 1, }}, nil, true}, {"fail Extension Value", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{ Certificate: badExtensionCert, ReasonCode: 1, }}, nil, true}, {"fail Certificate", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{ ReasonCode: 2, }}, nil, true}, {"fail ReasonCode", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{ Certificate: mustParseCertificate(t, testSignedCertificate), ReasonCode: 100, }}, nil, true}, {"fail ReasonCode 7", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{ Certificate: mustParseCertificate(t, testSignedCertificate), ReasonCode: 7, }}, nil, true}, {"fail ReasonCode 8", fields{okTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{ Certificate: mustParseCertificate(t, testSignedCertificate), ReasonCode: 8, }}, nil, true}, {"fail RevokeCertificate", fields{failTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{ Certificate: mustParseCertificate(t, testSignedCertificate), ReasonCode: 1, }}, nil, true}, {"fail ParseCertificate", fields{badTestClient(), testCertificateName}, args{&apiv1.RevokeCertificateRequest{ Certificate: mustParseCertificate(t, testSignedCertificate), ReasonCode: 1, }}, nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &CloudCAS{ client: tt.fields.client, certificateAuthority: tt.fields.certificateAuthority, } got, err := c.RevokeCertificate(tt.args.req) if (err != nil) != tt.wantErr { t.Errorf("CloudCAS.RevokeCertificate() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("CloudCAS.RevokeCertificate() = %v, want %v", got, tt.want) } }) } } func Test_createCertificateID(t *testing.T) { buf := new(bytes.Buffer) setTeeReader(t, buf) uuid, err := uuid.NewRandomFromReader(rand.Reader) if err != nil { t.Fatal(err) } rand.Reader = buf tests := []struct { name string want string wantErr bool }{ {"ok", uuid.String(), false}, {"fail", "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := createCertificateID() if (err != nil) != tt.wantErr { t.Errorf("createCertificateID() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { t.Errorf("createCertificateID() = %v, want %v", got, tt.want) } }) } } func Test_parseCertificate(t *testing.T) { type args struct { pemCert string } tests := []struct { name string args args want *x509.Certificate wantErr bool }{ {"ok", args{testLeafCertificate}, mustParseCertificate(t, testLeafCertificate), false}, {"ok intermediate", args{testIntermediateCertificate}, mustParseCertificate(t, testIntermediateCertificate), false}, {"fail pem", args{"not pem"}, nil, true}, {"fail parseCertificate", args{"-----BEGIN CERTIFICATE-----\nZm9vYmFyCg==\n-----END CERTIFICATE-----\n"}, nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := parseCertificate(tt.args.pemCert) if (err != nil) != tt.wantErr { t.Errorf("parseCertificate() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("parseCertificate() = %v, want %v", got, tt.want) } }) } } func Test_getCertificateAndChain(t *testing.T) { type args struct { certpb *pb.Certificate } tests := []struct { name string args args want *x509.Certificate want1 []*x509.Certificate wantErr bool }{ {"ok", args{&pb.Certificate{ Name: testCertificateName, PemCertificate: testSignedCertificate, PemCertificateChain: []string{testIntermediateCertificate, testRootCertificate}, }}, mustParseCertificate(t, testSignedCertificate), []*x509.Certificate{mustParseCertificate(t, testIntermediateCertificate)}, false}, {"fail PemCertificate", args{&pb.Certificate{ Name: testCertificateName, PemCertificate: "foobar", PemCertificateChain: []string{testIntermediateCertificate, testRootCertificate}, }}, nil, nil, true}, {"fail PemCertificateChain", args{&pb.Certificate{ Name: testCertificateName, PemCertificate: testSignedCertificate, PemCertificateChain: []string{"foobar", testRootCertificate}, }}, nil, nil, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, got1, err := getCertificateAndChain(tt.args.certpb) if (err != nil) != tt.wantErr { t.Errorf("getCertificateAndChain() error = %v, wantErr %v", err, tt.wantErr) return } if !reflect.DeepEqual(got, tt.want) { t.Errorf("getCertificateAndChain() got = %v, want %v", got, tt.want) } if !reflect.DeepEqual(got1, tt.want1) { t.Errorf("getCertificateAndChain() got1 = %v, want %v", got1, tt.want1) } }) } }