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

FIWARE / VCVerifier / 25492402266

07 May 2026 11:14AM UTC coverage: 60.916% (+0.5%) from 60.402%
25492402266

push

github

web-flow
Merge pull request #96 from FIWARE/ticket-34/work

291 of 427 new or added lines in 10 files covered. (68.15%)

1 existing line in 1 file now uncovered.

4244 of 6967 relevant lines covered (60.92%)

0.7 hits per line

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

53.27
/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
// globalProofChecker is the shared proof checker for deferred VP signature verification.
37
var globalProofChecker *JWTProofChecker
38

39
// parser interface
40
type PresentationParser interface {
41
        ParsePresentation(tokenBytes []byte) (*common.Presentation, error)
42
}
43

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

49
type ConfigurablePresentationParser struct {
50
        ProofChecker *JWTProofChecker
51
}
52

53
type ConfigurableSdJwtParser struct {
54
        ProofChecker *JWTProofChecker
55
}
56

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

67
// GetProofChecker returns the shared JWT proof checker for VP signature verification.
NEW
68
func GetProofChecker() *JWTProofChecker {
×
NEW
69
        return globalProofChecker
×
NEW
70
}
×
71

72
/**
73
* Global singelton access to the parser
74
**/
75
func GetPresentationParser() PresentationParser {
×
76
        if presentationParser == nil {
×
77
                logging.Log().Error("PresentationParser is not initialized.")
×
78
        }
×
79
        return presentationParser
×
80
}
81

82
// init the presentation parser depending on the config, either with or without did:elsi support
83
func InitPresentationParser(config *configModel.Configuration, healthCheck *health.Health) error {
×
84
        elsiConfig := &config.Elsi
×
85
        err := validateConfig(elsiConfig)
×
86
        if err != nil {
×
87
                logging.Log().Warnf("No valid elsi configuration provided. Error: %v", err)
×
88
                return err
×
89
        }
×
90

91
        registry := did.NewRegistry(did.WithVDR(did.NewWebVDR()), did.WithVDR(did.NewKeyVDR()), did.WithVDR(did.NewJWKVDR()))
×
92

×
93
        var jAdESValidator jades.JAdESValidator
×
94
        if elsiConfig.Enabled {
×
95
                externalValidator := &jades.ExternalJAdESValidator{
×
96
                        HttpClient:        &http.Client{},
×
97
                        ValidationAddress: buildAddress(elsiConfig.ValidationEndpoint.Host, elsiConfig.ValidationEndpoint.ValidationPath),
×
98
                        HealthAddress:     buildAddress(elsiConfig.ValidationEndpoint.Host, elsiConfig.ValidationEndpoint.HealthPath),
×
99
                }
×
100
                jAdESValidator = externalValidator
×
101

×
102
                healthCheck.Register(health.Config{
×
103
                        Name:      "JAdES-Validator",
×
104
                        Timeout:   time.Second * 5,
×
105
                        SkipOnErr: false,
×
106
                        Check: func(ctx context.Context) error {
×
107
                                return externalValidator.IsReady()
×
108
                        },
×
109
                })
110
        }
111

112
        checker := NewJWTProofChecker(registry, jAdESValidator)
×
NEW
113
        globalProofChecker = checker
×
114
        presentationParser = &ConfigurablePresentationParser{ProofChecker: checker}
×
115
        sdJwtParser = &ConfigurableSdJwtParser{
×
116
                ProofChecker: checker,
×
117
        }
×
118

×
119
        return nil
×
120
}
121

122
func validateConfig(elsiConfig *configModel.Elsi) error {
1✔
123
        if !elsiConfig.Enabled {
2✔
124
                return nil
1✔
125
        }
1✔
126
        if elsiConfig.ValidationEndpoint == nil {
2✔
127
                return ErrorNoValidationEndpoint
1✔
128
        }
1✔
129
        if elsiConfig.ValidationEndpoint.Host == "" {
2✔
130
                return ErrorNoValidationHost
1✔
131
        }
1✔
132
        return nil
1✔
133
}
134

135
func buildAddress(host, path string) string {
1✔
136
        return strings.TrimSuffix(host, "/") + "/" + strings.TrimPrefix(path, "/")
1✔
137
}
1✔
138

