• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

FIWARE / VCVerifier / 25166281377

30 Apr 2026 12:23PM UTC coverage: 59.991% (+0.7%) from 59.25%
25166281377

Pull #96

github

wistefan
Merge pull request 'Ticket #34 - Step 5: Comprehensive tests for refresh token flows' (#6) from ticket-34/step-5 into ticket-34/work

Reviewed-on: http://localhost:3001/general-agent-4/VCVerifier/pulls/6
Reviewed-by: wistefan <wistefan@dev-env.local>
Pull Request #96: Add refresh token support

195 of 248 new or added lines in 5 files covered. (78.63%)

568 existing lines in 10 files now uncovered.

3870 of 6451 relevant lines covered (59.99%)

0.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

52.87
/verifier/presentation_parser.go
1
package verifier
2

3
import (
4
        "context"
5
        "encoding/base64"
6
        "encoding/json"
7
        "errors"
8
        "net/http"
9
        "strings"
10
        "time"
11

12
        "github.com/fiware/VCVerifier/common"
13
        configModel "github.com/fiware/VCVerifier/config"
14
        "github.com/fiware/VCVerifier/did"
15
        "github.com/fiware/VCVerifier/jades"
16
        "github.com/fiware/VCVerifier/logging"
17
        "github.com/hellofresh/health-go/v5"
18
        "github.com/lestrrat-go/jwx/v3/jwk"
19
)
20

21
var ErrorNoValidationEndpoint = errors.New("no_validation_endpoint_configured")
22
var ErrorNoValidationHost = errors.New("no_validation_host_configured")
23
var ErrorInvalidSdJwt = errors.New("credential_is_not_sd_jwt")
24
var ErrorPresentationNoCredentials = errors.New("presentation_not_contains_credentials")
25
var ErrorInvalidProof = errors.New("invalid_vp_proof")
26
var ErrorVCNotArray = errors.New("verifiable_credential_not_array")
27
var ErrorInvalidJWTFormat = errors.New("invalid_jwt_format")
28
var ErrorCnfKeyMismatch = errors.New("cnf_key_does_not_match_vp_signer")
29

30
// allow singleton access to the parser
31
var presentationParser PresentationParser
32

33
// allow singleton access to the parser
34
var sdJwtParser SdJwtParser
35

36
// parser interface
37
type PresentationParser interface {
38
        ParsePresentation(tokenBytes []byte) (*common.Presentation, error)
39
}
40

41
type SdJwtParser interface {
42
        Parse(tokenString string) (map[string]interface{}, error)
43
        ParseWithSdJwt(tokenBytes []byte) (presentation *common.Presentation, err error)
44
}
45

46
type ConfigurablePresentationParser struct {
47
        ProofChecker *JWTProofChecker
48
}
49

50
type ConfigurableSdJwtParser struct {
51
        ProofChecker *JWTProofChecker
52
}
53

54
/**
55
* Global singelton access to the parser
56
**/
UNCOV
57
func GetSdJwtParser() SdJwtParser {
×
UNCOV
58
        if sdJwtParser == nil {
×
UNCOV
59
                logging.Log().Error("SdJwtParser is not initialized.")
×
60
        }
×
61
        return sdJwtParser
×
62
}
63

64
/**
65
* Global singelton access to the parser
66
**/
67
func GetPresentationParser() PresentationParser {
×
68
        if presentationParser == nil {
×
69
                logging.Log().Error("PresentationParser is not initialized.")
×
70
        }
×
71
        return presentationParser
×
72
}
73

74
// init the presentation parser depending on the config, either with or without did:elsi support
75
func InitPresentationParser(config *configModel.Configuration, healthCheck *health.Health) error {
×
76
        elsiConfig := &config.Elsi
×
77
        err := validateConfig(elsiConfig)
×
78
        if err != nil {
×
79
                logging.Log().Warnf("No valid elsi configuration provided. Error: %v", err)
×
UNCOV
80
                return err
×
UNCOV
81
        }
×
82

83
        registry := did.NewRegistry(did.WithVDR(did.NewWebVDR()), did.WithVDR(did.NewKeyVDR()), did.WithVDR(did.NewJWKVDR()))
×
84

×
85
        var jAdESValidator jades.JAdESValidator
×
86
        if elsiConfig.Enabled {
×
87
                externalValidator := &jades.ExternalJAdESValidator{
×
88
                        HttpClient:        &http.Client{},
×
89
                        ValidationAddress: buildAddress(elsiConfig.ValidationEndpoint.Host, elsiConfig.ValidationEndpoint.ValidationPath),
×
UNCOV
90
                        HealthAddress:     buildAddress(elsiConfig.ValidationEndpoint.Host, elsiConfig.ValidationEndpoint.HealthPath),
×
91
                }
×
92
                jAdESValidator = externalValidator
×
93

×
94
                healthCheck.Register(health.Config{
×
95
                        Name:      "JAdES-Validator",
×
96
                        Timeout:   time.Second * 5,
×
97
                        SkipOnErr: false,
×
98
                        Check: func(ctx context.Context) error {
×
99
                                return externalValidator.IsReady()
×
100
                        },
×
101
                })
102
        }
103

104
        checker := NewJWTProofChecker(registry, jAdESValidator)
×
105
        presentationParser = &ConfigurablePresentationParser{ProofChecker: checker}
×
106
        sdJwtParser = &ConfigurableSdJwtParser{
×
107
                ProofChecker: checker,
×
108
        }
×
UNCOV
109

×
UNCOV
110
        return nil
×
111
}
112

113
func validateConfig(elsiConfig *configModel.Elsi) error {
1✔
114
        if !elsiConfig.Enabled {
2✔
115
                return nil
1✔
116
        }
1✔
117
        if elsiConfig.ValidationEndpoint == nil {
2✔
118
                return ErrorNoValidationEndpoint
1✔
119
        }
1✔
120
        if elsiConfig.ValidationEndpoint.Host == "" {
2✔
121
                return ErrorNoValidationHost
1✔
122
        }
1✔
123
        return nil
1✔
124
}
125

126
func buildAddress(host, path string) string {
1✔
127
        return strings.TrimSuffix(host, "/") + "/" + strings.TrimPrefix(path, "/")
1✔
128
}
1✔
129

130
// ParsePresentation parses a VP from JWT or JSON-LD format.
131
func (cpp *ConfigurablePresentationParser) ParsePresentation(tokenBytes []byte) (*common.Presentation, error) {
1✔
132
        trimmed := strings.TrimSpace(string(tokenBytes))
1✔
133
        if len(trimmed) > 0 && trimmed[0] == '{' {
2✔
134
                return parseJSONLDPresentation([]byte(trimmed))
1✔
135
        }
1✔
136
        return cpp.parseJWTPresentation(tokenBytes)
1✔
137
}
138

139
// parseJWTPresentation parses a JWT-encoded VP, verifies the VP signature, and parses embedded VCs.
140
// If a VC contains a cnf (confirmation) claim, it is verified against the VP signer's key (RFC 7800).
141
func (cpp *ConfigurablePresentationParser) parseJWTPresentation(tokenBytes []byte) (*common.Presentation, error) {
1✔
142
        var payload []byte
1✔
143
        var holderKey jwk.Key
1✔
144
        var err error
1✔
145
        if cpp.ProofChecker != nil {
2✔
146
                payload, holderKey, err = cpp.ProofChecker.VerifyJWTAndReturnKey(tokenBytes)
1✔
147
        } else {
1✔
UNCOV
148
                payload, err = extractJWTPayload(tokenBytes)
×
UNCOV
149
        }
×
150
        if err != nil {
2✔
151
                return nil, err
1✔
152
        }
1✔
153

UNCOV
154
        var claims map[string]interface{}
×
UNCOV
155
        if err := json.Unmarshal(payload, &claims); err != nil {
×
UNCOV
156
                return nil, err
×
157
        }
×
158

UNCOV
159
        vpClaim, ok := claims[common.JWTClaimVP].(map[string]interface{})
×
UNCOV
160
        if !ok {
×
UNCOV
161
                return nil, ErrorPresentationNoCredentials
×
UNCOV
162
        }
×
163

164
        pres, _ := common.NewPresentation()
×
165
        if holderKey != nil {
×
166
                pres.SetHolderKey(holderKey)
×
UNCOV
167
        }
×
168

169
        // Holder from iss claim (standard JWT VP mapping)
170
        if iss, ok := claims[common.JWTClaimIss].(string); ok {
×
171
                pres.Holder = iss
×
UNCOV
172
        }
×
173

174
        vcsRaw, ok := vpClaim[common.VPKeyVerifiableCredential]
×
175
        if !ok {
×
176
                return pres, nil
×
UNCOV
177
        }
×
178

179
        vcList, ok := vcsRaw.([]interface{})
×
180
        if !ok {
×
181
                return nil, ErrorVCNotArray
×
UNCOV
182
        }
×
183

184
        for _, vc := range vcList {
×
185
                switch v := vc.(type) {
×
186
                case string:
×
UNCOV
187
                        cred, err := cpp.parseJWTCredential([]byte(v))
×
188
                        if err != nil {
×
189
                                return nil, err
×
190
                        }
×
191
                        // Verify cryptographic holder binding (cnf) if present
UNCOV
192
                        if holderKey != nil {
×
193
                                if err := verifyCnfBinding(cred, holderKey); err != nil {
×
194
                                        return nil, err
×
195
                                }
×
196
                        }
197
                        pres.AddCredentials(cred)
×
198
                case map[string]interface{}:
×
199
                        cred, err := parseJSONLDCredential(v)
×
UNCOV
200
                        if err != nil {
×
201
                                return nil, err
×
202
                        }
×
203
                        pres.AddCredentials(cred)
×
204
                }
205
        }
206

207
        return pres, nil
×
208
}
209

210
// parseJWTCredential parses and verifies a JWT-encoded VC.
211
func (cpp *ConfigurablePresentationParser) parseJWTCredential(tokenBytes []byte) (*common.Credential, error) {
×
212
        var payload []byte
×
UNCOV
213
        var err error
×
UNCOV
214
        if cpp.ProofChecker != nil {
×
UNCOV
215
                payload, err = cpp.ProofChecker.VerifyJWT(tokenBytes)
×
216
        } else {
×
UNCOV
217
                payload, err = extractJWTPayload(tokenBytes)
×
UNCOV
218
        }
×
UNCOV
219
        if err != nil {
×
220
                return nil, err
×
221
        }
×
222

223
        var claims map[string]interface{}
×
224
        if err := json.Unmarshal(payload, &claims); err != nil {
×
225
                return nil, err
×
226
        }
×
227

228
        return jwtClaimsToCredential(claims)
×
229
}
230

231
// jwtClaimsToCredential maps JWT VC claims to a common.Credential.
232
// Extracts standard JWT claims (iss, jti, nbf, iat, exp), VC-specific claims
233
// (type, @context, credentialSubject, credentialStatus), and the cnf claim
234
// for cryptographic holder binding verification.
235
func jwtClaimsToCredential(claims map[string]interface{}) (*common.Credential, error) {
1✔
236
        contents := common.CredentialContents{}
1✔
237

1✔
238
        if iss, ok := claims[common.JWTClaimIss].(string); ok {
2✔
239
                contents.Issuer = &common.Issuer{ID: iss}
1✔
240
        }
1✔
241
        if jti, ok := claims[common.JWTClaimJti].(string); ok {
2✔
242
                contents.ID = jti
1✔
243
        }
1✔
244

245
        customFields := common.CustomFields{}
1✔
246

1✔
247
        vcClaim, _ := claims[common.JWTClaimVC].(map[string]interface{})
1✔
248
        if vcClaim != nil {
2✔
249
                if types, ok := vcClaim[common.JSONLDKeyType].([]interface{}); ok {
2✔
250
                        for _, t := range types {
2✔
251
                                if s, ok := t.(string); ok {
2✔
252
                                        contents.Types = append(contents.Types, s)
1✔
253
                                }
1✔
254
                        }
255
                }
256
                if ctxs, ok := vcClaim[common.JSONLDKeyContext].([]interface{}); ok {
2✔
257
                        for _, c := range ctxs {
2✔
258
                                if s, ok := c.(string); ok {
2✔
259
                                        contents.Context = append(contents.Context, s)
1✔
260
                                }
1✔
261
                        }
262
                }
263
                if subject, ok := vcClaim[common.VCKeyCredentialSubject].(map[string]interface{}); ok {
2✔
264
                        s := common.Subject{CustomFields: common.CustomFields{}}
1✔
265
                        if id, ok := subject[common.JSONLDKeyID].(string); ok {
2✔
266
                                s.ID = id
1✔
267
                        }
1✔
268
                        for k, v := range subject {
2✔
269
                                if k != common.JSONLDKeyID {
2✔
270
                                        s.CustomFields[k] = v
1✔
271
                                }
1✔
272
                        }
273
                        contents.Subject = []common.Subject{s}
1✔
274
                }
275

276
                // Extract credentialStatus for revocation checking (W3C VC Data Model 2.0 §7.1).
277
                if status, ok := vcClaim[common.VCKeyCredentialStatus].(map[string]interface{}); ok {
1✔
UNCOV
278
                        contents.Status = &common.TypedID{
×
UNCOV
279
                                ID:   stringFromMap(status, common.JSONLDKeyID),
×
UNCOV
280
                                Type: stringFromMap(status, common.JSONLDKeyType),
×
UNCOV
281
                        }
×
UNCOV
282
                }
×
283
        }
284

285
        if nbf, ok := claims[common.JWTClaimNbf].(float64); ok {
2✔
286
                t := time.Unix(int64(nbf), 0)
1✔
287
                contents.ValidFrom = &t
1✔
288
        } else if iat, ok := claims[common.JWTClaimIat].(float64); ok {
1✔
289
                t := time.Unix(int64(iat), 0)
×
290
                contents.ValidFrom = &t
×
291
        }
×
292
        if exp, ok := claims[common.JWTClaimExp].(float64); ok {
2✔
293
                t := time.Unix(int64(exp), 0)
1✔
294
                contents.ValidUntil = &t
1✔
295
        }
1✔
296

297
        // Preserve cnf (confirmation) claim for cryptographic holder binding (RFC 7800).
298
        if cnf, ok := claims[common.JWTClaimCnf]; ok {
1✔
299
                customFields[common.JWTClaimCnf] = cnf
×
300
        }
×
301

302
        cred, err := common.CreateCredential(contents, customFields)
1✔
303
        if err != nil {
1✔
UNCOV
304
                return nil, err
×
UNCOV
305
        }
×
306

307
        if vcClaim != nil {
2✔
308
                cred.SetRawJSON(vcClaim)
1✔
309
        }
1✔
310

311
        return cred, nil
1✔
312
}
313

314
// stringFromMap safely extracts a string value from a map.
UNCOV
315
func stringFromMap(m map[string]interface{}, key string) string {
×
UNCOV
316
        if v, ok := m[key].(string); ok {
×
UNCOV
317
                return v
×
UNCOV
318
        }
×
UNCOV
319
        return ""
×
320
}
321

322
// parseJSONLDPresentation parses a JSON-LD VP (no proof verification).
323
func parseJSONLDPresentation(data []byte) (*common.Presentation, error) {
1✔
324
        var vpMap map[string]interface{}
1✔
325
        if err := json.Unmarshal(data, &vpMap); err != nil {
1✔
326
                return nil, err
×
327
        }
×
328

329
        pres, _ := common.NewPresentation()
1✔
330
        if holder, ok := vpMap[common.VPKeyHolder].(string); ok {
2✔
331
                pres.Holder = holder
1✔
332
        }
1✔
333

334
        vcsRaw, ok := vpMap[common.VPKeyVerifiableCredential]
1✔
335
        if !ok {
1✔
336
                return pres, nil
×
UNCOV
337
        }
×
338

339
        vcList, ok := vcsRaw.([]interface{})
1✔
340
        if !ok {
1✔
UNCOV
341
                return pres, nil
×
UNCOV
342
        }
×
343

344
        for _, vc := range vcList {
2✔
345
                switch v := vc.(type) {
1✔
346
                case string:
×
UNCOV
347
                        logging.Log().Warn("JWT VC embedded in JSON-LD VP — parsing without signature verification")
×
UNCOV
348
                        cred, err := parseUnsignedJWTCredential(v)
×
UNCOV
349
                        if err != nil {
×
350
                                return nil, err
×
351
                        }
×
UNCOV
352
                        pres.AddCredentials(cred)
×
353
                case map[string]interface{}:
1✔
354
                        cred, err := parseJSONLDCredential(v)
1✔
355
                        if err != nil {
1✔
356
                                return nil, err
×
357
                        }
×
358
                        pres.AddCredentials(cred)
1✔
359
                }
360
        }
361

362
        return pres, nil
1✔
363
}
364

365
// extractJWTPayload decodes the payload from a JWT without signature verification.
366
func extractJWTPayload(token []byte) ([]byte, error) {
×
UNCOV
367
        parts := strings.SplitN(string(token), ".", 3)
×
UNCOV
368
        if len(parts) < 2 {
×
UNCOV
369
                return nil, ErrorInvalidJWTFormat
×
UNCOV
370
        }
×
UNCOV
371
        return base64.RawURLEncoding.DecodeString(parts[1])
×
372
}
373

374
// parseUnsignedJWTCredential extracts claims from a JWT VC without signature verification.
375
func parseUnsignedJWTCredential(tokenString string) (*common.Credential, error) {
1✔
376
        parts := strings.SplitN(tokenString, ".", 3)
1✔
377
        if len(parts) < 2 {
2✔
378
                return nil, ErrorInvalidJWTFormat
1✔
379
        }
1✔
380
        payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
×
UNCOV
381
        if err != nil {
×
UNCOV
382
                return nil, err
×
UNCOV
383
        }
×
UNCOV
384
        var claims map[string]interface{}
×
UNCOV
385
        if err := json.Unmarshal(payloadBytes, &claims); err != nil {
×
UNCOV
386
                return nil, err
×
UNCOV
387
        }
×
UNCOV
388
        return jwtClaimsToCredential(claims)
×
389
}
390

391
// parseJSONLDCredential parses a JSON-LD VC from a map.
392
func parseJSONLDCredential(vcMap map[string]interface{}) (*common.Credential, error) {
1✔
393
        contents := common.CredentialContents{}
1✔
394

1✔
395
        if id, ok := vcMap[common.JSONLDKeyID].(string); ok {
2✔
396
                contents.ID = id
1✔
397
        }
1✔
398
        if types, ok := vcMap[common.JSONLDKeyType].([]interface{}); ok {
2✔
399
                for _, t := range types {
2✔
400
                        if s, ok := t.(string); ok {
2✔
401
                                contents.Types = append(contents.Types, s)
1✔
402
                        }
1✔
403
                }
404
        }
405
        if ctxs, ok := vcMap[common.JSONLDKeyContext].([]interface{}); ok {
2✔
406
                for _, c := range ctxs {
2✔
407
                        if s, ok := c.(string); ok {
2✔
408
                                contents.Context = append(contents.Context, s)
1✔
409
                        }
1✔
410
                }
411
        }
412

413
        switch issuer := vcMap[common.VCKeyIssuer].(type) {
1✔
414
        case string:
1✔
415
                contents.Issuer = &common.Issuer{ID: issuer}
1✔
UNCOV
416
        case map[string]interface{}:
×
UNCOV
417
                if id, ok := issuer[common.JSONLDKeyID].(string); ok {
×
UNCOV
418
                        contents.Issuer = &common.Issuer{ID: id}
×
UNCOV
419
                }
×
420
        }
421

422
        if subject, ok := vcMap[common.VCKeyCredentialSubject].(map[string]interface{}); ok {
2✔
423
                s := common.Subject{CustomFields: common.CustomFields{}}
1✔
424
                if id, ok := subject[common.JSONLDKeyID].(string); ok {
2✔
425
                        s.ID = id
1✔
426
                }
1✔
427
                for k, v := range subject {
2✔
428
                        if k != common.JSONLDKeyID {
2✔
429
                                s.CustomFields[k] = v
1✔
430
                        }
1✔
431
                }
432
                contents.Subject = []common.Subject{s}
1✔
433
        }
434

435
        // Extract credentialStatus for revocation checking.
436
        if status, ok := vcMap[common.VCKeyCredentialStatus].(map[string]interface{}); ok {
1✔
UNCOV
437
                contents.Status = &common.TypedID{
×
UNCOV
438
                        ID:   stringFromMap(status, common.JSONLDKeyID),
×
UNCOV
439
                        Type: stringFromMap(status, common.JSONLDKeyType),
×
UNCOV
440
                }
×
UNCOV
441
        }
×
442

443
        cred, err := common.CreateCredential(contents, common.CustomFields{})
1✔
444
        if err != nil {
1✔
UNCOV
445
                return nil, err
×
446
        }
×
447
        cred.SetRawJSON(vcMap)
1✔
448
        return cred, nil
1✔
449
}
450

451
func (sjp *ConfigurableSdJwtParser) Parse(tokenString string) (map[string]interface{}, error) {
1✔
452
        var verifyFunc func([]byte) ([]byte, error)
1✔
453
        if sjp.ProofChecker != nil {
2✔
454
                verifyFunc = sjp.ProofChecker.VerifyJWT
1✔
455
        }
1✔
456
        return common.ParseSDJWT(tokenString, verifyFunc)
1✔
457
}
458

459
func (sjp *ConfigurableSdJwtParser) ClaimsToCredential(claims map[string]interface{}) (credential *common.Credential, err error) {
1✔
460

1✔
461
        issuer, i_ok := claims[common.JWTClaimIss]
1✔
462
        vct, vct_ok := claims[common.JWTClaimVct]
1✔
463
        if !i_ok || !vct_ok {
2✔
464
                logging.Log().Warnf("Token does not contain issuer(%v) or vct(%v).", i_ok, vct_ok)
1✔
465
                return credential, ErrorInvalidSdJwt
1✔
466
        }
1✔
467
        customFields := common.CustomFields{}
1✔
468
        for k, v := range claims {
2✔
469
                if k != common.JWTClaimIss && k != common.JWTClaimVct {
2✔
470
                        customFields[k] = v
1✔
471
                }
1✔
472
        }
473
        subject := common.Subject{CustomFields: customFields}
1✔
474
        contents := common.CredentialContents{Issuer: &common.Issuer{ID: issuer.(string)}, Types: []string{vct.(string)}, Subject: []common.Subject{subject}}
1✔
475
        return common.CreateCredential(contents, common.CustomFields{})
1✔
476
}
477

478
func (sjp *ConfigurableSdJwtParser) ParseWithSdJwt(tokenBytes []byte) (presentation *common.Presentation, err error) {
1✔
479
        logging.Log().Debug("Parse with SD-Jwt")
1✔
480

1✔
481
        tokenString := string(tokenBytes)
1✔
482
        payloadString := strings.Split(tokenString, ".")[1]
1✔
483
        payloadBytes, _ := base64.RawURLEncoding.DecodeString(payloadString)
1✔
484

1✔
485
        var vpMap map[string]interface{}
1✔
486
        if err := json.Unmarshal(payloadBytes, &vpMap); err != nil {
2✔
487
                logging.Log().Warnf("Failed to unmarshal VP payload: %v", err)
1✔
488
                return nil, err
1✔
489
        }
1✔
490

491
        vp, ok := vpMap[common.JWTClaimVP].(map[string]interface{})
1✔
492
        if !ok {
2✔
493
                logging.Log().Warn("VP token does not contain vp claim")
1✔
494
                return presentation, ErrorPresentationNoCredentials
1✔
495
        }
1✔
496

497
        vcs, ok := vp[common.VPKeyVerifiableCredential]
1✔
498
        if !ok {
2✔
499
                logging.Log().Warn("VP does not contain verifiableCredential")
1✔
500
                return presentation, ErrorPresentationNoCredentials
1✔
501
        }
1✔
502

503
        presentation, err = common.NewPresentation()
1✔
504
        if err != nil {
1✔
UNCOV
505
                return nil, err
×
UNCOV
506
        }
×
507

508
        presentation.Holder = vp[common.VPKeyHolder].(string)
1✔
509

1✔
510
        // due to dcql, we only need to take care of presentations containing credentials of the same type.
1✔
511
        for _, vc := range vcs.([]interface{}) {
2✔
512
                logging.Log().Debugf("The vc %s", vc.(string))
1✔
513
                parsed, err := sjp.Parse(vc.(string))
1✔
514
                if err != nil {
2✔
515
                        logging.Log().Warnf("Failed to parse SD-JWT VC: %v", err)
1✔
516
                        return nil, err
1✔
517
                }
1✔
UNCOV
518
                credential, err := sjp.ClaimsToCredential(parsed)
×
UNCOV
519
                if err != nil {
×
UNCOV
520
                        logging.Log().Warnf("Failed to create credential from SD-JWT claims: %v", err)
×
UNCOV
521
                        return nil, err
×
UNCOV
522
                }
×
UNCOV
523
                presentation.AddCredentials(credential)
×
524
        }
525

526
        // Verify VP JWT signature and capture holder key
527
        if sjp.ProofChecker != nil {
×
528
                _, holderKey, err := sjp.ProofChecker.VerifyJWTAndReturnKey(tokenBytes)
×
529
                if err != nil {
×
530
                        logging.Log().Warnf("VP JWT signature verification failed: %v", err)
×
531
                        return nil, ErrorInvalidProof
×
532
                }
×
UNCOV
533
                if holderKey != nil {
×
UNCOV
534
                        presentation.SetHolderKey(holderKey)
×
535
                }
×
536
        }
537

538
        return presentation, nil
×
539
}
540

541
// verifyCnfBinding checks the cnf (confirmation) claim in a credential against the VP signer's key.
542
// Per RFC 7800, if the credential contains a cnf.jwk, the key must match the VP signer's public key.
543
// If no cnf claim is present, the check is skipped (no error).
544
func verifyCnfBinding(cred *common.Credential, holderKey jwk.Key) error {
1✔
545
        cnfRaw, ok := cred.CustomFields()[common.JWTClaimCnf]
1✔
546
        if !ok {
2✔
547
                return nil
1✔
548
        }
1✔
549

550
        cnfMap, ok := cnfRaw.(map[string]interface{})
1✔
551
        if !ok {
1✔
UNCOV
552
                return nil
×
553
        }
×
554

555
        jwkRaw, ok := cnfMap[common.CnfKeyJWK]
1✔
556
        if !ok {
1✔
UNCOV
557
                return nil
×
558
        }
×
559

560
        jwkMap, ok := jwkRaw.(map[string]interface{})
1✔
561
        if !ok {
1✔
UNCOV
562
                return nil
×
563
        }
×
564

565
        cnfKeyBytes, err := json.Marshal(jwkMap)
1✔
566
        if err != nil {
1✔
UNCOV
567
                return ErrorCnfKeyMismatch
×
568
        }
×
569

570
        cnfKey, err := jwk.ParseKey(cnfKeyBytes)
1✔
571
        if err != nil {
1✔
UNCOV
572
                logging.Log().Warnf("Failed to parse cnf.jwk: %v", err)
×
573
                return ErrorCnfKeyMismatch
×
574
        }
×
575

576
        // Compare using JWK thumbprints (RFC 7638)
577
        if !jwk.Equal(cnfKey, holderKey) {
2✔
578
                logging.Log().Warn("CNF key does not match VP signer key")
1✔
579
                return ErrorCnfKeyMismatch
1✔
580
        }
1✔
581

582
        return nil
1✔
583
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc