diff --git a/internal/handler/v2/service/service_create_blank_signature_object.go b/internal/handler/v2/service/service_create_blank_signature_object.go index 9668df5..2d7a336 100644 --- a/internal/handler/v2/service/service_create_blank_signature_object.go +++ b/internal/handler/v2/service/service_create_blank_signature_object.go @@ -28,6 +28,13 @@ func ServiceCreateBlankSignature(w http.ResponseWriter, r *http.Request) { darRepo := daRecord.DataAgreementRecordRepository{} darRepo.Init(organisationId) + _, err := darRepo.Get(dataAgreementRecordId) + if err != nil { + m := "Failed to fetch data agreement record" + common.HandleErrorV2(w, http.StatusBadRequest, m, err) + return + } + // Get latest revision for data agreement record daRecordRevision, err := revision.GetLatestByObjectIdAndSchemaName(dataAgreementRecordId, config.DataAgreementRecord) if err != nil { @@ -36,7 +43,7 @@ func ServiceCreateBlankSignature(w http.ResponseWriter, r *http.Request) { return } // create signature for data agreement record - toBeCreatedSignature, err := signature.CreateSignatureForObject("revision", daRecordRevision.Id, false, daRecordRevision, false, signature.Signature{}) + toBeCreatedSignature, err := signature.CreateSignatureForConsentRecord("Revision", daRecordRevision.Id, false, daRecordRevision.SerializedSnapshot, daRecordRevision.SerializedHash, signature.Signature{}) if err != nil { m := fmt.Sprintf("Failed to create signature for data agreement record: %v", dataAgreementRecordId) common.HandleErrorV2(w, http.StatusInternalServerError, m, err) diff --git a/internal/handler/v2/service/service_create_paired_dataagreement_record.go b/internal/handler/v2/service/service_create_paired_dataagreement_record.go index a33a669..fe7b432 100644 --- a/internal/handler/v2/service/service_create_paired_dataagreement_record.go +++ b/internal/handler/v2/service/service_create_paired_dataagreement_record.go @@ -28,11 +28,31 @@ func createPairedDataAgreementRecord(dataAgreementId string, rev revision.Revisi newDaRecord.DataAgreementRevisionId = rev.Id newDaRecord.IndividualId = individualId newDaRecord.OptIn = true - newDaRecord.State = config.Unsigned + newDaRecord.State = config.Signed return newDaRecord } +// createSignatureFromCreateSignatureRequestBody +func createSignatureFromCreateSignatureRequestBody(toBeCreatedSignature signature.Signature, signatureReq signature.Signature) signature.Signature { + + toBeCreatedSignature.Payload = signatureReq.Payload + toBeCreatedSignature.Signature = signatureReq.Signature + toBeCreatedSignature.VerificationMethod = signatureReq.VerificationMethod + toBeCreatedSignature.VerificationPayload = signatureReq.VerificationPayload + toBeCreatedSignature.VerificationPayloadHash = signatureReq.VerificationPayloadHash + toBeCreatedSignature.VerificationArtifact = signatureReq.VerificationArtifact + toBeCreatedSignature.VerificationSignedBy = signatureReq.VerificationSignedBy + toBeCreatedSignature.VerificationSignedAs = signatureReq.VerificationSignedAs + toBeCreatedSignature.VerificationJwsHeader = signatureReq.VerificationJwsHeader + toBeCreatedSignature.Timestamp = signatureReq.Timestamp + toBeCreatedSignature.SignedWithoutObjectReference = signatureReq.SignedWithoutObjectReference + toBeCreatedSignature.ObjectType = signatureReq.ObjectType + toBeCreatedSignature.ObjectReference = signatureReq.ObjectReference + + return toBeCreatedSignature +} + type dataAgreementRecordReq struct { Id string `json:"id" bson:"_id,omitempty"` DataAgreementId string `json:"dataAgreementId" valid:"required"` @@ -123,12 +143,29 @@ func ServiceCreatePairedDataAgreementRecord(w http.ResponseWriter, r *http.Reque newDataAgreementRecord := createPairedDataAgreementRecord(dataAgreement.Id, dataAgreementRevision, individual.Id) + var toBeCreatedSignature signature.Signature + toBeCreatedSignature.Id = primitive.NewObjectID().Hex() + + // verify signature + err = signature.VerifySignature(dataAgreementRecordReq.Signature.Signature, dataAgreementRecordReq.Signature.VerificationSignedBy) + if err != nil { + m := "Failed to verify signature for consent record" + common.HandleErrorV2(w, http.StatusBadRequest, m, err) + return + } + + // create signature for data agreement record + toBeCreatedSignature = createSignatureFromCreateSignatureRequestBody(toBeCreatedSignature, dataAgreementRecordReq.Signature) + if err != nil { + m := "Failed to create signature for consent record" + common.HandleErrorV2(w, http.StatusBadRequest, m, err) + return + } + dataAgreementRecord := newDataAgreementRecord dataAgreementRecord.OrganisationId = organisationId - currentSignature := dataAgreementRecordReq.Signature dataAgreementRecord.Id = primitive.NewObjectID().Hex() - currentSignature.Id = primitive.NewObjectID().Hex() - dataAgreementRecord.SignatureId = currentSignature.Id + dataAgreementRecord.SignatureId = toBeCreatedSignature.Id newRecordRevision, err := revision.CreateRevisionForDataAgreementRecord(dataAgreementRecord, individualId) if err != nil { @@ -136,13 +173,11 @@ func ServiceCreatePairedDataAgreementRecord(w http.ResponseWriter, r *http.Reque common.HandleErrorV2(w, http.StatusInternalServerError, m, err) return } - // create signature for data agreement record - toBeCreatedSignature, err := signature.CreateSignatureForObject("revision", newRecordRevision.Id, false, newRecordRevision, true, currentSignature) - if err != nil { - m := fmt.Sprintf("Failed to create signature for data agreement record: %v", dataAgreementRecord.Id) - common.HandleErrorV2(w, http.StatusInternalServerError, m, err) - return - } + + newRecordRevision.SerializedSnapshot = toBeCreatedSignature.VerificationPayload + newRecordRevision.SerializedHash = toBeCreatedSignature.VerificationPayloadHash + newRecordRevision.SignedWithoutObjectId = true + toBeCreatedSignature.SignedWithoutObjectReference = true savedDataAgreementRecord, err := darRepo.Add(dataAgreementRecord) if err != nil { diff --git a/internal/handler/v2/service/service_update_dataagreement_record.go b/internal/handler/v2/service/service_update_dataagreement_record.go index 713c933..6290d97 100644 --- a/internal/handler/v2/service/service_update_dataagreement_record.go +++ b/internal/handler/v2/service/service_update_dataagreement_record.go @@ -110,6 +110,7 @@ func ServiceUpdateDataAgreementRecord(w http.ResponseWriter, r *http.Request) { return } toBeUpdatedDaRecord.OptIn = optIn + toBeUpdatedDaRecord.State = config.Unsigned currentDataAgreementRevision, err := revision.GetLatestByObjectIdAndSchemaName(toBeUpdatedDaRecord.DataAgreementId, config.DataAgreement) if err != nil { diff --git a/internal/handler/v2/service/service_update_signature_object.go b/internal/handler/v2/service/service_update_signature_object.go index 5242158..503b5d7 100644 --- a/internal/handler/v2/service/service_update_signature_object.go +++ b/internal/handler/v2/service/service_update_signature_object.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "strings" "github.com/asaskevich/govalidator" "github.com/bb-consent/api/internal/common" @@ -13,6 +14,7 @@ import ( "github.com/bb-consent/api/internal/revision" "github.com/bb-consent/api/internal/signature" "github.com/gorilla/mux" + "go.mongodb.org/mongo-driver/bson/primitive" ) // createSignatureFromUpdateSignatureRequestBody @@ -83,10 +85,23 @@ func ServiceUpdateSignatureObject(w http.ResponseWriter, r *http.Request) { return } - toBeUpdatedSignatureObject, err := signature.Get(toBeUpdatedDaRecord.SignatureId) + var toBeUpdatedSignatureObject signature.Signature + + if len(strings.TrimSpace(toBeUpdatedDaRecord.SignatureId)) > 1 { + toBeUpdatedSignatureObject, err = signature.Get(toBeUpdatedDaRecord.SignatureId) + if err != nil { + m := "Failed to fetch signature for data agreement record" + common.HandleErrorV2(w, http.StatusBadRequest, m, err) + return + } + } else { + toBeUpdatedSignatureObject.Id = primitive.NewObjectID().Hex() + } + + err = signature.VerifySignature(signatureReq.Signature.Signature, signatureReq.Signature.VerificationSignedBy) if err != nil { - m := "Failed to fetch signature for data agreement record" - common.HandleErrorV2(w, http.StatusInternalServerError, m, err) + m := "Failed to verify signature for consent record" + common.HandleErrorV2(w, http.StatusBadRequest, m, err) return } @@ -118,6 +133,8 @@ func ServiceUpdateSignatureObject(w http.ResponseWriter, r *http.Request) { common.HandleErrorV2(w, http.StatusInternalServerError, m, err) return } + newRevision.SerializedSnapshot = savedSignature.VerificationPayload + newRevision.SerializedHash = savedSignature.VerificationPayloadHash // Save the revision to db _, err = revision.Add(newRevision) diff --git a/internal/jwk/jwk.go b/internal/jwk/jwk.go new file mode 100644 index 0000000..81f7d7c --- /dev/null +++ b/internal/jwk/jwk.go @@ -0,0 +1,100 @@ +package jwk + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "log" + "math/big" +) + +// JWK represents a JSON Web Key +type JWK struct { + Kty string `json:"kty"` + Crv string `json:"crv"` + X string `json:"x"` + Y string `json:"y"` + PublicKey *ecdsa.PublicKey `json:"-"` +} + +// FromECPublicKey creates JWK from elliptic curve public key +func (obj *JWK) FromECPublicKey(publicKey *ecdsa.PublicKey) *JWK { + return &JWK{ + Kty: "EC", + Crv: "P-256", + X: encodeBase64URL(publicKey.X.Bytes()), + Y: encodeBase64URL(publicKey.Y.Bytes()), + PublicKey: &ecdsa.PublicKey{}, + } +} + +// GenerateECKey generate ECDSA key pair using secp256r1 curve +func (obj *JWK) GenerateECKey() *ecdsa.PrivateKey { + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + log.Fatalf("Error generating ECDSA key pair: %v", err) + } + + obj.PublicKey = &privateKey.PublicKey + return privateKey +} + +// ToJSON to json string +func (obj *JWK) ToJSON() string { + jwk := obj.FromECPublicKey(obj.PublicKey) + jwkJSON, err := json.MarshalIndent(jwk, "", " ") + if err != nil { + log.Fatalf("Error marshaling JWK to JSON: %v", err) + } + + return string(jwkJSON) +} + +// ToECPublicKey to ECDSA public key +func (obj *JWK) ToECPublicKey() *ecdsa.PublicKey { + curve := elliptic.P256() // P-256 curve is assumed, adjust as needed + xBytes, err := decodeBase64URL(obj.X) + if err != nil { + log.Fatalf("failed to decode x parameter: %v", err) + } + + yBytes, err := decodeBase64URL(obj.Y) + if err != nil { + log.Fatalf("failed to decode y parameter: %v", err) + } + + pubKey := &ecdsa.PublicKey{ + Curve: curve, + X: new(big.Int).SetBytes(xBytes), + Y: new(big.Int).SetBytes(yBytes), + } + + return pubKey +} + +// FromJSON from json string to JWK +func FromJSON(jwkString string) JWK { + var jwk JWK + err := json.Unmarshal([]byte(jwkString), &jwk) + if err != nil { + log.Fatal("Failed to unmarshal JWK:", err) + } + return jwk +} + +// decodeBase64URL use base64.URLEncoding to decode base64 URL-encoded strings +func decodeBase64URL(input string) ([]byte, error) { + decoded, err := base64.RawURLEncoding.DecodeString(input) + if err != nil { + return nil, fmt.Errorf("base64 URL decoding failed: %w", err) + } + return decoded, nil +} + +// encodeBase64URL encodes the input bytes to base64url format +func encodeBase64URL(data []byte) string { + return base64.RawURLEncoding.EncodeToString(data) +} diff --git a/internal/jws/jws.go b/internal/jws/jws.go new file mode 100644 index 0000000..b429b1f --- /dev/null +++ b/internal/jws/jws.go @@ -0,0 +1,36 @@ +package jws + +import ( + "fmt" + + "github.com/bb-consent/api/internal/jwk" + "github.com/go-jose/go-jose/v3" +) + +// JWS represents a JSON Web Signature (JWS) +type JWS struct { + Claims string `json:"-"` + Key jwk.JWK `json:"-"` + Signature string `json:"-"` +} + +// Verify verify JSON web signature (JWS) +func (obj *JWS) Verify() error { + // Create EC public key + pubKey := obj.Key.ToECPublicKey() + // Deserialise to JWS + jws, err := jose.ParseSigned(obj.Signature) + if err != nil { + return err + } + // Verify signature and return decoded payload + payload, err := jws.Verify(pubKey) + if err != nil { + return err + } + // Print decoded payload + fmt.Println("Signature verified.") + fmt.Printf("\nPayload: \n\n%v\n", string(payload)) + + return nil +} diff --git a/internal/signature/signature.go b/internal/signature/signature.go index dcd92d5..0b75f4c 100644 --- a/internal/signature/signature.go +++ b/internal/signature/signature.go @@ -1,10 +1,10 @@ package signature import ( - "encoding/json" "time" - "github.com/bb-consent/api/internal/common" + "github.com/bb-consent/api/internal/jwk" + "github.com/bb-consent/api/internal/jws" ) type Signature struct { @@ -34,40 +34,32 @@ func (s *Signature) Init(ObjectType string, ObjectReference string, SignedWithou } // CreateSignature -func (s *Signature) CreateSignature(VerificationPayload interface{}, IsPayload bool) error { +func (s *Signature) CreateSignature(serialisedSnapshot string, serialisedHash string) error { - // Verification Payload - verificationPayloadSerialised, err := json.Marshal(VerificationPayload) - if err != nil { - return err - } - s.VerificationPayload = string(verificationPayloadSerialised) - - // Serialised hash using SHA-1 - s.VerificationPayloadHash, err = common.CalculateSHA1(string(verificationPayloadSerialised)) - if err != nil { - return err - } - - if IsPayload { - // Payload - payload, err := json.Marshal(s) - if err != nil { - return err - } - s.Payload = string(payload) - } + s.VerificationPayload = serialisedSnapshot + s.VerificationPayloadHash = serialisedHash return nil } -// CreateSignatureForPolicy -func CreateSignatureForObject(ObjectType string, ObjectReference string, SignedWithoutObjectReference bool, VerificationPayload interface{}, IsPayload bool, signature Signature) (Signature, error) { +// CreateSignatureForConsentRecord +func CreateSignatureForConsentRecord(ObjectType string, ObjectReference string, SignedWithoutObjectReference bool, serialisedSnapshot string, serialisedHash string, signature Signature) (Signature, error) { // Create signature signature.Init(ObjectType, ObjectReference, SignedWithoutObjectReference) - err := signature.CreateSignature(VerificationPayload, IsPayload) + err := signature.CreateSignature(serialisedSnapshot, serialisedHash) return signature, err } + +// VerifySignature +func VerifySignature(signature string, publicKey string) error { + + jwsObj := jws.JWS{Key: jwk.FromJSON(publicKey), Signature: signature} + err := jwsObj.Verify() + if err != nil { + return err + } + return nil +} diff --git a/resources/config b/resources/config index e8f8ccd..c052ed6 160000 --- a/resources/config +++ b/resources/config @@ -1 +1 @@ -Subproject commit e8f8ccdff8732dcf0f648598f9ed238185226da8 +Subproject commit c052ed6bebeac16555ece540396ac23774c40283