diff --git a/.dockerignore b/.dockerignore index f32fbc94f..a3ff5fb7c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -14,3 +14,4 @@ charts bundle/client/grpc/grpc bundle/client/coap/coap bundle/client/ob/ob +test-local diff --git a/certificate-authority/config.yaml b/certificate-authority/config.yaml index d96d07fc8..754a1ec13 100644 --- a/certificate-authority/config.yaml +++ b/certificate-authority/config.yaml @@ -48,6 +48,7 @@ apis: tokenTrustVerification: cacheExpiration: 30s http: + externalAddress: "https://0.0.0.0:9101" address: "0.0.0.0:9101" readTimeout: 8s readHeaderTimeout: 4s @@ -68,10 +69,6 @@ clients: keyFile: "/secrets/private/cert.key" certFile: "/secrets/public/cert.crt" useSystemCAPool: false - bulkWrite: - timeout: 1m0s - throttleTime: 500ms - documentLimit: 1000 cqlDB: table: "signedCertificateRecords" hosts: [] @@ -115,3 +112,5 @@ signer: certFile: "/secrets/public/intermediateca.crt" validFrom: "now-1h" expiresIn: "87600h" + crl: + expiresIn: "10m" diff --git a/certificate-authority/pb/README.md b/certificate-authority/pb/README.md index 0ca7dd372..34c6d0f06 100644 --- a/certificate-authority/pb/README.md +++ b/certificate-authority/pb/README.md @@ -89,8 +89,8 @@ | ----------- | ------------ | ------------- | ------------| | SignIdentityCertificate | [SignCertificateRequest](#certificateauthority-pb-SignCertificateRequest) | [SignCertificateResponse](#certificateauthority-pb-SignCertificateResponse) | SignIdentityCertificate sends a Identity Certificate Signing Request to the certificate authority and obtains a signed certificate. Both in the PEM format. It adds EKU: '1.3.6.1.4.1.44924.1.6' . | | SignCertificate | [SignCertificateRequest](#certificateauthority-pb-SignCertificateRequest) | [SignCertificateResponse](#certificateauthority-pb-SignCertificateResponse) | SignCertificate sends a Certificate Signing Request to the certificate authority and obtains a signed certificate. Both in the PEM format. | -| GetSigningRecords | [GetSigningRecordsRequest](#certificateauthority-pb-GetSigningRecordsRequest) | [SigningRecord](#certificateauthority-pb-SigningRecord) stream | Get signed certficate records. | -| DeleteSigningRecords | [DeleteSigningRecordsRequest](#certificateauthority-pb-DeleteSigningRecordsRequest) | [DeletedSigningRecords](#certificateauthority-pb-DeletedSigningRecords) | Delete signed certficate records. | +| GetSigningRecords | [GetSigningRecordsRequest](#certificateauthority-pb-GetSigningRecordsRequest) | [SigningRecord](#certificateauthority-pb-SigningRecord) stream | Get signed certificate records. | +| DeleteSigningRecords | [DeleteSigningRecordsRequest](#certificateauthority-pb-DeleteSigningRecordsRequest) | [DeletedSigningRecords](#certificateauthority-pb-DeletedSigningRecords) | Revoke signed certficate or delete expired signed certificate records. | @@ -120,6 +120,12 @@ | valid_until_date | [int64](#int64) | | Record valid until date, in unix nanoseconds timestamp format @gotags: bson:"validUntilDate" | +| serial | [string](#string) | | Serial number of the last certificat issued + +@gotags: bson:"serial" | +| issuer_id | [string](#string) | | Issuer id is calculated from the issuer's public certificate, and it is computed as uuid.NewSHA1(uuid.NameSpaceX500, publicKeyRaw) + +@gotags: bson:"issuerId" | @@ -145,7 +151,7 @@ ### DeletedSigningRecords - +Revoke or delete certificates | Field | Type | Label | Description | diff --git a/certificate-authority/pb/doc.html b/certificate-authority/pb/doc.html index 68d51f2f0..e0b12bf80 100644 --- a/certificate-authority/pb/doc.html +++ b/certificate-authority/pb/doc.html @@ -346,14 +346,14 @@

CertificateAuthority

GetSigningRecords GetSigningRecordsRequest SigningRecord stream -

Get signed certficate records.

+

Get signed certificate records.

DeleteSigningRecords DeleteSigningRecordsRequest DeletedSigningRecords -

Delete signed certficate records.

+

Revoke signed certficate or delete expired signed certificate records.

@@ -463,6 +463,24 @@

CredentialStatus

@gotags: bson:"validUntilDate"

+ + serial + string + +

Serial number of the last certificat issued + +@gotags: bson:"serial"

+ + + + issuer_id + string + +

Issuer id is calculated from the issuer's public certificate, and it is computed as uuid.NewSHA1(uuid.NameSpaceX500, publicKeyRaw) + +@gotags: bson:"issuerId"

+ + @@ -502,7 +520,7 @@

DeleteSigningRecord

DeletedSigningRecords

-

+

Revoke or delete certificates

diff --git a/certificate-authority/pb/service.proto b/certificate-authority/pb/service.proto index 7efca0723..439315af4 100644 --- a/certificate-authority/pb/service.proto +++ b/certificate-authority/pb/service.proto @@ -56,7 +56,7 @@ service CertificateAuthority { }; } - // Get signed certficate records. + // Get signed certificate records. rpc GetSigningRecords (GetSigningRecordsRequest) returns (stream SigningRecord) { option (google.api.http) = { get: "/api/v1/signing/records" @@ -66,7 +66,7 @@ service CertificateAuthority { }; }; - // Delete signed certficate records. + // Revoke signed certficate or delete expired signed certificate records. rpc DeleteSigningRecords (DeleteSigningRecordsRequest) returns (DeletedSigningRecords) { option (google.api.http) = { delete: "/api/v1/signing/records" diff --git a/certificate-authority/pb/service.swagger.json b/certificate-authority/pb/service.swagger.json index 2db104ed0..93bb25f90 100644 --- a/certificate-authority/pb/service.swagger.json +++ b/certificate-authority/pb/service.swagger.json @@ -98,7 +98,7 @@ }, "/api/v1/signing/records": { "get": { - "summary": "Get signed certficate records.", + "summary": "Get signed certificate records.", "operationId": "CertificateAuthority_GetSigningRecords", "responses": { "200": { @@ -163,7 +163,7 @@ ] }, "delete": { - "summary": "Delete signed certficate records.", + "summary": "Revoke signed certficate or delete expired signed certificate records.", "operationId": "CertificateAuthority_DeleteSigningRecords", "responses": { "200": { @@ -227,6 +227,16 @@ "format": "int64", "description": "@gotags: bson:\"validUntilDate\"", "title": "Record valid until date, in unix nanoseconds timestamp format" + }, + "serial": { + "type": "string", + "description": "@gotags: bson:\"serial\"", + "title": "Serial number of the last certificat issued" + }, + "issuerId": { + "type": "string", + "description": "@gotags: bson:\"issuerId\"", + "title": "Issuer id is calculated from the issuer's public certificate, and it is computed as uuid.NewSHA1(uuid.NameSpaceX500, publicKeyRaw)" } } }, @@ -238,7 +248,8 @@ "format": "int64", "description": "Number of deleted records." } - } + }, + "title": "Revoke or delete certificates" }, "pbSignCertificateRequest": { "type": "object", diff --git a/certificate-authority/pb/service_grpc.pb.go b/certificate-authority/pb/service_grpc.pb.go index 71dd16956..67bda0a08 100644 --- a/certificate-authority/pb/service_grpc.pb.go +++ b/certificate-authority/pb/service_grpc.pb.go @@ -35,9 +35,9 @@ type CertificateAuthorityClient interface { // SignCertificate sends a Certificate Signing Request to the certificate authority // and obtains a signed certificate. Both in the PEM format. SignCertificate(ctx context.Context, in *SignCertificateRequest, opts ...grpc.CallOption) (*SignCertificateResponse, error) - // Get signed certficate records. + // Get signed certificate records. GetSigningRecords(ctx context.Context, in *GetSigningRecordsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[SigningRecord], error) - // Delete signed certficate records. + // Revoke signed certficate or delete expired signed certificate records. DeleteSigningRecords(ctx context.Context, in *DeleteSigningRecordsRequest, opts ...grpc.CallOption) (*DeletedSigningRecords, error) } @@ -108,9 +108,9 @@ type CertificateAuthorityServer interface { // SignCertificate sends a Certificate Signing Request to the certificate authority // and obtains a signed certificate. Both in the PEM format. SignCertificate(context.Context, *SignCertificateRequest) (*SignCertificateResponse, error) - // Get signed certficate records. + // Get signed certificate records. GetSigningRecords(*GetSigningRecordsRequest, grpc.ServerStreamingServer[SigningRecord]) error - // Delete signed certficate records. + // Revoke signed certficate or delete expired signed certificate records. DeleteSigningRecords(context.Context, *DeleteSigningRecordsRequest) (*DeletedSigningRecords, error) mustEmbedUnimplementedCertificateAuthorityServer() } diff --git a/certificate-authority/pb/signingRecords.go b/certificate-authority/pb/signingRecords.go index b6eccf5cf..c9a2fdd54 100644 --- a/certificate-authority/pb/signingRecords.go +++ b/certificate-authority/pb/signingRecords.go @@ -3,6 +3,7 @@ package pb import ( "errors" "fmt" + "math/big" "sort" "github.com/google/uuid" @@ -17,6 +18,26 @@ func (p SigningRecords) Sort() { }) } +func (credential *CredentialStatus) Validate() error { + if credential.GetDate() == 0 { + return errors.New("empty signing credential date") + } + if credential.GetValidUntilDate() == 0 { + return errors.New("empty signing record credential expiration date") + } + if credential.GetCertificatePem() == "" { + return errors.New("empty signing record credential certificate") + } + serial := big.Int{} + if _, ok := serial.SetString(credential.GetSerial(), 10); !ok { + return errors.New("invalid signing record credential certificate serial number") + } + if _, err := uuid.Parse(credential.GetIssuerId()); err != nil { + return fmt.Errorf("invalid signing record issuer's ID(%v): %w", credential.GetIssuerId(), err) + } + return nil +} + func (signingRecord *SigningRecord) Marshal() ([]byte, error) { return proto.Marshal(signingRecord) } @@ -43,14 +64,9 @@ func (signingRecord *SigningRecord) Validate() error { if signingRecord.GetOwner() == "" { return errors.New("empty signing record owner") } - if signingRecord.GetCredential() != nil && signingRecord.GetCredential().GetDate() == 0 { - return errors.New("empty signing credential date") - } - if signingRecord.GetCredential() != nil && signingRecord.GetCredential().GetValidUntilDate() == 0 { - return errors.New("empty signing record credential expiration date") - } - if signingRecord.GetCredential() != nil && signingRecord.GetCredential().GetCertificatePem() == "" { - return errors.New("empty signing record credential certificate") + credential := signingRecord.GetCredential() + if credential != nil { + return credential.Validate() } return nil } diff --git a/certificate-authority/pb/signingRecords.pb.go b/certificate-authority/pb/signingRecords.pb.go index 8ce4fbb3d..5b8a059a9 100644 --- a/certificate-authority/pb/signingRecords.pb.go +++ b/certificate-authority/pb/signingRecords.pb.go @@ -97,6 +97,10 @@ type CredentialStatus struct { CertificatePem string `protobuf:"bytes,2,opt,name=certificate_pem,json=certificatePem,proto3" json:"certificate_pem,omitempty" bson:"identityCertificate"` // @gotags: bson:"identityCertificate" // Record valid until date, in unix nanoseconds timestamp format ValidUntilDate int64 `protobuf:"varint,3,opt,name=valid_until_date,json=validUntilDate,proto3" json:"valid_until_date,omitempty" bson:"validUntilDate"` // @gotags: bson:"validUntilDate" + // Serial number of the last certificat issued + Serial string `protobuf:"bytes,4,opt,name=serial,proto3" json:"serial,omitempty" bson:"serial"` // @gotags: bson:"serial" + // Issuer id is calculated from the issuer's public certificate, and it is computed as uuid.NewSHA1(uuid.NameSpaceX500, publicKeyRaw) + IssuerId string `protobuf:"bytes,5,opt,name=issuer_id,json=issuerId,proto3" json:"issuer_id,omitempty" bson:"issuerId"` // @gotags: bson:"issuerId" } func (x *CredentialStatus) Reset() { @@ -152,6 +156,20 @@ func (x *CredentialStatus) GetValidUntilDate() int64 { return 0 } +func (x *CredentialStatus) GetSerial() string { + if x != nil { + return x.Serial + } + return "" +} + +func (x *CredentialStatus) GetIssuerId() string { + if x != nil { + return x.IssuerId + } + return "" +} + type SigningRecord struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -311,6 +329,7 @@ func (x *DeleteSigningRecordsRequest) GetDeviceIdFilter() []string { return nil } +// Revoke or delete certificates type DeletedSigningRecords struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -375,45 +394,48 @@ var file_certificate_authority_pb_signingRecords_proto_rawDesc = []byte{ 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x28, 0x0a, 0x10, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x64, 0x65, 0x76, 0x69, - 0x63, 0x65, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x79, 0x0a, 0x10, 0x43, 0x72, - 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, - 0x0a, 0x04, 0x64, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x64, 0x61, - 0x74, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, - 0x65, 0x5f, 0x70, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x65, 0x72, - 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x65, 0x6d, 0x12, 0x28, 0x0a, 0x10, 0x76, - 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x5f, 0x64, 0x61, 0x74, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, 0x6e, 0x74, 0x69, - 0x6c, 0x44, 0x61, 0x74, 0x65, 0x22, 0x82, 0x02, 0x0a, 0x0d, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, - 0x67, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x12, 0x1f, 0x0a, - 0x0b, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, - 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x70, - 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x72, - 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x61, 0x74, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x61, 0x74, 0x65, 0x12, - 0x49, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, - 0x65, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, - 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, - 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x22, 0x64, 0x0a, 0x1b, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x63, 0x6f, 0x72, - 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x64, 0x5f, - 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x69, 0x64, - 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x28, 0x0a, 0x10, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, - 0x5f, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x0e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, - 0x22, 0x2d, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x53, 0x69, 0x67, 0x6e, 0x69, - 0x6e, 0x67, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x42, - 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x6c, - 0x67, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x68, 0x75, 0x62, 0x2f, 0x76, 0x32, 0x2f, 0x63, 0x65, - 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x2d, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x74, 0x79, 0x2f, 0x70, 0x62, 0x3b, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x63, 0x65, 0x49, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0xae, 0x01, 0x0a, 0x10, 0x43, + 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, + 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x64, + 0x61, 0x74, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x5f, 0x70, 0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x65, + 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x50, 0x65, 0x6d, 0x12, 0x28, 0x0a, 0x10, + 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x75, 0x6e, 0x74, 0x69, 0x6c, 0x5f, 0x64, 0x61, 0x74, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x55, 0x6e, 0x74, + 0x69, 0x6c, 0x44, 0x61, 0x74, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x12, 0x1b, + 0x0a, 0x09, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x49, 0x64, 0x22, 0x82, 0x02, 0x0a, 0x0d, + 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, + 0x05, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6f, 0x77, + 0x6e, 0x65, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, + 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, + 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, + 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x61, 0x74, + 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x44, 0x61, 0x74, 0x65, 0x12, 0x49, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x61, 0x6c, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x65, 0x72, 0x74, + 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, + 0x2e, 0x70, 0x62, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, + 0x22, 0x64, 0x0a, 0x1b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, + 0x67, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x1b, 0x0a, 0x09, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x08, 0x69, 0x64, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x28, 0x0a, 0x10, + 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x64, 0x65, 0x76, 0x69, 0x63, 0x65, 0x49, 0x64, + 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x2d, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x64, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x73, 0x12, + 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x6c, 0x67, 0x64, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x68, 0x75, 0x62, + 0x2f, 0x76, 0x32, 0x2f, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x2d, + 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x74, 0x79, 0x2f, 0x70, 0x62, 0x3b, 0x70, 0x62, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/certificate-authority/pb/signingRecords.proto b/certificate-authority/pb/signingRecords.proto index 79166bfaf..8981cac10 100644 --- a/certificate-authority/pb/signingRecords.proto +++ b/certificate-authority/pb/signingRecords.proto @@ -20,6 +20,10 @@ message CredentialStatus { string certificate_pem = 2; // @gotags: bson:"identityCertificate" // Record valid until date, in unix nanoseconds timestamp format int64 valid_until_date = 3; // @gotags: bson:"validUntilDate" + // Serial number of the last certificat issued + string serial = 4; // @gotags: bson:"serial" + // Issuer id is calculated from the issuer's public certificate, and it is computed as uuid.NewSHA1(uuid.NameSpaceX500, publicKeyRaw) + string issuer_id = 5; // @gotags: bson:"issuerId" } message SigningRecord { @@ -46,7 +50,8 @@ message DeleteSigningRecordsRequest { repeated string device_id_filter = 2; } +// Revoke or delete certificates message DeletedSigningRecords { // Number of deleted records. int64 count = 1; -} \ No newline at end of file +} diff --git a/certificate-authority/pb/signingRecords_test.go b/certificate-authority/pb/signingRecords_test.go new file mode 100644 index 000000000..42e9e5d16 --- /dev/null +++ b/certificate-authority/pb/signingRecords_test.go @@ -0,0 +1,189 @@ +package pb_test + +import ( + "testing" + + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/certificate-authority/pb" + "github.com/stretchr/testify/require" +) + +func TestCredentialStatusValidate(t *testing.T) { + tests := []struct { + name string + input *pb.CredentialStatus + wantErr bool + }{ + { + name: "Valid credential", + input: &pb.CredentialStatus{ + Date: 1659462400000000000, + ValidUntilDate: 1669462400000000000, + CertificatePem: "valid-cert", + Serial: "1234567890", + IssuerId: uuid.New().String(), + }, + wantErr: false, + }, + { + name: "Missing signing credential date", + input: &pb.CredentialStatus{ + Date: 0, + ValidUntilDate: 1669462400000000000, + CertificatePem: "valid-cert", + Serial: "1234567890", + IssuerId: uuid.New().String(), + }, + wantErr: true, + }, + { + name: "Missing signing credential expiration date", + input: &pb.CredentialStatus{ + Date: 1659462400000000000, + ValidUntilDate: 0, + CertificatePem: "valid-cert", + Serial: "1234567890", + IssuerId: uuid.New().String(), + }, + wantErr: true, + }, + { + name: "Missing signing record credential certificate", + input: &pb.CredentialStatus{ + Date: 1659462400000000000, + ValidUntilDate: 1669462400000000000, + CertificatePem: "", + Serial: "1234567890", + IssuerId: uuid.New().String(), + }, + wantErr: true, + }, + { + name: "Invalid certificate serial number", + input: &pb.CredentialStatus{ + Date: 1659462400000000000, + ValidUntilDate: 1669462400000000000, + CertificatePem: "valid-cert", + Serial: "invalid-serial", + IssuerId: uuid.New().String(), + }, + wantErr: true, + }, + { + name: "Invalid issuer ID", + input: &pb.CredentialStatus{ + Date: 1659462400000000000, + ValidUntilDate: 1669462400000000000, + CertificatePem: "valid-cert", + Serial: "1234567890", + IssuerId: "invalid-uuid", + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.input.Validate() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestSigningRecordValidate(t *testing.T) { + validCredential := &pb.CredentialStatus{ + Date: 1659462400000000000, + ValidUntilDate: 1669462400000000000, + CertificatePem: "valid-cert", + Serial: "1234567890", + IssuerId: uuid.New().String(), + } + + tests := []struct { + name string + input *pb.SigningRecord + wantErr bool + }{ + { + name: "Valid signing record", + input: &pb.SigningRecord{ + Id: uuid.New().String(), + Owner: "owner", + CommonName: "common_name", + DeviceId: uuid.New().String(), + Credential: validCredential, + }, + wantErr: false, + }, + { + name: "Missing signing record ID", + input: &pb.SigningRecord{ + Id: "", + Owner: "owner", + CommonName: "common_name", + DeviceId: uuid.New().String(), + Credential: validCredential, + }, + wantErr: true, + }, + { + name: "Invalid signing record ID", + input: &pb.SigningRecord{ + Id: "invalid-uuid", + Owner: "owner", + CommonName: "common_name", + DeviceId: uuid.New().String(), + Credential: validCredential, + }, + wantErr: true, + }, + { + name: "Invalid device ID", + input: &pb.SigningRecord{ + Id: uuid.New().String(), + Owner: "owner", + CommonName: "common_name", + DeviceId: "invalid-uuid", + Credential: validCredential, + }, + wantErr: true, + }, + { + name: "Missing common name", + input: &pb.SigningRecord{ + Id: uuid.New().String(), + Owner: "owner", + CommonName: "", + DeviceId: uuid.New().String(), + Credential: validCredential, + }, + wantErr: true, + }, + { + name: "Missing owner", + input: &pb.SigningRecord{ + Id: uuid.New().String(), + Owner: "", + CommonName: "common_name", + DeviceId: uuid.New().String(), + Credential: validCredential, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.input.Validate() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/certificate-authority/service/cleanDatabase_test.go b/certificate-authority/service/cleanDatabase_test.go index afe8d2c87..0db8d0540 100644 --- a/certificate-authority/service/cleanDatabase_test.go +++ b/certificate-authority/service/cleanDatabase_test.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "math/big" "testing" "time" @@ -17,7 +18,7 @@ import ( "github.com/plgd-dev/hub/v2/identity-store/events" "github.com/plgd-dev/hub/v2/pkg/fsnotify" "github.com/plgd-dev/hub/v2/pkg/log" - kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" "github.com/plgd-dev/hub/v2/test/config" testService "github.com/plgd-dev/hub/v2/test/service" "github.com/stretchr/testify/require" @@ -48,6 +49,8 @@ func TestCertificateAuthorityServerCleanUpSigningRecords(t *testing.T) { CertificatePem: "certificate1", Date: date.UnixNano(), ValidUntilDate: date.UnixNano(), + Serial: big.NewInt(42).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, } @@ -64,7 +67,7 @@ func TestCertificateAuthorityServerCleanUpSigningRecords(t *testing.T) { }() ch := new(inprocgrpc.Channel) - ca, err := grpc.NewCertificateAuthorityServer(ownerClaim, config.HubID(), test.MakeConfig(t).Signer, storeDB, fileWatcher, logger) + ca, err := grpc.NewCertificateAuthorityServer(ownerClaim, config.HubID(), config.CERTIFICATE_AUTHORITY_HTTP_HOST, test.MakeConfig(t).Signer, storeDB, fileWatcher, logger) require.NoError(t, err) defer ca.Close() @@ -73,7 +76,7 @@ func TestCertificateAuthorityServerCleanUpSigningRecords(t *testing.T) { token := config.CreateJwtToken(t, jwt.MapClaims{ ownerClaim: owner, }) - ctx := kitNetGrpc.CtxWithToken(context.Background(), token) + ctx := pkgGrpc.CtxWithToken(context.Background(), token) client, err := grpcClient.GetSigningRecords(ctx, &pb.GetSigningRecordsRequest{}) require.NoError(t, err) var got pb.SigningRecords diff --git a/certificate-authority/service/config.go b/certificate-authority/service/config.go index 7a0852e2c..e08518c3c 100644 --- a/certificate-authority/service/config.go +++ b/certificate-authority/service/config.go @@ -3,6 +3,7 @@ package service import ( "fmt" "net" + "net/url" "time" "github.com/go-co-op/gocron/v2" @@ -40,7 +41,7 @@ func (c *Config) Validate() error { return fmt.Errorf("hubID('%v') - %w", c.HubID, err) } - _, err := grpcService.NewSigner(c.APIs.GRPC.Authorization.OwnerClaim, c.HubID, c.Signer) + _, err := grpcService.NewSigner(c.APIs.GRPC.Authorization.OwnerClaim, c.HubID, c.APIs.HTTP.ExternalAddress, c.Signer) if err != nil { return fmt.Errorf("signer('%v') - %w", c.Signer, err) } @@ -65,11 +66,15 @@ func (c *APIsConfig) Validate() error { } type HTTPConfig struct { - Addr string `yaml:"address" json:"address"` - Server httpServer.Config `yaml:",inline" json:",inline"` + ExternalAddress string `yaml:"externalAddress" json:"externalAddress"` + Addr string `yaml:"address" json:"address"` + Server httpServer.Config `yaml:",inline" json:",inline"` } func (c *HTTPConfig) Validate() error { + if _, err := url.ParseRequestURI(c.ExternalAddress); err != nil { + return fmt.Errorf("externalAddress('%v') invalid", c.ExternalAddress) + } if _, err := net.ResolveTCPAddr("tcp", c.Addr); err != nil { return fmt.Errorf("address('%v') - %w", c.Addr, err) } diff --git a/certificate-authority/service/config_test.go b/certificate-authority/service/config_test.go index 58427612d..6b96edb37 100644 --- a/certificate-authority/service/config_test.go +++ b/certificate-authority/service/config_test.go @@ -147,6 +147,57 @@ func TestConfigValidate(t *testing.T) { } } +func TestHTTPConfigValidate(t *testing.T) { + type args struct { + cfg service.HTTPConfig + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "valid", + args: args{ + cfg: test.MakeHTTPConfig(), + }, + }, + { + name: "invalid external address", + args: args{ + cfg: func() service.HTTPConfig { + cfg := test.MakeHTTPConfig() + cfg.ExternalAddress = "invalid" + return cfg + }(), + }, + wantErr: true, + }, + { + name: "invalid address", + args: args{ + cfg: func() service.HTTPConfig { + cfg := test.MakeHTTPConfig() + cfg.Addr = "invalid" + return cfg + }(), + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.args.cfg.Validate() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + func TestStorageConfigValidate(t *testing.T) { type args struct { cfg service.StorageConfig diff --git a/certificate-authority/service/grpc/config.go b/certificate-authority/service/grpc/config.go index 325b98da2..4871b7afa 100644 --- a/certificate-authority/service/grpc/config.go +++ b/certificate-authority/service/grpc/config.go @@ -13,12 +13,31 @@ import ( type Config = server.Config +type CRLConfig struct { + ExpiresIn time.Duration `yaml:"expiresIn" json:"expiresIn"` + + // needed by tests with cqldb - remove once support for CRL + // is implemented in cqldb or cqldb is removed + Enabled bool `yaml:"-" json:"-"` +} + +func (c *CRLConfig) Validate() error { + if !c.Enabled { + return nil + } + if c.ExpiresIn <= time.Minute { + return fmt.Errorf("expiresIn('%v') - less than %v", c.ExpiresIn, time.Minute) + } + return nil +} + type SignerConfig struct { CAPool interface{} `yaml:"caPool" json:"caPool" description:"file path to the root certificates in PEM format"` KeyFile urischeme.URIScheme `yaml:"keyFile" json:"keyFile" description:"file name of CA private key in PEM format"` CertFile urischeme.URIScheme `yaml:"certFile" json:"certFile" description:"file name of CA certificate in PEM format"` ValidFrom string `yaml:"validFrom" json:"validFrom" description:"format https://github.com/karrick/tparse"` ExpiresIn time.Duration `yaml:"expiresIn" json:"expiresIn"` + CRL CRLConfig `yaml:"crl" json:"crl"` caPoolArray []urischeme.URIScheme `yaml:"-" json:"-"` } @@ -36,13 +55,15 @@ func (c *SignerConfig) Validate() error { return fmt.Errorf("keyFile('%v')", c.KeyFile) } if c.ExpiresIn <= 0 { - return fmt.Errorf("expiresIn('%v')", c.KeyFile) + return fmt.Errorf("expiresIn('%v')", c.ExpiresIn) } _, err := tparse.ParseNow(time.RFC3339, c.ValidFrom) if err != nil { - return fmt.Errorf("validFrom('%v')", c.ValidFrom) + return fmt.Errorf("validFrom('%v').%w", c.ValidFrom, err) + } + if err := c.CRL.Validate(); err != nil { + return fmt.Errorf("crl.%w", err) } - return nil } diff --git a/certificate-authority/service/grpc/config_test.go b/certificate-authority/service/grpc/config_test.go new file mode 100644 index 000000000..560a62434 --- /dev/null +++ b/certificate-authority/service/grpc/config_test.go @@ -0,0 +1,158 @@ +package grpc_test + +import ( + "testing" + "time" + + "github.com/plgd-dev/hub/v2/certificate-authority/service/grpc" + "github.com/plgd-dev/hub/v2/pkg/config/property/urischeme" + "github.com/stretchr/testify/require" +) + +func TestCRLConfigValidate(t *testing.T) { + tests := []struct { + name string + input grpc.CRLConfig + wantErr bool + }{ + { + name: "Disabled CRLConfig", + input: grpc.CRLConfig{ + Enabled: false, + }, + }, + { + name: "Enabled CRLConfig with valid ExternalAddress and ExpiresIn", + input: grpc.CRLConfig{ + Enabled: true, + ExpiresIn: time.Hour, + }, + }, + { + name: "Enabled CRLConfig with ExpiresIn less than 1 minute", + input: grpc.CRLConfig{ + Enabled: true, + ExpiresIn: 30 * time.Second, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.input.Validate() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestSignerConfigValidate(t *testing.T) { + crl := grpc.CRLConfig{ + Enabled: true, + ExpiresIn: time.Hour, + } + tests := []struct { + name string + input grpc.SignerConfig + wantErr bool + }{ + { + name: "Valid SignerConfig", + input: grpc.SignerConfig{ + CAPool: []string{"ca1.pem", "ca2.pem"}, + KeyFile: urischeme.URIScheme("key.pem"), + CertFile: urischeme.URIScheme("cert.pem"), + ValidFrom: time.Now().Format(time.RFC3339), + ExpiresIn: time.Hour * 24, + CRL: crl, + }, + }, + { + name: "Invalid CA Pool", + input: grpc.SignerConfig{ + CAPool: 42, + KeyFile: urischeme.URIScheme("key.pem"), + CertFile: urischeme.URIScheme("cert.pem"), + ValidFrom: time.Now().Format(time.RFC3339), + ExpiresIn: time.Hour * 24, + CRL: crl, + }, + wantErr: true, + }, + { + name: "Empty CertFile", + input: grpc.SignerConfig{ + CAPool: []string{"ca1.pem"}, + KeyFile: urischeme.URIScheme("key.pem"), + CertFile: "", + ValidFrom: time.Now().Format(time.RFC3339), + ExpiresIn: time.Hour * 24, + CRL: crl, + }, + wantErr: true, + }, + { + name: "Empty KeyFile", + input: grpc.SignerConfig{ + CAPool: []string{"ca1.pem"}, + KeyFile: "", + CertFile: urischeme.URIScheme("cert.pem"), + ValidFrom: time.Now().Format(time.RFC3339), + ExpiresIn: time.Hour * 24, + CRL: crl, + }, + wantErr: true, + }, + { + name: "Invalid ExpiresIn", + input: grpc.SignerConfig{ + CAPool: []string{"ca1.pem", "ca2.pem"}, + KeyFile: urischeme.URIScheme("key.pem"), + CertFile: urischeme.URIScheme("cert.pem"), + ValidFrom: time.Now().Format(time.RFC3339), + ExpiresIn: -1, + CRL: crl, + }, + wantErr: true, + }, + { + name: "Invalid ValidFrom format", + input: grpc.SignerConfig{ + CAPool: []string{"ca1.pem"}, + KeyFile: urischeme.URIScheme("key.pem"), + CertFile: urischeme.URIScheme("cert.pem"), + ValidFrom: "invalid-date", + ExpiresIn: time.Hour * 24, + CRL: crl, + }, + wantErr: true, + }, + { + name: "Invalid CRL", + input: grpc.SignerConfig{ + CAPool: []string{"ca1.pem", "ca2.pem"}, + KeyFile: urischeme.URIScheme("key.pem"), + CertFile: urischeme.URIScheme("cert.pem"), + ValidFrom: time.Now().Format(time.RFC3339), + ExpiresIn: time.Hour * 24, + CRL: grpc.CRLConfig{ + Enabled: true, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.input.Validate() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/certificate-authority/service/grpc/deleteSigningRecords.go b/certificate-authority/service/grpc/deleteSigningRecords.go index 53456c26a..42637619d 100644 --- a/certificate-authority/service/grpc/deleteSigningRecords.go +++ b/certificate-authority/service/grpc/deleteSigningRecords.go @@ -8,16 +8,20 @@ import ( "google.golang.org/grpc/status" ) +func errDeleteSigningRecords(err error) error { + return status.Errorf(codes.InvalidArgument, "cannot delete signing records: %v", err) +} + func (s *CertificateAuthorityServer) DeleteSigningRecords(ctx context.Context, req *pb.DeleteSigningRecordsRequest) (*pb.DeletedSigningRecords, error) { owner, err := ownerToUUID(ctx, s.ownerClaim) if err != nil { - return nil, s.logger.LogAndReturnError(status.Errorf(codes.InvalidArgument, "cannot delete signing records: %v", err)) + return nil, s.logger.LogAndReturnError(errDeleteSigningRecords(err)) } - n, err := s.store.DeleteSigningRecords(ctx, owner, req) + count, err := s.store.RevokeSigningRecords(ctx, owner, req) if err != nil { - return nil, s.logger.LogAndReturnError(status.Errorf(codes.InvalidArgument, "cannot delete signing records: %v", err)) + return nil, s.logger.LogAndReturnError(errDeleteSigningRecords(err)) } return &pb.DeletedSigningRecords{ - Count: n, + Count: count, }, nil } diff --git a/certificate-authority/service/grpc/deleteSigningRecords_test.go b/certificate-authority/service/grpc/deleteSigningRecords_test.go index 573fdff84..fd45ec0b8 100644 --- a/certificate-authority/service/grpc/deleteSigningRecords_test.go +++ b/certificate-authority/service/grpc/deleteSigningRecords_test.go @@ -2,6 +2,7 @@ package grpc_test import ( "context" + "math/big" "testing" "github.com/fullstorydev/grpchan/inprocgrpc" @@ -13,7 +14,7 @@ import ( "github.com/plgd-dev/hub/v2/identity-store/events" "github.com/plgd-dev/hub/v2/pkg/fsnotify" "github.com/plgd-dev/hub/v2/pkg/log" - kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" "github.com/plgd-dev/hub/v2/test/config" "github.com/stretchr/testify/require" ) @@ -21,6 +22,10 @@ import ( func TestCertificateAuthorityServerDeleteSigningRecords(t *testing.T) { owner := events.OwnerToUUID("owner") const ownerClaim = "sub" + token := config.CreateJwtToken(t, jwt.MapClaims{ + ownerClaim: owner, + }) + ctx := pkgGrpc.CtxWithToken(context.Background(), token) r := &store.SigningRecord{ Id: "9d017fad-2961-4fcc-94a9-1e1291a88ffc", Owner: owner, @@ -31,10 +36,13 @@ func TestCertificateAuthorityServerDeleteSigningRecords(t *testing.T) { CertificatePem: "certificate1", Date: constDate().UnixNano(), ValidUntilDate: constDate().UnixNano(), + Serial: big.NewInt(42).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, } type args struct { req *pb.DeleteSigningRecordsRequest + ctx context.Context } tests := []struct { name string @@ -42,14 +50,24 @@ func TestCertificateAuthorityServerDeleteSigningRecords(t *testing.T) { want int64 wantErr bool }{ + { + name: "missing token with ownerClaim in ctx", + args: args{ + req: &pb.DeleteSigningRecordsRequest{ + IdFilter: []string{r.GetId()}, + }, + ctx: context.Background(), + }, + wantErr: true, + }, { name: "invalidID", args: args{ req: &pb.DeleteSigningRecordsRequest{ IdFilter: []string{"invalidID"}, }, + ctx: ctx, }, - wantErr: true, }, { name: "valid", @@ -57,6 +75,7 @@ func TestCertificateAuthorityServerDeleteSigningRecords(t *testing.T) { req: &pb.DeleteSigningRecordsRequest{ IdFilter: []string{r.GetId()}, }, + ctx: ctx, }, want: 1, }, @@ -78,20 +97,20 @@ func TestCertificateAuthorityServerDeleteSigningRecords(t *testing.T) { }() ch := new(inprocgrpc.Channel) - ca, err := grpc.NewCertificateAuthorityServer(ownerClaim, config.HubID(), test.MakeConfig(t).Signer, store, fileWatcher, logger) + ca, err := grpc.NewCertificateAuthorityServer(ownerClaim, config.HubID(), config.CERTIFICATE_AUTHORITY_HTTP_HOST, test.MakeConfig(t).Signer, store, fileWatcher, logger) require.NoError(t, err) defer ca.Close() pb.RegisterCertificateAuthorityServer(ch, ca) grpcClient := pb.NewCertificateAuthorityClient(ch) - token := config.CreateJwtToken(t, jwt.MapClaims{ - ownerClaim: owner, - }) - ctx := kitNetGrpc.CtxWithToken(context.Background(), token) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := grpcClient.DeleteSigningRecords(ctx, tt.args.req) + got, err := grpcClient.DeleteSigningRecords(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + return + } require.NoError(t, err) require.Equal(t, tt.want, got.GetCount()) }) diff --git a/certificate-authority/service/grpc/getSigningRecords.go b/certificate-authority/service/grpc/getSigningRecords.go index 9236524a9..875954746 100644 --- a/certificate-authority/service/grpc/getSigningRecords.go +++ b/certificate-authority/service/grpc/getSigningRecords.go @@ -1,10 +1,7 @@ package grpc import ( - "context" - "github.com/plgd-dev/hub/v2/certificate-authority/pb" - "github.com/plgd-dev/hub/v2/certificate-authority/store" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -14,16 +11,11 @@ func (s *CertificateAuthorityServer) GetSigningRecords(req *pb.GetSigningRecords if err != nil { return s.logger.LogAndReturnError(status.Errorf(codes.InvalidArgument, "cannot get signing records: %v", err)) } - err = s.store.LoadSigningRecords(srv.Context(), owner, req, func(ctx context.Context, iter store.SigningRecordIter) (err error) { - for { - var sub pb.SigningRecord - if ok := iter.Next(ctx, &sub); !ok { - return iter.Err() - } - if err = srv.Send(&sub); err != nil { - return err - } + err = s.store.LoadSigningRecords(srv.Context(), owner, req, func(sr *pb.SigningRecord) (err error) { + if err = srv.Send(sr); err != nil { + return err } + return nil }) if err != nil { return s.logger.LogAndReturnError(status.Errorf(codes.InvalidArgument, "cannot get signing records: %v", err)) diff --git a/certificate-authority/service/grpc/getSigningRecords_test.go b/certificate-authority/service/grpc/getSigningRecords_test.go index c7f1d859a..14d48a7a3 100644 --- a/certificate-authority/service/grpc/getSigningRecords_test.go +++ b/certificate-authority/service/grpc/getSigningRecords_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "io" + "math/big" "testing" "time" @@ -16,7 +17,7 @@ import ( "github.com/plgd-dev/hub/v2/identity-store/events" "github.com/plgd-dev/hub/v2/pkg/fsnotify" "github.com/plgd-dev/hub/v2/pkg/log" - kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" hubTest "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" "github.com/stretchr/testify/require" @@ -39,6 +40,8 @@ func TestCertificateAuthorityServerGetSigningRecords(t *testing.T) { CertificatePem: "certificate1", Date: constDate().UnixNano(), ValidUntilDate: constDate().UnixNano(), + Serial: big.NewInt(42).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, } type args struct { @@ -86,7 +89,7 @@ func TestCertificateAuthorityServerGetSigningRecords(t *testing.T) { }() ch := new(inprocgrpc.Channel) - ca, err := grpc.NewCertificateAuthorityServer(ownerClaim, config.HubID(), test.MakeConfig(t).Signer, store, fileWatcher, logger) + ca, err := grpc.NewCertificateAuthorityServer(ownerClaim, config.HubID(), config.CERTIFICATE_AUTHORITY_HTTP_HOST, test.MakeConfig(t).Signer, store, fileWatcher, logger) require.NoError(t, err) defer ca.Close() @@ -95,7 +98,7 @@ func TestCertificateAuthorityServerGetSigningRecords(t *testing.T) { token := config.CreateJwtToken(t, jwt.MapClaims{ ownerClaim: owner, }) - ctx := kitNetGrpc.CtxWithToken(context.Background(), token) + ctx := pkgGrpc.CtxWithToken(context.Background(), token) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/certificate-authority/service/grpc/server.go b/certificate-authority/service/grpc/server.go index bddabf426..20c27930a 100644 --- a/certificate-authority/service/grpc/server.go +++ b/certificate-authority/service/grpc/server.go @@ -23,18 +23,20 @@ type CertificateAuthorityServer struct { hubID string fileWatcher *fsnotify.Watcher onFileChangeFunc func(event fsnotify.Event) + crlServerAddress string signer atomic.Pointer[Signer] } -func NewCertificateAuthorityServer(ownerClaim string, hubID string, signerConfig SignerConfig, store store.Store, fileWatcher *fsnotify.Watcher, logger log.Logger) (*CertificateAuthorityServer, error) { +func NewCertificateAuthorityServer(ownerClaim, hubID, crlServerAddress string, signerConfig SignerConfig, store store.Store, fileWatcher *fsnotify.Watcher, logger log.Logger) (*CertificateAuthorityServer, error) { s := &CertificateAuthorityServer{ - signerConfig: signerConfig, - logger: logger, - ownerClaim: ownerClaim, - store: store, - hubID: hubID, - fileWatcher: fileWatcher, + signerConfig: signerConfig, + logger: logger, + ownerClaim: ownerClaim, + store: store, + hubID: hubID, + fileWatcher: fileWatcher, + crlServerAddress: crlServerAddress, } _, err := s.load() @@ -100,7 +102,7 @@ func (s *CertificateAuthorityServer) Close() { } func (s *CertificateAuthorityServer) load() (bool, error) { - signer, err := NewSigner(s.ownerClaim, s.hubID, s.signerConfig) + signer, err := NewSigner(s.ownerClaim, s.hubID, s.crlServerAddress, s.signerConfig) if err != nil { return false, fmt.Errorf("cannot create signer: %w", err) } diff --git a/certificate-authority/service/grpc/server_test.go b/certificate-authority/service/grpc/server_test.go index 34a21297b..da24e3053 100644 --- a/certificate-authority/service/grpc/server_test.go +++ b/certificate-authority/service/grpc/server_test.go @@ -85,7 +85,7 @@ func TestReloadCerts(t *testing.T) { err = s.Validate() require.NoError(t, err) - ca, err := grpc.NewCertificateAuthorityServer(ownerClaim, config.HubID(), s, store, fileWatcher, logger) + ca, err := grpc.NewCertificateAuthorityServer(ownerClaim, config.HubID(), config.CERTIFICATE_AUTHORITY_HTTP_HOST, s, store, fileWatcher, logger) require.NoError(t, err) defer ca.Close() diff --git a/certificate-authority/service/grpc/signCertificate.go b/certificate-authority/service/grpc/signCertificate.go index 45252f07c..bb8b0e4ca 100644 --- a/certificate-authority/service/grpc/signCertificate.go +++ b/certificate-authority/service/grpc/signCertificate.go @@ -26,35 +26,7 @@ func (s *CertificateAuthorityServer) validateRequest(csr []byte) error { return nil } -func (s *CertificateAuthorityServer) updateSigningIdentityCertificateRecord(ctx context.Context, updateSigningRecord *pb.SigningRecord) error { - var found bool - now := time.Now().UnixNano() - err := s.store.LoadSigningRecords(ctx, updateSigningRecord.GetOwner(), &store.SigningRecordsQuery{ - CommonNameFilter: []string{updateSigningRecord.GetCommonName()}, - }, func(ctx context.Context, iter store.SigningRecordIter) (err error) { - for { - var signingRecord pb.SigningRecord - ok := iter.Next(ctx, &signingRecord) - if !ok { - break - } - if updateSigningRecord.GetPublicKey() != signingRecord.GetPublicKey() && signingRecord.GetCredential().GetValidUntilDate() > now { - return fmt.Errorf("common name %v with different public key fingerprint exist", signingRecord.GetCommonName()) - } - found = true - } - return nil - }) - if err != nil { - return err - } - if found { - return s.store.UpdateSigningRecord(ctx, updateSigningRecord) - } - return s.store.CreateSigningRecord(ctx, updateSigningRecord) -} - -func toSigningRecord(owner string, template *x509.Certificate) (*pb.SigningRecord, error) { +func toSigningRecord(owner, issuerID string, template *x509.Certificate) (*pb.SigningRecord, error) { publicKeyRaw, err := x509.MarshalPKIXPublicKey(template.PublicKey) if err != nil { return nil, err @@ -82,19 +54,74 @@ func toSigningRecord(owner string, template *x509.Certificate) (*pb.SigningRecor CertificatePem: "", Date: now, ValidUntilDate: template.NotAfter.UnixNano(), + Serial: template.SerialNumber.String(), + IssuerId: issuerID, }, }, nil } +func (s *CertificateAuthorityServer) getSigningRecord(ctx context.Context, signingRecord *pb.SigningRecord) (*pb.SigningRecord, error) { + checkForIdentity := signingRecord.GetDeviceId() != "" && signingRecord.GetDeviceId() != signingRecord.GetOwner() + var err error + var originalSr *store.SigningRecord + if checkForIdentity { + now := time.Now().UnixNano() + err = s.store.LoadSigningRecords(ctx, signingRecord.GetOwner(), &store.SigningRecordsQuery{ + CommonNameFilter: []string{signingRecord.GetCommonName()}, + }, func(sr *store.SigningRecord) (err error) { + // _id is calculated as uuid.NewSHA1(uuid.NameSpaceX500, CommonName + PublicKey) -> thus same CommonName and PublicKey == same _id + if signingRecord.GetPublicKey() != sr.GetPublicKey() && + sr.GetCredential().GetValidUntilDate() > now { + return fmt.Errorf("common name %v with different public key fingerprint exist", sr.GetCommonName()) + } + if signingRecord.GetId() == sr.GetId() { + originalSr = sr + } + return nil + }) + } else { + err = s.store.LoadSigningRecords(ctx, signingRecord.GetOwner(), &store.SigningRecordsQuery{ + IdFilter: []string{signingRecord.GetId()}, + }, func(sr *store.SigningRecord) (err error) { + originalSr = sr + return nil + }) + } + if err != nil { + return nil, err + } + return originalSr, nil +} + func (s *CertificateAuthorityServer) updateSigningRecord(ctx context.Context, signingRecord *pb.SigningRecord) error { - var checkForIdentity bool - if signingRecord.GetDeviceId() != "" && signingRecord.GetDeviceId() != signingRecord.GetOwner() { - checkForIdentity = true + // try to get previous signing record + prevSr, err := s.getSigningRecord(ctx, signingRecord) + if err != nil { + return err } - if checkForIdentity { - return s.updateSigningIdentityCertificateRecord(ctx, signingRecord) + if s.store.SupportsRevocationList() && prevSr != nil { + // revoke previous signing record + prevCred := prevSr.GetCredential() + if prevCred != nil { + query := store.UpdateRevocationListQuery{ + IssuerID: prevCred.GetIssuerId(), + RevokedCertificates: []*store.RevocationListCertificate{ + { + Serial: prevCred.GetSerial(), + ValidUntil: prevCred.GetValidUntilDate(), + Revocation: time.Now().UnixNano(), + }, + }, + } + _, err = s.store.UpdateRevocationList(ctx, &query) + if err != nil { + return err + } + } } - return s.store.UpdateSigningRecord(ctx, signingRecord) + // upsert new one + err = s.store.UpdateSigningRecord(ctx, signingRecord) + return err } func (s *CertificateAuthorityServer) SignCertificate(ctx context.Context, req *pb.SignCertificateRequest) (*pb.SignCertificateResponse, error) { @@ -111,10 +138,11 @@ func (s *CertificateAuthorityServer) SignCertificate(ctx context.Context, req *p if err != nil { return nil, logger.LogAndReturnError(status.Errorf(codes.InvalidArgument, fmtError, err)) } - if signingRecord.GetCredential() == nil { - return nil, logger.LogAndReturnError(status.Errorf(codes.InvalidArgument, "cannot sign certificate: cannot create signing record")) + credential := signingRecord.GetCredential() + if credential == nil { + return nil, logger.LogAndReturnError(status.Errorf(codes.InvalidArgument, fmtError, errors.New("cannot create signing record"))) } - signingRecord.Credential.CertificatePem = string(cert) + credential.CertificatePem = string(cert) if err := s.updateSigningRecord(ctx, signingRecord); err != nil { return nil, logger.LogAndReturnError(status.Errorf(codes.InvalidArgument, fmtError, err)) } diff --git a/certificate-authority/service/grpc/signCertificate_test.go b/certificate-authority/service/grpc/signCertificate_test.go index 133de2e8b..1da435196 100644 --- a/certificate-authority/service/grpc/signCertificate_test.go +++ b/certificate-authority/service/grpc/signCertificate_test.go @@ -10,12 +10,13 @@ import ( "testing" "time" + "github.com/google/uuid" "github.com/plgd-dev/device/v2/pkg/security/generateCertificate" "github.com/plgd-dev/hub/v2/certificate-authority/pb" caTest "github.com/plgd-dev/hub/v2/certificate-authority/test" m2mOauthTest "github.com/plgd-dev/hub/v2/m2m-oauth-server/test" m2mOauthUri "github.com/plgd-dev/hub/v2/m2m-oauth-server/uri" - kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" "github.com/plgd-dev/hub/v2/pkg/security/jwt/validator" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" @@ -69,7 +70,7 @@ func testSigningByFunction(t *testing.T, signFn ClientSignFunc, csr ...[]byte) { tearDown := service.SetUp(ctx, t) defer tearDown() - ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) + ctx = pkgGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) conn, err := grpc.NewClient(config.CERTIFICATE_AUTHORITY_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ RootCAs: test.GetRootCertificatePool(t), @@ -90,9 +91,7 @@ func testSigningByFunction(t *testing.T, signFn ClientSignFunc, csr ...[]byte) { } } -func createCSR(t *testing.T, commonName string) []byte { - priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err) +func createCSRWithKey(t *testing.T, commonName string, priv *ecdsa.PrivateKey) []byte { var cfg generateCertificate.Configuration cfg.Subject.CommonName = commonName csr, err := generateCertificate.GenerateCSR(cfg, priv) @@ -100,6 +99,12 @@ func createCSR(t *testing.T, commonName string) []byte { return csr } +func createCSR(t *testing.T, commonName string) []byte { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + return createCSRWithKey(t, commonName, priv) +} + func TestCertificateAuthorityServerSignCSR(t *testing.T) { csr := createCSR(t, "aa") testSigningByFunction(t, func(ctx context.Context, c pb.CertificateAuthorityClient, req *pb.SignCertificateRequest) (*pb.SignCertificateResponse, error) { @@ -130,7 +135,42 @@ func TestCertificateAuthorityServerSignCSRWithDifferentPublicKeys(t *testing.T) tearDown := service.SetUp(ctx, t, service.WithCAConfig(cfg), service.WithM2MOAuthConfig(m2mCfg)) defer tearDown() - ctx = kitNetGrpc.CtxWithToken(ctx, m2mOauthTest.GetDefaultAccessToken(t)) + ctx = pkgGrpc.CtxWithToken(ctx, m2mOauthTest.GetDefaultAccessToken(t)) + + conn, err := grpc.NewClient(config.CERTIFICATE_AUTHORITY_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ + RootCAs: test.GetRootCertificatePool(t), + }))) + require.NoError(t, err) + c := pb.NewCertificateAuthorityClient(conn) + + _, err = c.SignIdentityCertificate(ctx, &pb.SignCertificateRequest{CertificateSigningRequest: csr}) + require.NoError(t, err) + + _, err = c.SignIdentityCertificate(ctx, &pb.SignCertificateRequest{CertificateSigningRequest: csr1}) + require.NoError(t, err) +} + +func TestCertificateAuthorityServerSignCSRWithSameDevice(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + cfg := caTest.MakeConfig(t) + cfg.APIs.GRPC.Authorization.Endpoints = append(cfg.APIs.GRPC.Authorization.Endpoints, validator.AuthorityConfig{ + Authority: "https://" + config.M2M_OAUTH_SERVER_HTTP_HOST + m2mOauthUri.Base, + HTTP: config.MakeHttpClientConfig(), + }) + + m2mCfg := m2mOauthTest.MakeConfig(t) + serviceOAuthClient := m2mOauthTest.ServiceOAuthClient + serviceOAuthClient.InsertTokenClaims = map[string]interface{}{ + config.OWNER_CLAIM: oauthService.DeviceUserID, + } + m2mCfg.OAuthSigner.Clients[0] = &serviceOAuthClient + + tearDown := service.SetUp(ctx, t, service.WithCAConfig(cfg), service.WithM2MOAuthConfig(m2mCfg)) + defer tearDown() + + ctx = pkgGrpc.CtxWithToken(ctx, m2mOauthTest.GetDefaultAccessToken(t)) conn, err := grpc.NewClient(config.CERTIFICATE_AUTHORITY_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ RootCAs: test.GetRootCertificatePool(t), @@ -138,11 +178,23 @@ func TestCertificateAuthorityServerSignCSRWithDifferentPublicKeys(t *testing.T) require.NoError(t, err) c := pb.NewCertificateAuthorityClient(conn) + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + deviceID := uuid.NewString() + + csr := createCSRWithKey(t, "uuid:"+deviceID, priv) _, err = c.SignIdentityCertificate(ctx, &pb.SignCertificateRequest{CertificateSigningRequest: csr}) require.NoError(t, err) + csr1 := createCSRWithKey(t, "uuid:"+deviceID, priv) _, err = c.SignIdentityCertificate(ctx, &pb.SignCertificateRequest{CertificateSigningRequest: csr1}) require.NoError(t, err) + + priv2, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + csr2 := createCSRWithKey(t, "uuid:"+deviceID, priv2) + _, err = c.SignIdentityCertificate(ctx, &pb.SignCertificateRequest{CertificateSigningRequest: csr2}) + require.Error(t, err) } func TestCertificateAuthorityServerSignCSRWithEmptyCommonName(t *testing.T) { diff --git a/certificate-authority/service/grpc/signIdentityCertificate_test.go b/certificate-authority/service/grpc/signIdentityCertificate_test.go index a4a1b4260..7ee6528d9 100644 --- a/certificate-authority/service/grpc/signIdentityCertificate_test.go +++ b/certificate-authority/service/grpc/signIdentityCertificate_test.go @@ -12,7 +12,7 @@ import ( "github.com/plgd-dev/device/v2/pkg/security/generateCertificate" "github.com/plgd-dev/hub/v2/certificate-authority/pb" "github.com/plgd-dev/hub/v2/identity-store/events" - kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" "github.com/plgd-dev/hub/v2/test" "github.com/plgd-dev/hub/v2/test/config" oauthService "github.com/plgd-dev/hub/v2/test/oauth-server/service" @@ -49,7 +49,7 @@ func TestCertificateAuthorityServerSignDeviceIdentityCSRWithDifferentPublicKeys( tearDown := service.SetUp(ctx, t) defer tearDown() - ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) + ctx = pkgGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) conn, err := grpc.NewClient(config.CERTIFICATE_AUTHORITY_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ RootCAs: test.GetRootCertificatePool(t), @@ -77,7 +77,7 @@ func TestCertificateAuthorityServerSignOwnerIdentityCSRWithDifferentPublicKeys(t tearDown := service.SetUp(ctx, t) defer tearDown() - ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) + ctx = pkgGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) conn, err := grpc.NewClient(config.CERTIFICATE_AUTHORITY_HOST, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{ RootCAs: test.GetRootCertificatePool(t), diff --git a/certificate-authority/service/grpc/signer.go b/certificate-authority/service/grpc/signer.go index 0b4866e0a..a8fd505b9 100644 --- a/certificate-authority/service/grpc/signer.go +++ b/certificate-authority/service/grpc/signer.go @@ -2,14 +2,15 @@ package grpc import ( "context" - "crypto" "crypto/ecdsa" "crypto/x509" "errors" "time" + "github.com/google/uuid" "github.com/karrick/tparse/v2" "github.com/plgd-dev/hub/v2/certificate-authority/pb" + "github.com/plgd-dev/hub/v2/certificate-authority/service/uri" "github.com/plgd-dev/hub/v2/pkg/security/certificateSigner" pkgX509 "github.com/plgd-dev/hub/v2/pkg/security/x509" ) @@ -18,9 +19,14 @@ type Signer struct { validFrom func() time.Time validFor time.Duration certificate []*x509.Certificate - privateKey crypto.PrivateKey + privateKey *ecdsa.PrivateKey + issuerID string ownerClaim string hubID string + crl struct { + serverAddress string + validFor time.Duration // TODO: schedule the CRL to be regenerated -> increment the Number, ThisUpdate and NextUpdate fields + } } func checkCertificatePrivateKey(cert []*x509.Certificate, priv *ecdsa.PrivateKey) error { @@ -39,7 +45,39 @@ func checkCertificatePrivateKey(cert []*x509.Certificate, priv *ecdsa.PrivateKey return nil } -func NewSigner(ownerClaim string, hubID string, signerConfig SignerConfig) (*Signer, error) { +func getIssuerID(rootCertificate *x509.Certificate) (string, error) { + publicKeyRaw, err := x509.MarshalPKIXPublicKey(rootCertificate.PublicKey) + if err != nil { + return "", err + } + return uuid.NewSHA1(uuid.NameSpaceX500, publicKeyRaw).String(), nil +} + +func newSigner(ownerClaim, hubID, crlServerAddress string, signerConfig SignerConfig, privateKey *ecdsa.PrivateKey, certificate []*x509.Certificate) (*Signer, error) { + issuerID, err := getIssuerID(certificate[0]) + if err != nil { + return nil, err + } + signer := &Signer{ + validFrom: func() time.Time { + t, _ := tparse.ParseNow(time.RFC3339, signerConfig.ValidFrom) + return t + }, + validFor: signerConfig.ExpiresIn, + certificate: certificate, + privateKey: privateKey, + issuerID: issuerID, + ownerClaim: ownerClaim, + hubID: hubID, + } + if signerConfig.CRL.Enabled { + signer.crl.serverAddress = crlServerAddress + signer.crl.validFor = signerConfig.CRL.ExpiresIn + } + return signer, nil +} + +func NewSigner(ownerClaim, hubID, crlServerAddress string, signerConfig SignerConfig) (*Signer, error) { data, err := signerConfig.CertFile.Read() if err != nil { return nil, err @@ -60,19 +98,8 @@ func NewSigner(ownerClaim string, hubID string, signerConfig SignerConfig) (*Sig return nil, err } if len(certificate) == 1 && pkgX509.IsRootCA(certificate[0]) { - return &Signer{ - validFrom: func() time.Time { - t, _ := tparse.ParseNow(time.RFC3339, signerConfig.ValidFrom) - return t - }, - validFor: signerConfig.ExpiresIn, - certificate: certificate, - privateKey: privateKey, - ownerClaim: ownerClaim, - hubID: hubID, - }, nil + return newSigner(ownerClaim, hubID, crlServerAddress, signerConfig, privateKey, certificate) } - certificateAuthorities := make([]*x509.Certificate, 0, len(signerConfig.caPoolArray)*4) for _, caFile := range signerConfig.caPoolArray { data, errR := caFile.Read() @@ -93,18 +120,7 @@ func NewSigner(ownerClaim string, hubID string, signerConfig SignerConfig) (*Sig if err != nil { return nil, err } - - return &Signer{ - validFrom: func() time.Time { - t, _ := tparse.ParseNow(time.RFC3339, signerConfig.ValidFrom) - return t - }, - validFor: signerConfig.ExpiresIn, - certificate: chains[0], - privateKey: privateKey, - ownerClaim: ownerClaim, - hubID: hubID, - }, nil + return newSigner(ownerClaim, hubID, crlServerAddress, signerConfig, privateKey, chains[0]) } func (s *Signer) prepareSigningRecord(ctx context.Context, template *x509.Certificate) (*pb.SigningRecord, error) { @@ -117,31 +133,62 @@ func (s *Signer) prepareSigningRecord(ctx context.Context, template *x509.Certif if err != nil { return nil, err } - return toSigningRecord(owner, template) + return toSigningRecord(owner, s.issuerID, template) } -func (s *Signer) Sign(ctx context.Context, csr []byte) ([]byte, *pb.SigningRecord, error) { - notBefore := s.validFrom() - notAfter := notBefore.Add(s.validFor) - var signingRecord *pb.SigningRecord - signer := certificateSigner.New(s.certificate, s.privateKey, certificateSigner.WithNotBefore(notBefore), certificateSigner.WithNotAfter(notAfter), certificateSigner.WithOverrideCertTemplate(func(template *x509.Certificate) error { - var err error - signingRecord, err = s.prepareSigningRecord(ctx, template) - return err - })) - crt, err := signer.Sign(ctx, csr) - return crt, signingRecord, err +func (s *Signer) GetCertificate() *x509.Certificate { + return s.certificate[0] } -func (s *Signer) SignIdentityCSR(ctx context.Context, csr []byte) ([]byte, *pb.SigningRecord, error) { +func (s *Signer) GetPrivateKey() *ecdsa.PrivateKey { + return s.privateKey +} + +func (s *Signer) GetCRLConfiguation() (string, time.Duration) { + return s.crl.serverAddress, s.crl.validFor +} + +func (s *Signer) IsCRLEnabled() bool { + return s.crl.serverAddress != "" +} + +func (s *Signer) newCertificateSigner(identitySigner bool, opts ...func(cfg *certificateSigner.SignerConfig)) *certificateSigner.CertificateSigner { + if identitySigner { + return certificateSigner.NewIdentityCertificateSigner(s.certificate, s.privateKey, opts...) + } + return certificateSigner.New(s.certificate, s.privateKey, opts...) +} + +func (s *Signer) sign(ctx context.Context, isIdentityCertificate bool, csr []byte) ([]byte, *pb.SigningRecord, error) { notBefore := s.validFrom() notAfter := notBefore.Add(s.validFor) var signingRecord *pb.SigningRecord - signer := certificateSigner.NewIdentityCertificateSigner(s.certificate, s.privateKey, certificateSigner.WithNotBefore(notBefore), certificateSigner.WithNotAfter(notAfter), certificateSigner.WithOverrideCertTemplate(func(template *x509.Certificate) error { - var err error - signingRecord, err = s.prepareSigningRecord(ctx, template) - return err - })) + opts := []certificateSigner.Opt{ + certificateSigner.WithNotBefore(notBefore), + certificateSigner.WithNotAfter(notAfter), + certificateSigner.WithOverrideCertTemplate(func(template *x509.Certificate) error { + var err error + signingRecord, err = s.prepareSigningRecord(ctx, template) + return err + }), + } + if s.IsCRLEnabled() { + opts = append(opts, certificateSigner.WithCRLDistributionPoints( + []string{s.crl.serverAddress + uri.SigningRevocationListBase + "/" + s.issuerID}, + )) + } + signer := s.newCertificateSigner(isIdentityCertificate, opts...) cert, err := signer.Sign(ctx, csr) - return cert, signingRecord, err + if err != nil { + return nil, nil, err + } + return cert, signingRecord, nil +} + +func (s *Signer) Sign(ctx context.Context, csr []byte) ([]byte, *pb.SigningRecord, error) { + return s.sign(ctx, false, csr) +} + +func (s *Signer) SignIdentityCSR(ctx context.Context, csr []byte) ([]byte, *pb.SigningRecord, error) { + return s.sign(ctx, true, csr) } diff --git a/certificate-authority/service/grpc/signer_internal_test.go b/certificate-authority/service/grpc/signer_internal_test.go index 7c0464214..ab785ea33 100644 --- a/certificate-authority/service/grpc/signer_internal_test.go +++ b/certificate-authority/service/grpc/signer_internal_test.go @@ -116,7 +116,7 @@ func TestNewSigner(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := NewSigner("tt.args.ownerClaim", "tt.args.hubID", tt.args.signerConfig) + got, err := NewSigner("ownerClaim", "hubID", "", tt.args.signerConfig) if tt.wantErr { require.Error(t, err) return diff --git a/certificate-authority/service/http/config.go b/certificate-authority/service/http/config.go index f4392ddc6..d30b24556 100644 --- a/certificate-authority/service/http/config.go +++ b/certificate-authority/service/http/config.go @@ -12,6 +12,8 @@ type Config struct { Connection listener.Config `yaml:",inline" json:",inline"` Authorization validator.Config `yaml:"authorization" json:"authorization"` Server server.Config `yaml:",inline" json:",inline"` + + CRLEnabled bool `yaml:"-" json:"-"` } func (c *Config) Validate() error { diff --git a/certificate-authority/service/http/requestHandler.go b/certificate-authority/service/http/requestHandler.go index 4f8fd1953..21900b884 100644 --- a/certificate-authority/service/http/requestHandler.go +++ b/certificate-authority/service/http/requestHandler.go @@ -3,30 +3,45 @@ package http import ( "context" "fmt" + "net/http" "github.com/fullstorydev/grpchan/inprocgrpc" "github.com/gorilla/mux" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/plgd-dev/hub/v2/certificate-authority/pb" grpcService "github.com/plgd-dev/hub/v2/certificate-authority/service/grpc" + "github.com/plgd-dev/hub/v2/certificate-authority/service/uri" + "github.com/plgd-dev/hub/v2/certificate-authority/store" "github.com/plgd-dev/hub/v2/http-gateway/serverMux" + "github.com/plgd-dev/hub/v2/pkg/log" ) // RequestHandler for handling incoming request type RequestHandler struct { config *Config mux *runtime.ServeMux + + cas *grpcService.CertificateAuthorityServer + store store.Store + logger log.Logger } // NewHTTP returns HTTP handler -func NewRequestHandler(config *Config, r *mux.Router, certificateAuthorityServer *grpcService.CertificateAuthorityServer) (*RequestHandler, error) { +func NewRequestHandler(config *Config, r *mux.Router, cas *grpcService.CertificateAuthorityServer, s store.Store, logger log.Logger) (*RequestHandler, error) { requestHandler := &RequestHandler{ config: config, mux: serverMux.New(), + cas: cas, + store: s, + logger: logger, + } + + if config.CRLEnabled { + r.HandleFunc(uri.SigningRevocationList, requestHandler.revocationList).Methods(http.MethodGet) } ch := new(inprocgrpc.Channel) - pb.RegisterCertificateAuthorityServer(ch, certificateAuthorityServer) + pb.RegisterCertificateAuthorityServer(ch, cas) grpcClient := pb.NewCertificateAuthorityClient(ch) // register grpc-proxy handler if err := pb.RegisterCertificateAuthorityHandlerClient(context.Background(), requestHandler.mux, grpcClient); err != nil { diff --git a/certificate-authority/service/http/revocationList.go b/certificate-authority/service/http/revocationList.go new file mode 100644 index 000000000..b0e2b9f16 --- /dev/null +++ b/certificate-authority/service/http/revocationList.go @@ -0,0 +1,72 @@ +package http + +import ( + "crypto" + "crypto/rand" + "crypto/x509" + "net/http" + + "github.com/google/uuid" + "github.com/gorilla/mux" + "github.com/plgd-dev/hub/v2/certificate-authority/service/uri" + "github.com/plgd-dev/hub/v2/certificate-authority/store" + "github.com/plgd-dev/hub/v2/http-gateway/serverMux" + pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + pkgHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgTime "github.com/plgd-dev/hub/v2/pkg/time" + "google.golang.org/grpc/codes" +) + +func errCannotGetRevocationList(err error) error { + return pkgGrpc.ForwardErrorf(codes.Internal, "cannot get revocation list: %v", err) +} + +func createCRL(rl *store.RevocationList, issuer *x509.Certificate, priv crypto.Signer) ([]byte, error) { + number, err := store.ParseBigInt(rl.Number) + if err != nil { + return nil, err + } + template := &x509.RevocationList{ + Number: number, + ThisUpdate: pkgTime.Unix(0, rl.IssuedAt), + NextUpdate: pkgTime.Unix(0, rl.ValidUntil), + } + for _, c := range rl.Certificates { + sn, errP := store.ParseBigInt(c.Serial) + if errP != nil { + return nil, errP + } + template.RevokedCertificateEntries = append(template.RevokedCertificateEntries, x509.RevocationListEntry{ + SerialNumber: sn, + RevocationTime: pkgTime.Unix(0, c.Revocation), + }) + } + return x509.CreateRevocationList(rand.Reader, template, issuer, priv) +} + +func (requestHandler *RequestHandler) writeRevocationList(w http.ResponseWriter, r *http.Request) error { + vars := mux.Vars(r) + issuerID := vars[uri.IssuerIDKey] + if _, err := uuid.Parse(issuerID); err != nil { + return err + } + signer := requestHandler.cas.GetSigner() + _, validFor := signer.GetCRLConfiguation() + rl, err := requestHandler.store.GetLatestIssuedOrIssueRevocationList(r.Context(), issuerID, validFor) + if err != nil { + return err + } + crl, err := createCRL(rl, signer.GetCertificate(), signer.GetPrivateKey()) + if err != nil { + return err + } + w.Header().Set(pkgHttp.ContentTypeHeaderKey, "application/pkix-crl") + _, err = w.Write(crl) + return err +} + +func (requestHandler *RequestHandler) revocationList(w http.ResponseWriter, r *http.Request) { + if err := requestHandler.writeRevocationList(w, r); err != nil { + serverMux.WriteError(w, errCannotGetRevocationList(err)) + } +} diff --git a/certificate-authority/service/http/revocationList_test.go b/certificate-authority/service/http/revocationList_test.go new file mode 100644 index 000000000..16039bf9f --- /dev/null +++ b/certificate-authority/service/http/revocationList_test.go @@ -0,0 +1,114 @@ +package http_test + +import ( + "context" + "crypto/x509" + "io" + "net/http" + "testing" + "time" + + certAuthURI "github.com/plgd-dev/hub/v2/certificate-authority/service/uri" + "github.com/plgd-dev/hub/v2/certificate-authority/store" + "github.com/plgd-dev/hub/v2/certificate-authority/test" + httpgwTest "github.com/plgd-dev/hub/v2/http-gateway/test" + "github.com/plgd-dev/hub/v2/pkg/config/database" + pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + pkgTime "github.com/plgd-dev/hub/v2/pkg/time" + "github.com/plgd-dev/hub/v2/test/config" + oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" + testService "github.com/plgd-dev/hub/v2/test/service" + "github.com/stretchr/testify/require" +) + +func checkRevocationList(t *testing.T, crl *x509.RevocationList, certificates []*store.RevocationListCertificate) { + require.NotEmpty(t, crl.ThisUpdate) + require.NotEmpty(t, crl.NextUpdate) + expected := make([]x509.RevocationListEntry, 0, len(certificates)) + for _, cert := range certificates { + serial, err := store.ParseBigInt(cert.Serial) + require.NoError(t, err) + expected = append(expected, x509.RevocationListEntry{ + SerialNumber: serial, + RevocationTime: pkgTime.Unix(pkgTime.Unix(0, cert.Revocation).Unix(), 0).UTC(), + }) + } + actual := make([]x509.RevocationListEntry, 0, len(crl.RevokedCertificateEntries)) + for _, cert := range crl.RevokedCertificateEntries { + newCert := cert + newCert.Raw = nil + actual = append(actual, newCert) + } + require.Equal(t, expected, actual) +} + +func TestRevocationList(t *testing.T) { + if config.ACTIVE_DATABASE() == database.CqlDB { + t.Skip("revocation list not supported for CqlDB") + } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + shutDown := testService.SetUpServices(context.Background(), t, testService.SetUpServicesOAuth|testService.SetUpServicesMachine2MachineOAuth) + defer shutDown() + caShutdown := test.New(t, test.MakeConfig(t)) + defer caShutdown() + s, cleanUpStore := test.NewStore(t) + defer cleanUpStore() + + token := oauthTest.GetDefaultAccessToken(t) + ctx = pkgGrpc.CtxWithToken(ctx, token) + + stored := test.AddRevocationListToStore(ctx, t, s, time.Now().Add(-2*time.Hour-time.Minute)) + + type args struct { + issuer string + } + tests := []struct { + name string + args args + verifyCRL func(crl *x509.RevocationList) + wantErr bool + }{ + { + name: "invalid issuerID", + args: args{ + issuer: "invalid", + }, + wantErr: true, + }, + { + name: "valid", + args: args{ + issuer: test.GetIssuerID(7), + }, + verifyCRL: func(crl *x509.RevocationList) { + var certificates []*store.RevocationListCertificate + for _, issuerCerts := range stored { + if issuerCerts.Id != test.GetIssuerID(7) { + continue + } + certificates = append(certificates, issuerCerts.Certificates...) + } + checkRevocationList(t, crl, certificates) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + request := httpgwTest.NewRequest(http.MethodGet, certAuthURI.SigningRevocationList, nil).Host(config.CERTIFICATE_AUTHORITY_HTTP_HOST).AuthToken(token).AddIssuerID(tt.args.issuer).Build() + httpResp := httpgwTest.HTTPDo(t, request) + respBody, err := io.ReadAll(httpResp.Body) + require.NoError(t, err) + err = httpResp.Body.Close() + require.NoError(t, err) + crl, err := x509.ParseRevocationList(respBody) + if tt.wantErr { + require.Error(t, err) + return + } + tt.verifyCRL(crl) + }) + } +} diff --git a/certificate-authority/service/http/service.go b/certificate-authority/service/http/service.go index ab124b375..6ad0efb53 100644 --- a/certificate-authority/service/http/service.go +++ b/certificate-authority/service/http/service.go @@ -2,12 +2,16 @@ package http import ( "fmt" + "net/http" + "regexp" grpcService "github.com/plgd-dev/hub/v2/certificate-authority/service/grpc" - "github.com/plgd-dev/hub/v2/http-gateway/uri" + "github.com/plgd-dev/hub/v2/certificate-authority/service/uri" + "github.com/plgd-dev/hub/v2/certificate-authority/store" "github.com/plgd-dev/hub/v2/pkg/fsnotify" "github.com/plgd-dev/hub/v2/pkg/log" kitNetHttp "github.com/plgd-dev/hub/v2/pkg/net/http" + pkgHttpJwt "github.com/plgd-dev/hub/v2/pkg/net/http/jwt" httpService "github.com/plgd-dev/hub/v2/pkg/net/http/service" "github.com/plgd-dev/hub/v2/pkg/security/jwt/validator" "go.opentelemetry.io/otel/trace" @@ -20,24 +24,32 @@ type Service struct { } // New parses configuration and creates new Server with provided store and bus -func New(serviceName string, config Config, ca *grpcService.CertificateAuthorityServer, validator *validator.Validator, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) (*Service, error) { +func New(serviceName string, config Config, s store.Store, ca *grpcService.CertificateAuthorityServer, validator *validator.Validator, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) (*Service, error) { + var whiteList []pkgHttpJwt.RequestMatcher + if config.CRLEnabled { + whiteList = append(whiteList, pkgHttpJwt.RequestMatcher{ + Method: http.MethodGet, + URI: regexp.MustCompile(regexp.QuoteMeta(uri.SigningRevocationListBase) + `\/.*`), + }) + } + service, err := httpService.New(httpService.Config{ - HTTPConnection: config.Connection, - HTTPServer: config.Server, - ServiceName: serviceName, - AuthRules: kitNetHttp.NewDefaultAuthorizationRules(uri.API), - // WhiteEndpointList: whiteList, - FileWatcher: fileWatcher, - Logger: logger, - TraceProvider: tracerProvider, - Validator: validator, - // QueryCaseInsensitive: map[string]string{}, + HTTPConnection: config.Connection, + HTTPServer: config.Server, + ServiceName: serviceName, + AuthRules: kitNetHttp.NewDefaultAuthorizationRules(uri.API), + WhiteEndpointList: whiteList, + FileWatcher: fileWatcher, + Logger: logger, + TraceProvider: tracerProvider, + Validator: validator, + QueryCaseInsensitive: uri.QueryCaseInsensitive, }) if err != nil { return nil, fmt.Errorf("cannot create http service: %w", err) } - requestHandler, err := NewRequestHandler(&config, service.GetRouter(), ca) + requestHandler, err := NewRequestHandler(&config, service.GetRouter(), ca, s, logger) if err != nil { _ = service.Close() return nil, err diff --git a/certificate-authority/service/http/signCertificate_test.go b/certificate-authority/service/http/signCertificate_test.go index 10cfc1b36..ce64799d5 100644 --- a/certificate-authority/service/http/signCertificate_test.go +++ b/certificate-authority/service/http/signCertificate_test.go @@ -13,8 +13,9 @@ import ( "github.com/plgd-dev/device/v2/pkg/security/generateCertificate" "github.com/plgd-dev/hub/v2/certificate-authority/pb" + certAuthURI "github.com/plgd-dev/hub/v2/certificate-authority/service/uri" httpgwTest "github.com/plgd-dev/hub/v2/http-gateway/test" - kitNetGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" + pkgGrpc "github.com/plgd-dev/hub/v2/pkg/net/grpc" "github.com/plgd-dev/hub/v2/test/config" oauthTest "github.com/plgd-dev/hub/v2/test/oauth-server/test" "github.com/plgd-dev/hub/v2/test/service" @@ -26,11 +27,6 @@ import ( type ClientSignFunc = func(context.Context, *pb.SignCertificateRequest) (*pb.SignCertificateResponse, error) -const ( - URISignIdentityCertificate = "/api/v1/sign/identity-csr" - URISignCertificate = "/api/v1/sign/csr" -) - func testSigningByFunction(t *testing.T, signFn ClientSignFunc, csr []byte) { type args struct { req *pb.SignCertificateRequest @@ -55,7 +51,14 @@ func testSigningByFunction(t *testing.T, signFn ClientSignFunc, csr []byte) { CertificateSigningRequest: csr, }, }, - wantErr: false, + }, + { + name: "valid - new with the same csr", + args: args{ + req: &pb.SignCertificateRequest{ + CertificateSigningRequest: csr, + }, + }, }, } @@ -64,7 +67,7 @@ func testSigningByFunction(t *testing.T, signFn ClientSignFunc, csr []byte) { tearDown := service.SetUp(ctx, t) defer tearDown() - ctx = kitNetGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) + ctx = pkgGrpc.CtxWithToken(ctx, oauthTest.GetDefaultAccessToken(t)) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -80,7 +83,7 @@ func testSigningByFunction(t *testing.T, signFn ClientSignFunc, csr []byte) { } func httpDoSign(ctx context.Context, t *testing.T, uri string, req *pb.SignCertificateRequest, resp *pb.SignCertificateResponse) error { - token, err := kitNetGrpc.TokenFromOutgoingMD(ctx) + token, err := pkgGrpc.TokenFromOutgoingMD(ctx) require.NoError(t, err) reqBody, err := protojson.Marshal(req) require.NoError(t, err) @@ -110,7 +113,7 @@ func TestCertificateAuthorityServerSignCSR(t *testing.T) { require.NoError(t, err) testSigningByFunction(t, func(ctx context.Context, req *pb.SignCertificateRequest) (*pb.SignCertificateResponse, error) { var resp pb.SignCertificateResponse - return &resp, httpDoSign(ctx, t, URISignCertificate, req, &resp) + return &resp, httpDoSign(ctx, t, certAuthURI.SignCertificate, req, &resp) }, csr) } @@ -122,6 +125,6 @@ func TestCertificateAuthorityServerSignCSRWithEmptyCommonName(t *testing.T) { require.NoError(t, err) testSigningByFunction(t, func(ctx context.Context, req *pb.SignCertificateRequest) (*pb.SignCertificateResponse, error) { var resp pb.SignCertificateResponse - return &resp, httpDoSign(ctx, t, URISignCertificate, req, &resp) + return &resp, httpDoSign(ctx, t, certAuthURI.SignCertificate, req, &resp) }, csr) } diff --git a/certificate-authority/service/http/signIdentityCertificate_test.go b/certificate-authority/service/http/signIdentityCertificate_test.go index d42505067..2270674fa 100644 --- a/certificate-authority/service/http/signIdentityCertificate_test.go +++ b/certificate-authority/service/http/signIdentityCertificate_test.go @@ -9,6 +9,7 @@ import ( "github.com/plgd-dev/device/v2/pkg/security/generateCertificate" "github.com/plgd-dev/hub/v2/certificate-authority/pb" + certAuthURI "github.com/plgd-dev/hub/v2/certificate-authority/service/uri" "github.com/stretchr/testify/require" ) @@ -21,7 +22,7 @@ func TestCertificateAuthorityServerSignIdentityCSR(t *testing.T) { require.NoError(t, err) testSigningByFunction(t, func(ctx context.Context, req *pb.SignCertificateRequest) (*pb.SignCertificateResponse, error) { var resp pb.SignCertificateResponse - return &resp, httpDoSign(ctx, t, URISignIdentityCertificate, req, &resp) + return &resp, httpDoSign(ctx, t, certAuthURI.SignIdentityCertificate, req, &resp) }, csr) } @@ -32,6 +33,6 @@ func TestCertificateAuthorityServerSignIdentityCSRWithEmptyCN(t *testing.T) { require.NoError(t, err) testSigningByFunction(t, func(ctx context.Context, req *pb.SignCertificateRequest) (*pb.SignCertificateResponse, error) { var resp pb.SignCertificateResponse - return &resp, httpDoSign(ctx, t, URISignIdentityCertificate, req, &resp) + return &resp, httpDoSign(ctx, t, certAuthURI.SignIdentityCertificate, req, &resp) }, csr) } diff --git a/certificate-authority/service/service.go b/certificate-authority/service/service.go index c4c240c5c..4cee5f7fc 100644 --- a/certificate-authority/service/service.go +++ b/certificate-authority/service/service.go @@ -100,7 +100,7 @@ func New(ctx context.Context, config Config, fileWatcher *fsnotify.Watcher, logg } closerFn.AddFunc(closeStore) - ca, err := grpcService.NewCertificateAuthorityServer(config.APIs.GRPC.Authorization.OwnerClaim, config.HubID, config.Signer, dbStorage, fileWatcher, logger) + ca, err := grpcService.NewCertificateAuthorityServer(config.APIs.GRPC.Authorization.OwnerClaim, config.HubID, config.APIs.HTTP.Addr, config.Signer, dbStorage, fileWatcher, logger) if err != nil { closerFn.Execute() return nil, fmt.Errorf("cannot create grpc certificate authority server: %w", err) @@ -119,7 +119,8 @@ func New(ctx context.Context, config Config, fileWatcher *fsnotify.Watcher, logg }, Authorization: config.APIs.GRPC.Authorization.Config, Server: config.APIs.HTTP.Server, - }, ca, httpValidator, fileWatcher, logger, tracerProvider) + CRLEnabled: config.Signer.CRL.Enabled, + }, dbStorage, ca, httpValidator, fileWatcher, logger, tracerProvider) if err != nil { closerFn.Execute() return nil, fmt.Errorf("cannot create http service: %w", err) diff --git a/certificate-authority/service/uri/uri.go b/certificate-authority/service/uri/uri.go new file mode 100644 index 000000000..b6e7e2410 --- /dev/null +++ b/certificate-authority/service/uri/uri.go @@ -0,0 +1,20 @@ +package uri + +import "strings" + +const ( + API string = "/api/v1" + Sign string = API + "/sign" + + SignIdentityCertificate string = Sign + "/identity-csr" + SignCertificate string = Sign + "/csr" + + IssuerIDKey string = "issuerId" + + SigningRevocationListBase string = API + "/signing/crl" + SigningRevocationList string = SigningRevocationListBase + "/{" + IssuerIDKey + "}" +) + +var QueryCaseInsensitive = map[string]string{ + strings.ToLower(IssuerIDKey): IssuerIDKey, +} diff --git a/certificate-authority/store/cqldb/revocationList.go b/certificate-authority/store/cqldb/revocationList.go new file mode 100644 index 000000000..02d174f88 --- /dev/null +++ b/certificate-authority/store/cqldb/revocationList.go @@ -0,0 +1,24 @@ +package cqldb + +import ( + "context" + "time" + + "github.com/plgd-dev/hub/v2/certificate-authority/store" +) + +func (s *Store) SupportsRevocationList() bool { + return false +} + +func (s *Store) InsertRevocationLists(context.Context, ...*store.RevocationList) error { + return store.ErrNotSupported +} + +func (s *Store) UpdateRevocationList(context.Context, *store.UpdateRevocationListQuery) (*store.RevocationList, error) { + return nil, store.ErrNotSupported +} + +func (s *Store) GetLatestIssuedOrIssueRevocationList(context.Context, string, time.Duration) (*store.RevocationList, error) { + return nil, store.ErrNotSupported +} diff --git a/certificate-authority/store/cqldb/signingRecords.go b/certificate-authority/store/cqldb/signingRecords.go index feaa18b9b..3ec5d4687 100644 --- a/certificate-authority/store/cqldb/signingRecords.go +++ b/certificate-authority/store/cqldb/signingRecords.go @@ -339,15 +339,25 @@ func (s *Store) DeleteNonDeviceExpiredRecords(_ context.Context, _ time.Time) (i return 0, store.ErrNotSupported } -func (s *Store) LoadSigningRecords(ctx context.Context, owner string, query *store.SigningRecordsQuery, h store.LoadSigningRecordsFunc) error { +func (s *Store) LoadSigningRecords(ctx context.Context, owner string, query *store.SigningRecordsQuery, p store.Process[store.SigningRecord]) error { i := SigningRecordsIterator{ ctx: ctx, s: s, queries: toSigningRecordsQueryFilter(owner, query, true), provided: make(map[string]struct{}, 32), } - err := h(ctx, &i) - + var err error + for { + var stored store.SigningRecord + if !i.Next(ctx, &stored) { + err = i.Err() + break + } + err = p(&stored) + if err != nil { + break + } + } errClose := i.close() if err == nil { return errClose @@ -355,6 +365,14 @@ func (s *Store) LoadSigningRecords(ctx context.Context, owner string, query *sto return err } +func (s *Store) RevokeSigningRecords(ctx context.Context, ownerID string, query *store.RevokeSigningRecordsQuery) (int64, error) { + // TODO: revocation list not yet supported by cqldb, so for now we just delete the records + return s.DeleteSigningRecords(ctx, ownerID, &store.DeleteSigningRecordsQuery{ + IdFilter: query.GetIdFilter(), + DeviceIdFilter: query.GetDeviceIdFilter(), + }) +} + type SigningRecordsIterator struct { ctx context.Context queries []string diff --git a/certificate-authority/store/cqldb/signingRecords_test.go b/certificate-authority/store/cqldb/signingRecords_test.go index f2421c41d..a953edbf9 100644 --- a/certificate-authority/store/cqldb/signingRecords_test.go +++ b/certificate-authority/store/cqldb/signingRecords_test.go @@ -2,6 +2,7 @@ package cqldb_test import ( "context" + "math/big" "strconv" "sync" "testing" @@ -31,6 +32,8 @@ func TestStoreInsertSigningRecord(t *testing.T) { CertificatePem: "certificate", Date: date.UnixNano() - 1, ValidUntilDate: date.UnixNano() - 1, + Serial: big.NewInt(42).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, } tests := []struct { @@ -49,6 +52,8 @@ func TestStoreInsertSigningRecord(t *testing.T) { CertificatePem: "certificate", Date: date.UnixNano(), ValidUntilDate: date.UnixNano(), + Serial: big.NewInt(42).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, }, }, @@ -102,6 +107,8 @@ func TestStoreUpdateSigningRecord(t *testing.T) { CertificatePem: "certificate", Date: date.UnixNano() - 1, ValidUntilDate: date.UnixNano() - 1, + Serial: big.NewInt(42).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, } tests := []struct { @@ -120,6 +127,8 @@ func TestStoreUpdateSigningRecord(t *testing.T) { CertificatePem: "certificate", Date: date.UnixNano(), ValidUntilDate: date.UnixNano(), + Serial: big.NewInt(42).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, }, }, @@ -138,6 +147,8 @@ func TestStoreUpdateSigningRecord(t *testing.T) { CertificatePem: "certificate", Date: date.UnixNano(), ValidUntilDate: date.UnixNano(), + Serial: big.NewInt(42).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, }, }, @@ -155,6 +166,8 @@ func TestStoreUpdateSigningRecord(t *testing.T) { CertificatePem: "certificate1", Date: date1.UnixNano(), ValidUntilDate: date1.UnixNano(), + Serial: big.NewInt(43).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, }, }, @@ -177,7 +190,7 @@ func TestStoreUpdateSigningRecord(t *testing.T) { var h testSigningRecordHandler err = s.LoadSigningRecords(ctx, tt.args.sub.GetOwner(), &pb.GetSigningRecordsRequest{ IdFilter: []string{tt.args.sub.GetId()}, - }, h.Handle) + }, h.process) require.NoError(t, err) require.Len(t, h.lcs, 1) hubTest.CheckProtobufs(t, tt.args.sub, h.lcs[0], hubTest.RequireToCheckFunc(require.Equal)) @@ -278,6 +291,8 @@ func TestStoreDeleteSigningRecord(t *testing.T) { CertificatePem: "certificate", Date: date.UnixNano(), ValidUntilDate: date.UnixNano(), + Serial: big.NewInt(42).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, }) require.NoError(t, err) @@ -292,6 +307,8 @@ func TestStoreDeleteSigningRecord(t *testing.T) { CertificatePem: "certificate", Date: date.UnixNano(), ValidUntilDate: date.UnixNano(), + Serial: big.NewInt(43).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, }) require.NoError(t, err) @@ -306,6 +323,8 @@ func TestStoreDeleteSigningRecord(t *testing.T) { CertificatePem: "certificate", Date: date.UnixNano(), ValidUntilDate: date.UnixNano(), + Serial: big.NewInt(44).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, }) require.NoError(t, err) @@ -341,11 +360,13 @@ func TestStoreDeleteExpiredRecords(t *testing.T) { CertificatePem: "certificate", Date: date.UnixNano(), ValidUntilDate: date.UnixNano(), + Serial: big.NewInt(42).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, }) require.NoError(t, err) var h testSigningRecordHandler - err = s.LoadSigningRecords(ctx, "owner", nil, h.Handle) + err = s.LoadSigningRecords(ctx, "owner", nil, h.process) require.NoError(t, err) require.Len(t, h.lcs, 1) time.Sleep(time.Second * 3) @@ -353,7 +374,7 @@ func TestStoreDeleteExpiredRecords(t *testing.T) { require.Error(t, err) require.Equal(t, store.ErrNotSupported, err) var h1 testSigningRecordHandler - err = s.LoadSigningRecords(ctx, "owner", nil, h1.Handle) + err = s.LoadSigningRecords(ctx, "owner", nil, h1.process) require.NoError(t, err) require.Empty(t, h1.lcs) } @@ -362,15 +383,9 @@ type testSigningRecordHandler struct { lcs pb.SigningRecords } -func (h *testSigningRecordHandler) Handle(ctx context.Context, iter store.SigningRecordIter) (err error) { - for { - var sub store.SigningRecord - if !iter.Next(ctx, &sub) { - break - } - h.lcs = append(h.lcs, &sub) - } - return iter.Err() +func (h *testSigningRecordHandler) process(sr *store.SigningRecord) (err error) { + h.lcs = append(h.lcs, sr) + return nil } func TestStoreLoadSigningRecords(t *testing.T) { @@ -390,6 +405,8 @@ func TestStoreLoadSigningRecords(t *testing.T) { CertificatePem: "certificate", Date: date.UnixNano(), ValidUntilDate: date.UnixNano(), + Serial: big.NewInt(42).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, }, { @@ -403,6 +420,8 @@ func TestStoreLoadSigningRecords(t *testing.T) { CertificatePem: "certificate", Date: date.UnixNano(), ValidUntilDate: date.UnixNano(), + Serial: big.NewInt(43).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, }, { @@ -416,6 +435,8 @@ func TestStoreLoadSigningRecords(t *testing.T) { CertificatePem: "certificate", Date: date.UnixNano(), ValidUntilDate: date.UnixNano(), + Serial: big.NewInt(44).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, }, } @@ -518,7 +539,7 @@ func TestStoreLoadSigningRecords(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var h testSigningRecordHandler - err := s.LoadSigningRecords(ctx, "owner", tt.args.query, h.Handle) + err := s.LoadSigningRecords(ctx, "owner", tt.args.query, h.process) if tt.wantErr { require.Error(t, err) return @@ -550,6 +571,8 @@ func BenchmarkSigningRecords(b *testing.B) { CertificatePem: "certificate", Date: date.UnixNano(), ValidUntilDate: date.UnixNano(), + Serial: big.NewInt(42).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, }) } diff --git a/certificate-authority/store/mongodb/bulkWriter.go b/certificate-authority/store/mongodb/bulkWriter.go deleted file mode 100644 index 795ed327e..000000000 --- a/certificate-authority/store/mongodb/bulkWriter.go +++ /dev/null @@ -1,229 +0,0 @@ -package mongodb - -import ( - "context" - "sync" - "time" - - "github.com/hashicorp/go-multierror" - "github.com/plgd-dev/hub/v2/certificate-authority/store" - "github.com/plgd-dev/hub/v2/pkg/log" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" -) - -type bulkWriter struct { - col *mongo.Collection - documentLimit uint16 // https://www.mongodb.com/docs/manual/reference/limits/#mongodb-limit-Write-Command-Batch-Limit-Size - must be <= 100000 - throttleTime time.Duration - flushTimeout time.Duration - logger log.Logger - - done chan struct{} - trigger chan bool - - mutex sync.Mutex - models map[string]*store.SigningRecord - wg sync.WaitGroup -} - -func newBulkWriter(col *mongo.Collection, documentLimit uint16, throttleTime time.Duration, flushTimeout time.Duration, logger log.Logger) *bulkWriter { - r := &bulkWriter{ - col: col, - documentLimit: documentLimit, - throttleTime: throttleTime, - flushTimeout: flushTimeout, - done: make(chan struct{}), - trigger: make(chan bool, 1), - logger: logger, - } - - r.wg.Add(1) - go func() { - defer r.wg.Done() - r.run() - }() - return r -} - -func toSigningRecordFilter(signingRecord *store.SigningRecord) bson.M { - res := bson.M{"_id": signingRecord.GetId()} - return res -} - -func getSigningRecordCreationDate(defaultTime time.Time, signingRecord *store.SigningRecord) int64 { - ret := defaultTime.UTC().UnixNano() - if signingRecord.GetCredential().GetDate() > 0 && signingRecord.GetCredential().GetDate() < ret { - ret = signingRecord.GetCredential().GetDate() - } - return ret -} - -func setValueByDate(key, datePath string, dateOperator string, date int64, value interface{}) bson.M { - return bson.M{ - "$set": bson.M{ - key: bson.M{ - "$ifNull": bson.A{ - bson.M{ - "$cond": bson.M{ - "if": bson.M{ - dateOperator: bson.A{"$" + datePath, date}, - }, - "then": value, - "else": "$" + key, - }, - }, value, - }, - }, - }, - } -} - -func updateSigningRecord(signingRecord *store.SigningRecord) []bson.M { - creationDate := signingRecord.GetCreationDate() - if creationDate == 0 { - creationDate = getSigningRecordCreationDate(time.Now(), signingRecord) - } - ret := []bson.M{ - {"$set": bson.M{ - "_id": signingRecord.GetId(), - store.CommonNameKey: signingRecord.GetCommonName(), - store.OwnerKey: signingRecord.GetOwner(), - store.PublicKeyKey: signingRecord.GetPublicKey(), - }}, - } - ret = append(ret, setValueByDate(store.CreationDateKey, store.CreationDateKey, "$gt", creationDate, creationDate)) - if signingRecord.GetCredential() != nil { - ret = append(ret, setValueByDate(store.CredentialKey, store.CredentialKey+"."+store.DateKey, "$lt", signingRecord.GetCredential().GetDate(), signingRecord.GetCredential())) - } - return ret -} - -func convertSigningRecordToWriteModel(signingRecord *store.SigningRecord) mongo.WriteModel { - return mongo.NewUpdateOneModel().SetFilter(toSigningRecordFilter(signingRecord)).SetUpdate(updateSigningRecord(signingRecord)).SetUpsert(true) -} - -func mergeLatestUpdateSigningRecord(toUpdate *store.SigningRecord, latest *store.SigningRecord) *store.SigningRecord { - if toUpdate == nil { - return latest - } - if latest.GetCredential().GetDate() > toUpdate.GetCredential().GetDate() { - toUpdate.Credential = latest.GetCredential() - } - if latest.GetCreationDate() < toUpdate.GetCreationDate() { - toUpdate.CreationDate = latest.GetCreationDate() - if toUpdate.GetCommonName() == "" { - toUpdate.CommonName = latest.GetCommonName() - } - if toUpdate.GetOwner() == "" { - toUpdate.Owner = latest.GetOwner() - } - if toUpdate.GetPublicKey() == "" { - toUpdate.PublicKey = latest.GetPublicKey() - } - } - return toUpdate -} - -func (b *bulkWriter) popSigningRecords() map[string]*store.SigningRecord { - b.mutex.Lock() - defer b.mutex.Unlock() - models := b.models - b.models = nil - return models -} - -func (b *bulkWriter) Push(signingRecords ...*store.SigningRecord) { - b.mutex.Lock() - defer b.mutex.Unlock() - if b.models == nil { - b.models = make(map[string]*store.SigningRecord) - } - for _, signingRecord := range signingRecords { - b.models[signingRecord.GetId()] = mergeLatestUpdateSigningRecord(b.models[signingRecord.GetId()], signingRecord) - } - select { - case b.trigger <- true: - default: - } -} - -func (b *bulkWriter) numSigningRecords() int { - b.mutex.Lock() - defer b.mutex.Unlock() - return len(b.models) -} - -func (b *bulkWriter) run() { - ticker := time.NewTicker(b.throttleTime) - tickerRunning := true - defer ticker.Stop() - for { - select { - case <-ticker.C: - if b.tryBulkWrite() == 0 && tickerRunning { - ticker.Stop() - tickerRunning = false - } - case <-b.trigger: - if !tickerRunning { - ticker.Reset(b.throttleTime) - tickerRunning = true - } - if b.numSigningRecords() > int(b.documentLimit) { - b.tryBulkWrite() - } - case <-b.done: - return - } - } -} - -func (b *bulkWriter) bulkWrite() (int, error) { - SigningRecords := b.popSigningRecords() - if len(SigningRecords) == 0 { - return 0, nil - } - ctx := context.Background() - if b.flushTimeout != 0 { - ctx1, cancel := context.WithTimeout(context.Background(), b.flushTimeout) - defer cancel() - ctx = ctx1 - } - m := make([]mongo.WriteModel, 0, int(b.documentLimit)+1) - - var errors *multierror.Error - for _, SigningRecord := range SigningRecords { - m = append(m, convertSigningRecordToWriteModel(SigningRecord)) - if b.documentLimit == 0 || len(m)%int(b.documentLimit) == 0 { - _, err := b.col.BulkWrite(ctx, m, options.BulkWrite().SetOrdered(false)) - if err != nil { - errors = multierror.Append(errors, err) - } - m = m[:0] - } - } - - if len(m) > 0 { - _, err := b.col.BulkWrite(ctx, m, options.BulkWrite().SetOrdered(false)) - if err != nil { - errors = multierror.Append(errors, err) - } - } - return len(SigningRecords), errors.ErrorOrNil() -} - -func (b *bulkWriter) tryBulkWrite() int { - n, err := b.bulkWrite() - if err != nil { - b.logger.Errorf("failed to bulk update Signing records: %w", err) - } - return n -} - -func (b *bulkWriter) Close() { - close(b.done) - b.wg.Wait() - b.tryBulkWrite() -} diff --git a/certificate-authority/store/mongodb/config.go b/certificate-authority/store/mongodb/config.go index b2d839864..8034a68c7 100644 --- a/certificate-authority/store/mongodb/config.go +++ b/certificate-authority/store/mongodb/config.go @@ -1,38 +1,13 @@ package mongodb import ( - "fmt" - "time" - pkgMongo "github.com/plgd-dev/hub/v2/pkg/mongodb" ) -const minDuration = time.Millisecond * 100 - -type BulkWriteConfig struct { - Timeout time.Duration `yaml:"timeout"` - ThrottleTime time.Duration `yaml:"throttleTime"` - DocumentLimit uint16 `yaml:"documentLimit"` -} - -func (c *BulkWriteConfig) Validate() error { - if c.Timeout <= minDuration { - return fmt.Errorf("timeout('%v')", c.Timeout) - } - if c.ThrottleTime <= minDuration { - return fmt.Errorf("throttleTime('%v')", c.ThrottleTime) - } - return nil -} - type Config struct { - Mongo pkgMongo.Config `yaml:",inline"` - BulkWrite BulkWriteConfig `yaml:"bulkWrite"` + Mongo pkgMongo.Config `yaml:",inline"` } func (c *Config) Validate() error { - if err := c.BulkWrite.Validate(); err != nil { - return fmt.Errorf("bulkWrite.%w", err) - } return c.Mongo.Validate() } diff --git a/certificate-authority/store/mongodb/revocationList.go b/certificate-authority/store/mongodb/revocationList.go new file mode 100644 index 000000000..c0049b7a4 --- /dev/null +++ b/certificate-authority/store/mongodb/revocationList.go @@ -0,0 +1,215 @@ +package mongodb + +import ( + "context" + "errors" + "math/big" + "time" + + "github.com/plgd-dev/hub/v2/certificate-authority/store" + "github.com/plgd-dev/hub/v2/pkg/mongodb" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "golang.org/x/exp/maps" +) + +const revocationListCol = "revocationList" + +func (s *Store) SupportsRevocationList() bool { + return true +} + +func (s *Store) InsertRevocationLists(ctx context.Context, rls ...*store.RevocationList) error { + documents := make([]interface{}, 0, len(rls)) + for _, rl := range rls { + if err := rl.Validate(); err != nil { + return err + } + documents = append(documents, rl) + } + _, err := s.Collection(revocationListCol).InsertMany(ctx, documents) + return err +} + +type revocationListUpdate struct { + originalRevocationList *store.RevocationList + certificatesToInsert map[string]*store.RevocationListCertificate +} + +// check the database and remove serials that are already in the array +func (s *Store) getRevocationListUpdate(ctx context.Context, query *store.UpdateRevocationListQuery) (revocationListUpdate, bool, error) { + cmap := make(map[string]*store.RevocationListCertificate) + for _, cert := range query.RevokedCertificates { + if _, ok := cmap[cert.Serial]; ok { + s.logger.Debugf("skipping duplicate serial number(%v) in query", cert.Serial) + continue + } + if err := cert.Validate(); err != nil { + return revocationListUpdate{}, false, err + } + cmap[cert.Serial] = cert + } + pl := mongo.Pipeline{ + bson.D{{Key: mongodb.Match, Value: bson.D{{Key: "_id", Value: query.IssuerID}}}}, + } + if len(cmap) > 0 { + pl = append(pl, bson.D{{Key: "$addFields", Value: bson.M{ + "duplicates": bson.M{ + "$filter": bson.M{ + "input": "$" + store.CertificatesKey, + "as": "cert", + "cond": bson.M{mongodb.In: bson.A{"$$cert." + store.SerialKey, maps.Keys(cmap)}}, + }, + }, + }}}) + } + cur, err := s.Collection(revocationListCol).Aggregate(ctx, pl) + if err != nil { + return revocationListUpdate{}, false, err + } + type revocationListWithNewCertificates struct { + *store.RevocationList `bson:",inline"` + Duplicates []*store.RevocationListCertificate `bson:"duplicates,omitempty"` + } + var rl *revocationListWithNewCertificates + count, err := processCursor(ctx, cur, func(item *revocationListWithNewCertificates) error { + rl = item + return nil + }) + if err != nil { + return revocationListUpdate{}, false, err + } + if count == 0 { + return revocationListUpdate{ + certificatesToInsert: cmap, + }, true, nil + } + for _, c := range rl.Duplicates { + s.logger.Debugf("skipping duplicate serial number(%v)", c.Serial) + delete(cmap, c.Serial) + } + if len(cmap) == 0 && (!query.UpdateIfExpired || !rl.IsExpired()) { + return revocationListUpdate{ + originalRevocationList: rl.RevocationList, + }, false, nil + } + return revocationListUpdate{ + originalRevocationList: rl.RevocationList, + certificatesToInsert: cmap, + }, true, nil +} + +func (s *Store) UpdateRevocationList(ctx context.Context, query *store.UpdateRevocationListQuery) (*store.RevocationList, error) { + if err := query.Validate(); err != nil { + return nil, err + } + upd, needsUpdate, err := s.getRevocationListUpdate(ctx, query) + if err != nil { + return nil, err + } + if !needsUpdate { + return upd.originalRevocationList, nil + } + + if upd.originalRevocationList == nil { + newRL := &store.RevocationList{ + Id: query.IssuerID, + Number: "1", // the sequence for the CRL number field starts from 1 + IssuedAt: query.IssuedAt, + ValidUntil: query.ValidUntil, + Certificates: maps.Values(upd.certificatesToInsert), + } + if err = s.InsertRevocationLists(ctx, newRL); err != nil { + return nil, err + } + return newRL, nil + } + + number, err := store.ParseBigInt(upd.originalRevocationList.Number) + if err != nil { + return nil, err + } + filter := bson.M{ + "_id": query.IssuerID, + store.NumberKey: number.String(), + } + nextNumber := number.Add(number, big.NewInt(1)) + update := bson.M{ + "$set": bson.M{ + store.NumberKey: nextNumber.String(), + store.IssuedAtKey: query.IssuedAt, + store.ValidUntilKey: query.ValidUntil, + }, + } + if len(upd.certificatesToInsert) > 0 { + update["$push"] = bson.M{ + store.CertificatesKey: bson.M{"$each": maps.Values(upd.certificatesToInsert)}, + } + } + opts := options.FindOneAndUpdate().SetReturnDocument(options.After) + var updatedRL store.RevocationList + if err = s.Collection(revocationListCol).FindOneAndUpdate(ctx, filter, update, opts).Decode(&updatedRL); err != nil { + return nil, err + } + return &updatedRL, nil +} + +func (s *Store) GetRevocationList(ctx context.Context, issuerID string, includeExpired bool) (*store.RevocationList, error) { + now := time.Now().UnixNano() + filter := bson.M{ + "_id": issuerID, + } + var opts []*options.FindOneOptions + if !includeExpired { + filter[store.CertificatesKey] = bson.M{ + "$elemMatch": bson.M{ + store.ValidUntilKey: bson.M{"$gte": now}, // non-expired certificates + }, + } + projection := bson.M{ + "_id": 1, + store.NumberKey: 1, + store.IssuedAtKey: 1, + store.ValidUntilKey: 1, + store.CertificatesKey: bson.M{ + "$filter": bson.M{ + "input": "$" + store.CertificatesKey, + "as": "cert", + "cond": bson.M{ + "$gte": []interface{}{"$$cert." + store.ValidUntilKey, now}, // non-expired certificates + }, + }, + }, + } + opts = append(opts, options.FindOne().SetProjection(projection)) + } + + var rl store.RevocationList + err := s.Collection(revocationListCol).FindOne(ctx, filter, opts...).Decode(&rl) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return nil, store.ErrNotFound + } + return nil, err + } + return &rl, nil +} + +func (s *Store) GetLatestIssuedOrIssueRevocationList(ctx context.Context, issuerID string, validFor time.Duration) (*store.RevocationList, error) { + rl, err := s.GetRevocationList(ctx, issuerID, true) + if err != nil { + return nil, err + } + if rl.IssuedAt > 0 && !rl.IsExpired() { + return rl, nil + } + issuedAt := time.Now() + validUntil := issuedAt.Add(validFor) + return s.UpdateRevocationList(ctx, &store.UpdateRevocationListQuery{ + IssuerID: issuerID, + IssuedAt: issuedAt.UnixNano(), + ValidUntil: validUntil.UnixNano(), + UpdateIfExpired: true, + }) +} diff --git a/certificate-authority/store/mongodb/revocationList_test.go b/certificate-authority/store/mongodb/revocationList_test.go new file mode 100644 index 000000000..c596d16a5 --- /dev/null +++ b/certificate-authority/store/mongodb/revocationList_test.go @@ -0,0 +1,250 @@ +package mongodb_test + +import ( + "context" + "testing" + "time" + + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/certificate-authority/store" + "github.com/plgd-dev/hub/v2/certificate-authority/test" + "github.com/stretchr/testify/require" +) + +func TestUpdateRevocationList(t *testing.T) { + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30*10) + defer cancel() + + id := uuid.NewString() + id2 := uuid.NewString() + // id3 := uuid.NewString() + cert1 := &store.RevocationListCertificate{ + Serial: "1", + ValidUntil: time.Now().Add(time.Hour).Unix(), + Revocation: time.Now().Unix(), + } + cert2 := &store.RevocationListCertificate{ + Serial: "2", + ValidUntil: time.Now().Add(time.Hour).Unix(), + Revocation: time.Now().Unix(), + } + cert3 := &store.RevocationListCertificate{ + Serial: "2", + ValidUntil: time.Now().Add(time.Hour).Unix(), + Revocation: time.Now().Unix(), + } + type args struct { + query store.UpdateRevocationListQuery + } + tests := []struct { + name string + args args + want *store.RevocationList + wantErr bool + }{ + { + name: "missing ID", + args: args{ + query: store.UpdateRevocationListQuery{ + IssuerID: "", + RevokedCertificates: []*store.RevocationListCertificate{cert1}, + }, + }, + wantErr: true, + }, + { + name: "missing serial number", + args: args{ + query: store.UpdateRevocationListQuery{ + IssuerID: id, + RevokedCertificates: []*store.RevocationListCertificate{{ + Revocation: time.Now().UnixNano(), + }}, + }, + }, + wantErr: true, + }, + { + name: "missing revocation time", + args: args{ + query: store.UpdateRevocationListQuery{ + IssuerID: id, + RevokedCertificates: []*store.RevocationListCertificate{{ + Serial: "1", + }}, + }, + }, + wantErr: true, + }, + { + name: "valid - new document", + args: args{ + query: store.UpdateRevocationListQuery{ + IssuerID: id, + RevokedCertificates: []*store.RevocationListCertificate{cert1}, + }, + }, + want: &store.RevocationList{ + Id: id, + Number: "1", + Certificates: []*store.RevocationListCertificate{ + cert1, + }, + }, + }, + { + name: "valid - add to existing document", + args: args{ + query: store.UpdateRevocationListQuery{ + IssuerID: id, + RevokedCertificates: []*store.RevocationListCertificate{cert2}, + }, + }, + want: &store.RevocationList{ + Id: id, + Number: "2", + Certificates: []*store.RevocationListCertificate{ + cert1, + cert2, + }, + }, + }, + { + name: "valid - duplicate serial, noop", + args: args{ + query: store.UpdateRevocationListQuery{ + IssuerID: id, + RevokedCertificates: []*store.RevocationListCertificate{{ + Serial: cert2.Serial, + ValidUntil: time.Now().Add(time.Hour).Unix(), + Revocation: time.Now().Unix(), + }}, + }, + }, + want: &store.RevocationList{ + Id: id, + Number: "2", + Certificates: []*store.RevocationListCertificate{ + cert1, + cert2, + }, + }, + }, + { + name: "valid - different issuer, existing serial", + args: args{ + query: store.UpdateRevocationListQuery{ + IssuerID: id2, + RevokedCertificates: []*store.RevocationListCertificate{cert3}, + }, + }, + want: &store.RevocationList{ + Id: id2, + Number: "1", + Certificates: []*store.RevocationListCertificate{cert3}, + }, + }, + // { + // name: "valid - no certificates, set to expired", + // args: args{ + // query: store.UpdateRevocationListQuery{ + // IssuerID: id3, + // IssuedAt: time.Now().Add(-time.Minute).UnixNano(), + // ValidUntil: time.Now().UnixNano(), + // }, + // }, + // want: &store.RevocationList{ + // Id: id3, + // Number: "1", + // Certificates: nil, + // }, + // }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + updatedRL, err := s.UpdateRevocationList(ctx, &tt.args.query) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + test.CheckRevocationList(t, tt.want, updatedRL, false) + }) + } +} + +func TestGetRevocationLists(t *testing.T) { + s, cleanUpStore := test.NewMongoStore(t) + defer cleanUpStore() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + stored := test.AddRevocationListToStore(ctx, t, s, time.Now().Add(-2*time.Hour-time.Minute)) + + type args struct { + issuerID string + includeExpired bool + } + tests := []struct { + name string + args args + want *store.RevocationList + wantErr bool + }{ + { + name: "no matching ID", + args: args{ + issuerID: "00000000-0000-0000-0000-123456789012", + }, + wantErr: true, + }, + { + name: "all from issuer0", + args: args{ + issuerID: test.GetIssuerID(0), + includeExpired: true, + }, + want: func() *store.RevocationList { + expected, ok := stored[test.GetIssuerID(0)] + require.True(t, ok) + return expected + }(), + }, + { + name: "no valid from issuer0", + args: args{ + issuerID: test.GetIssuerID(0), + }, + wantErr: true, + }, + { + name: "non-expired from issuer4", + args: args{ + issuerID: test.GetIssuerID(4), + }, + want: func() *store.RevocationList { + expected, ok := stored[test.GetIssuerID(4)] + require.True(t, ok) + return expected + }(), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + retrieved, err := s.GetRevocationList(ctx, tt.args.issuerID, tt.args.includeExpired) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.NoError(t, err) + test.CheckRevocationList(t, tt.want, retrieved, false) + }) + } +} diff --git a/certificate-authority/store/mongodb/signingRecords.go b/certificate-authority/store/mongodb/signingRecords.go index 1217f9ca7..c3013c8e4 100644 --- a/certificate-authority/store/mongodb/signingRecords.go +++ b/certificate-authority/store/mongodb/signingRecords.go @@ -6,9 +6,11 @@ import ( "time" "github.com/hashicorp/go-multierror" + "github.com/plgd-dev/hub/v2/certificate-authority/pb" "github.com/plgd-dev/hub/v2/certificate-authority/store" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" ) const signingRecordsCol = "signedCertificateRecords" @@ -20,20 +22,20 @@ func (s *Store) CreateSigningRecord(ctx context.Context, signingRecord *store.Si return err } _, err := s.Collection(signingRecordsCol).InsertOne(ctx, signingRecord) - if err != nil { - return err - } - - return nil + return err } -func (s *Store) UpdateSigningRecord(_ context.Context, signingRecord *store.SigningRecord) error { +func (s *Store) UpdateSigningRecord(ctx context.Context, signingRecord *store.SigningRecord) error { if err := signingRecord.Validate(); err != nil { return err } - - s.bulkWriter.Push(signingRecord) - return nil + filter := bson.M{"_id": signingRecord.GetId()} + upsert := true + opts := &options.UpdateOptions{ + Upsert: &upsert, + } + _, err := s.Collection(signingRecordsCol).UpdateOne(ctx, filter, bson.M{"$set": signingRecord}, opts) + return err } func toCommonNameQueryFilter(owner string, commonName string) bson.D { @@ -112,44 +114,77 @@ func (s *Store) DeleteNonDeviceExpiredRecords(ctx context.Context, now time.Time if err != nil { return -1, multierror.Append(ErrCannotRemoveSigningRecord, err) } - return res.DeletedCount, nil } -func (s *Store) LoadSigningRecords(ctx context.Context, owner string, query *store.SigningRecordsQuery, h store.LoadSigningRecordsFunc) error { - col := s.Collection(signingRecordsCol) - iter, err := col.Find(ctx, toSigningRecordsQueryFilter(owner, query)) - if errors.Is(err, mongo.ErrNilDocument) { - return nil +func (s *Store) RevokeSigningRecords(ctx context.Context, ownerID string, query *store.RevokeSigningRecordsQuery) (int64, error) { + now := time.Now().UnixNano() + // get signing records to be deleted + type issuersRecord struct { + ids []string + certificates []*store.RevocationListCertificate } + idFilter := []string{} + irs := make(map[string]issuersRecord) + err := s.LoadSigningRecords(ctx, ownerID, &pb.GetSigningRecordsRequest{ + IdFilter: query.GetIdFilter(), + DeviceIdFilter: query.GetDeviceIdFilter(), + }, func(v *pb.SigningRecord) error { + credential := v.GetCredential() + if credential == nil { + return nil + } + if credential.GetValidUntilDate() <= now { + idFilter = append(idFilter, v.GetId()) + return nil + } + record := irs[credential.GetIssuerId()] + record.ids = append(record.ids, v.GetId()) + record.certificates = append(record.certificates, &store.RevocationListCertificate{ + Serial: credential.GetSerial(), + ValidUntil: credential.GetValidUntilDate(), + Revocation: now, + }) + irs[credential.GetIssuerId()] = record + return nil + }) if err != nil { - return err + return 0, err } - i := SigningRecordsIterator{ - iter: iter, + // add certificates for the signing records to revocation lists + for issuerID, record := range irs { + query := store.UpdateRevocationListQuery{ + IssuerID: issuerID, + RevokedCertificates: record.certificates, + } + _, err := s.UpdateRevocationList(ctx, &query) + if err != nil { + return 0, err + } + // TODO + // idFilter = append(idFilter, serials...) } - err = h(ctx, &i) - errClose := iter.Close(ctx) - if err == nil { - return errClose + if len(idFilter) == 0 { + return 0, nil } - return err -} -type SigningRecordsIterator struct { - iter *mongo.Cursor + // delete the signing records + return s.DeleteSigningRecords(ctx, ownerID, &pb.DeleteSigningRecordsRequest{ + IdFilter: idFilter, + }) } -func (i *SigningRecordsIterator) Next(ctx context.Context, s *store.SigningRecord) bool { - if !i.iter.Next(ctx) { - return false +func (s *Store) LoadSigningRecords(ctx context.Context, owner string, query *store.SigningRecordsQuery, p store.Process[store.SigningRecord]) error { + col := s.Collection(signingRecordsCol) + cur, err := col.Find(ctx, toSigningRecordsQueryFilter(owner, query)) + if err != nil { + if errors.Is(err, mongo.ErrNilDocument) { + return nil + } + return err } - err := i.iter.Decode(s) - return err == nil -} - -func (i *SigningRecordsIterator) Err() error { - return i.iter.Err() + _, err = processCursor(ctx, cur, p) + return err } diff --git a/certificate-authority/store/mongodb/signingRecords_test.go b/certificate-authority/store/mongodb/signingRecords_test.go index 312f1a29b..b83852161 100644 --- a/certificate-authority/store/mongodb/signingRecords_test.go +++ b/certificate-authority/store/mongodb/signingRecords_test.go @@ -2,8 +2,7 @@ package mongodb_test import ( "context" - "strconv" - "sync" + "math/big" "testing" "time" @@ -11,7 +10,6 @@ import ( "github.com/plgd-dev/hub/v2/certificate-authority/store" "github.com/plgd-dev/hub/v2/certificate-authority/test" hubTest "github.com/plgd-dev/hub/v2/test" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -43,6 +41,8 @@ func TestStoreUpdateSigningRecord(t *testing.T) { CertificatePem: "certificate", Date: constDate().UnixNano(), ValidUntilDate: constDate().UnixNano(), + Serial: big.NewInt(42).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, }, }, @@ -61,6 +61,8 @@ func TestStoreUpdateSigningRecord(t *testing.T) { CertificatePem: "certificate", Date: constDate().UnixNano(), ValidUntilDate: constDate().UnixNano(), + Serial: big.NewInt(42).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, }, }, @@ -78,6 +80,8 @@ func TestStoreUpdateSigningRecord(t *testing.T) { CertificatePem: "certificate1", Date: constDate1().UnixNano(), ValidUntilDate: constDate1().UnixNano(), + Serial: big.NewInt(42).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, }, }, @@ -94,10 +98,8 @@ func TestStoreUpdateSigningRecord(t *testing.T) { err := s.UpdateSigningRecord(ctx, tt.args.sub) if tt.wantErr { require.Error(t, err) - } else { - require.NoError(t, err) + return } - err = s.FlushBulkWriter() require.NoError(t, err) }) } @@ -212,6 +214,8 @@ func TestStoreDeleteSigningRecords(t *testing.T) { CertificatePem: "certificate", Date: constDate().UnixNano(), ValidUntilDate: constDate().UnixNano(), + Serial: big.NewInt(42).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, }) require.NoError(t, err) @@ -276,6 +280,8 @@ func TestStoreDeleteExpiredRecords(t *testing.T) { CertificatePem: "certificate", Date: constDate().UnixNano(), ValidUntilDate: constDate().UnixNano(), + Serial: big.NewInt(42).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, }) require.NoError(t, err) @@ -293,15 +299,9 @@ type testSigningRecordHandler struct { lcs pb.SigningRecords } -func (h *testSigningRecordHandler) Handle(ctx context.Context, iter store.SigningRecordIter) (err error) { - for { - var sub store.SigningRecord - if !iter.Next(ctx, &sub) { - break - } - h.lcs = append(h.lcs, &sub) - } - return iter.Err() +func (h *testSigningRecordHandler) process(sr *store.SigningRecord) (err error) { + h.lcs = append(h.lcs, sr) + return nil } func TestStoreLoadSigningRecords(t *testing.T) { @@ -323,6 +323,8 @@ func TestStoreLoadSigningRecords(t *testing.T) { CertificatePem: "certificate", Date: constDate().UnixNano(), ValidUntilDate: constDate().UnixNano(), + Serial: big.NewInt(42).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, }, { @@ -336,6 +338,8 @@ func TestStoreLoadSigningRecords(t *testing.T) { CertificatePem: "certificate", Date: constDate().UnixNano(), ValidUntilDate: constDate().UnixNano(), + Serial: big.NewInt(42).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, }, { @@ -349,6 +353,8 @@ func TestStoreLoadSigningRecords(t *testing.T) { CertificatePem: "certificate", Date: constDate().UnixNano(), ValidUntilDate: constDate().UnixNano(), + Serial: big.NewInt(42).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, }, { @@ -362,6 +368,8 @@ func TestStoreLoadSigningRecords(t *testing.T) { CertificatePem: "certificate", Date: constDate().UnixNano(), ValidUntilDate: constDate().UnixNano(), + Serial: big.NewInt(42).String(), + IssuerId: "42424242-4242-4242-4242-424242424242", }, }, } @@ -466,7 +474,7 @@ func TestStoreLoadSigningRecords(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var h testSigningRecordHandler - err := s.LoadSigningRecords(ctx, tt.args.owner, tt.args.query, h.Handle) + err := s.LoadSigningRecords(ctx, tt.args.owner, tt.args.query, h.process) require.NoError(t, err) require.Len(t, h.lcs, len(tt.want)) h.lcs.Sort() @@ -478,47 +486,3 @@ func TestStoreLoadSigningRecords(t *testing.T) { }) } } - -func BenchmarkSigningRecords(b *testing.B) { - data := make([]*store.SigningRecord, 0, 5001) - dataCap := cap(data) - for i := 0; i < dataCap; i++ { - data = append(data, &store.SigningRecord{ - Id: hubTest.GenerateDeviceIDbyIdx(i), - Owner: "owner", - CommonName: "commonName" + strconv.Itoa(i), - CreationDate: constDate().UnixNano(), - PublicKey: "publicKey", - Credential: &pb.CredentialStatus{ - CertificatePem: "certificate", - Date: constDate().UnixNano(), - ValidUntilDate: constDate().UnixNano(), - }, - }) - } - - ctx := context.Background() - b.ResetTimer() - s, cleanUpStore := test.NewMongoStore(b) - defer cleanUpStore() - for i := uint32(0); i < uint32(b.N); i++ { - b.StopTimer() - err := s.Clear(ctx) - require.NoError(b, err) - b.StartTimer() - func() { - var wg sync.WaitGroup - wg.Add(len(data)) - for _, l := range data { - go func(l *pb.SigningRecord) { - defer wg.Done() - err := s.UpdateSigningRecord(ctx, l) - assert.NoError(b, err) - }(l) - } - wg.Wait() - err := s.FlushBulkWriter() - assert.NoError(b, err) - }() - } -} diff --git a/certificate-authority/store/mongodb/store.go b/certificate-authority/store/mongodb/store.go index 72f535d39..afbc1243b 100644 --- a/certificate-authority/store/mongodb/store.go +++ b/certificate-authority/store/mongodb/store.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/hashicorp/go-multierror" "github.com/plgd-dev/hub/v2/certificate-authority/store" "github.com/plgd-dev/hub/v2/pkg/fsnotify" "github.com/plgd-dev/hub/v2/pkg/log" @@ -16,7 +17,7 @@ import ( type Store struct { *pkgMongo.Store - bulkWriter *bulkWriter + logger log.Logger } var deviceIDKeyQueryIndex = mongo.IndexModel{ @@ -33,22 +34,62 @@ var commonNameKeyQueryIndex = mongo.IndexModel{ }, } +type MongoIterator[T any] struct { + Cursor *mongo.Cursor +} + +func (i *MongoIterator[T]) Next(ctx context.Context, s *T) bool { + if !i.Cursor.Next(ctx) { + return false + } + err := i.Cursor.Decode(s) + return err == nil +} + +func (i *MongoIterator[T]) Err() error { + return i.Cursor.Err() +} + +func processCursor[T any](ctx context.Context, cr *mongo.Cursor, p store.Process[T]) (int, error) { + var errors *multierror.Error + iter := MongoIterator[T]{ + Cursor: cr, + } + count := 0 + for { + var stored T + if !iter.Next(ctx, &stored) { + break + } + err := p(&stored) + if err != nil { + errors = multierror.Append(errors, err) + break + } + count++ + } + errors = multierror.Append(errors, iter.Err()) + errClose := cr.Close(ctx) + errors = multierror.Append(errors, errClose) + return count, errors.ErrorOrNil() +} + func New(ctx context.Context, cfg *Config, fileWatcher *fsnotify.Watcher, logger log.Logger, tracerProvider trace.TracerProvider) (*Store, error) { certManager, err := client.New(cfg.Mongo.TLS, fileWatcher, logger) if err != nil { return nil, fmt.Errorf("could not create cert manager: %w", err) } - m, err := pkgMongo.NewStore(ctx, &cfg.Mongo, certManager.GetTLSConfig(), tracerProvider) + m, err := pkgMongo.NewStoreWithCollections(ctx, &cfg.Mongo, certManager.GetTLSConfig(), tracerProvider, map[string][]mongo.IndexModel{ + signingRecordsCol: {commonNameKeyQueryIndex, deviceIDKeyQueryIndex}, + revocationListCol: nil, + }) if err != nil { certManager.Close() return nil, err } - bulkWriter := newBulkWriter(m.Collection(signingRecordsCol), cfg.BulkWrite.DocumentLimit, cfg.BulkWrite.ThrottleTime, cfg.BulkWrite.Timeout, logger) - s := Store{Store: m, bulkWriter: bulkWriter} - err = s.EnsureIndex(ctx, signingRecordsCol, commonNameKeyQueryIndex, deviceIDKeyQueryIndex) - if err != nil { - certManager.Close() - return nil, err + s := Store{ + Store: m, + logger: logger, } s.SetOnClear(s.clearDatabases) s.AddCloseFunc(certManager.Close) @@ -56,15 +97,12 @@ func New(ctx context.Context, cfg *Config, fileWatcher *fsnotify.Watcher, logger } func (s *Store) clearDatabases(ctx context.Context) error { - return s.Collection(signingRecordsCol).Drop(ctx) + var errs *multierror.Error + errs = multierror.Append(errs, s.Collection(signingRecordsCol).Drop(ctx)) + errs = multierror.Append(errs, s.Collection(revocationListCol).Drop(ctx)) + return errs.ErrorOrNil() } func (s *Store) Close(ctx context.Context) error { - s.bulkWriter.Close() return s.Store.Close(ctx) } - -func (s *Store) FlushBulkWriter() error { - _, err := s.bulkWriter.bulkWrite() - return err -} diff --git a/certificate-authority/store/revocationList.go b/certificate-authority/store/revocationList.go new file mode 100644 index 000000000..67bd8a1dd --- /dev/null +++ b/certificate-authority/store/revocationList.go @@ -0,0 +1,83 @@ +package store + +import ( + "errors" + "fmt" + "math/big" + "time" + + "github.com/google/uuid" +) + +const ( + CertificatesKey = "certificates" // must match with RevocationList.Certificates bson tag + IssuedAtKey = "issuedAt" // must match with RevocationListCertificate.IssuedAt bson tag + NumberKey = "number" // must match with RevocationListCertificate.NumberKey bson tag + SerialKey = "serial" // must match with RevocationListCertificate.Serial bson tag + ValidUntilKey = "validUntil" // must match with RevocationListCertificate.ValidUntil bson tag + RevocationKey = "revocation" // must match with RevocationListCertificate.Revocation bson tag +) + +type RevocationListCertificate struct { + // Serial number + Serial string `bson:"serial"` + // Time until the record is valid in Unix nanoseconds timestamp format + ValidUntil int64 `bson:"validUntil,omitempty"` + // Revocation time of the certificate in Unix nanoseconds timestamp format. 0 means that the certificate hasn't been revoked. + Revocation int64 `bson:"revocation"` +} + +func (rlc *RevocationListCertificate) Validate() error { + if rlc.Serial == "" { + return errors.New("serial number not set") + } + if rlc.Revocation == 0 { + return errors.New("revocation time not set") + } + return nil +} + +type RevocationList struct { + // The record ID is determined by applying a formula that utilizes the public key of the issuer, and it is computed as uuid.NewSHA1(uuid.NameSpaceX500, publicKeyRaw). + Id string `bson:"_id"` + // Number is used to populate the X.509 v2 cRLNumber extension in the CRL, which should be a monotonically increasing sequence number for a given + // CRL scope and CRL issuer. + Number string `bson:"number"` + // Time when the CRL was issued in Unix timestamp format + IssuedAt int64 `bson:"issuedAt"` + // Time until the issued CRL is valid in Unix nanoseconds timestamp format + ValidUntil int64 `bson:"validUntil"` + // List of certificates issued by the issuer + Certificates []*RevocationListCertificate `bson:"certificates"` +} + +func ParseBigInt(s string) (*big.Int, error) { + var number big.Int + if _, ok := number.SetString(s, 10); !ok { + return nil, fmt.Errorf("invalid numeric string(%v)", s) + } + return &number, nil +} + +// TODO: use some delta to check expiration +func (rl *RevocationList) IsExpired() bool { + return rl.ValidUntil <= time.Now().UnixNano() +} + +func (rl *RevocationList) Validate() error { + if _, err := uuid.Parse(rl.Id); err != nil { + return fmt.Errorf("invalid ID(%v): %w", rl.Id, err) + } + if (rl.IssuedAt == 0 && rl.ValidUntil != 0) || (rl.ValidUntil < rl.IssuedAt) { + return fmt.Errorf("invalid validity period timestamps(from %v to %v)", rl.IssuedAt, rl.ValidUntil) + } + if _, err := ParseBigInt(rl.Number); err != nil { + return err + } + for _, c := range rl.Certificates { + if err := c.Validate(); err != nil { + return err + } + } + return nil +} diff --git a/certificate-authority/store/revocationList_test.go b/certificate-authority/store/revocationList_test.go new file mode 100644 index 000000000..1327df431 --- /dev/null +++ b/certificate-authority/store/revocationList_test.go @@ -0,0 +1,145 @@ +package store_test + +import ( + "testing" + "time" + + "github.com/google/uuid" + "github.com/plgd-dev/hub/v2/certificate-authority/store" + "github.com/stretchr/testify/require" +) + +func TestRevocationListCertificateValidate(t *testing.T) { + tests := []struct { + name string + input store.RevocationListCertificate + wantErr bool + }{ + { + name: "Valid certificate", + input: store.RevocationListCertificate{ + Serial: "12345", + Revocation: time.Now().UnixNano(), + }, + wantErr: false, + }, + { + name: "Missing serial number", + input: store.RevocationListCertificate{ + Serial: "", + Revocation: time.Now().UnixNano(), + }, + wantErr: true, + }, + { + name: "Missing revocation time", + input: store.RevocationListCertificate{ + Serial: "12345", + Revocation: 0, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.input.Validate() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} + +func TestRevocationListValidate(t *testing.T) { + validCertificate := &store.RevocationListCertificate{ + Serial: "12345", + Revocation: time.Now().UnixNano(), + } + invalidCertificate := &store.RevocationListCertificate{ + Serial: "", + Revocation: time.Now().UnixNano(), + } + + tests := []struct { + name string + input store.RevocationList + wantErr bool + }{ + { + name: "Valid revocation list", + input: store.RevocationList{ + Id: uuid.New().String(), + IssuedAt: time.Now().UnixNano(), + ValidUntil: time.Now().Add(time.Minute).UnixNano(), + Number: "1", + Certificates: []*store.RevocationListCertificate{validCertificate}, + }, + wantErr: false, + }, + { + name: "Valid not-issued revocation list", + input: store.RevocationList{ + Id: uuid.New().String(), + Number: "1", + Certificates: []*store.RevocationListCertificate{validCertificate}, + }, + wantErr: false, + }, + { + name: "Invalid UUID", + input: store.RevocationList{ + Id: "invalid-uuid", + IssuedAt: time.Now().UnixNano(), + ValidUntil: time.Now().Add(time.Minute).UnixNano(), + Number: "1", + Certificates: []*store.RevocationListCertificate{validCertificate}, + }, + wantErr: true, + }, + { + name: "Missing issuedAt time", + input: store.RevocationList{ + Id: uuid.New().String(), + ValidUntil: time.Now().Add(time.Minute).UnixNano(), + Number: "1", + Certificates: []*store.RevocationListCertificate{validCertificate}, + }, + wantErr: true, + }, + { + name: "Missing validUntil time", + input: store.RevocationList{ + Id: uuid.New().String(), + IssuedAt: time.Now().UnixNano(), + Number: "1", + Certificates: []*store.RevocationListCertificate{validCertificate}, + }, + wantErr: true, + }, + { + name: "Invalid certificate in the list", + input: store.RevocationList{ + Id: uuid.New().String(), + IssuedAt: time.Now().UnixNano(), + ValidUntil: time.Now().Add(time.Minute).UnixNano(), + Number: "1", + Certificates: []*store.RevocationListCertificate{invalidCertificate}, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.input.Validate() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/certificate-authority/store/store.go b/certificate-authority/store/store.go index 87bd470f6..51d271d2e 100644 --- a/certificate-authority/store/store.go +++ b/certificate-authority/store/store.go @@ -3,25 +3,32 @@ package store import ( "context" "errors" + "fmt" "time" + "github.com/google/uuid" "github.com/plgd-dev/hub/v2/certificate-authority/pb" ) -var ErrNotSupported = errors.New("not supported") +var ( + ErrNotSupported = errors.New("not supported") + ErrNotFound = errors.New("no document found") +) type ( + Process[T any] func(v *T) error + SigningRecordsQuery = pb.GetSigningRecordsRequest DeleteSigningRecordsQuery = pb.DeleteSigningRecordsRequest -) - -type SigningRecordIter interface { - Next(ctx context.Context, SigningRecord *SigningRecord) bool - Err() error -} + RevokeSigningRecordsQuery = pb.DeleteSigningRecordsRequest -type ( - LoadSigningRecordsFunc = func(ctx context.Context, iter SigningRecordIter) (err error) + UpdateRevocationListQuery struct { + IssuerID string + IssuedAt int64 // 0 is allowed, the timestamp will be generated when the CRL is first issued + ValidUntil int64 // 0 is allowed, the timestamp will be generated when the CRL is first issued + UpdateIfExpired bool + RevokedCertificates []*RevocationListCertificate + } ) type Store interface { @@ -30,11 +37,30 @@ type Store interface { // UpdateSigningRecord updates an existing signing record. If the record does not exist, it will create a new one. UpdateSigningRecord(ctx context.Context, record *SigningRecord) error DeleteSigningRecords(ctx context.Context, ownerID string, query *DeleteSigningRecordsQuery) (int64, error) - LoadSigningRecords(ctx context.Context, ownerID string, query *SigningRecordsQuery, h LoadSigningRecordsFunc) error + LoadSigningRecords(ctx context.Context, ownerID string, query *SigningRecordsQuery, p Process[SigningRecord]) error // DeleteNonDeviceExpiredRecords deletes all expired records that are not associated with a device. // For CqlDB, this is a no-op because expired records are deleted by Cassandra automatically. DeleteNonDeviceExpiredRecords(ctx context.Context, now time.Time) (int64, error) + // Check if the implementation supports the RevocationList feature + SupportsRevocationList() bool + // InsertRevocationLists adds revocations lists to the database + InsertRevocationLists(ctx context.Context, rls ...*RevocationList) error + // UpdateRevocationList updates revocation list number and validity and adds certificates to revocation list. Returns the updated revocation list. + UpdateRevocationList(ctx context.Context, query *UpdateRevocationListQuery) (*RevocationList, error) + // Get valid latest issued or issue a new one revocation list + GetLatestIssuedOrIssueRevocationList(ctx context.Context, issuerID string, validFor time.Duration) (*RevocationList, error) + + // Removed matched signing records and move them to a revocation list. + RevokeSigningRecords(ctx context.Context, ownerID string, query *RevokeSigningRecordsQuery) (int64, error) + Close(ctx context.Context) error } + +func (q *UpdateRevocationListQuery) Validate() error { + if _, err := uuid.Parse(q.IssuerID); err != nil { + return fmt.Errorf("invalid revocation list issuerID(%v): %w", q.IssuerID, err) + } + return nil +} diff --git a/certificate-authority/test/revocationList.go b/certificate-authority/test/revocationList.go new file mode 100644 index 000000000..d3feafa6e --- /dev/null +++ b/certificate-authority/test/revocationList.go @@ -0,0 +1,81 @@ +package test + +import ( + "context" + "fmt" + "math/rand" + "strconv" + "testing" + "time" + + "github.com/plgd-dev/hub/v2/certificate-authority/store" + pkgTime "github.com/plgd-dev/hub/v2/pkg/time" + "github.com/stretchr/testify/require" + "golang.org/x/exp/maps" +) + +var ( + serial0 = rand.Int31() + serials = make(map[int]string) +) + +func GetIssuerID(i int) string { + return fmt.Sprintf("49000000-0000-0000-0000-%012d", i) +} + +func GetCertificateSerial(i int) string { + id, ok := serials[i] + if !ok { + id = strconv.FormatInt(int64(serial0)+int64(i), 10) + serials[i] = id + } + return id +} + +func getCertificate(c int, rev, exp time.Time) *store.RevocationListCertificate { + return &store.RevocationListCertificate{ + Serial: GetCertificateSerial(c), + ValidUntil: pkgTime.UnixNano(exp), + Revocation: pkgTime.UnixNano(rev), + } +} + +func AddRevocationListToStore(ctx context.Context, t *testing.T, s store.Store, expirationStart time.Time) map[string]*store.RevocationList { + rlm := make(map[string]*store.RevocationList) + c := 0 + for i := range 10 { + now := time.Now() + rlID := GetIssuerID(i) + actual := &store.RevocationList{ + Id: rlID, + IssuedAt: now.UnixNano(), + ValidUntil: now.Add(time.Minute * 10).UnixNano(), + Number: strconv.Itoa(i), + } + exp := expirationStart.Add(time.Duration(i) * time.Hour) + for range 10 { + rlc := getCertificate(c, now, exp) + actual.Certificates = append(actual.Certificates, rlc) + c++ + } + rlm[rlID] = actual + } + + err := s.InsertRevocationLists(ctx, maps.Values(rlm)...) + require.NoError(t, err) + return rlm +} + +func CheckRevocationList(t *testing.T, expected, actual *store.RevocationList, ignoreRevocationTime bool) { + require.Equal(t, expected.Number, actual.Number) + require.Equal(t, expected.IssuedAt, actual.IssuedAt) + require.Equal(t, expected.ValidUntil, actual.ValidUntil) + require.Len(t, actual.Certificates, len(expected.Certificates)) + for i := range actual.Certificates { + require.Equal(t, expected.Certificates[i].Serial, actual.Certificates[i].Serial) + require.Equal(t, expected.Certificates[i].ValidUntil, actual.Certificates[i].ValidUntil) + if !ignoreRevocationTime { + require.Equal(t, expected.Certificates[i].Revocation, actual.Certificates[i].Revocation) + } + } +} diff --git a/certificate-authority/test/service.go b/certificate-authority/test/service.go index dd5669513..516d5712d 100644 --- a/certificate-authority/test/service.go +++ b/certificate-authority/test/service.go @@ -7,6 +7,7 @@ import ( "time" "github.com/plgd-dev/hub/v2/certificate-authority/service" + "github.com/plgd-dev/hub/v2/certificate-authority/service/grpc" "github.com/plgd-dev/hub/v2/certificate-authority/store" storeConfig "github.com/plgd-dev/hub/v2/certificate-authority/store/config" storeCqlDB "github.com/plgd-dev/hub/v2/certificate-authority/store/cqldb" @@ -21,6 +22,26 @@ import ( "go.opentelemetry.io/otel/trace/noop" ) +func MakeHTTPConfig() service.HTTPConfig { + return service.HTTPConfig{ + ExternalAddress: "https://" + config.CERTIFICATE_AUTHORITY_HOST, + Addr: config.CERTIFICATE_AUTHORITY_HTTP_HOST, + Server: config.MakeHttpServerConfig(), + } +} + +func MakeCRLConfig() grpc.CRLConfig { + if config.ACTIVE_DATABASE() == database.MongoDB { + return grpc.CRLConfig{ + Enabled: true, + ExpiresIn: time.Hour, + } + } + return grpc.CRLConfig{ + Enabled: false, + } +} + func MakeConfig(t require.TestingT) service.Config { var cfg service.Config @@ -28,14 +49,14 @@ func MakeConfig(t require.TestingT) service.Config { cfg.Log = log.MakeDefaultConfig() cfg.APIs.GRPC = config.MakeGrpcServerConfig(config.CERTIFICATE_AUTHORITY_HOST) - cfg.APIs.HTTP.Addr = config.CERTIFICATE_AUTHORITY_HTTP_HOST - cfg.APIs.HTTP.Server = config.MakeHttpServerConfig() cfg.APIs.GRPC.TLS.ClientCertificateRequired = false + cfg.APIs.HTTP = MakeHTTPConfig() cfg.Signer.CAPool = []urischeme.URIScheme{urischeme.URIScheme(os.Getenv("TEST_ROOT_CA_CERT"))} cfg.Signer.KeyFile = urischeme.URIScheme(os.Getenv("TEST_ROOT_CA_KEY")) cfg.Signer.CertFile = urischeme.URIScheme(os.Getenv("TEST_ROOT_CA_CERT")) cfg.Signer.ValidFrom = "now-1h" cfg.Signer.ExpiresIn = time.Hour * 2 + cfg.Signer.CRL = MakeCRLConfig() cfg.Clients.OpenTelemetryCollector = config.MakeOpenTelemetryCollectorClient() cfg.Clients.Storage = MakeStorageConfig() @@ -88,11 +109,6 @@ func MakeStorageConfig() service.StorageConfig { Database: "certificateAuthority", TLS: config.MakeTLSClientConfig(), }, - BulkWrite: storeMongo.BulkWriteConfig{ - Timeout: time.Minute, - ThrottleTime: time.Millisecond * 500, - DocumentLimit: 1000, - }, }, CqlDB: &storeCqlDB.Config{ Embedded: config.MakeCqlDBConfig(), diff --git a/charts/plgd-hub/README.md b/charts/plgd-hub/README.md index 1ca616b16..930bc7146 100644 --- a/charts/plgd-hub/README.md +++ b/charts/plgd-hub/README.md @@ -79,9 +79,6 @@ global: | certificateauthority.clients.storage.cqlDB.tls.keyFile | string | `nil` | | | certificateauthority.clients.storage.cqlDB.tls.useSystemCAPool | bool | `false` | | | certificateauthority.clients.storage.cqlDB.useHostnameResolution | bool | `true` | Resolve IP address to hostname before validate certificate. If false, the TLS validator will use ip/hostname advertised by the Cassandra node. | -| certificateauthority.clients.storage.mongoDB.bulkWrite.documentLimit | int | `1000` | The maximum number of documents to cache before an immediate write. | -| certificateauthority.clients.storage.mongoDB.bulkWrite.throttleTime | string | `"500ms"` | The amount of time to wait until a record is written to mongodb. Any records collected during the throttle time will also be written. A throttle time of zero writes immediately. If recordLimit is reached, all records are written immediately | -| certificateauthority.clients.storage.mongoDB.bulkWrite.timeout | string | `"1m0s"` | A time limit for write bulk to mongodb. A Timeout of zero means no timeout. | | certificateauthority.clients.storage.mongoDB.database | string | `"certificateAuthorityService"` | | | certificateauthority.clients.storage.mongoDB.maxConnIdleTime | string | `"4m0s"` | | | certificateauthority.clients.storage.mongoDB.maxPoolSize | int | `16` | | diff --git a/charts/plgd-hub/templates/certificate-authority/config.yaml b/charts/plgd-hub/templates/certificate-authority/config.yaml index a72d72326..af49dec39 100644 --- a/charts/plgd-hub/templates/certificate-authority/config.yaml +++ b/charts/plgd-hub/templates/certificate-authority/config.yaml @@ -61,10 +61,6 @@ data: {{- $mongoDbTls := .clients.storage.mongoDB.tls }} {{- include "plgd-hub.internalCertificateConfig" (list $ $mongoDbTls $cert ) | indent 10 }} useSystemCAPool: {{ .clients.storage.mongoDB.tls.useSystemCAPool }} - bulkWrite: - timeout: {{ .clients.storage.mongoDB.bulkWrite.timeout | quote }} - throttleTime: {{ .clients.storage.mongoDB.bulkWrite.throttleTime | quote }} - documentLimit: {{ .clients.storage.mongoDB.bulkWrite.documentLimit }} cqlDB: hosts: {{- include "plgd-hub.cqlDBHosts" (list $ .clients.storage.cqlDB.hosts ) | indent 8 }} diff --git a/charts/plgd-hub/values.yaml b/charts/plgd-hub/values.yaml index 4ec9e6f4b..3fbaaeccf 100644 --- a/charts/plgd-hub/values.yaml +++ b/charts/plgd-hub/values.yaml @@ -2412,13 +2412,6 @@ certificateauthority: keyFile: certFile: useSystemCAPool: false - bulkWrite: - # -- A time limit for write bulk to mongodb. A Timeout of zero means no timeout. - timeout: 1m0s - # -- The amount of time to wait until a record is written to mongodb. Any records collected during the throttle time will also be written. A throttle time of zero writes immediately. If recordLimit is reached, all records are written immediately - throttleTime: 500ms - # -- The maximum number of documents to cache before an immediate write. - documentLimit: 1000 cqlDB: table: signedCertificateRecords hosts: [] diff --git a/coap-gateway/service/config.go b/coap-gateway/service/config.go index 9c3695a74..6673ee6e5 100644 --- a/coap-gateway/service/config.go +++ b/coap-gateway/service/config.go @@ -133,10 +133,6 @@ type InjectedCOAPConfig struct { TLSConfig InjectedTLSConfig `yaml:"tls" json:"tls"` } -func (c *InjectedCOAPConfig) Validate() error { - return nil -} - type DeviceTwinConfig struct { MaxETagsCountInRequest uint32 `yaml:"maxETagsCountInRequest" json:"maxETagsCountInRequest"` UseETags bool `yaml:"useETags" json:"useETags"` @@ -183,9 +179,6 @@ func (c *COAPConfigMarshalerUnmarshaler) Validate() error { if err := c.COAPConfig.Validate(); err != nil { return err } - if err := c.InjectedCOAPConfig.Validate(); err != nil { - return err - } if !c.InjectedCOAPConfig.TLSConfig.IdentityPropertiesRequired && c.Authorization.DeviceIDClaim != "" { return fmt.Errorf("tls.identityPropertiesRequired('%v') - %w", c.InjectedCOAPConfig.TLSConfig.IdentityPropertiesRequired, errors.New("combination with authorization.deviceIDClaim is not supported")) } diff --git a/http-gateway/test/http.go b/http-gateway/test/http.go index 14d738176..cf920ea5b 100644 --- a/http-gateway/test/http.go +++ b/http-gateway/test/http.go @@ -193,6 +193,11 @@ func (c *RequestBuilder) AddTimeToLive(ttl time.Duration) *RequestBuilder { return c } +func (c *RequestBuilder) AddIssuerID(issuerID string) *RequestBuilder { + c.uriParams[uri.IssuerIDKey] = issuerID + return c +} + func (c *RequestBuilder) SetQuery(value string) *RequestBuilder { c.query = value return c diff --git a/http-gateway/uri/uri.go b/http-gateway/uri/uri.go index 05aba37dd..653b1eeef 100644 --- a/http-gateway/uri/uri.go +++ b/http-gateway/uri/uri.go @@ -24,6 +24,7 @@ const ( OnlyContentQueryKey = "onlyContent" IncludeHiddenResourcesQueryKey = "includeHiddenResources" ForceQueryKey = "force" + IssuerIDKey = "issuerId" AliasInterfaceQueryKey = "interface" AliasCommandFilterQueryKey = "command" diff --git a/pkg/security/certificateSigner/certificateSigner.go b/pkg/security/certificateSigner/certificateSigner.go index 707588448..acbbc2f76 100644 --- a/pkg/security/certificateSigner/certificateSigner.go +++ b/pkg/security/certificateSigner/certificateSigner.go @@ -15,9 +15,10 @@ import ( ) type SignerConfig struct { - ValidNotBefore time.Time - ValidNotAfter time.Time - OverrideCertTemplate func(template *x509.Certificate) error + ValidNotBefore time.Time + ValidNotAfter time.Time + CRLDistributionPoints []string + OverrideCertTemplate func(template *x509.Certificate) error } type Opt = func(cfg *SignerConfig) @@ -34,6 +35,12 @@ func WithNotAfter(validNotAfter time.Time) Opt { } } +func WithCRLDistributionPoints(crlDistributionPoints []string) Opt { + return func(cfg *SignerConfig) { + cfg.CRLDistributionPoints = crlDistributionPoints + } +} + func WithOverrideCertTemplate(overrideCertTemplate func(template *x509.Certificate) error) Opt { return func(cfg *SignerConfig) { cfg.OverrideCertTemplate = overrideCertTemplate @@ -56,10 +63,7 @@ func New(caCert []*x509.Certificate, caKey crypto.PrivateKey, opts ...Opt) *Cert return &CertificateSigner{caCert: caCert, caKey: caKey, cfg: cfg} } -func (s *CertificateSigner) Sign(_ context.Context, csr []byte) ([]byte, error) { - if len(s.caCert) == 0 { - return nil, errors.New("cannot sign with empty signer CA certificates") - } +func parseCertificateRequest(csr []byte) (*x509.CertificateRequest, error) { csrBlock, _ := pem.Decode(csr) if csrBlock == nil { return nil, errors.New("pem not found") @@ -74,7 +78,17 @@ func (s *CertificateSigner) Sign(_ context.Context, csr []byte) ([]byte, error) if err != nil { return nil, err } + return certificateRequest, nil +} +func (s *CertificateSigner) Sign(_ context.Context, csr []byte) ([]byte, error) { + if len(s.caCert) == 0 { + return nil, errors.New("cannot sign with empty signer CA certificates") + } + parsedCSR, err := parseCertificateRequest(csr) + if err != nil { + return nil, err + } notBefore := s.cfg.ValidNotBefore notAfter := s.cfg.ValidNotAfter for _, c := range s.caCert { @@ -92,25 +106,26 @@ func (s *CertificateSigner) Sign(_ context.Context, csr []byte) ([]byte, error) } template := x509.Certificate{ - SerialNumber: serialNumber, - NotBefore: notBefore, - NotAfter: notAfter, - Subject: certificateRequest.Subject, - PublicKeyAlgorithm: certificateRequest.PublicKeyAlgorithm, - PublicKey: certificateRequest.PublicKey, - SignatureAlgorithm: s.caCert[0].SignatureAlgorithm, - DNSNames: certificateRequest.DNSNames, - IPAddresses: certificateRequest.IPAddresses, - URIs: certificateRequest.URIs, - EmailAddresses: certificateRequest.EmailAddresses, - ExtraExtensions: certificateRequest.Extensions, + SerialNumber: serialNumber, + NotBefore: notBefore, + NotAfter: notAfter, + Subject: parsedCSR.Subject, + PublicKeyAlgorithm: parsedCSR.PublicKeyAlgorithm, + PublicKey: parsedCSR.PublicKey, + SignatureAlgorithm: s.caCert[0].SignatureAlgorithm, + DNSNames: parsedCSR.DNSNames, + IPAddresses: parsedCSR.IPAddresses, + URIs: parsedCSR.URIs, + EmailAddresses: parsedCSR.EmailAddresses, + ExtraExtensions: parsedCSR.Extensions, + CRLDistributionPoints: s.cfg.CRLDistributionPoints, } if s.cfg.OverrideCertTemplate != nil { if err = s.cfg.OverrideCertTemplate(&template); err != nil { return nil, err } } - signedCsr, err := x509.CreateCertificate(rand.Reader, &template, s.caCert[0], certificateRequest.PublicKey, s.caKey) + signedCsr, err := x509.CreateCertificate(rand.Reader, &template, s.caCert[0], parsedCSR.PublicKey, s.caKey) if err != nil { return nil, err } diff --git a/pkg/security/certificateSigner/certificateSigner_test.go b/pkg/security/certificateSigner/certificateSigner_test.go index 180f1f86c..031655178 100644 --- a/pkg/security/certificateSigner/certificateSigner_test.go +++ b/pkg/security/certificateSigner/certificateSigner_test.go @@ -72,7 +72,6 @@ func TestCertificateSignerSign(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := s.Sign(context.Background(), tt.args.csr) - if tt.wantErr { require.Error(t, err) return diff --git a/pkg/security/certificateSigner/identityCertificateSigner_test.go b/pkg/security/certificateSigner/identityCertificateSigner_test.go index 27b1baa82..7f95d1e4d 100644 --- a/pkg/security/certificateSigner/identityCertificateSigner_test.go +++ b/pkg/security/certificateSigner/identityCertificateSigner_test.go @@ -72,7 +72,6 @@ func TestIdentityCertificateSignerSign(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := s.Sign(context.Background(), tt.args.csr) - if tt.wantErr { require.Error(t, err) return