Skip to content

Commit 2edfda2

Browse files
committed
Address SonarCloud code quality issues
- Reduce cognitive complexity in Sign and VerifyWithProof by extracting helpers - Add constants for JSON-LD keys (@context, proof, proofValue) - Define test constants for duplicate literals in suite_test.go - Define W3C context URL constants in w3c_test_vectors_test.go - Extract buildW3CProofOptions helper to avoid duplication
1 parent 9c1bb80 commit 2edfda2

File tree

3 files changed

+220
-156
lines changed

3 files changed

+220
-156
lines changed

pkg/vc20/crypto/jcs/suite.go

Lines changed: 119 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ const (
2626

2727
// ProofTypeDataIntegrity is the W3C Data Integrity proof type
2828
ProofTypeDataIntegrity = "DataIntegrityProof"
29+
30+
// JSON-LD keys
31+
keyContext = "@context"
32+
keyProof = "proof"
33+
keyProofValue = "proofValue"
2934
)
3035

3136
// Suite implements the eddsa-jcs-2022 cryptosuite for W3C Data Integrity proofs.
@@ -59,34 +64,49 @@ type DataIntegrityProof struct {
5964

6065
// Sign creates a Data Integrity proof for a JSON document using eddsa-jcs-2022.
6166
func (s *Suite) Sign(document any, key ed25519.PrivateKey, opts *SignOptions) (map[string]any, error) {
67+
if err := validateSignInputs(document, key, opts); err != nil {
68+
return nil, err
69+
}
70+
71+
docMap, err := toMap(document)
72+
if err != nil {
73+
return nil, fmt.Errorf("failed to convert document to map: %w", err)
74+
}
75+
76+
docWithoutProof := copyMapWithoutKey(docMap, keyProof)
77+
proofConfig := buildProofConfig(opts, docWithoutProof)
78+
79+
proofValue, err := createSignature(docWithoutProof, proofConfig, key)
80+
if err != nil {
81+
return nil, err
82+
}
83+
proofConfig[keyProofValue] = proofValue
84+
85+
return buildSignedDocument(docMap, proofConfig), nil
86+
}
87+
88+
// validateSignInputs validates the inputs for the Sign method.
89+
func validateSignInputs(document any, key ed25519.PrivateKey, opts *SignOptions) error {
6290
if document == nil {
63-
return nil, fmt.Errorf("document is nil")
91+
return fmt.Errorf("document is nil")
6492
}
6593
if key == nil {
66-
return nil, fmt.Errorf("private key is nil")
94+
return fmt.Errorf("private key is nil")
6795
}
6896
if opts == nil {
69-
return nil, fmt.Errorf("sign options are nil")
97+
return fmt.Errorf("sign options are nil")
7098
}
7199
if opts.VerificationMethod == "" {
72-
return nil, fmt.Errorf("verificationMethod is required")
100+
return fmt.Errorf("verificationMethod is required")
73101
}
74102
if opts.ProofPurpose == "" {
75-
return nil, fmt.Errorf("proofPurpose is required")
76-
}
77-
78-
docMap, err := toMap(document)
79-
if err != nil {
80-
return nil, fmt.Errorf("failed to convert document to map: %w", err)
81-
}
82-
83-
docWithoutProof := make(map[string]any)
84-
for k, v := range docMap {
85-
if k != "proof" {
86-
docWithoutProof[k] = v
87-
}
103+
return fmt.Errorf("proofPurpose is required")
88104
}
105+
return nil
106+
}
89107

108+
// buildProofConfig creates the proof configuration object.
109+
func buildProofConfig(opts *SignOptions, docWithoutProof map[string]any) map[string]any {
90110
created := opts.Created
91111
if created.IsZero() {
92112
created = time.Now().UTC()
@@ -109,18 +129,23 @@ func (s *Suite) Sign(document any, key ed25519.PrivateKey, opts *SignOptions) (m
109129

110130
// Per W3C eddsa-jcs-2022 spec Section 3.3.1 step 2:
111131
// If unsecuredDocument.@context is present, set proof.@context to unsecuredDocument.@context
112-
if ctx, ok := docWithoutProof["@context"]; ok {
113-
proofConfig["@context"] = ctx
132+
if ctx, ok := docWithoutProof[keyContext]; ok {
133+
proofConfig[keyContext] = ctx
114134
}
115135

136+
return proofConfig
137+
}
138+
139+
// createSignature generates the signature for the document and proof config.
140+
func createSignature(docWithoutProof, proofConfig map[string]any, key ed25519.PrivateKey) (string, error) {
116141
docCanonical, err := Canonicalize(docWithoutProof)
117142
if err != nil {
118-
return nil, fmt.Errorf("failed to canonicalize document: %w", err)
143+
return "", fmt.Errorf("failed to canonicalize document: %w", err)
119144
}
120145

121146
proofCanonical, err := Canonicalize(proofConfig)
122147
if err != nil {
123-
return nil, fmt.Errorf("failed to canonicalize proof config: %w", err)
148+
return "", fmt.Errorf("failed to canonicalize proof config: %w", err)
124149
}
125150

126151
docHash := sha256.Sum256(docCanonical)
@@ -131,32 +156,43 @@ func (s *Suite) Sign(document any, key ed25519.PrivateKey, opts *SignOptions) (m
131156

132157
proofValue, err := multibase.Encode(multibase.Base58BTC, signature)
133158
if err != nil {
134-
return nil, fmt.Errorf("failed to encode signature: %w", err)
159+
return "", fmt.Errorf("failed to encode signature: %w", err)
135160
}
136161

137-
proofConfig["proofValue"] = proofValue
162+
return proofValue, nil
163+
}
138164

139-
result := make(map[string]any)
140-
for k, v := range docMap {
141-
if k != "proof" {
142-
result[k] = v
143-
}
165+
// buildSignedDocument creates the result document with the proof attached.
166+
func buildSignedDocument(docMap, proofConfig map[string]any) map[string]any {
167+
result := copyMapWithoutKey(docMap, keyProof)
168+
169+
existingProof, ok := docMap[keyProof]
170+
if !ok {
171+
result[keyProof] = proofConfig
172+
return result
144173
}
145174

146-
if existingProof, ok := docMap["proof"]; ok {
147-
switch p := existingProof.(type) {
148-
case []any:
149-
result["proof"] = append(p, proofConfig)
150-
case map[string]any:
151-
result["proof"] = []any{p, proofConfig}
152-
default:
153-
result["proof"] = proofConfig
154-
}
155-
} else {
156-
result["proof"] = proofConfig
175+
switch p := existingProof.(type) {
176+
case []any:
177+
result[keyProof] = append(p, proofConfig)
178+
case map[string]any:
179+
result[keyProof] = []any{p, proofConfig}
180+
default:
181+
result[keyProof] = proofConfig
157182
}
158183

159-
return result, nil
184+
return result
185+
}
186+
187+
// copyMapWithoutKey creates a shallow copy of a map excluding a specific key.
188+
func copyMapWithoutKey(src map[string]any, excludeKey string) map[string]any {
189+
result := make(map[string]any)
190+
for k, v := range src {
191+
if k != excludeKey {
192+
result[k] = v
193+
}
194+
}
195+
return result
160196
}
161197

162198
// Verify verifies an eddsa-jcs-2022 Data Integrity proof on a document.
@@ -180,60 +216,71 @@ func (s *Suite) VerifyWithProof(document map[string]any, proof map[string]any, p
180216
return fmt.Errorf("public key is nil")
181217
}
182218

219+
signatureBytes, err := validateAndDecodeProof(proof)
220+
if err != nil {
221+
return err
222+
}
223+
224+
docWithoutProof := copyMapWithoutKey(document, keyProof)
225+
proofConfig := copyMapWithoutKey(proof, keyProofValue)
226+
227+
if err := validateContext(docWithoutProof, proofConfig); err != nil {
228+
return err
229+
}
230+
231+
return verifySignature(docWithoutProof, proofConfig, signatureBytes, publicKey)
232+
}
233+
234+
// validateAndDecodeProof validates the proof structure and decodes the signature.
235+
func validateAndDecodeProof(proof map[string]any) ([]byte, error) {
183236
proofType, _ := getString(proof, "type")
184237
if proofType != ProofTypeDataIntegrity {
185-
return fmt.Errorf("invalid proof type: expected %s, got %s", ProofTypeDataIntegrity, proofType)
238+
return nil, fmt.Errorf("invalid proof type: expected %s, got %s", ProofTypeDataIntegrity, proofType)
186239
}
187240

188241
cryptosuite, _ := getString(proof, "cryptosuite")
189242
if cryptosuite != CryptosuiteEdDSAJCS2022 {
190-
return fmt.Errorf("invalid cryptosuite: expected %s, got %s", CryptosuiteEdDSAJCS2022, cryptosuite)
243+
return nil, fmt.Errorf("invalid cryptosuite: expected %s, got %s", CryptosuiteEdDSAJCS2022, cryptosuite)
191244
}
192245

193-
proofValue, ok := getString(proof, "proofValue")
246+
proofValue, ok := getString(proof, keyProofValue)
194247
if !ok || proofValue == "" {
195-
return fmt.Errorf("proof is missing proofValue")
248+
return nil, fmt.Errorf("proof is missing proofValue")
196249
}
197250

198251
_, signatureBytes, err := multibase.Decode(proofValue)
199252
if err != nil {
200-
return fmt.Errorf("failed to decode proofValue: %w", err)
253+
return nil, fmt.Errorf("failed to decode proofValue: %w", err)
201254
}
202255

203256
if len(signatureBytes) != ed25519.SignatureSize {
204-
return fmt.Errorf("invalid signature length: expected %d, got %d", ed25519.SignatureSize, len(signatureBytes))
257+
return nil, fmt.Errorf("invalid signature length: expected %d, got %d", ed25519.SignatureSize, len(signatureBytes))
205258
}
206259

207-
docWithoutProof := make(map[string]any)
208-
for k, v := range document {
209-
if k != "proof" {
210-
docWithoutProof[k] = v
211-
}
212-
}
260+
return signatureBytes, nil
261+
}
213262

214-
proofConfig := make(map[string]any)
215-
for k, v := range proof {
216-
if k != "proofValue" {
217-
proofConfig[k] = v
218-
}
263+
// validateContext validates @context per W3C eddsa-jcs-2022 spec Section 3.3.2 steps 4.1-4.2.
264+
func validateContext(docWithoutProof, proofConfig map[string]any) error {
265+
proofCtx, ok := proofConfig[keyContext]
266+
if !ok {
267+
return nil
219268
}
220269

221-
// Per W3C eddsa-jcs-2022 spec Section 3.3.2 steps 4.1-4.2:
222-
// If proofOptions.@context exists:
223-
// - Check that document.@context starts with all values in proofOptions.@context
224-
// - Set unsecuredDocument.@context equal to proofOptions.@context
225-
if proofCtx, ok := proofConfig["@context"]; ok {
226-
docCtx, hasDocCtx := docWithoutProof["@context"]
227-
if !hasDocCtx {
228-
return fmt.Errorf("proof has @context but document does not")
229-
}
230-
if !contextStartsWith(docCtx, proofCtx) {
231-
return fmt.Errorf("document @context does not start with proof @context values")
232-
}
233-
// Set unsecuredDocument.@context equal to proofOptions.@context
234-
docWithoutProof["@context"] = proofCtx
270+
docCtx, hasDocCtx := docWithoutProof[keyContext]
271+
if !hasDocCtx {
272+
return fmt.Errorf("proof has @context but document does not")
273+
}
274+
if !contextStartsWith(docCtx, proofCtx) {
275+
return fmt.Errorf("document @context does not start with proof @context values")
235276
}
277+
// Set unsecuredDocument.@context equal to proofOptions.@context
278+
docWithoutProof[keyContext] = proofCtx
279+
return nil
280+
}
236281

282+
// verifySignature verifies the Ed25519 signature.
283+
func verifySignature(docWithoutProof, proofConfig map[string]any, signatureBytes []byte, publicKey ed25519.PublicKey) error {
237284
docCanonical, err := Canonicalize(docWithoutProof)
238285
if err != nil {
239286
return fmt.Errorf("failed to canonicalize document: %w", err)
@@ -291,7 +338,7 @@ func toMap(data any) (map[string]any, error) {
291338

292339
// findJCSProof finds the first eddsa-jcs-2022 proof in a document.
293340
func findJCSProof(doc map[string]any) (map[string]any, error) {
294-
proofVal, ok := doc["proof"]
341+
proofVal, ok := doc[keyProof]
295342
if !ok {
296343
return nil, fmt.Errorf("document has no proof")
297344
}

0 commit comments

Comments
 (0)