From dda051f968f97fcfd8734394e38f3016c249f0c4 Mon Sep 17 00:00:00 2001 From: Sandra Vrtikapa Date: Tue, 21 Feb 2023 11:06:32 -0500 Subject: [PATCH] doc: Add SD-JWT Docs Add SD-JST Docs Signed-off-by: Sandra Vrtikapa --- pkg/doc/sdjwt/example_test.go | 284 +++++++++++++++++++++++++ pkg/doc/sdjwt/holder/example_test.go | 216 +++++++++++++++++++ pkg/doc/sdjwt/holder/holder.go | 21 ++ pkg/doc/sdjwt/issuer/example_test.go | 95 +++++++++ pkg/doc/sdjwt/issuer/issuer.go | 79 ++++++- pkg/doc/sdjwt/sdjwt-doc.go | 36 ++++ pkg/doc/sdjwt/verifier/example_test.go | 96 +++++++++ pkg/doc/sdjwt/verifier/verifier.go | 20 +- 8 files changed, 837 insertions(+), 10 deletions(-) create mode 100644 pkg/doc/sdjwt/example_test.go create mode 100644 pkg/doc/sdjwt/holder/example_test.go create mode 100644 pkg/doc/sdjwt/issuer/example_test.go create mode 100644 pkg/doc/sdjwt/sdjwt-doc.go create mode 100644 pkg/doc/sdjwt/verifier/example_test.go diff --git a/pkg/doc/sdjwt/example_test.go b/pkg/doc/sdjwt/example_test.go new file mode 100644 index 000000000..47e33dd98 --- /dev/null +++ b/pkg/doc/sdjwt/example_test.go @@ -0,0 +1,284 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package sdjwt + +import ( + "crypto/ed25519" + "crypto/rand" + "encoding/json" + "fmt" + "time" + + "github.com/go-jose/go-jose/v3/jwt" + + "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk" + "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk/jwksupport" + afjwt "github.com/hyperledger/aries-framework-go/pkg/doc/jwt" + "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/holder" + "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/issuer" + "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/verifier" +) + +func ExampleSimpleClaims() { //nolint:govet + signer, signatureVerifier, err := setUp() + if err != nil { + fmt.Println("failed to set-up test: %w", err.Error()) + } + + claims := map[string]interface{}{ + "given_name": "Albert", + "last_name": "Smith", + } + + // Issuer will issue SD-JWT for specified claims. + token, err := issuer.New(testIssuer, claims, nil, signer) + if err != nil { + fmt.Println("failed to issue SD-JWT: %w", err.Error()) + } + + combinedFormatForIssuance, err := token.Serialize(false) + if err != nil { + fmt.Println("failed to issue SD-JWT: %w", err.Error()) + } + + // Holder will parse combined format for issuance and hold on to that + // combined format for issuance and the claims that can be selected. + holderClaims, err := holder.Parse(combinedFormatForIssuance, holder.WithSignatureVerifier(signatureVerifier)) + if err != nil { + fmt.Println("holder failed to parse SD-JWT: %w", err.Error()) + } + + // The Holder will only select given_name + selectedDisclosures := getDisclosuresFromClaimNames([]string{"given_name"}, holderClaims) + + // Holder will disclose only sub-set of claims to verifier. + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, selectedDisclosures) + if err != nil { + fmt.Println("holder failed to create presentation: %w", err.Error()) + } + + // Verifier will validate combined format for presentation and create verified claims. + verifiedClaims, err := verifier.Parse(combinedFormatForPresentation, + verifier.WithSignatureVerifier(signatureVerifier)) + if err != nil { + fmt.Println("verifier failed to parse holder presentation: %w", err.Error()) + } + + verifiedClaimsJSON, err := marshalObj(verifiedClaims) + if err != nil { + fmt.Println("verifier failed to marshal verified claims: %w", err.Error()) + } + + fmt.Println(verifiedClaimsJSON) + + // Output: { + // "given_name": "Albert", + // "iss": "https://example.com/issuer" + //} +} + +func ExampleComplexClaimsWithHolderBinding() { //nolint:govet + signer, signatureVerifier, err := setUp() + if err != nil { + fmt.Println("failed to set-up test: %w", err.Error()) + } + + holderSigner, holderJWK, err := setUpHolderBinding() + if err != nil { + fmt.Println("failed to set-up test: %w", err.Error()) + } + + claims := map[string]interface{}{ + "sub": "john_doe_42", + "given_name": "John", + "family_name": "Doe", + "email": "johndoe@example.com", + "phone_number": "+1-202-555-0101", + "birthdate": "1940-01-01", + "address": map[string]interface{}{ + "street_address": "123 Main St", + "locality": "Anytown", + "region": "Anystate", + "country": "US", + }, + } + + // Issuer will issue SD-JWT for specified claims. Structured claims not selected as an option hence complex object + // address will be treated as an object not as a set of properties. Holder public key is provided therefore it will + // be added as "cnf" claim. + token, err := issuer.New(testIssuer, claims, nil, signer, + issuer.WithHolderPublicKey(holderJWK), + ) + if err != nil { + fmt.Println("failed to issue SD-JWT: %w", err.Error()) + } + + combinedFormatForIssuance, err := token.Serialize(false) + if err != nil { + fmt.Println("failed to issue SD-JWT: %w", err.Error()) + } + + // Holder will parse combined format for issuance and hold on to that + // combined format for issuance and the claims that can be selected. + holderClaims, err := holder.Parse(combinedFormatForIssuance, holder.WithSignatureVerifier(signatureVerifier)) + if err != nil { + fmt.Println("holder failed to parse SD-JWT: %w", err.Error()) + } + + // The Holder will only select given_name, address + selectedDisclosures := getDisclosuresFromClaimNames([]string{"given_name", "address"}, holderClaims) + + // Holder will disclose only sub-set of claims to verifier. + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, selectedDisclosures, + holder.WithHolderBinding(&holder.BindingInfo{ + Payload: holder.BindingPayload{ + Nonce: "nonce", + Audience: "https://test.com/verifier", + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + Signer: holderSigner, + })) + if err != nil { + fmt.Println("holder failed to create presentation: %w", err.Error()) + } + + // Verifier will validate combined format for presentation and create verified claims. + verifiedClaims, err := verifier.Parse(combinedFormatForPresentation, + verifier.WithSignatureVerifier(signatureVerifier)) + if err != nil { + fmt.Println("verifier failed to parse holder presentation: %w", err.Error()) + } + + addressClaimsJSON, err := marshalObj(verifiedClaims["address"]) + if err != nil { + fmt.Println("verifier failed to marshal verified claims: %w", err.Error()) + } + + fmt.Println(addressClaimsJSON) + + // Output: { + // "country": "US", + // "locality": "Anytown", + // "region": "Anystate", + // "street_address": "123 Main St" + //} +} + +func ExampleComplexObjectWithStructuredClaims() { //nolint:govet + signer, signatureVerifier, err := setUp() + if err != nil { + fmt.Println("failed to set-up test: %w", err.Error()) + } + + claims := map[string]interface{}{ + "sub": "john_doe_42", + "given_name": "John", + "family_name": "Doe", + "email": "johndoe@example.com", + "phone_number": "+1-202-555-0101", + "birthdate": "1940-01-01", + "address": map[string]interface{}{ + "street_address": "123 Main St", + "locality": "Anytown", + "region": "Anystate", + "country": "US", + }, + } + + // Issuer will issue SD-JWT for specified claims. + token, err := issuer.New(testIssuer, claims, nil, signer, + issuer.WithStructuredClaims(true), + issuer.WithNonSelectivelyDisclosableClaims([]string{"address.country"}), + ) + if err != nil { + fmt.Println("failed to issue SD-JWT: %w", err.Error()) + } + + combinedFormatForIssuance, err := token.Serialize(false) + if err != nil { + fmt.Println("failed to issue SD-JWT: %w", err.Error()) + } + + // Holder will parse combined format for issuance and hold on to that + // combined format for issuance and the claims that can be selected. + holderClaims, err := holder.Parse(combinedFormatForIssuance, holder.WithSignatureVerifier(signatureVerifier)) + if err != nil { + fmt.Println("holder failed to parse SD-JWT: %w", err.Error()) + } + + // The Holder will only select given_name, street_address + selectedDisclosures := getDisclosuresFromClaimNames([]string{"given_name", "street_address"}, holderClaims) + + // Holder will disclose only sub-set of claims to verifier. + combinedFormatForPresentation, err := holder.CreatePresentation(combinedFormatForIssuance, selectedDisclosures) + if err != nil { + fmt.Println("holder failed to create presentation: %w", err.Error()) + } + + // Verifier will validate combined format for presentation and create verified claims. + verifiedClaims, err := verifier.Parse(combinedFormatForPresentation, + verifier.WithSignatureVerifier(signatureVerifier)) + if err != nil { + fmt.Println("verifier failed to parse holder presentation: %w", err.Error()) + } + + verifiedClaimsJSON, err := marshalObj(verifiedClaims) + if err != nil { + fmt.Println("verifier failed to marshal verified claims: %w", err.Error()) + } + + fmt.Println(verifiedClaimsJSON) + + // Output: { + // "address": { + // "country": "US", + // "street_address": "123 Main St" + // }, + // "given_name": "John", + // "iss": "https://example.com/issuer" + //} +} +func setUp() (*afjwt.JoseED25519Signer, *afjwt.JoseEd25519Verifier, error) { + issuerPublicKey, issuerPrivateKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, nil, err + } + + signer := afjwt.NewEd25519Signer(issuerPrivateKey) + + signatureVerifier, err := afjwt.NewEd25519Verifier(issuerPublicKey) + if err != nil { + return nil, nil, err + } + + return signer, signatureVerifier, nil +} + +func setUpHolderBinding() (*afjwt.JoseED25519Signer, *jwk.JWK, error) { + holderPublicKey, holderPrivateKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, nil, err + } + + holderPublicJWK, err := jwksupport.JWKFromKey(holderPublicKey) + if err != nil { + return nil, nil, err + } + + holderSigner := afjwt.NewEd25519Signer(holderPrivateKey) + + return holderSigner, holderPublicJWK, nil +} + +func marshalObj(obj interface{}) (string, error) { + objBytes, err := json.Marshal(obj) + if err != nil { + fmt.Println("failed to marshal object: %w", err.Error()) + } + + return prettyPrint(objBytes) +} diff --git a/pkg/doc/sdjwt/holder/example_test.go b/pkg/doc/sdjwt/holder/example_test.go new file mode 100644 index 000000000..320e28899 --- /dev/null +++ b/pkg/doc/sdjwt/holder/example_test.go @@ -0,0 +1,216 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package holder + +import ( + "bytes" + "crypto/ed25519" + "crypto/rand" + "encoding/json" + "fmt" + "sort" + "time" + + "github.com/go-jose/go-jose/v3/jwt" + + "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk" + "github.com/hyperledger/aries-framework-go/pkg/doc/jose/jwk/jwksupport" + afjwt "github.com/hyperledger/aries-framework-go/pkg/doc/jwt" + "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/common" + "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/issuer" +) + +func ExampleParse() { + signer, signatureVerifier, err := setUp() + if err != nil { + fmt.Println("failed to set-up test: %w", err.Error()) + } + + claims := map[string]interface{}{ + "given_name": "Albert", + "last_name": "Smith", + } + + // Issuer will issue SD-JWT for specified claims. Salt function is only provided to keep example outcome the same. + token, err := issuer.New(testIssuer, claims, nil, signer, + issuer.WithSaltFnc(func() (string, error) { + return "3jqcb67z9wks08zwiK7EyQ", nil + })) + if err != nil { + fmt.Println("failed to issue SD-JWT: %w", err.Error()) + } + + combinedFormatForIssuance, err := token.Serialize(false) + if err != nil { + fmt.Println("failed to issue SD-JWT: %w", err.Error()) + } + + // Holder will parse combined format for issuance and hold on to that + // combined format for issuance and the claims that can be selected. + holderClaims, err := Parse(combinedFormatForIssuance, WithSignatureVerifier(signatureVerifier)) + if err != nil { + fmt.Println("holder failed to parse SD-JWT: %w", err.Error()) + } + + // Sort by claim name, keeping original order or equal elements. + sort.SliceStable(holderClaims, func(i, j int) bool { + return holderClaims[i].Name < holderClaims[j].Name + }) + + holderClaimsJSON, err := marshalObj(holderClaims) + if err != nil { + fmt.Println("verifier failed to marshal holder claims: %w", err.Error()) + } + + fmt.Println(holderClaimsJSON) + + // Output: [ + // { + // "Disclosure": "WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwiZ2l2ZW5fbmFtZSIsIkFsYmVydCJd", + // "Name": "given_name", + // "Value": "Albert" + // }, + // { + // "Disclosure": "WyIzanFjYjY3ejl3a3MwOHp3aUs3RXlRIiwibGFzdF9uYW1lIiwiU21pdGgiXQ", + // "Name": "last_name", + // "Value": "Smith" + // } + //] +} + +func ExampleCreatePresentation() { + signer, signatureVerifier, err := setUp() + if err != nil { + fmt.Println("failed to set-up test: %w", err.Error()) + } + + holderSigner, holderJWK, err := setUpHolderBinding() + if err != nil { + fmt.Println("failed to set-up test: %w", err.Error()) + } + + claims := map[string]interface{}{ + "given_name": "Albert", + "last_name": "Smith", + } + + // Issuer will issue SD-JWT for specified claims and holder public key. + token, err := issuer.New(testIssuer, claims, nil, signer, + issuer.WithHolderPublicKey(holderJWK)) + if err != nil { + fmt.Println("failed to issue SD-JWT: %w", err.Error()) + } + + combinedFormatForIssuance, err := token.Serialize(false) + if err != nil { + fmt.Println("failed to issue SD-JWT: %w", err.Error()) + } + + // Holder will parse combined format for issuance and hold on to that + // combined format for issuance and the claims that can be selected. + holderClaims, err := Parse(combinedFormatForIssuance, WithSignatureVerifier(signatureVerifier)) + if err != nil { + fmt.Println("holder failed to parse SD-JWT: %w", err.Error()) + } + + // The Holder will only select given_name + selectedDisclosures := getDisclosuresFromClaimNames([]string{"given_name"}, holderClaims) + + // Holder will disclose only sub-set of claims to verifier and create holder binding for the verifier. + combinedFormatForPresentation, err := CreatePresentation(combinedFormatForIssuance, selectedDisclosures, + WithHolderBinding(&BindingInfo{ + Payload: BindingPayload{ + Nonce: "nonce", + Audience: "https://test.com/verifier", + IssuedAt: jwt.NewNumericDate(time.Now()), + }, + Signer: holderSigner, + })) + if err != nil { + fmt.Println("holder failed to create presentation: %w", err.Error()) + } + + cfp := common.ParseCombinedFormatForPresentation(combinedFormatForPresentation) + + fmt.Println(cfp.HolderBinding != "") + + // Output: true +} + +func setUp() (*afjwt.JoseED25519Signer, *afjwt.JoseEd25519Verifier, error) { + issuerPublicKey, issuerPrivateKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, nil, err + } + + signer := afjwt.NewEd25519Signer(issuerPrivateKey) + + signatureVerifier, err := afjwt.NewEd25519Verifier(issuerPublicKey) + if err != nil { + return nil, nil, err + } + + return signer, signatureVerifier, nil +} + +func setUpHolderBinding() (*afjwt.JoseED25519Signer, *jwk.JWK, error) { + holderPublicKey, holderPrivateKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, nil, err + } + + holderPublicJWK, err := jwksupport.JWKFromKey(holderPublicKey) + if err != nil { + return nil, nil, err + } + + holderSigner := afjwt.NewEd25519Signer(holderPrivateKey) + + return holderSigner, holderPublicJWK, nil +} + +func marshalObj(obj interface{}) (string, error) { + objBytes, err := json.Marshal(obj) + if err != nil { + fmt.Println("failed to marshal object: %w", err.Error()) + } + + return prettyPrint(objBytes) +} + +func prettyPrint(msg []byte) (string, error) { + var prettyJSON bytes.Buffer + + err := json.Indent(&prettyJSON, msg, "", "\t") + if err != nil { + return "", err + } + + return prettyJSON.String(), nil +} + +func getDisclosuresFromClaimNames(selectedClaimNames []string, claims []*Claim) []string { + var disclosures []string + + for _, c := range claims { + if contains(selectedClaimNames, c.Name) { + disclosures = append(disclosures, c.Disclosure) + } + } + + return disclosures +} + +func contains(values []string, val string) bool { + for _, v := range values { + if v == val { + return true + } + } + + return false +} diff --git a/pkg/doc/sdjwt/holder/holder.go b/pkg/doc/sdjwt/holder/holder.go index 565871485..0d94f46c4 100644 --- a/pkg/doc/sdjwt/holder/holder.go +++ b/pkg/doc/sdjwt/holder/holder.go @@ -4,6 +4,7 @@ Copyright SecureKey Technologies Inc. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ +// Package holder enables the Holder: an entity that receives SD-JWTs from the Issuer and has control over them. package holder import ( @@ -47,6 +48,20 @@ func WithSignatureVerifier(signatureVerifier jose.SignatureVerifier) ParseOpt { } // Parse parses issuer SD-JWT and returns claims that can be selected. +// The Holder MUST perform the following (or equivalent) steps when receiving a Combined Format for Issuance: +// +// - Separate the SD-JWT and the Disclosures in the Combined Format for Issuance. +// +// - Hash all the Disclosures separately. +// +// - Find the places in the SD-JWT where the digests of the Disclosures are included. +// +// - If any of the digests cannot be found in the SD-JWT, the Holder MUST reject the SD-JWT. +// +// - Decode Disclosures and obtain plaintext of the claim values. +// +// It is up to the Holder how to maintain the mapping between the Disclosures and the plaintext claim values to +// be able to display them to the End-User when needed. func Parse(combinedFormatForIssuance string, opts ...ParseOpt) ([]*Claim, error) { pOpts := &parseOpts{ sigVerifier: &NoopSignatureVerifier{}, @@ -126,6 +141,12 @@ func WithHolderBinding(info *BindingInfo) Option { // CreatePresentation is a convenience method to assemble combined format for presentation // using selected disclosures (claimsToDisclose) and optional holder binding. // This call assumes that combinedFormatForIssuance has already been parsed and verified using Parse() function. +// +// For presentation to a Verifier, the Holder MUST perform the following (or equivalent) steps: +// - Decide which Disclosures to release to the Verifier, obtaining proper End-User consent if necessary. +// - If Holder Binding is required, create a Holder Binding JWT. +// - Create the Combined Format for Presentation from selected Disclosures and Holder Binding JWT(if applicable). +// - Send the Presentation to the Verifier. func CreatePresentation(combinedFormatForIssuance string, claimsToDisclose []string, opts ...Option) (string, error) { hOpts := &options{} diff --git a/pkg/doc/sdjwt/issuer/example_test.go b/pkg/doc/sdjwt/issuer/example_test.go new file mode 100644 index 000000000..58550a02e --- /dev/null +++ b/pkg/doc/sdjwt/issuer/example_test.go @@ -0,0 +1,95 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package issuer + +import ( + "crypto/ed25519" + "crypto/rand" + "encoding/json" + "fmt" + + afjwt "github.com/hyperledger/aries-framework-go/pkg/doc/jwt" +) + +func ExampleNew() { + signer, _, err := setUp() + if err != nil { + fmt.Println("failed to set-up test: %w", err.Error()) + } + + claims := map[string]interface{}{ + "last_name": "Smith", + "address": map[string]interface{}{ + "street_address": "123 Main St", + "country": "US", + }, + } + + // Issuer will issue SD-JWT for specified claims. Salt function is only provided to keep example outcome the same. + token, err := New("https://example.com/issuer", claims, nil, signer, + WithStructuredClaims(true), + WithNonSelectivelyDisclosableClaims([]string{"address.country"}), + WithSaltFnc(func() (string, error) { + return sampleSalt, nil + })) + if err != nil { + fmt.Println("failed to issue SD-JWT: %w", err.Error()) + } + + var decoded map[string]interface{} + + err = token.DecodeClaims(&decoded) + if err != nil { + fmt.Println("failed to decode SD-JWT claims: %w", err.Error()) + } + + issuerClaimsJSON, err := marshalObj(decoded) + if err != nil { + fmt.Println("verifier failed to marshal verified claims: %w", err.Error()) + } + + fmt.Println(issuerClaimsJSON) + + // Output: { + // "_sd": [ + // "V9-Eiizd3iJpdlxojQuwps44Zba7z6R08S7rPCDg_wU" + // ], + // "_sd_alg": "sha-256", + // "address": { + // "_sd": [ + // "tD1XVFffEo0KTGuvHn9UlXCBgt3vot5xAanqXMdvVMg" + // ], + // "country": "US" + // }, + // "iss": "https://example.com/issuer" + //} +} + +func setUp() (*afjwt.JoseED25519Signer, *afjwt.JoseEd25519Verifier, error) { + issuerPublicKey, issuerPrivateKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, nil, err + } + + signer := afjwt.NewEd25519Signer(issuerPrivateKey) + + signatureVerifier, err := afjwt.NewEd25519Verifier(issuerPublicKey) + if err != nil { + return nil, nil, err + } + + return signer, signatureVerifier, nil +} + +func marshalObj(obj interface{}) (string, error) { + objBytes, err := json.Marshal(obj) + if err != nil { + fmt.Println("failed to marshal object: %w", err.Error()) + } + + return prettyPrint(objBytes) +} diff --git a/pkg/doc/sdjwt/issuer/issuer.go b/pkg/doc/sdjwt/issuer/issuer.go index 01acb48df..6ae847d58 100644 --- a/pkg/doc/sdjwt/issuer/issuer.go +++ b/pkg/doc/sdjwt/issuer/issuer.go @@ -4,6 +4,40 @@ Copyright SecureKey Technologies Inc. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ +/* +Package issuer enables the Issuer: An entity that creates SD-JWTs. + +An SD-JWT is a digitally signed document containing digests over the claims +(per claim: a random salt, the claim name and the claim value). +It MAY further contain clear-text claims that are always disclosed to the Verifier. +It MUST be digitally signed using the Issuer's private key. + + SD-JWT-DOC = (METADATA, SD-CLAIMS, NON-SD-CLAIMS) + SD-JWT = SD-JWT-DOC | SIG(SD-JWT-DOC, ISSUER-PRIV-KEY) + +SD-CLAIMS is an array of digest values that ensure the integrity of +and map to the respective Disclosures. Digest values are calculated +over the Disclosures, each of which contains the claim name (CLAIM-NAME), +the claim value (CLAIM-VALUE), and a random salt (SALT). +Digests are calculated using a hash function: + +SD-CLAIMS = ( +HASH(SALT, CLAIM-NAME, CLAIM-VALUE) +)* + +SD-CLAIMS can also be nested deeper to capture more complex objects. + +The Issuer further creates a set of Disclosures for all claims in the +SD-JWT. The Disclosures are sent to the Holder together with the SD-JWT: + +DISCLOSURES = ( +(SALT, CLAIM-NAME, CLAIM-VALUE) +)* + +The SD-JWT and the Disclosures are sent to the Holder by the Issuer: + +COMBINED-ISSUANCE = SD-JWT | DISCLOSURES +*/ package issuer import ( @@ -76,56 +110,59 @@ func WithJSONMarshaller(jsonMarshal func(v interface{}) ([]byte, error)) NewOpt } } -// WithSaltFnc is option is for marshalling disclosure. +// WithSaltFnc is an option for generating salt. Mostly used for testing. +// A new salt MUST be chosen for each claim independently of other salts. +// The RECOMMENDED minimum length of the randomly-generated portion of the salt is 128 bits. +// It is RECOMMENDED to base64url-encode the salt value, producing a string. func WithSaltFnc(fnc func() (string, error)) NewOpt { return func(opts *newOpts) { opts.getSalt = fnc } } -// WithIssuedAt is an option for SD-JWT payload. +// WithIssuedAt is an option for SD-JWT payload. This is a clear-text claim that is always disclosed. func WithIssuedAt(issuedAt *jwt.NumericDate) NewOpt { return func(opts *newOpts) { opts.IssuedAt = issuedAt } } -// WithAudience is an option for SD-JWT payload. +// WithAudience is an option for SD-JWT payload. This is a clear-text claim that is always disclosed. func WithAudience(audience string) NewOpt { return func(opts *newOpts) { opts.Audience = audience } } -// WithExpiry is an option for SD-JWT payload. +// WithExpiry is an option for SD-JWT payload. This is a clear-text claim that is always disclosed. func WithExpiry(expiry *jwt.NumericDate) NewOpt { return func(opts *newOpts) { opts.Expiry = expiry } } -// WithNotBefore is an option for SD-JWT payload. +// WithNotBefore is an option for SD-JWT payload. This is a clear-text claim that is always disclosed. func WithNotBefore(notBefore *jwt.NumericDate) NewOpt { return func(opts *newOpts) { opts.NotBefore = notBefore } } -// WithSubject is an option for SD-JWT payload. +// WithSubject is an option for SD-JWT payload. This is a clear-text claim that is always disclosed. func WithSubject(subject string) NewOpt { return func(opts *newOpts) { opts.Subject = subject } } -// WithJTI is an option for SD-JWT payload. +// WithJTI is an option for SD-JWT payload. This is a clear-text claim that is always disclosed. func WithJTI(jti string) NewOpt { return func(opts *newOpts) { opts.JTI = jti } } -// WithID is an option for SD-JWT payload. +// WithID is an option for SD-JWT payload. This is a clear-text claim that is always disclosed. func WithID(id string) NewOpt { return func(opts *newOpts) { opts.ID = id @@ -133,6 +170,10 @@ func WithID(id string) NewOpt { } // WithHolderPublicKey is an option for SD-JWT payload. +// The Holder can prove legitimate possession of an SD-JWT by proving control over the same private key during +// the issuance and presentation. An SD-JWT with Holder Binding contains a public key or a reference to a public key +// that matches to the private key controlled by the Holder. +// The "cnf" claim value MUST represent only a single proof-of-possession key. This implementation is using CNF "jwk". func WithHolderPublicKey(jwk *jwk.JWK) NewOpt { return func(opts *newOpts) { opts.HolderPublicKey = jwk @@ -181,6 +222,16 @@ func WithNonSelectivelyDisclosableClaims(nonSDClaims []string) NewOpt { } // New creates new signed Selective Disclosure JWT based on input claims. +// The Issuer MUST create a Disclosure for each selectively disclosable claim as follows: +// Create an array of three elements in this order: +// A salt value. Generated by the system, the salt value MUST be unique for each claim that is to be selectively +// disclosed. +// The claim name, or key, as it would be used in a regular JWT body. This MUST be a string. +// The claim's value, as it would be used in a regular JWT body. The value MAY be of any type that is allowed in JSON, +// including numbers, strings, booleans, arrays, and objects. +// +// Then JSON-encode the array such that an UTF-8 string is produced. +// Then base64url-encode the byte representation of the UTF-8 string to create the Disclosure. func New(issuer string, claims interface{}, headers jose.Headers, signer jose.Signer, opts ...NewOpt) (*SelectiveDisclosureJWT, error) { nOpts := &newOpts{ @@ -223,7 +274,17 @@ func New(issuer string, claims interface{}, headers jose.Headers, return &SelectiveDisclosureJWT{Disclosures: disclosures, SignedJWT: signedJWT}, nil } -// NewFromVC creates new signed Selective Disclosure JWT based on vc. +/* +NewFromVC creates new signed Selective Disclosure JWT based on Verifiable Credential. + +Algorithm: + - extract credential subject map from verifiable credential + - create un-signed SD-JWT plus Disclosures with credential subject map + - decode claims from SD-JWT to get credential subject map with selective disclosures + - replace VC credential subject with newly created credential subject with selective disclosures + - create signed SD-JWT based on VC + - return signed SD-JWT plus Disclosures +*/ func NewFromVC(vc map[string]interface{}, headers jose.Headers, signer jose.Signer, opts ...NewOpt) (*SelectiveDisclosureJWT, error) { csObj, ok := common.GetKeyFromVC(credentialSubjectKey, vc) diff --git a/pkg/doc/sdjwt/sdjwt-doc.go b/pkg/doc/sdjwt/sdjwt-doc.go new file mode 100644 index 000000000..278d3c493 --- /dev/null +++ b/pkg/doc/sdjwt/sdjwt-doc.go @@ -0,0 +1,36 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +// Package sdjwt implements creating JSON Web Token (JWT) documents that support selective disclosure of JWT claims. +// +// In an SD-JWT, claims can be hidden, but cryptographically protected against undetected modification. +// +// When issuing the SD-JWT to the Holder, the Issuer also sends the cleartext counterparts of all hidden claims, +// the so-called Disclosures, separate from the SD-JWT itself. +// +// The Holder decides which claims to disclose to a Verifier and forwards the respective Disclosures +// together with the SD-JWT to the Verifier. +// +// The Verifier has to verify that all disclosed claim values were part of the original, Issuer-signed SD-JWT. +// The Verifier will not, however, learn any claim values not disclosed in the Disclosures. +// +// This implementation supports: +// +// - selectively disclosable claims in flat data structures as well as more complex, nested data structures +// +// - combining selectively disclosable claims with clear-text claims that are always disclosed +// +// - options for specifying registered claim names that will be included in plaintext (e.g. iss, exp, or nbf) +// +// - option for configuring clear-text claims +// +// For selectively disclosable claims, claim names are always blinded. +// +// This implementation also supports an optional mechanism for Holder Binding, +// the concept of binding an SD-JWT to key material controlled by the Holder. +// The strength of the Holder Binding is conditional upon the trust in the protection +// of the private key of the key pair an SD-JWT is bound to. +package sdjwt diff --git a/pkg/doc/sdjwt/verifier/example_test.go b/pkg/doc/sdjwt/verifier/example_test.go new file mode 100644 index 000000000..98dceb6a2 --- /dev/null +++ b/pkg/doc/sdjwt/verifier/example_test.go @@ -0,0 +1,96 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package verifier + +import ( + "crypto/ed25519" + "crypto/rand" + "encoding/json" + "fmt" + + afjwt "github.com/hyperledger/aries-framework-go/pkg/doc/jwt" + "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/common" + "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/holder" + "github.com/hyperledger/aries-framework-go/pkg/doc/sdjwt/issuer" +) + +func ExampleParse() { + signer, signatureVerifier, err := setUp() + if err != nil { + fmt.Println("failed to set-up test: %w", err.Error()) + } + + claims := map[string]interface{}{ + "given_name": "Albert", + "last_name": "Smith", + } + + // Issuer will issue SD-JWT for specified claims. + token, err := issuer.New(testIssuer, claims, nil, signer) + if err != nil { + fmt.Println("failed to issue SD-JWT: %w", err.Error()) + } + + combinedFormatForIssuance, err := token.Serialize(false) + if err != nil { + fmt.Println("failed to issue SD-JWT: %w", err.Error()) + } + + // Holder will parse combined format for issuance for verification purposes. + _, err = holder.Parse(combinedFormatForIssuance, holder.WithSignatureVerifier(signatureVerifier)) + if err != nil { + fmt.Println("holder failed to parse SD-JWT: %w", err.Error()) + } + + // The Holder will disclose all claims. + combinedFormatForPresentation := combinedFormatForIssuance + common.CombinedFormatSeparator + + // Verifier will validate combined format for presentation and create verified claims. + verifiedClaims, err := Parse(combinedFormatForPresentation, + WithSignatureVerifier(signatureVerifier)) + if err != nil { + fmt.Println("verifier failed to parse holder presentation: %w", err.Error()) + } + + verifiedClaimsJSON, err := marshalObj(verifiedClaims) + if err != nil { + fmt.Println("verifier failed to marshal verified claims: %w", err.Error()) + } + + fmt.Println(verifiedClaimsJSON) + + // Output: { + // "given_name": "Albert", + // "iss": "https://example.com/issuer", + // "last_name": "Smith" + //} +} + +func setUp() (*afjwt.JoseED25519Signer, *afjwt.JoseEd25519Verifier, error) { + issuerPublicKey, issuerPrivateKey, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, nil, err + } + + signer := afjwt.NewEd25519Signer(issuerPrivateKey) + + signatureVerifier, err := afjwt.NewEd25519Verifier(issuerPublicKey) + if err != nil { + return nil, nil, err + } + + return signer, signatureVerifier, nil +} + +func marshalObj(obj interface{}) (string, error) { + objBytes, err := json.Marshal(obj) + if err != nil { + fmt.Println("failed to marshal object: %w", err.Error()) + } + + return prettyPrint(objBytes) +} diff --git a/pkg/doc/sdjwt/verifier/verifier.go b/pkg/doc/sdjwt/verifier/verifier.go index a8531e11c..7b83b4f8f 100644 --- a/pkg/doc/sdjwt/verifier/verifier.go +++ b/pkg/doc/sdjwt/verifier/verifier.go @@ -4,6 +4,10 @@ Copyright SecureKey Technologies Inc. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ +/* +Package verifier enables the Verifier: An entity that requests, checks and +extracts the claims from an SD-JWT and respective Disclosures. +*/ package verifier import ( @@ -45,7 +49,7 @@ func WithJWTDetachedPayload(payload []byte) ParseOpt { } } -// WithSignatureVerifier option is for definition of JWT detached payload. +// WithSignatureVerifier option is for definition of signature verifier. func WithSignatureVerifier(signatureVerifier jose.SignatureVerifier) ParseOpt { return func(opts *parseOpts) { opts.sigVerifier = signatureVerifier @@ -95,6 +99,20 @@ func WithLeewayForClaimsValidation(duration time.Duration) ParseOpt { } // Parse parses combined format for presentation and returns verified claims. +// The Verifier has to verify that all disclosed claim values were part of the original, Issuer-signed SD-JWT. +// +// At a high level, the Verifier: +// - receives the Combined Format for Presentation from the Holder and verifies the signature of the SD-JWT using the +// Issuer's public key, +// - verifies the Holder Binding JWT, if Holder Binding is required by the Verifier's policy, +// using the public key included in the SD-JWT, +// - calculates the digests over the Holder-Selected Disclosures and verifies that each digest +// is contained in the SD-JWT. +// +// Detailed algorithm: +// https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-02.html#name-verification-by-the-verifier +// +// The Verifier will not, however, learn any claim values not disclosed in the Disclosures. func Parse(combinedFormatForPresentation string, opts ...ParseOpt) (map[string]interface{}, error) { defaultSigningAlgorithms := []string{"EdDSA", "RS256"} pOpts := &parseOpts{