diff --git a/cas/apiv1/extension_test.go b/cas/apiv1/extension_test.go new file mode 100644 index 00000000..113e3de1 --- /dev/null +++ b/cas/apiv1/extension_test.go @@ -0,0 +1,56 @@ +package apiv1 + +import ( + "crypto/x509/pkix" + "fmt" + "reflect" + "testing" +) + +func TestCreateCertificateAuthorityExtension(t *testing.T) { + type args struct { + typ Type + certificateID string + keyValuePairs []string + } + tests := []struct { + name string + args args + want pkix.Extension + wantErr bool + }{ + {"ok", args{Type(CloudCAS), "1ac75689-cd3f-482e-a695-8a13daf39dc4", nil}, pkix.Extension{ + Id: oidStepCertificateAuthority, + Critical: false, + Value: []byte{ + 0x30, 0x30, 0x13, 0x08, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x63, 0x61, 0x73, 0x13, 0x24, 0x31, 0x61, + 0x63, 0x37, 0x35, 0x36, 0x38, 0x39, 0x2d, 0x63, 0x64, 0x33, 0x66, 0x2d, 0x34, 0x38, 0x32, 0x65, + 0x2d, 0x61, 0x36, 0x39, 0x35, 0x2d, 0x38, 0x61, 0x31, 0x33, 0x64, 0x61, 0x66, 0x33, 0x39, 0x64, + 0x63, 0x34, + }, + }, false}, + {"ok", args{Type(CloudCAS), "1ac75689-cd3f-482e-a695-8a13daf39dc4", []string{"foo", "bar"}}, pkix.Extension{ + Id: oidStepCertificateAuthority, + Critical: false, + Value: []byte{ + 0x30, 0x3c, 0x13, 0x08, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x63, 0x61, 0x73, 0x13, 0x24, 0x31, 0x61, + 0x63, 0x37, 0x35, 0x36, 0x38, 0x39, 0x2d, 0x63, 0x64, 0x33, 0x66, 0x2d, 0x34, 0x38, 0x32, 0x65, + 0x2d, 0x61, 0x36, 0x39, 0x35, 0x2d, 0x38, 0x61, 0x31, 0x33, 0x64, 0x61, 0x66, 0x33, 0x39, 0x64, + 0x63, 0x34, 0x30, 0x0a, 0x13, 0x03, 0x66, 0x6f, 0x6f, 0x13, 0x03, 0x62, 0x61, 0x72, + }, + }, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := CreateCertificateAuthorityExtension(tt.args.typ, tt.args.certificateID, tt.args.keyValuePairs...) + if (err != nil) != tt.wantErr { + t.Errorf("CreateCertificateAuthorityExtension() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("CreateCertificateAuthorityExtension() = %v, want %v", got, tt.want) + fmt.Printf("%x\n", got.Value) + } + }) + } +} diff --git a/cas/cloudcas/certificate.go b/cas/cloudcas/certificate.go index 31da2c7d..6c19adbf 100644 --- a/cas/cloudcas/certificate.go +++ b/cas/cloudcas/certificate.go @@ -70,7 +70,7 @@ func createPublicKey(key crypto.PublicKey) (*pb.PublicKey, error) { return nil, errors.Wrap(err, "error marshaling public key") } return &pb.PublicKey{ - Type: pb.PublicKey_PEM_RSA_KEY, + Type: pb.PublicKey_PEM_EC_KEY, Key: pem.EncodeToMemory(&pem.Block{ Type: "PUBLIC KEY", Bytes: asn1Bytes, @@ -215,9 +215,9 @@ func createReusableConfig(cert *x509.Certificate) *pb.ReusableConfigWrapper { unknownEKUs = append(unknownEKUs, createObjectID(oid)) } - policyIDs := make([]*pb.ObjectId, len(cert.PolicyIdentifiers)) - for i, oid := range cert.PolicyIdentifiers { - policyIDs[i] = createObjectID(oid) + var policyIDs []*pb.ObjectId + for _, oid := range cert.PolicyIdentifiers { + policyIDs = append(policyIDs, createObjectID(oid)) } var caOptions *pb.ReusableConfigValues_CaOptions diff --git a/cas/cloudcas/certificate_test.go b/cas/cloudcas/certificate_test.go new file mode 100644 index 00000000..4c0505c1 --- /dev/null +++ b/cas/cloudcas/certificate_test.go @@ -0,0 +1,213 @@ +package cloudcas + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "reflect" + "testing" + + pb "google.golang.org/genproto/googleapis/cloud/security/privateca/v1beta1" +) + +var ( + testLeafPrivateKey = `-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAdUSRBrpgHFilN4eaGlNnX2+xfjX +a1Iwk2/+AensjFTXJi1UAIB0e+4pqi7Sen5E2QVBhntEHCrA3xOf7czgPw== +-----END PUBLIC KEY----- +` + testRSACertificate = `-----BEGIN CERTIFICATE----- +MIICozCCAkmgAwIBAgIRANNhMpODj7ThgviZCoF6kj8wCgYIKoZIzj0EAwIwKjEo +MCYGA1UEAxMfR29vZ2xlIENBUyBUZXN0IEludGVybWVkaWF0ZSBDQTAeFw0yMDA5 +MTUwMTUxMDdaFw0zMDA5MTMwMTUxMDNaMB0xGzAZBgNVBAMTEnRlc3Quc21hbGxz +dGVwLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANPRjuIlsP5Z +672syAsHlbILFabG/xmrlsO0UdcLo4Yjf9WPAFA+7q+CsVDFh4dQbMv96fsHtdYP +E9wlWyMqYG+5E8QT2i0WNFEoYcXOGZuXdyD/TA5Aucu1RuYLrZXQrXWDnvaWOgvr +EZ6s9VsPCzzkL8KBejIMQIMY0KXEJfB/HgXZNn8V2trZkWT5CzxbcOF3s3UC1Z6F +Ja6zjpxhSyRkqgknJxv6yK4t7HEwdhrDI8uyxJYHPQWKNRjWecHWE9E+MtoS7D08 +mTh8qlAKoBbkGolR2nJSXffU09F3vSg+MIfjPiRqjf6394cQ3T9D5yZK//rCrxWU +8KKBQMEmdKcCAwEAAaOBkTCBjjAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYI +KwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQWBBQffuoYvH1+IF1cipl35gXJxSJE +SjAfBgNVHSMEGDAWgBRIOVqyLDSlErJLuWWEvRm5UU1r1TAdBgNVHREEFjAUghJ0 +ZXN0LnNtYWxsc3RlcC5jb20wCgYIKoZIzj0EAwIDSAAwRQIhAL9AAw/LVLvvxBkM +sJnHd+RIk7ZblkgcArwpIS2+Z5xNAiBtUED4zyimz9b4aQiXdw4IMd2CKxVyW8eE +6x1vSZMvzQ== +-----END CERTIFICATE-----` + testRSAPublicKey = `-----BEGIN RSA PUBLIC KEY----- +MIIBCgKCAQEA09GO4iWw/lnrvazICweVsgsVpsb/GauWw7RR1wujhiN/1Y8AUD7u +r4KxUMWHh1Bsy/3p+we11g8T3CVbIypgb7kTxBPaLRY0UShhxc4Zm5d3IP9MDkC5 +y7VG5gutldCtdYOe9pY6C+sRnqz1Ww8LPOQvwoF6MgxAgxjQpcQl8H8eBdk2fxXa +2tmRZPkLPFtw4XezdQLVnoUlrrOOnGFLJGSqCScnG/rIri3scTB2GsMjy7LElgc9 +BYo1GNZ5wdYT0T4y2hLsPTyZOHyqUAqgFuQaiVHaclJd99TT0Xe9KD4wh+M+JGqN +/rf3hxDdP0PnJkr/+sKvFZTwooFAwSZ0pwIDAQAB +-----END RSA PUBLIC KEY----- +` +) + +func Test_createCertificateConfig(t *testing.T) { + cert := mustParseCertificate(t, testLeafCertificate) + type args struct { + tpl *x509.Certificate + } + tests := []struct { + name string + args args + want *pb.Certificate_Config + wantErr bool + }{ + {"ok", args{cert}, &pb.Certificate_Config{ + Config: &pb.CertificateConfig{ + SubjectConfig: &pb.CertificateConfig_SubjectConfig{ + Subject: &pb.Subject{}, + CommonName: "test.smallstep.com", + SubjectAltName: &pb.SubjectAltNames{ + DnsNames: []string{"test.smallstep.com"}, + }, + }, + ReusableConfig: &pb.ReusableConfigWrapper{ + ConfigValues: &pb.ReusableConfigWrapper_ReusableConfigValues{ + ReusableConfigValues: &pb.ReusableConfigValues{ + KeyUsage: &pb.KeyUsage{ + BaseKeyUsage: &pb.KeyUsage_KeyUsageOptions{ + DigitalSignature: true, + }, + ExtendedKeyUsage: &pb.KeyUsage_ExtendedKeyUsageOptions{ + ClientAuth: true, + ServerAuth: true, + }, + }, + }, + }, + }, + PublicKey: &pb.PublicKey{ + Type: pb.PublicKey_PEM_EC_KEY, + Key: []byte(testLeafPrivateKey), + }, + }, + }, false}, + {"fail", args{&x509.Certificate{}}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := createCertificateConfig(tt.args.tpl) + if (err != nil) != tt.wantErr { + t.Errorf("createCertificateConfig() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("createCertificateConfig() = %v, want %v", got.Config.ReusableConfig, tt.want.Config.ReusableConfig) + } + }) + } +} + +func Test_createPublicKey(t *testing.T) { + edpub, _, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + ecCert := mustParseCertificate(t, testLeafCertificate) + rsaCert := mustParseCertificate(t, testRSACertificate) + type args struct { + key crypto.PublicKey + } + tests := []struct { + name string + args args + want *pb.PublicKey + wantErr bool + }{ + {"ok ec", args{ecCert.PublicKey}, &pb.PublicKey{ + Type: pb.PublicKey_PEM_EC_KEY, + Key: []byte(testLeafPrivateKey), + }, false}, + {"ok rsa", args{rsaCert.PublicKey}, &pb.PublicKey{ + Type: pb.PublicKey_PEM_RSA_KEY, + Key: []byte(testRSAPublicKey), + }, false}, + {"fail ed25519", args{edpub}, nil, true}, + {"fail ec marshal", args{&ecdsa.PublicKey{ + Curve: &elliptic.CurveParams{Name: "FOO", BitSize: 256}, + X: ecCert.PublicKey.(*ecdsa.PublicKey).X, + Y: ecCert.PublicKey.(*ecdsa.PublicKey).Y, + }}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := createPublicKey(tt.args.key) + if (err != nil) != tt.wantErr { + t.Errorf("createPublicKey() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("createPublicKey() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_createSubject(t *testing.T) { + type args struct { + cert *x509.Certificate + } + tests := []struct { + name string + args args + want *pb.Subject + }{ + {"ok empty", args{&x509.Certificate{}}, &pb.Subject{}}, + {"ok all", args{&x509.Certificate{ + Subject: pkix.Name{ + Country: []string{"US"}, + Organization: []string{"Smallstep Labs"}, + OrganizationalUnit: []string{"Engineering"}, + Locality: []string{"San Francisco"}, + Province: []string{"California"}, + StreetAddress: []string{"1 A St."}, + PostalCode: []string{"12345"}, + SerialNumber: "1234567890", + CommonName: "test.smallstep.com", + }, + }}, &pb.Subject{ + CountryCode: "US", + Organization: "Smallstep Labs", + OrganizationalUnit: "Engineering", + Locality: "San Francisco", + Province: "California", + StreetAddress: "1 A St.", + PostalCode: "12345", + }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := createSubject(tt.args.cert); !reflect.DeepEqual(got, tt.want) { + t.Errorf("createSubject() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_createSubjectAlternativeNames(t *testing.T) { + type args struct { + cert *x509.Certificate + } + tests := []struct { + name string + args args + want *pb.SubjectAltNames + }{ + {"ok empty", args{&x509.Certificate{}}, &pb.SubjectAltNames{}}, + // TODO + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := createSubjectAlternativeNames(tt.args.cert); !reflect.DeepEqual(got, tt.want) { + t.Errorf("createSubjectAlternativeNames() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cas/cloudcas/cloudcas.go b/cas/cloudcas/cloudcas.go index 0a7a064d..e6aa9a49 100644 --- a/cas/cloudcas/cloudcas.go +++ b/cas/cloudcas/cloudcas.go @@ -2,15 +2,15 @@ package cloudcas import ( "context" + "crypto/rand" "crypto/x509" "encoding/asn1" - "encoding/json" "encoding/pem" - "fmt" "time" privateca "cloud.google.com/go/security/privateca/apiv1beta1" "github.com/google/uuid" + gax "github.com/googleapis/gax-go/v2" "github.com/pkg/errors" "github.com/smallstep/certificates/cas/apiv1" "google.golang.org/api/option" @@ -24,9 +24,11 @@ func init() { }) } -func debug(v interface{}) { - b, _ := json.MarshalIndent(v, "", " ") - fmt.Println(string(b)) +// CertificateAuthorityClient is the interface implemented by the Google CAS +// client. +type CertificateAuthorityClient interface { + CreateCertificate(ctx context.Context, req *pb.CreateCertificateRequest, opts ...gax.CallOption) (*pb.Certificate, error) + RevokeCertificate(ctx context.Context, req *pb.RevokeCertificateRequest, opts ...gax.CallOption) (*pb.Certificate, error) } var ( @@ -34,13 +36,40 @@ var ( stepOIDCertificateAuthority = append(asn1.ObjectIdentifier(nil), append(stepOIDRoot, 2)...) ) +// recocationCodeMap maps revocation reason codes from RFC 5280, to Google CAS +// revocation reasons. Revocation reason 7 is not used, and revocation reason 8 +// (removeFromCRL) is not supported by Google CAS. +var revocationCodeMap = map[int]pb.RevocationReason{ + 0: pb.RevocationReason_REVOCATION_REASON_UNSPECIFIED, + 1: pb.RevocationReason_KEY_COMPROMISE, + 2: pb.RevocationReason_CERTIFICATE_AUTHORITY_COMPROMISE, + 3: pb.RevocationReason_AFFILIATION_CHANGED, + 4: pb.RevocationReason_SUPERSEDED, + 5: pb.RevocationReason_CESSATION_OF_OPERATION, + 6: pb.RevocationReason_CERTIFICATE_HOLD, + 9: pb.RevocationReason_PRIVILEGE_WITHDRAWN, + 10: pb.RevocationReason_ATTRIBUTE_AUTHORITY_COMPROMISE, +} + // CloudCAS implements a Certificate Authority Service using Google Cloud CAS. type CloudCAS struct { - client *privateca.CertificateAuthorityClient + client CertificateAuthorityClient certificateAuthority string } -type caClient interface{} +// newCertificateAuthorityClient creates the certificate authority client. This +// function is used for testing purposes. +var newCertificateAuthorityClient = func(ctx context.Context, credentialsFile string) (CertificateAuthorityClient, error) { + var cloudOpts []option.ClientOption + if credentialsFile != "" { + cloudOpts = append(cloudOpts, option.WithCredentialsFile(credentialsFile)) + } + client, err := privateca.NewCertificateAuthorityClient(ctx, cloudOpts...) + if err != nil { + return nil, errors.Wrap(err, "error creating client") + } + return client, nil +} // New creates a new CertificateAuthorityService implementation using Google // Cloud CAS. @@ -49,14 +78,9 @@ func New(ctx context.Context, opts apiv1.Options) (*CloudCAS, error) { return nil, errors.New("cloudCAS 'certificateAuthority' cannot be empty") } - var cloudOpts []option.ClientOption - if opts.CredentialsFile != "" { - cloudOpts = append(cloudOpts, option.WithCredentialsFile(opts.CredentialsFile)) - } - - client, err := privateca.NewCertificateAuthorityClient(ctx, cloudOpts...) + client, err := newCertificateAuthorityClient(ctx, opts.CredentialsFile) if err != nil { - return nil, errors.Wrap(err, "error creating client") + return nil, err } return &CloudCAS{ @@ -109,7 +133,11 @@ func (c *CloudCAS) RenewCertificate(req *apiv1.RenewCertificateRequest) (*apiv1. // RevokeCertificate a certificate using Google Cloud CAS. func (c *CloudCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv1.RevokeCertificateResponse, error) { - if req.Certificate == nil { + reason, ok := revocationCodeMap[req.ReasonCode] + switch { + case !ok: + return nil, errors.Errorf("revokeCertificate 'reasonCode=%d' is invalid or not supported", req.ReasonCode) + case req.Certificate == nil: return nil, errors.New("revokeCertificateRequest `certificate` cannot be nil") } @@ -119,7 +147,7 @@ func (c *CloudCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv } var cae apiv1.CertificateAuthorityExtension - if _, err := asn1.Unmarshal(ext.Value, &ext); err != nil { + if _, err := asn1.Unmarshal(ext.Value, &cae); err != nil { return nil, errors.Wrap(err, "error unmarshaling certificate authority extension") } @@ -127,8 +155,9 @@ func (c *CloudCAS) RevokeCertificate(req *apiv1.RevokeCertificateRequest) (*apiv defer cancel() certpb, err := c.client.RevokeCertificate(ctx, &pb.RevokeCertificateRequest{ - Name: c.certificateAuthority + "/certificates/" + cae.CertificateID, - Reason: pb.RevocationReason_REVOCATION_REASON_UNSPECIFIED, + Name: c.certificateAuthority + "/certificates/" + cae.CertificateID, + Reason: reason, + RequestId: req.RequestID, }) if err != nil { return nil, errors.Wrap(err, "cloudCAS RevokeCertificate failed") @@ -192,7 +221,7 @@ func defaultContext() (context.Context, context.CancelFunc) { } func createCertificateID() (string, error) { - id, err := uuid.NewRandom() + id, err := uuid.NewRandomFromReader(rand.Reader) if err != nil { return "", errors.Wrap(err, "error creating certificate id") } diff --git a/cas/cloudcas/cloudcas_test.go b/cas/cloudcas/cloudcas_test.go new file mode 100644 index 00000000..56579ce6 --- /dev/null +++ b/cas/cloudcas/cloudcas_test.go @@ -0,0 +1,581 @@ +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) + } + }) + } +}