diff --git a/credential/exchange/submission.go b/credential/exchange/submission.go index 74d13eec..9a991c70 100644 --- a/credential/exchange/submission.go +++ b/credential/exchange/submission.go @@ -43,7 +43,7 @@ type PresentationClaim struct { LDPFormat *LinkedDataFormat // If we have a token, we assume we have a JWT format value - Token *string + TokenJSON *string JWTFormat *JWTFormat // The algorithm or Linked Data proof type by which the claim was signed must be present @@ -51,7 +51,7 @@ type PresentationClaim struct { } func (pc *PresentationClaim) IsEmpty() bool { - if pc == nil || (pc.Credential == nil && pc.Presentation == nil && pc.Token == nil) { + if pc == nil || (pc.Credential == nil && pc.Presentation == nil && pc.TokenJSON == nil) { return true } return reflect.DeepEqual(pc, &PresentationClaim{}) @@ -66,8 +66,8 @@ func (pc *PresentationClaim) GetClaimValue() (interface{}, error) { if pc.Presentation != nil { return *pc.Presentation, nil } - if pc.Token != nil { - return *pc.Token, nil + if pc.TokenJSON != nil { + return *pc.TokenJSON, nil } return nil, errors.New("claim is empty") } @@ -88,7 +88,7 @@ func (pc *PresentationClaim) GetClaimFormat() (string, error) { } return string(*pc.LDPFormat), nil } - if pc.Token != nil { + if pc.TokenJSON != nil { if pc.JWTFormat == nil { return "", errors.New("JWT claim has no JWT format set") } diff --git a/credential/exchange/submission_test.go b/credential/exchange/submission_test.go index 6a33d2fe..a2247bf2 100644 --- a/credential/exchange/submission_test.go +++ b/credential/exchange/submission_test.go @@ -247,7 +247,7 @@ func TestBuildPresentationSubmissionVP(t *testing.T) { } testVCJWT := getTestJWTVerifiableCredential() presentationClaimJWT := PresentationClaim{ - Token: &testVCJWT, + TokenJSON: &testVCJWT, JWTFormat: JWTVC.Ptr(), SignatureAlgorithmOrProofType: string(crypto.EdDSA), } @@ -676,7 +676,7 @@ func TestNormalizePresentationClaims(t *testing.T) { assert.NotEmpty(tt, jwtVC) presentationClaim := PresentationClaim{ - Token: &jwtVC, + TokenJSON: &jwtVC, JWTFormat: JWTVC.Ptr(), SignatureAlgorithmOrProofType: string(crypto.EdDSA), } diff --git a/example/use_cases/apartment_application/apartment_application.go b/example/use_cases/apartment_application/apartment_application.go new file mode 100644 index 00000000..39b8dfe1 --- /dev/null +++ b/example/use_cases/apartment_application/apartment_application.go @@ -0,0 +1,171 @@ +// This is a full example flow of an apartment's manager verifying the age of a potential tenant. + +// The apartment manager will create a Presentation Request that is to be fulfilled by the tenant. +// The tenant will fulfil the Presentation Request by submitting a Presentation Submission. +// This presentation submission will contain a verifiable credential that has been previously issued and signed from the government issuer. + +// The tenant will verify that the apartment's presentation request is valid and the apartment will also verify that the tenant's +// presentation submission is valid. + +// At the end the apartment manager will verify the authenticity of the presentation submission and will be able to verify the birthdate of the tenant. + +package main + +import ( + "crypto/ed25519" + "fmt" + "github.com/TBD54566975/ssi-sdk/credential" + "github.com/TBD54566975/ssi-sdk/credential/exchange" + "github.com/TBD54566975/ssi-sdk/credential/signing" + "github.com/TBD54566975/ssi-sdk/crypto" + "github.com/TBD54566975/ssi-sdk/cryptosuite" + "github.com/TBD54566975/ssi-sdk/did" + "github.com/TBD54566975/ssi-sdk/example" + "github.com/TBD54566975/ssi-sdk/util" + "github.com/goccy/go-json" +) + +func main() { + + /** + Step 1: Create new entities as DIDs. Govt Issuer, User Holder, and Apartment Verifier. + **/ + + // User Holder + holderDIDPrivateKey, holderDIDKey, err := did.GenerateDIDKey(crypto.Ed25519) + example.HandleExampleError(err, "Failed to generate DID") + holderJWK, err := cryptosuite.JSONWebKey2020FromEd25519(holderDIDPrivateKey.(ed25519.PrivateKey)) + example.HandleExampleError(err, "Failed to generate JWK") + holderSigner, err := cryptosuite.NewJSONWebKeySigner(holderJWK.ID, holderJWK.PrivateKeyJWK, cryptosuite.Authentication) + example.HandleExampleError(err, "Failed to generate signer") + holderVerifier, err := cryptosuite.NewJSONWebKeyVerifier(holderJWK.ID, holderJWK.PublicKeyJWK) + example.HandleExampleError(err, "Failed to generate verifier") + + // Apt Verifier + aptDIDPrivateKey, aptDIDKey, err := did.GenerateDIDKey(crypto.Ed25519) + example.HandleExampleError(err, "Failed to generate DID") + aptJWK, err := cryptosuite.JSONWebKey2020FromEd25519(aptDIDPrivateKey.(ed25519.PrivateKey)) + example.HandleExampleError(err, "Failed to generate JWK") + aptSigner, err := cryptosuite.NewJSONWebKeySigner(aptJWK.ID, aptJWK.PrivateKeyJWK, cryptosuite.Authentication) + example.HandleExampleError(err, "Failed to generate signer") + aptVerifier, err := cryptosuite.NewJSONWebKeyVerifier(aptJWK.ID, aptJWK.PublicKeyJWK) + example.HandleExampleError(err, "Failed to generate verifier") + + // Government Issuer + govtDIDPrivateKey, govtDIDKey, err := did.GenerateDIDKey(crypto.Ed25519) + example.HandleExampleError(err, "Failed to generate key") + govtJWK, err := cryptosuite.JSONWebKey2020FromEd25519(govtDIDPrivateKey.(ed25519.PrivateKey)) + example.HandleExampleError(err, "Failed to generate JWK") + govtSigner, err := cryptosuite.NewJSONWebKeySigner(govtJWK.ID, govtJWK.PrivateKeyJWK, cryptosuite.Authentication) + example.HandleExampleError(err, "Failed to generate signer") + + fmt.Print("\n\nStep 1: Create new DIDs for entities\n\n") + fmt.Printf("Tenant: %s\n", string(*holderDIDKey)) + fmt.Printf("Apartment: %s\n", string(*aptDIDKey)) + fmt.Printf("Government: %s\n", string(*govtDIDKey)) + + /** + Step 2: Government issuer issues a credential to the holder providing their age. The government issuer then signs the verifiable credentials to holder claiming age. + **/ + + knownIssuer := govtDIDKey + knownIssuanceDate := "2020-01-01T19:23:24Z" + knownSubject := map[string]interface{}{ + "id": string(*holderDIDKey), + "birthdate": "1975-01-01", + } + + vcBuilder := credential.NewVerifiableCredentialBuilder() + + vcBuilder.SetIssuer(string(*knownIssuer)) + vcBuilder.SetIssuanceDate(knownIssuanceDate) + vcBuilder.SetCredentialSubject(knownSubject) + + vc, err := vcBuilder.Build() + example.HandleExampleError(err, "Failed to make verifiable credential") + example.HandleExampleError(vc.IsValid(), "Verifiable credential is not valid") + + signedVCBytes, err := signing.SignVerifiableCredentialJWT(*govtSigner, *vc) + + example.HandleExampleError(err, "Failed to sign vc") + + fmt.Print("\n\nStep 2: Government issues Verifiable Credential new for tenant verifying birthdate and signs\n\n") + if dat, err := util.PrettyJSON(vc); err == nil { + fmt.Printf("Verifiable Credential:%s\n", string(dat)) + } + + /** + Step 3: Create presentation definition from the apartment manager to the holder which goes into a presentation request. + The apartment manager is saying "here tenant, here is my what information I am requesting from you." + **/ + + presentationDefinitionBuilder := exchange.NewPresentationDefinitionBuilder() + + presentationDefinitionBuilder.SetInputDescriptors([]exchange.InputDescriptor{ + { + ID: "birthdate", + Purpose: "Age verification", + Format: &exchange.ClaimFormat{ + JWTVC: &exchange.JWTType{Alg: []crypto.SignatureAlgorithm{crypto.EdDSA}}, + }, + Constraints: &exchange.Constraints{Fields: []exchange.Field{ + { + Path: []string{"$.credentialSubject.birthdate"}, + ID: "birthdate", + }, + }}, + }, + }) + + presentationDefinition, err := presentationDefinitionBuilder.Build() + example.HandleExampleError(err, "Failed to make presentation definition") + example.HandleExampleError(presentationDefinition.IsValid(), "Presentation definition is not valid") + + presentationRequestBytes, err := exchange.BuildPresentationRequest(aptSigner, exchange.JWTRequest, *presentationDefinition, string(*holderDIDKey)) + example.HandleExampleError(err, "Failed to make presentation request") + + fmt.Print("\n\nStep 3: The apartment creates a presentation request that confirms which information is required from the tenant\n\n") + if dat, err := util.PrettyJSON(presentationDefinition); err == nil { + fmt.Printf("Presentation Definition that gets added to presentation request:%s\n", string(dat)) + } + + /** + Step 4: Tenant holder verifies the presentation request from the apt is valid and then constructs and signs a presentation submission. + **/ + + verifiedPresentationDefinition, err := exchange.VerifyPresentationRequest(aptVerifier, exchange.JWTRequest, presentationRequestBytes) + example.HandleExampleError(err, "Failed to verify presentation request") + example.HandleExampleError(verifiedPresentationDefinition.IsValid(), "Verified presentation definition is not valid") + + // TODO: (neal) (issue https://github.com/TBD54566975/ssi-sdk/issues/165) + // Have the presentation claim's token format support signedVCBytes for the BuildPresentationSubmission function + vcJson, err := signing.ParseVerifiableCredentialFromJWT(string(signedVCBytes)) + example.HandleExampleError(err, "Failed to parse VC") + vcJsonBytes, err := json.Marshal(vcJson) + example.HandleExampleError(err, "Failed to marshal vc jwt") + + presentationClaim := exchange.PresentationClaim{ + TokenJSON: util.StringPtr(string(vcJsonBytes)), + JWTFormat: exchange.JWTVC.Ptr(), + SignatureAlgorithmOrProofType: string(crypto.EdDSA), + } + + presentationSubmissionBytes, err := exchange.BuildPresentationSubmission(holderSigner, *presentationDefinition, []exchange.PresentationClaim{presentationClaim}, exchange.JWTVPTarget) + example.HandleExampleError(err, "Failed to create presentation submission") + + fmt.Print("\n\nStep 4: The holder creates a presentation submission to give to the apartment\n\n") + if dat, err := util.PrettyJSON(presentationClaim); err == nil { + fmt.Printf("Presentation Claim that gets added to presentation submission:%s\n", string(dat)) + } + + /** + Step 5: The apartment will verify the presentation submission. This is done to make sure the presentation is in compliance with the definition. + **/ + + err = exchange.VerifyPresentationSubmission(holderVerifier, exchange.JWTVPTarget, *presentationDefinition, presentationSubmissionBytes) + example.HandleExampleError(err, "Failed to verify presentation submission") + + fmt.Print("\n\nStep 5: The apartment verifies that the presentation submission is valid and then can cryptographically verify that the birthdate of the tenant is authentic\n\n") + + fmt.Print("\n\n\nπŸŽ‰ The tenant's age has now been verified and can now move into the apartment! πŸŽ‰\n\n\n") +} diff --git a/example/use_cases/apartment_application/apartment_application.md b/example/use_cases/apartment_application/apartment_application.md new file mode 100644 index 00000000..9410b7dc --- /dev/null +++ b/example/use_cases/apartment_application/apartment_application.md @@ -0,0 +1,120 @@ +# Apartment Age Verification Use Case + + +# Introduction +This is a full example flow of an apartment verifying the age of a potential tenant. The flow goes as follows: + +The apartment will create a presentation request that is to be fulfilled by the tenant. The tenant will fulfil the Presentation Request by submitting a Presentation Submission. This presentation submission will contain a verifiable credential that has been previously issued and signed from the government issuer. The tenant will verify that the apartment's presentation request is valid and the apartment will also verify that the tenant's presentation submission is valid. + +At the end the apartment will verify the authenticity of the presentation submission and will be able to cryptographically verify the birthdate of the tenant. + + +## Step 1 +Create new decentralized identities for an apartment, a government agency, and a future tenant. + +![ssi-sdk](docs/dids.png) + +## Step 2 +Government issuer using the holder’s DID issues credentials claiming age. The government issuer then signs the verifiable credentials to holder claiming age +![ssi-sdk](docs/issuevc.png) + +## Step 3 +Create Presentation Definition from the apartment to the holder which goes into a presentation request. +The apartment is saying "here tenant, here is my what information I am requesting from you" +![ssi-sdk](docs/presentationrequest.png) + +## Step 4 +The tenant verifies the presentation request from the apartment is valid and then constructs and signs a presentation submission + +![ssi-sdk](docs/presentationsubmission.png) + +## Step 5 +The apartment verifies that the presentation submission is valid and then can cryptographically verify that the birthdate of the tenant is authentic. The tenant now has their age verified and they can move into the apartment! πŸŽ‰ +![ssi-sdk](docs/aptverify.png) + +# Running +Navigate to the ssi-sdk/example/use_cases/apartment_application directory and run + +``` +go run apartment_application.go +``` + +# Output + +``` +Step 1: Create new DIDs for entities + +Tenant: did:key:z6Mkj2VE6wby9NzHD6TLd7vmbUCCNXzAhv9Dtio6PcWNnwgn +Apartment: did:key:z6Mksqn5HLWNNau6humKzsvNLHDbErez867gYtPHbN3b269y +Government: did:key:z6Mkpfz5uy9bkBLDLQmarYKVmJCDBd6qKxXLF2VRtcwPD74m + + +Step 2: Government issues Verifiable Credential new for tenant verifying birthdate and signs + +Verifiable Credential:{ + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "id": "1b81525a-ae7f-4f90-94ef-ebb04e348556", + "type": [ + "VerifiableCredential" + ], + "issuer": "did:key:z6Mkpfz5uy9bkBLDLQmarYKVmJCDBd6qKxXLF2VRtcwPD74m", + "issuanceDate": "2020-01-01T19:23:24Z", + "credentialSubject": { + "birthdate": "1975-01-01", + "id": "did:key:z6Mkj2VE6wby9NzHD6TLd7vmbUCCNXzAhv9Dtio6PcWNnwgn" + } +} + + +Step 3: The apartment creates a presentation request that confirms which information is required from the tenant + +Presentation Definition that gets added to presentation request:{ + "id": "48e93dce-12a0-4cf7-a016-44927c98e4dc", + "input_descriptors": [ + { + "id": "birthdate", + "purpose": "Age verification", + "format": { + "jwt_vc": { + "alg": [ + "EdDSA" + ] + } + }, + "constraints": { + "fields": [ + { + "path": [ + "$.credentialSubject.birthdate" + ], + "id": "birthdate" + } + ] + } + } + ] +} + + +Step 4: The holder creates a presentation submission to give to the apartment + +Presentation Claim that gets added to presentation submission:{ + "Credential": null, + "Presentation": null, + "LDPFormat": null, + "Token": "{\"@context\":[\"https://www.w3.org/2018/credentials/v1\"],\"id\":\"1b81525a-ae7f-4f90-94ef-ebb04e348556\",\"type\":[\"VerifiableCredential\"],\"issuer\":\"did:key:z6Mkpfz5uy9bkBLDLQmarYKVmJCDBd6qKxXLF2VRtcwPD74m\",\"issuanceDate\":\"2020-01-01T19:23:24Z\",\"credentialSubject\":{\"birthdate\":\"1975-01-01\",\"id\":\"did:key:z6Mkj2VE6wby9NzHD6TLd7vmbUCCNXzAhv9Dtio6PcWNnwgn\"}}", + "JWTFormat": "jwt_vc", + "SignatureAlgorithmOrProofType": "EdDSA" +} + + +Step 5: The apartment verifies that the presentation submission is valid and then can cryptographically verify that the birthdate of the tenant is authentic + + + + +πŸŽ‰ The tenant's age has now been verified and can now move into the apartment! πŸŽ‰ + +``` \ No newline at end of file diff --git a/example/use_cases/apartment_application/apartment_application_test.go b/example/use_cases/apartment_application/apartment_application_test.go new file mode 100644 index 00000000..12c9b666 --- /dev/null +++ b/example/use_cases/apartment_application/apartment_application_test.go @@ -0,0 +1,9 @@ +package main + +import "testing" + +func TestApartmentApplicationUseCase(t *testing.T) { + + // If there is an error in main this test will fail + main() +} diff --git a/example/use_cases/apartment_application/docs/aptverify.png b/example/use_cases/apartment_application/docs/aptverify.png new file mode 100644 index 00000000..9ec22476 Binary files /dev/null and b/example/use_cases/apartment_application/docs/aptverify.png differ diff --git a/example/use_cases/apartment_application/docs/dids.png b/example/use_cases/apartment_application/docs/dids.png new file mode 100644 index 00000000..976955cc Binary files /dev/null and b/example/use_cases/apartment_application/docs/dids.png differ diff --git a/example/use_cases/apartment_application/docs/issuevc.png b/example/use_cases/apartment_application/docs/issuevc.png new file mode 100644 index 00000000..1af12340 Binary files /dev/null and b/example/use_cases/apartment_application/docs/issuevc.png differ diff --git a/example/use_cases/apartment_application/docs/presentationrequest.png b/example/use_cases/apartment_application/docs/presentationrequest.png new file mode 100644 index 00000000..7e5c3202 Binary files /dev/null and b/example/use_cases/apartment_application/docs/presentationrequest.png differ diff --git a/example/use_cases/apartment_application/docs/presentationsubmission.png b/example/use_cases/apartment_application/docs/presentationsubmission.png new file mode 100644 index 00000000..3537c60a Binary files /dev/null and b/example/use_cases/apartment_application/docs/presentationsubmission.png differ