139
// ParsePresentation parses a VP from JWT or JSON-LD format.
140
func (cpp *ConfigurablePresentationParser) ParsePresentation(tokenBytes []byte) (*common.Presentation, error) {
1✔
141
        trimmed := strings.TrimSpace(string(tokenBytes))
1✔
142
        if len(trimmed) > 0 && trimmed[0] == '{' {
2✔
143
                return parseJSONLDPresentation([]byte(trimmed))
1✔
144
        }
1✔
145
        return cpp.parseJWTPresentation(tokenBytes)
1✔
146
}
147

148
// parseJWTPresentation parses a JWT-encoded VP, verifies the VP signature, and parses embedded VCs.
149
// If a VC contains a cnf (confirmation) claim, it is verified against the VP signer's key (RFC 7800).
150
func (cpp *ConfigurablePresentationParser) parseJWTPresentation(tokenBytes []byte) (*common.Presentation, error) {
1✔
151
        var payload []byte
1✔
152
        var holderKey jwk.Key
1✔
153
        var err error
1✔
154
        if cpp.ProofChecker != nil {
2✔
155
                payload, holderKey, err = cpp.ProofChecker.VerifyJWTAndReturnKey(tokenBytes)
1✔
156
        } else {
1✔
157
                payload, err = extractJWTPayload(tokenBytes)
×
158
        }
×
159
        if err != nil {
2✔
160
                return nil, err
1✔
161
        }
1✔
162

163
        var claims map[string]interface{}
×
164
        if err := json.Unmarshal(payload, &claims); err != nil {
×
165
                return nil, err
×
166
        }
×
167

168
        vpClaim, ok := claims[common.JWTClaimVP].(map[string]interface{})
×
169
        if !ok {
×
170
                return nil, ErrorPresentationNoCredentials
×
171
        }
×
172

173
        pres, _ := common.NewPresentation()
×
174
        if holderKey != nil {
×
175
                pres.SetHolderKey(holderKey)
×
176
        }
×
177

178
        // Holder from iss claim (standard JWT VP mapping)
179
        if iss, ok := claims[common.JWTClaimIss].(string); ok {
×
180
                pres.Holder = iss
×
181
        }
×
182

183
        vcsRaw, ok := vpClaim[common.VPKeyVerifiableCredential]
×
184
        if !ok {
×
185
                return pres, nil
×
186
        }
×
187

188
        vcList, ok := vcsRaw.([]interface{})
×
189
        if !ok {
×
190
                return nil, ErrorVCNotArray
×
191
        }
×
192

193
        for _, vc := range vcList {
×
194
                switch v := vc.(type) {
×
195
                case string:
×
196
                        cred, err := cpp.parseJWTCredential([]byte(v))
×
197
                        if err != nil {
×
198
                                return nil, err
×
199
                        }
×
200
                        // Verify cryptographic holder binding (cnf) if present
201
                        if holderKey != nil {
×
202
                                if err := verifyCnfBinding(cred, holderKey); err != nil {
×
203
                                        return nil, err
×
204
                                }
×
205
                        }
206
                        pres.AddCredentials(cred)
×
207
                case map[string]interface{}:
×
208
                        cred, err := parseJSONLDCredential(v)
×
209
                        if err != nil {
×
210
                                return nil, err
×
211
                        }
×
212
                        pres.AddCredentials(cred)
×
213
                }
214
        }
215

216
        return pres, nil
×
217
}
218

219
// parseJWTCredential parses and verifies a JWT-encoded VC.
220
func (cpp *ConfigurablePresentationParser) parseJWTCredential(tokenBytes []byte) (*common.Credential, error) {
×
221
        var payload []byte
×
222
        var err error
×
223
        if cpp.ProofChecker != nil {
×
224
                payload, err = cpp.ProofChecker.VerifyJWT(tokenBytes)
×
225
        } else {
×
226
                payload, err = extractJWTPayload(tokenBytes)
×
227
        }
×
228
        if err != nil {
×
229
                return nil, err
×
230
        }
×
231

232
        var claims map[string]interface{}
×
233
        if err := json.Unmarshal(payload, &claims); err != nil {
×
234
                return nil, err
×
235
        }
×
236

237
        return jwtClaimsToCredential(claims)
×
238
}
239

