Walkthrough: Build a Vaccine Card¶
This walkthrough demonstrates how a vaccination card can be issued, held, and shared using Verifiable Credentials with Trinsic.
Meet Allison¶
We'll follow Allison as she obtains a vaccine certificate, stores it in her digital wallet, and presents it to board an airplane.
In most credential exchange scenarios, there are three primary roles: Issuer, Holder, and Verifier.
Holder: Stores credentials received from issuers, and presents them to verifiers. (Said credentials are often, but not always, attesting information about the holder)
Issuer: Signs and issues credentials which attest information about a credential subject.
Verifier: Verifies credentials presented by holders.
In this case, Allison will be the holder, a vaccination clinic will be the issuer, and an airline will be the verifier.
Our SDKs¶
You can follow along using one of our SDKs, or use the Trinsic CLI, which implements full platform functionality.
Click here for installation instructions for the Trinsic CLI.
Click here for installation instructions for the Node/Browser SDK.
Click here for installation instructions for the .NET SDK.
Click here for installation instructions for the Python SDK.
Click here for installation instructions for the Java SDK.
Click here for installation instructions for the Go SDK.
Click here for installation instructions for the Ruby SDK.
Ecosystem Setup¶
Before we begin, you'll need an ecosystem -- somewhere for the resources we're about to create (wallets, templates, credentials) to live.
Use Existing Ecosystem¶
If you've already signed up as a customer, you'll have received an email with an ecosystem ID and authentication token.
Copy this ecosystem ID down, and skip to the next step.
Create New Ecosystem¶
If you don't already have an ecosystem provisioned for you, you'll need to create one first.
This will be a sandbox ecosystem; suitable for prototyping and testing, but not production purposes. To receive a production ecosystem, sign up.
trinsic provider create-ecosystem
const ecosystem = await trinsic
.provider()
.createEcosystem(CreateEcosystemRequest.fromPartial({}));
const ecosystemId = ecosystem.ecosystem!.id;
var trinsic = new TrinsicService(_options);
var (ecosystem, authToken) = await trinsic.Provider.CreateEcosystemAsync(new());
var ecosystemId = ecosystem?.Id;
ecosystem = await trinsic_service.provider.create_ecosystem()
ecosystem_id = ecosystem.ecosystem.id
var ecosystemResponse =
trinsic.provider().createEcosystem(CreateEcosystemRequest.getDefaultInstance()).get();
var ecosystemId = ecosystemResponse.getEcosystem().getId();
ecosystem, _ := trinsic.Provider().CreateEcosystem(context.Background(), nil)
ecosystemId := ecosystem.Ecosystem.Id
ecosystem = trinsic.provider_service.create_ecosystem
ecosystem_id = ecosystem.ecosystem.id
The response to this call contains the name and ID of your newly-created ecosystem; copy either of these down.
Further Reading: Ecosystems
- Learn more about Ecosystems
- Browse the Provider API reference
Create Accounts¶
We need to create Trinsic accounts for the participants in this credential exchange. Accounts and wallets can be considered interchangeably; all accounts have exactly one associated wallet.
Accounts can be created with a single call; they're designed to minimize onboarding friction for your users.
The clinic's account will issue the credential, Allison's account will hold it, and the airline's account will verify its contents.
The CLI makes it easy to create wallets. For demo purposes, we'll create all three on the same machine.
When using the CLI, the authentication token of the most recently used account is saved in ~/.trinsic
. In a real-world scenario, you should back this token up securely.
trinsic account login --ecosystem {ECOSYSTEM_ID}
# Save auth token in `allison.txt` before continuing
trinsic account login --ecosystem {ECOSYSTEM_ID}
# Save auth token in `airline.txt` before continuing
trinsic account login --ecosystem {ECOSYSTEM_ID}
# Save auth token in `clinic.txt` before continuing
// Create 3 different profiles for each participant in the scenario
const allison = await trinsic.account().loginAnonymous(ecosystemId);
const clinic = await trinsic.account().loginAnonymous(ecosystemId);
const airline = await trinsic.account().loginAnonymous(ecosystemId);
If you would like to save the account for future use, simply write the auth token to storage. Take care to store it in a secure location.
var allison = await trinsic.Account.LoginAnonymousAsync(ecosystemId!);
var clinic = await trinsic.Account.LoginAnonymousAsync(ecosystemId!);
var airline = await trinsic.Account.LoginAnonymousAsync(ecosystemId!);
If you would like to save an account for future use, simply write the auth token to storage. Take care to store it in a secure location.
# Create an account for each participant in the scenario
allison = await trinsic_service.account.login_anonymous(ecosystem_id=ecosystem_id)
airline = await trinsic_service.account.login_anonymous(ecosystem_id=ecosystem_id)
clinic = await trinsic_service.account.login_anonymous(ecosystem_id=ecosystem_id)
If you would like to save an account for future use, simply write the auth token to storage. Take care to store it in a secure location.
// Create an account for each participant in the scenario
var allison = trinsic.account().loginAnonymous(ecosystemId).get();
var clinic = trinsic.account().loginAnonymous(ecosystemId).get();
var airline = trinsic.account().loginAnonymous(ecosystemId).get();
If you would like to save an account for future use, simply write the auth token to storage. Take care to store it in a secure location.
// Create an account for each participant in the scenario
allison, _ := trinsic.Account().LoginAnonymous(context.Background(), ecosystemId)
airline, _ := trinsic.Account().LoginAnonymous(context.Background(), ecosystemId)
clinic, _ := trinsic.Account().LoginAnonymous(context.Background(), ecosystemId)
If you would like to save an account for future use, simply write the auth token to storage. Take care to store it in a secure location.
# Create an account for each participant in the scenario
allison = trinsic.account_service.login_anonymous(ecosystem_id)
clinic = trinsic.account_service.login_anonymous(ecosystem_id)
airline = trinsic.account_service.login_anonymous(ecosystem_id)
Production Usage
In this example, we've created anonymous accounts; the only way to access them is by saving the authentication token generated on account creation.
In a production scenario, you may want to create accounts tied to a user's email address or phone number. This allows users to securely access their Trinsic cloud wallets at any time.
Note that accounts are tied to their ecosystem. If you create an account tied to bob@example.com
in the example1
ecosystem, it will not be visible in any other ecosystem. The same email address can be used to create accounts in multiple ecosystems.
Further Reading: Accounts and Wallets
- Learn more about Wallets
- Browse the Account API reference
- Read about authentication tokens and security
Define a Template¶
Before we can issue a credential, we need to create a Template for it.
Templates are simply a list of the fields that a credential can have.
First, prepare a JSON file which describes your template:
{
"firstName": {
"type": "string",
"description": "First name of vaccine recipient"
},
"lastName": {
"type": "string",
"description": "Last name of vaccine recipient"
},
"batchNumber":{
"type": "string",
"description": "Batch number of vaccine"
},
"countryOfVaccination":{
"type": "string",
"description": "Country in which the subject was vaccinated"
}
}
Then create the template:
trinsic template create -n "VaccinationCertificate" --fields-file templateData.json
The output of this command will include a template ID; copy this down for later use.
//Define all fields
const firstNameField = TemplateField.fromPartial({
description: "First name of vaccine recipient",
type: FieldType.STRING,
});
const lastNameField = TemplateField.fromPartial({
type: FieldType.STRING,
description: "Last name of vaccine recipient",
});
const batchNumberField = TemplateField.fromPartial({
type: FieldType.STRING,
description: "Batch number of vaccine",
});
const countryOfVaccinationField = TemplateField.fromPartial({
type: FieldType.STRING,
description: "Country in which the subject was vaccinated",
});
//Create request
let request = CreateCredentialTemplateRequest.fromPartial({
name: `VaccinationCertificate-${uuid()}`,
fields: {
firstName: firstNameField,
lastName: lastNameField,
batchNumber: batchNumberField,
countryOfVaccination: countryOfVaccinationField,
},
});
//Create template
const response = await trinsicService
.template()
.create(request);
const template = response.data;
// Set active profile to `clinic` so we can create a template
trinsic.SetAuthToken(clinic!);
// Prepare request to create template
CreateCredentialTemplateRequest templateRequest = new() {
Name = "VaccinationCertificate",
AllowAdditionalFields = false
};
templateRequest.Fields.Add("firstName", new() { Description = "First name of vaccine recipient" });
templateRequest.Fields.Add("lastName", new() { Description = "Last name of vaccine recipient" });
templateRequest.Fields.Add("batchNumber", new() { Description = "Batch number of vaccine", Type = FieldType.String });
templateRequest.Fields.Add("countryOfVaccination", new() { Description = "Country in which the subject was vaccinated" });
// Create template
var template = await trinsic.Template.CreateAsync(templateRequest);
var templateId = template?.Data?.Id;
template = await trinsic_service.template.create(
request=CreateCredentialTemplateRequest(
name=f"VaccinationCertificate-{uuid.uuid4()}",
allow_additional_fields=False,
fields={
"firstName": TemplateField(
description="First name of vaccine recipient"
),
"lastName": TemplateField(description="Last name of vaccine recipient"),
"batchNumber": TemplateField(
description="Batch number of vaccine", type=FieldType.STRING
),
"countryOfVaccination": TemplateField(
description="Country in which the subject was vaccinated"
),
},
)
)
template_id = template.data.id
// Set active profile to 'clinic'
templateService.setAuthToken(clinic);
// Define fields for template
var fields = new HashMap<String, TemplateField>();
fields.put(
"firstName",
TemplateField.newBuilder().setDescription("First name of vaccine recipient").build());
fields.put(
"lastName",
TemplateField.newBuilder().setDescription("Last name of vaccine recipient").build());
fields.put(
"batchNumber",
TemplateField.newBuilder()
.setType(FieldType.STRING)
.setDescription("Batch number of vaccine")
.build());
fields.put(
"countryOfVaccination",
TemplateField.newBuilder()
.setDescription("Country in which the subject was vaccinated")
.build());
// Create template request
var templateRequest =
CreateCredentialTemplateRequest.newBuilder()
.setName("VaccinationCertificate")
.setAllowAdditionalFields(false)
.putAllFields(fields)
.build();
// Execute template creation
var template = templateService.create(templateRequest).get();
var templateId = template.getData().getId();
templateRequest := &template.CreateCredentialTemplateRequest{Name: "VaccinationCertificate", AllowAdditionalFields: false, Fields: make(map[string]*template.TemplateField)}
templateRequest.Fields["firstName"] = &template.TemplateField{Description: "First name of vaccine recipient"}
templateRequest.Fields["lastName"] = &template.TemplateField{Description: "Last name of vaccine recipient"}
templateRequest.Fields["batchNumber"] = &template.TemplateField{Description: "Batch number of vaccine", Type: template.FieldType_STRING}
templateRequest.Fields["countryOfVaccination"] = &template.TemplateField{Description: "Country in which the subject was vaccinated"}
createdTemplate, _ := trinsic.Template().Create(context.Background(), templateRequest)
templateId := createdTemplate.Data.Id
request = Trinsic::Template::CreateCredentialTemplateRequest.new(name: "VaccinationCertificate-#{SecureRandom.uuid}",
allow_additional_fields: false)
request.fields['firstName'] = Trinsic::Template::TemplateField.new(description: 'First name of vaccine recipient')
request.fields['lastName'] = Trinsic::Template::TemplateField.new(description: 'Last name of vaccine recipient')
request.fields['batchNumber'] =
Trinsic::Template::TemplateField.new(description: 'Batch number of vaccine',
type: Trinsic::Template::FieldType::STRING)
request.fields['countryOfVaccination'] =
Trinsic::Template::TemplateField.new(description: 'Country in which the subject was vaccinated')
template = trinsic.template_service.create(request)
template_id = template.data.id
Templates are Optional
Templates are an optional helpful abstraction which removes the need to work directly with complex data formats such as JSON-LD.
When a template is used to issue a credential, the result is a valid, interoperable JSON-LD Verifiable Credential.
Trinsic's SDKs support issuing JSON-LD credentials that you create yourself, should you choose not to use templates.
Further Reading: Templates
- Learn more about Templates
- Browse the Template API reference
Issue a Credential¶
Upon receiving her vaccine, the clinic issues Allison a Verifiable Credential, which proves that she was given the vaccine by the clinic.
A credential is a JSON document that has been cryptographically signed; this signature enables verifiers to trust that the data comes a trusted source, and has not been tampered with.
To issue a vaccine certificate, we'll use the template we created in the last step.
First, prepare a file named values.json
with the following content:
{
"firstName": "Allison",
"lastName": "Allisonne",
"batchNumber": "123454321",
"countryOfVaccination": "US"
}
Then issue the credential:
trinsic config --auth-token $(cat clinic.txt)
trinsic vc issue-from-template --template-id {TEMPLATE_ID} --values-file values.json --out credential.json
The output of this command will contain a signed JSON document, which has been saved to credential.json
.
// Prepare the credential values JSON document
const credentialValues = JSON.stringify({
firstName: "Allison",
lastName: "Allisonne",
batchNumber: "123454321",
countryOfVaccination: "US",
});
// Sign a credential as the clinic and send it to Allison
trinsic.options.authToken = clinic;
const issueResponse = await trinsic.credential().issueFromTemplate(
IssueFromTemplateRequest.fromPartial({
templateId: template.id,
valuesJson: credentialValues,
})
);
// Prepare credential values
var credentialValues = new Dictionary<string, string>() {
{ "firstName", "Allison" },
{ "lastName", "Allisonne" },
{ "batchNumber", "123454321" },
{ "countryOfVaccination", "US" }
};
// Issue credential as clinic
var issueResponse = await trinsic.Credential.IssueFromTemplateAsync(new() {
TemplateId = templateId,
ValuesJson = JsonSerializer.Serialize(credentialValues)
});
var signedCredential = issueResponse?.DocumentJson;
# Prepare values for credential
values = json.dumps(
{
"firstName": "Allison",
"lastName": "Allisonne",
"batchNumber": "123454321",
"countryOfVaccination": "US",
}
)
# Issue credential
issue_response = await trinsic_service.credential.issue_from_template(
request=IssueFromTemplateRequest(template_id=template.id, values_json=values)
)
credential = issue_response.document_json
// Set active profile to 'clinic' so we can issue credential signed
// with the clinic's signing keys
trinsicService.setAuthToken(clinic);
// Prepare credential values
var valuesMap = new HashMap<String, Object>();
valuesMap.put("firstName", "Allison");
valuesMap.put("lastName", "Allissonne");
valuesMap.put("batchNumber", "123454321");
valuesMap.put("countryOfVaccination", "US");
// Serialize values to JSON
var valuesJson = new Gson().toJson(valuesMap);
// Issue credential
var issueResponse =
trinsicService
.credential()
.issueFromTemplate(
IssueFromTemplateRequest.newBuilder()
.setTemplateId(templateId)
.setValuesJson(valuesJson)
.build())
.get();
var credential = issueResponse.getDocumentJson();
// Prepare values for credential
valuesStruct := struct {
FirstName string
LastName string
batchNumber string
countryOfVaccination string
}{
FirstName: "Allison",
LastName: "Allisonne",
batchNumber: "123454321",
countryOfVaccination: "US",
}
values, _ := json.Marshal(valuesStruct)
// Issue credential
issueResponse, _ := trinsic.Credential().IssueFromTemplate(context.Background(), &credential.IssueFromTemplateRequest{
TemplateId: createdTemplate.Id,
ValuesJson: string(values),
})
issuedCredential := issueResponse.DocumentJson
# Prepares values for credential
values = JSON.generate({ firstName: 'Allison', lastName: 'Allisonne', batchNumber: '123454321',
countryOfVaccination: 'US' })
# Issue credential
issue_response = trinsic.credential_service.issue_from_template(Trinsic::Credentials::IssueFromTemplateRequest.new(
template_id: template.id, values_json: values
))
credential = issue_response.document_json
Further Reading: Issuance and Credentials
- Learn more about Verifiable Credentials
- Browse the Credential API reference
Send Credential to Allison¶
Now that the clinic has a signed credential, it must be securely transmitted to Allison, so she can store it in her wallet.
Because it's just a JSON string, it could be delivered in many ways -- for example, in the response to an HTTPS request which triggered the issuance process.
Send via Trinsic
In the future, we will offer the ability to send a credential directly to a Trinsic user's wallet.
Click here to learn more about this feature.
Store Credential in Wallet¶
Once Allison receives the credential, it must be stored in her wallet.
trinsic config --auth-token $(cat allison.txt)
trinsic wallet insert-item --item credential.json
// Alice stores the credential in her cloud wallet.
trinsic.options.authToken = allison;
const insertResponse = await trinsic.wallet().insertItem(
InsertItemRequest.fromPartial({
itemJson: issueResponse.documentJson,
})
);
// Set active profile to 'allison' so we can manage her cloud wallet
trinsic.SetAuthToken(allison!);
// Insert credential into Allison's wallet
var insertItemResponse = await trinsic.Wallet.InsertItemAsync(new() {
ItemJson = signedCredential
});
var itemId = insertItemResponse?.ItemId;
# Allison stores the credential in her cloud wallet
trinsic_service.service_options.auth_token = allison
insert_response = await trinsic_service.wallet.insert_item(
request=InsertItemRequest(item_json=credential)
)
item_id = insert_response.item_id
// Set active profile to 'allison' so we can manage her cloud wallet
trinsic.setAuthToken(allison);
// Allison stores the credential in her cloud wallet.
var insertItemResponse =
trinsic
.wallet()
.insertItem(InsertItemRequest.newBuilder().setItemJson(credential).build())
.get();
final var itemId = insertItemResponse.getItemId();
// Allison stores the credential in her cloud wallet
trinsic.SetAuthToken(allison)
insertResponse, _ := trinsic.Wallet().InsertItem(context.Background(), &wallet.InsertItemRequest{ItemJson: issuedCredential})
itemId := insertResponse.ItemId
# Allison stores the credential in her cloud wallet
trinsic.auth_token = allison
insert_response = trinsic.wallet_service.insert_item(Trinsic::Wallet::InsertItemRequest.new(item_json: credential))
item_id = insert_response.item_id
The response to this call contains an Item ID; copy this down.
Further Reading: Wallets
- Learn more about Wallets
- Browse the Wallet API reference
Create a Proof of Vaccination¶
Before boarding, the airline requests proof of vaccination from Allison. Specifically, they want to see proof that she holds a VaccinationCertificate
credential.
Let's use the CreateProof call to build a proof for Allison's held credential.
trinsic config --auth-token $(cat allison.txt)
trinsic vc create-proof --item-id "{ITEM_ID}" --out proof.json
// Allison shares the credential with the venue.
trinsic.options.authToken = allison;
const proofResponse = await trinsic.credential().createProof(
CreateProofRequest.fromPartial({
itemId: insertResponse.itemId,
})
);
// Build a proof for the signed credential as allison
var proofResponse = await trinsic.Credential.CreateProofAsync(new() {
ItemId = itemId
});
var proofJSON = proofResponse?.ProofDocumentJson;
# Allison shares the credential with the airline
trinsic_service.service_options.auth_token = allison
proof_response = await trinsic_service.credential.create_proof(
request=CreateProofRequest(item_id=item_id)
)
credential_proof = proof_response.proof_document_json
// Set active profile to 'allison' so we can create a proof using her key
trinsic.setAuthToken(allison);
// Allison shares the credential with the venue
var createProofResponse =
trinsic
.credential()
.createProof(CreateProofRequest.newBuilder().setItemId(itemId).build())
.get();
var credentialProof = createProofResponse.getProofDocumentJson();
// Allison shares the credential with the airline
trinsic.SetAuthToken(allison)
proofResponse, _ := trinsic.Credential().CreateProof(context.Background(), &credential.CreateProofRequest{
Proof: &credential.CreateProofRequest_ItemId{ItemId: itemId},
})
credentialProof := proofResponse.ProofDocumentJson
# Allison shares the credential with the airline
trinsic.auth_token = allison
proof_response = trinsic.credential_service.create_proof(Trinsic::Credentials::CreateProofRequest.new(item_id: item_id))
credential_proof = proof_response.proof_document_json
Allison sends this proof to the airline for them to verify.
Partial Proofs
In this example, the proof is being created over the entire credential; all of its fields are revealed to the verifier.
It is possible for the airline to send Allison a frame which requests only certain fields of the credential. The airline would not be able to see other fields of the credential, but cryptographic guarantees would still hold over the revealed fields.
See the CreateProof reference for more information.
OpenID Connect for Presentation
Trinsic offers an OpenID Connect service as an alternative flow for the exchange of a credential between a holder and a verifier.
In this flow, a holder simply clicks a link (or scans a QR code), logs into their Trinsic cloud wallet, and selects a credential to share.
Verify Proof¶
Once the airline receives the proof, they can use the VerifyProof call to ensure its authenticity.
trinsic config --auth-token $(cat airline.txt)
trinsic vc verify-proof --proof-document proof.json
// The airline verifies the credential
trinsic.options.authToken = airline;
const verifyResponse = await trinsic.credential().verifyProof(
VerifyProofRequest.fromPartial({
proofDocumentJson: proofResponse.proofDocumentJson,
})
);
// Set active profile to `airline`
trinsic.SetAuthToken(airline!);
// Verify that Allison has provided a valid proof
var verifyResponse = await trinsic.Credential.VerifyProofAsync(new() {
ProofDocumentJson = proofJSON
});
bool credentialValid = verifyResponse?.IsValid ?? false;
# The airline verifies the credential
trinsic_service.service_options.auth_token = airline
verify_result = await trinsic_service.credential.verify_proof(
request=VerifyProofRequest(proof_document_json=credential_proof)
)
valid = verify_result.is_valid
trinsic.setAuthToken(airline);
// Verify that Allison has provided a valid proof
var verifyProofResponse =
trinsic
.credential()
.verifyProof(
VerifyProofRequest.newBuilder().setProofDocumentJson(credentialProof).build())
.get();
boolean isValid = verifyProofResponse.getIsValid();
// The airline verifies the credential
trinsic.SetAuthToken(airline)
verifyResult, _ := trinsic.Credential().VerifyProof(context.Background(), &credential.VerifyProofRequest{ProofDocumentJson: credentialProof})
valid := verifyResult.IsValid
# The airline verifies the credential
trinsic.auth_token = airline
verify_result = trinsic.credential_service.verify_proof(
Trinsic::Credentials::VerifyProofRequest.new(proof_document_json: credential_proof)
)
valid = verify_result.is_valid
Interoperability
The Verifiable Credentials and Proofs that Trinsic's platform produces are based on open standards.
Although we use the VerifyProof call in this example, the proof could be verified using any standards-compliant software.
Full Source Code¶
This sample is available as VaccineDemoShared.ts
in our SDK repository.
This sample is available as VaccineWalkthroughTests.cs
in our SDK repository.
This sample is available as vaccine_demo.py
in our SDK repository.
This sample is available as VaccineDemo.java
in our SDK repository.
This sample is available as vaccine_test.go
in our SDK repository.
This sample is available as vaccine_demo.rb
in our SDK repository.
Next Steps¶
Congratulations! If you've completed all the steps of this walkthrough, you've just created a mini ecosystem of issuers, verifiers, and holders all exchanging credentials. Depending on your goals, there are a couple of possible next steps to take.
- Try out a sample app
- Browse the Service Reference
- Learn more about the key concepts and technologies at play