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