240
// jwtClaimsToCredential maps JWT VC claims to a common.Credential.
241
// Extracts standard JWT claims (iss, jti, nbf, iat, exp), VC-specific claims
242
// (type, @context, credentialSubject, credentialStatus), and the cnf claim
243
// for cryptographic holder binding verification.
244
func jwtClaimsToCredential(claims map[string]interface{}) (*common.Credential, error) {
1✔
245
        contents := common.CredentialContents{}
1✔
246

1✔
247
        if iss, ok := claims[common.JWTClaimIss].(string); ok {
2✔
248
                contents.Issuer = &common.Issuer{ID: iss}
1✔
249
        }
1✔
250
        if jti, ok := claims[common.JWTClaimJti].(string); ok {
2✔
251
                contents.ID = jti
1✔
252
        }
1✔
253

254
        customFields := common.CustomFields{}
1✔
255

1✔
256
        vcClaim, _ := claims[common.JWTClaimVC].(map[string]interface{})
1✔
257
        if vcClaim != nil {
2✔
258
                if types, ok := vcClaim[common.JSONLDKeyType].([]interface{}); ok {
2✔
259
                        for _, t := range types {
2✔
260
                                if s, ok := t.(string); ok {
2✔
261
                                        contents.Types = append(contents.Types, s)
1✔
262
                                }
1✔
263
                        }
264
                }
265
                if ctxs, ok := vcClaim[common.JSONLDKeyContext].([]interface{}); ok {
2✔
266
                        for _, c := range ctxs {
2✔
267
                                if s, ok := c.(string); ok {
2✔
268
                                        contents.Context = append(contents.Context, s)
1✔
269
                                }
1✔
270
                        }
271
                }
272
                if subject, ok := vcClaim[common.VCKeyCredentialSubject].(map[string]interface{}); ok {
2✔
273
                        s := common.Subject{CustomFields: common.CustomFields{}}
1✔
274
                        if id, ok := subject[common.JSONLDKeyID].(string); ok {
2✔
275
                                s.ID = id
1✔
276
                        }
1✔
277
                        for k, v := range subject {
2✔
278
                                if k != common.JSONLDKeyID {
2✔
279
                                        s.CustomFields[k] = v
1✔
280
                                }
1✔
281
                        }
282
                        contents.Subject = []common.Subject{s}
1✔
283
                }
284

285
                // Extract credentialStatus for revocation checking (W3C VC Data Model 2.0 §7.1).
286
                if status, ok := vcClaim[common.VCKeyCredentialStatus].(map[string]interface{}); ok {
1✔
287
                        contents.Status = &common.TypedID{
×
288
                                ID:   stringFromMap(status, common.JSONLDKeyID),
×
289
                                Type: stringFromMap(status, common.JSONLDKeyType),
×
290
                        }
×
291
                }
×
292
        }
293

294
        if nbf, ok := claims[common.JWTClaimNbf].(float64); ok {
2✔
295
                t := time.Unix(int64(nbf), 0)
1✔
296
                contents.ValidFrom = &t
1✔
297
        } else if iat, ok := claims[common.JWTClaimIat].(float64); ok {
1✔
298
                t := time.Unix(int64(iat), 0)
×
299
                contents.ValidFrom = &t
×
300
        }
×
301
        if exp, ok := claims[common.JWTClaimExp].(float64); ok {
2✔
302
                t := time.Unix(int64(exp), 0)
1✔
303
                contents.ValidUntil = &t
1✔
304
        }
1✔
305

306
        // Preserve cnf (confirmation) claim for cryptographic holder binding (RFC 7800).
307
        if cnf, ok := claims[common.JWTClaimCnf]; ok {
1✔
308
                customFields[common.JWTClaimCnf] = cnf
×
309
        }
×
310

311
        cred, err := common.CreateCredential(contents, customFields)
1✔
312
        if err != nil {
1✔
313
                return nil, err
×
314
        }
×
315

316
        if vcClaim != nil {
2✔
317
                cred.SetRawJSON(vcClaim)
1✔
318
        }
1✔
319

320
        return cred, nil
1✔
321
}
322

323
// stringFromMap safely extracts a string value from a map.
324
func stringFromMap(m map[string]interface{}, key string) string {
×
325
        if v, ok := m[key].(string); ok {
×
326
                return v
×
327
        }
×
328
        return ""
×
329
}
330

