Skip to content

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


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 [email protected] 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


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


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.

Note that TEMPLATE_ID refers to the "Schema" URI of the template you created earlier called "VaccinationCertificate". More specifically, it's the property 'schema_uri' in the JSON returned by the trinsic template create... command.

// 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


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


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.