github.com/hyperledger/aries-framework-go@v0.3.2/pkg/doc/verifiable/credential_sdjwt.go (about) 1 /* 2 Copyright Avast Software. All Rights Reserved. 3 4 SPDX-License-Identifier: Apache-2.0 5 */ 6 7 package verifiable 8 9 import ( 10 "crypto" 11 "encoding/json" 12 "fmt" 13 14 "github.com/hyperledger/aries-framework-go/pkg/doc/jose" 15 "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/common" 16 "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/holder" 17 "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/issuer" 18 json2 "github.com/hyperledger/aries-framework-go/pkg/doc/util/json" 19 ) 20 21 type marshalDisclosureOpts struct { 22 includeAllDisclosures bool 23 discloseIfAvailable []string 24 discloseRequired []string 25 holderBinding *holder.BindingInfo 26 signer jose.Signer 27 signingKeyID string 28 } 29 30 // MarshalDisclosureOption provides an option for Credential.MarshalWithDisclosure. 31 type MarshalDisclosureOption func(opts *marshalDisclosureOpts) 32 33 // TODO: should DiscloseGiven(IfAvailable|Required) have path semantics for disclosure? 34 35 // DiscloseGivenIfAvailable sets that the disclosures with the given claim names will be disclosed by 36 // Credential.MarshalWithDisclosure. 37 // 38 // If any name provided does not have a matching disclosure, Credential.MarshalWithDisclosure will skip the name. 39 // 40 // Will result in an error if this option is provided alongside DiscloseAll. 41 func DiscloseGivenIfAvailable(disclosureNames []string) MarshalDisclosureOption { 42 return func(opts *marshalDisclosureOpts) { 43 opts.discloseIfAvailable = disclosureNames 44 } 45 } 46 47 // DiscloseGivenRequired sets that the disclosures with the given claim names will be disclosed by 48 // Credential.MarshalWithDisclosure. 49 // 50 // If any name provided does not have a matching disclosure, Credential.MarshalWithDisclosure will return an error. 51 // 52 // Will result in an error if this option is provided alongside DiscloseAll. 53 func DiscloseGivenRequired(disclosureNames []string) MarshalDisclosureOption { 54 return func(opts *marshalDisclosureOpts) { 55 opts.discloseRequired = disclosureNames 56 } 57 } 58 59 // DiscloseAll sets that all disclosures in the given Credential will be disclosed by Credential.MarshalWithDisclosure. 60 // 61 // Will result in an error if this option is provided alongside DiscloseGivenIfAvailable or DiscloseGivenRequired. 62 func DiscloseAll() MarshalDisclosureOption { 63 return func(opts *marshalDisclosureOpts) { 64 opts.includeAllDisclosures = true 65 } 66 } 67 68 // DisclosureHolderBinding option configures Credential.MarshalWithDisclosure to include a holder binding. 69 func DisclosureHolderBinding(binding *holder.BindingInfo) MarshalDisclosureOption { 70 return func(opts *marshalDisclosureOpts) { 71 opts.holderBinding = binding 72 } 73 } 74 75 // DisclosureSigner option provides Credential.MarshalWithDisclosure with a signer that will be used to create an SD-JWT 76 // if the given Credential wasn't already parsed from SD-JWT. 77 func DisclosureSigner(signer jose.Signer, signingKeyID string) MarshalDisclosureOption { 78 return func(opts *marshalDisclosureOpts) { 79 opts.signer = signer 80 opts.signingKeyID = signingKeyID 81 } 82 } 83 84 // MarshalWithDisclosure marshals a SD-JWT credential in combined format for presentation, including precisely 85 // the disclosures indicated by provided options, and optionally a holder binding if given the requisite option. 86 func (vc *Credential) MarshalWithDisclosure(opts ...MarshalDisclosureOption) (string, error) { 87 options := &marshalDisclosureOpts{} 88 89 for _, opt := range opts { 90 opt(options) 91 } 92 93 if options.includeAllDisclosures && (len(options.discloseIfAvailable) > 0 || len(options.discloseRequired) > 0) { 94 return "", fmt.Errorf("incompatible options provided") 95 } 96 97 if vc.JWT != "" && vc.SDJWTHashAlg != "" { 98 return filterSDJWTVC(vc, options) 99 } 100 101 if options.signer == nil { 102 return "", fmt.Errorf("credential needs signer to create SD-JWT") 103 } 104 105 return createSDJWTPresentation(vc, options) 106 } 107 108 func filterSDJWTVC(vc *Credential, options *marshalDisclosureOpts) (string, error) { 109 disclosureCodes, err := filteredDisclosureCodes(vc.SDJWTDisclosures, options) 110 if err != nil { 111 return "", err 112 } 113 114 cf := common.CombinedFormatForPresentation{ 115 SDJWT: vc.JWT, 116 Disclosures: disclosureCodes, 117 HolderBinding: vc.SDHolderBinding, 118 } 119 120 if options.holderBinding != nil { 121 cf.HolderBinding, err = holder.CreateHolderBinding(options.holderBinding) 122 if err != nil { 123 return "", fmt.Errorf("failed to create holder binding: %w", err) 124 } 125 } 126 127 return cf.Serialize(), nil 128 } 129 130 func createSDJWTPresentation(vc *Credential, options *marshalDisclosureOpts) (string, error) { 131 issued, err := makeSDJWT(vc, options.signer, options.signingKeyID) 132 if err != nil { 133 return "", fmt.Errorf("creating SD-JWT from Credential: %w", err) 134 } 135 136 disclosureClaims, err := common.GetDisclosureClaims(issued.Disclosures) 137 if err != nil { 138 return "", fmt.Errorf("parsing disclosure claims from vc sdjwt: %w", err) 139 } 140 141 disclosureCodes, err := filteredDisclosureCodes(disclosureClaims, options) 142 if err != nil { 143 return "", err 144 } 145 146 var presOpts []holder.Option 147 148 if options.holderBinding != nil { 149 presOpts = append(presOpts, holder.WithHolderBinding(options.holderBinding)) 150 } 151 152 issuedSerialized, err := issued.Serialize(false) 153 if err != nil { 154 return "", fmt.Errorf("serializing SD-JWT for presentation: %w", err) 155 } 156 157 combinedSDJWT, err := holder.CreatePresentation(issuedSerialized, disclosureCodes, presOpts...) 158 if err != nil { 159 return "", fmt.Errorf("create SD-JWT presentation: %w", err) 160 } 161 162 return combinedSDJWT, nil 163 } 164 165 func filteredDisclosureCodes( 166 availableDisclosures []*common.DisclosureClaim, 167 options *marshalDisclosureOpts, 168 ) ([]string, error) { 169 var ( 170 useDisclosures []*common.DisclosureClaim 171 err error 172 disclosureCodes []string 173 ) 174 175 if options.includeAllDisclosures { 176 useDisclosures = availableDisclosures 177 } else { 178 useDisclosures, err = filterDisclosures(availableDisclosures, 179 options.discloseIfAvailable, options.discloseRequired) 180 if err != nil { 181 return nil, err 182 } 183 } 184 185 for _, disclosure := range useDisclosures { 186 disclosureCodes = append(disclosureCodes, disclosure.Disclosure) 187 } 188 189 return disclosureCodes, nil 190 } 191 192 func filterDisclosures( 193 disclosures []*common.DisclosureClaim, 194 ifAvailable, required []string, 195 ) ([]*common.DisclosureClaim, error) { 196 ifAvailMap := map[string]*common.DisclosureClaim{} 197 reqMap := map[string]*common.DisclosureClaim{} 198 199 for _, name := range ifAvailable { 200 ifAvailMap[name] = nil 201 } 202 203 for _, name := range required { 204 reqMap[name] = nil 205 206 delete(ifAvailMap, name) // avoid listing a disclosure twice, if it's in both lists 207 } 208 209 for _, disclosure := range disclosures { 210 if _, ok := ifAvailMap[disclosure.Name]; ok { 211 ifAvailMap[disclosure.Name] = disclosure 212 } 213 214 if _, ok := reqMap[disclosure.Name]; ok { 215 reqMap[disclosure.Name] = disclosure 216 } 217 } 218 219 var out []*common.DisclosureClaim 220 221 for _, claim := range ifAvailMap { 222 if claim != nil { 223 out = append(out, claim) 224 } 225 } 226 227 for _, claim := range reqMap { 228 if claim == nil { 229 return nil, fmt.Errorf("disclosure list missing required claim") 230 } 231 232 out = append(out, claim) 233 } 234 235 return out, nil 236 } 237 238 type makeSDJWTOpts struct { 239 hashAlg crypto.Hash 240 } 241 242 // MakeSDJWTOption provides an option for creating an SD-JWT from a VC. 243 type MakeSDJWTOption func(opts *makeSDJWTOpts) 244 245 // MakeSDJWTWithHash sets the hash to use for an SD-JWT VC. 246 func MakeSDJWTWithHash(hash crypto.Hash) MakeSDJWTOption { 247 return func(opts *makeSDJWTOpts) { 248 opts.hashAlg = hash 249 } 250 } 251 252 // MakeSDJWT creates an SD-JWT in combined format for issuance, with all fields in credentialSubject converted 253 // recursively into selectively-disclosable SD-JWT claims. 254 func (vc *Credential) MakeSDJWT(signer jose.Signer, signingKeyID string, options ...MakeSDJWTOption) (string, error) { 255 sdjwt, err := makeSDJWT(vc, signer, signingKeyID, options...) 256 if err != nil { 257 return "", err 258 } 259 260 sdjwtSerialized, err := sdjwt.Serialize(false) 261 if err != nil { 262 return "", fmt.Errorf("serializing SD-JWT: %w", err) 263 } 264 265 return sdjwtSerialized, nil 266 } 267 268 func makeSDJWT(vc *Credential, signer jose.Signer, signingKeyID string, options ...MakeSDJWTOption, 269 ) (*issuer.SelectiveDisclosureJWT, error) { 270 opts := &makeSDJWTOpts{} 271 272 for _, option := range options { 273 option(opts) 274 } 275 276 claims, err := vc.JWTClaims(false) 277 if err != nil { 278 return nil, fmt.Errorf("constructing VC JWT claims: %w", err) 279 } 280 281 claimBytes, err := json.Marshal(claims) 282 if err != nil { 283 return nil, err 284 } 285 286 claimMap := map[string]interface{}{} 287 288 err = json.Unmarshal(claimBytes, &claimMap) 289 if err != nil { 290 return nil, err 291 } 292 293 headers := map[string]interface{}{ 294 jose.HeaderKeyID: signingKeyID, 295 } 296 297 issuerOptions := []issuer.NewOpt{ 298 issuer.WithStructuredClaims(true), 299 issuer.WithNonSelectivelyDisclosableClaims([]string{"id"}), 300 } 301 302 if opts.hashAlg != 0 { 303 issuerOptions = append(issuerOptions, issuer.WithHashAlgorithm(opts.hashAlg)) 304 } 305 306 sdjwt, err := issuer.NewFromVC(claimMap, headers, signer, issuerOptions...) 307 if err != nil { 308 return nil, fmt.Errorf("creating SD-JWT from VC: %w", err) 309 } 310 311 return sdjwt, nil 312 } 313 314 type displayCredOpts struct { 315 displayAll bool 316 displayGiven []string 317 } 318 319 // DisplayCredentialOption provides an option for Credential.CreateDisplayCredential. 320 type DisplayCredentialOption func(opts *displayCredOpts) 321 322 // DisplayAllDisclosures sets that Credential.CreateDisplayCredential will include all disclosures in the generated 323 // credential. 324 func DisplayAllDisclosures() DisplayCredentialOption { 325 return func(opts *displayCredOpts) { 326 opts.displayAll = true 327 } 328 } 329 330 // DisplayGivenDisclosures sets that Credential.CreateDisplayCredential will include only the given disclosures in the 331 // generated credential. 332 func DisplayGivenDisclosures(given []string) DisplayCredentialOption { 333 return func(opts *displayCredOpts) { 334 opts.displayGiven = append(opts.displayGiven, given...) 335 } 336 } 337 338 // CreateDisplayCredential creates, for SD-JWT credentials, a Credential whose selective-disclosure subject fields 339 // are replaced with the disclosure data. 340 // 341 // Options may be provided to filter the disclosures that will be included in the display credential. If a disclosure is 342 // not included, the associated claim will not be present in the returned credential. 343 // 344 // If the calling Credential is not an SD-JWT credential, this method returns the credential itself. 345 func (vc *Credential) CreateDisplayCredential( // nolint:funlen,gocyclo 346 opts ...DisplayCredentialOption, 347 ) (*Credential, error) { 348 options := &displayCredOpts{} 349 350 for _, opt := range opts { 351 opt(options) 352 } 353 354 if options.displayAll && len(options.displayGiven) > 0 { 355 return nil, fmt.Errorf("incompatible options provided") 356 } 357 358 if vc.SDJWTHashAlg == "" || vc.JWT == "" { 359 return vc, nil 360 } 361 362 credClaims, err := unmarshalJWSClaims(vc.JWT, false, nil) 363 if err != nil { 364 return nil, fmt.Errorf("unmarshal VC JWT claims: %w", err) 365 } 366 367 credClaims.refineFromJWTClaims() 368 369 useDisclosures := filterDisclosureList(vc.SDJWTDisclosures, options) 370 371 newVCObj, err := common.GetDisclosedClaims(useDisclosures, credClaims.VC) 372 if err != nil { 373 return nil, fmt.Errorf("assembling disclosed claims into vc: %w", err) 374 } 375 376 if subj, ok := newVCObj["credentialSubject"].(map[string]interface{}); ok { 377 clearEmpty(subj) 378 } 379 380 vcBytes, err := json.Marshal(&newVCObj) 381 if err != nil { 382 return nil, fmt.Errorf("marshalling vc object to JSON: %w", err) 383 } 384 385 newVC, err := populateCredential(vcBytes, nil) 386 if err != nil { 387 return nil, fmt.Errorf("parsing new VC from JSON: %w", err) 388 } 389 390 return newVC, nil 391 } 392 393 // CreateDisplayCredentialMap creates, for SD-JWT credentials, a Credential whose selective-disclosure subject fields 394 // are replaced with the disclosure data. 395 // 396 // Options may be provided to filter the disclosures that will be included in the display credential. If a disclosure is 397 // not included, the associated claim will not be present in the returned credential. 398 // 399 // If the calling Credential is not an SD-JWT credential, this method returns the credential itself. 400 func (vc *Credential) CreateDisplayCredentialMap( // nolint:funlen,gocyclo 401 opts ...DisplayCredentialOption, 402 ) (map[string]interface{}, error) { 403 options := &displayCredOpts{} 404 405 for _, opt := range opts { 406 opt(options) 407 } 408 409 if options.displayAll && len(options.displayGiven) > 0 { 410 return nil, fmt.Errorf("incompatible options provided") 411 } 412 413 if vc.SDJWTHashAlg == "" || vc.JWT == "" { 414 bytes, err := vc.MarshalJSON() 415 if err != nil { 416 return nil, err 417 } 418 419 return json2.ToMap(bytes) 420 } 421 422 credClaims, err := unmarshalJWSClaims(vc.JWT, false, nil) 423 if err != nil { 424 return nil, fmt.Errorf("unmarshal VC JWT claims: %w", err) 425 } 426 427 credClaims.refineFromJWTClaims() 428 429 useDisclosures := filterDisclosureList(vc.SDJWTDisclosures, options) 430 431 newVCObj, err := common.GetDisclosedClaims(useDisclosures, credClaims.VC) 432 if err != nil { 433 return nil, fmt.Errorf("assembling disclosed claims into vc: %w", err) 434 } 435 436 if subj, ok := newVCObj["credentialSubject"].(map[string]interface{}); ok { 437 clearEmpty(subj) 438 } 439 440 return newVCObj, nil 441 } 442 443 func filterDisclosureList(disclosures []*common.DisclosureClaim, options *displayCredOpts) []*common.DisclosureClaim { 444 if options.displayAll { 445 return disclosures 446 } 447 448 displayGivenMap := map[string]struct{}{} 449 450 for _, given := range options.displayGiven { 451 displayGivenMap[given] = struct{}{} 452 } 453 454 var out []*common.DisclosureClaim 455 456 for _, disclosure := range disclosures { 457 if _, ok := displayGivenMap[disclosure.Name]; ok { 458 out = append(out, disclosure) 459 } 460 } 461 462 return out 463 } 464 465 func clearEmpty(claims map[string]interface{}) { 466 for name, value := range claims { 467 if valueObj, ok := value.(map[string]interface{}); ok { 468 clearEmpty(valueObj) 469 470 if len(valueObj) == 0 { 471 delete(claims, name) 472 } 473 } 474 } 475 }