331
// parseJSONLDPresentation parses a JSON-LD VP (no proof verification).
332
func parseJSONLDPresentation(data []byte) (*common.Presentation, error) {
1✔
333
        var vpMap map[string]interface{}
1✔
334
        if err := json.Unmarshal(data, &vpMap); err != nil {
1✔
335
                return nil, err
×
336
        }
×
337

338
        pres, _ := common.NewPresentation()
1✔
339
        if holder, ok := vpMap[common.VPKeyHolder].(string); ok {
2✔
340
                pres.Holder = holder
1✔
341
        }
1✔
342

343
        vcsRaw, ok := vpMap[common.VPKeyVerifiableCredential]
1✔
344
        if !ok {
1✔
345
                return pres, nil
×
346
        }
×
347

348
        vcList, ok := vcsRaw.([]interface{})
1✔
349
        if !ok {
1✔
350
                return pres, nil
×
351
        }
×
352

353
        for _, vc := range vcList {
2✔
354
                switch v := vc.(type) {
1✔
355
                case string:
×
356
                        logging.Log().Warn("JWT VC embedded in JSON-LD VP — parsing without signature verification")
×
357
                        cred, err := parseUnsignedJWTCredential(v)
×
358
                        if err != nil {
×
359
                                return nil, err
×
360
                        }
×
361
                        pres.AddCredentials(cred)
×
362
                case map[string]interface{}:
1✔
363
                        cred, err := parseJSONLDCredential(v)
1✔
364
                        if err != nil {
1✔
365
                                return nil, err
×
366
                        }
×
367
                        pres.AddCredentials(cred)
1✔
368
                }
369
        }
370

371
        return pres, nil
1✔
372
}
373

374
// extractJWTPayload decodes the payload from a JWT without signature verification.
375
func extractJWTPayload(token []byte) ([]byte, error) {
×
376
        parts := strings.SplitN(string(token), ".", 3)
×
377
        if len(parts) < 2 {
×
378
                return nil, ErrorInvalidJWTFormat
×
379
        }
×
380
        return base64.RawURLEncoding.DecodeString(parts[1])
×
381
}
382

383
// parseUnsignedJWTCredential extracts claims from a JWT VC without signature verification.
384
func parseUnsignedJWTCredential(tokenString string) (*common.Credential, error) {
1✔
385
        parts := strings.SplitN(tokenString, ".", 3)
1✔
386
        if len(parts) < 2 {
2✔
387
                return nil, ErrorInvalidJWTFormat
1✔
388
        }
1✔
389
        payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
×
390
        if err != nil {
×
391
                return nil, err
×
392
        }
×
393
        var claims map[string]interface{}
×
394
        if err := json.Unmarshal(payloadBytes, &claims); err != nil {
×
395
                return nil, err
×
396
        }
×
397
        return jwtClaimsToCredential(claims)
×
398
}
399

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

1✔
404
        if id, ok := vcMap[common.JSONLDKeyID].(string); ok {
2✔
405
                contents.ID = id
1✔
406
        }
1✔
407
        if types, ok := vcMap[common.JSONLDKeyType].([]interface{}); ok {
2✔
408
                for _, t := range types {
2✔
409
                        if s, ok := t.(string); ok {
2✔
410
                                contents.Types = append(contents.Types, s)
1✔
411
                        }
1✔
412
                }
413
        }
414
        if ctxs, ok := vcMap[common.JSONLDKeyContext].([]interface{}); ok {
2✔
415
                for _, c := range ctxs {
2✔
416
                        if s, ok := c.(string); ok {
2✔
417
                                contents.Context = append(contents.Context, s)
1✔
418
                        }
1✔
419
                }
420
        }
421

422
        switch issuer := vcMap[common.VCKeyIssuer].(type) {
1✔
423
        case string:
1✔
424
                contents.Issuer = &common.Issuer{ID: issuer}
1✔
425
        case map[string]interface{}:
×
426
                if id, ok := issuer[common.JSONLDKeyID].(string); ok {
×
427
                        contents.Issuer = &common.Issuer{ID: id}
×
428
                }
×
429
        }
430

431
        if subject, ok := vcMap[common.VCKeyCredentialSubject].(map[string]interface{}); ok {
2✔
432
                s := common.Subject{CustomFields: common.CustomFields{}}
1✔
433
                if id, ok := subject[common.JSONLDKeyID].(string); ok {
2✔
434
                        s.ID = id
1✔
435
                }
1✔
436
                for k, v := range subject {
2✔
437
                        if k != common.JSONLDKeyID {
2✔
438
                                s.CustomFields[k] = v
1✔
439
                        }
1✔
440
                }
441
                contents.Subject = []common.Subject{s}
1✔
442
        }
443

444
        // Extract credentialStatus for revocation checking.
445
        if status, ok := vcMap[common.VCKeyCredentialStatus].(map[string]interface{}); ok {
1✔
446
                contents.Status = &common.TypedID{
×
447
                        ID:   stringFromMap(status, common.JSONLDKeyID),
×
448
                        Type: stringFromMap(status, common.JSONLDKeyType),
×
449
                }
×
450
        }
×
451

452
        cred, err := common.CreateCredential(contents, common.CustomFields{})
1✔
453
        if err != nil {
1✔
454
                return nil, err
×
455
        }
×
456
        cred.SetRawJSON(vcMap)
1✔
457
        return cred, nil
1✔
458
}
459

460
func (sjp *ConfigurableSdJwtParser) Parse(tokenString string) (map[string]interface{}, error) {
1✔
461
        var verifyFunc func([]byte) ([]byte, error)
1✔
462
        if sjp.ProofChecker != nil {
2✔
463
                verifyFunc = sjp.ProofChecker.VerifyJWT
1✔
464
        }
1✔
465
        return common.ParseSDJWT(tokenString, verifyFunc)
1✔
466
}
467

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

1✔
470
        issuer, i_ok := claims[common.JWTClaimIss]
1✔
471
        vct, vct_ok := claims[common.JWTClaimVct]
1✔
472
        if !i_ok || !vct_ok {
2✔
473
                logging.Log().Warnf("Token does not contain issuer(%v) or vct(%v).", i_ok, vct_ok)
1✔
474
                return credential, ErrorInvalidSdJwt
1✔
475
        }
1✔
476
        customFields := common.CustomFields{}
1✔
477
        for k, v := range claims {
2✔
478
                if k != common.JWTClaimIss && k != common.JWTClaimVct {
2✔
479
                        customFields[k] = v
1✔
480
                }
1✔
481
        }
482
        subject := common.Subject{CustomFields: customFields}
1✔
483
        contents := common.CredentialContents{Issuer: &common.Issuer{ID: issuer.(string)}, Types: []string{vct.(string)}, Subject: []common.Subject{subject}}
1✔
484
        return common.CreateCredential(contents, common.CustomFields{})
1✔
485
}
486

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

1✔
490
        tokenString := string(tokenBytes)
1✔
491
        payloadString := strings.Split(tokenString, ".")[1]
1✔
492
        payloadBytes, _ := base64.RawURLEncoding.DecodeString(payloadString)
1✔
493

1✔
494
        var vpMap map[string]interface{}
1✔
495
        if err := json.Unmarshal(payloadBytes, &vpMap); err != nil {
2✔
496
                logging.Log().Warnf("Failed to unmarshal VP payload: %v", err)
1✔
497
                return nil, err
1✔
498
        }
1✔
499

500
        vp, ok := vpMap[common.JWTClaimVP].(map[string]interface{})
1✔
501
        if !ok {
2✔
502
                logging.Log().Warn("VP token does not contain vp claim")
1✔
503
                return presentation, ErrorPresentationNoCredentials
1✔
504
        }
1✔
505

506
        vcs, ok := vp[common.VPKeyVerifiableCredential]
1✔
507
        if !ok {
2✔
508
                logging.Log().Warn("VP does not contain verifiableCredential")
1✔
509
                return presentation, ErrorPresentationNoCredentials
1✔
510
        }
1✔
511

512
        presentation, err = common.NewPresentation()
1✔
513
        if err != nil {
1✔
514
                return nil, err
×
515
        }
×
516

517
        presentation.Holder = vp[common.VPKeyHolder].(string)
1✔
518

1✔
519
        // due to dcql, we only need to take care of presentations containing credentials of the same type.
1✔
520
        for _, vc := range vcs.([]interface{}) {
2✔
521
                logging.Log().Debugf("The vc %s", vc.(string))
1✔
522
                parsed, err := sjp.Parse(vc.(string))
1✔
523
                if err != nil {
2✔
524
                        logging.Log().Warnf("Failed to parse SD-JWT VC: %v", err)
1✔
525
                        return nil, err
1✔
526
                }
1✔
527
                credential, err := sjp.ClaimsToCredential(parsed)
×
528
                if err != nil {
×
529
                        logging.Log().Warnf("Failed to create credential from SD-JWT claims: %v", err)
×
530
                        return nil, err
×
531
                }
×
532
                presentation.AddCredentials(credential)
×
533
        }
534

535
        // Store raw token for deferred VP signature verification.
536
        // Verification happens in GenerateToken only when holder binding is required by the service config.
NEW
537
        presentation.SetRawToken(tokenBytes)
×
538

×
539
        return presentation, nil
×
540
}
541

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

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

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

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

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

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

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

583
        return nil
1✔
584
}
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