Skip to content

Walkthrough

This walkthrough will show how a vaccination card can be issued, held, and proven using verifiable credentials with the Trinsic CLI. Feel free to follow along in a terminal using the CLI. We'll cover each part of Trinsic's platform during the tutorial.

Meet Allison

In this walkthrough we'll explore a scenario where Allison gets her vaccination certificate. She then uses this certificate to board an airline that requires proof of vaccination.

In most credential exchange scenarios there are three primary roles - Issuer, Holder, and Verifier.

Holder: Stores issued credentials from an issuer. Most often this is the credential subject. Also generates passes to share with verifiers.

Issuer: Responsible for issuing signed credentials that attest information about a credential subject

Verifier: Verifies passes presented from holders.

In this case, Allison will be the holder, a vaccination clinic will be an issuer, and an airline will be the verifier.

walkthrough
|- allison - Holder
|- clinic - Issuer
|- airline - Verifier

Our SDKs

This tutorial is meant to share the concepts of verifiable credentials. So feel free to sit back and read through this without running any code. However, you can also follow along using one of our SDKs.

We've set up some environments to use on Repl.it. If your language of choice isn't supported on repl.it, you can also run locally by installing the SDK of your choice.

Once the CLI is installed, clone our CLI example repository on Github to download the credential data for this walkthrough.

git clone https://github.com/trinsic-id/cli-example && cd cli-example

If you don't want to install locally, we also have a replit environment for you to use. In a new tab, you can open our to use the CLI. This demo environment works best when run side-by-side the following walkthrough using two tabs in your browser.

Installation instructions for the Node/Browser SDk.

Let's create a new .NET console app that we will use to add our sample code

dotnet new console -n TrinsicSample && cd TrinsicSample

Now we'll follow the installation instructions for Dotnet.

In this project, we'll be following along the java/src/test/java/trinsic/VaccineDemo.java in our SDK repository


Creating Accounts

We'll start by creating a Trinsic Wallet for each participant in this credential exchange. Wallets can be created by anyone, for anyone. In this scenario, we'll have three wallets. Allison will be the credential holder, the Airline will be the verifier, and the vaccination clinic will be the issuer.

When a new Trinsic account is created, a cloud wallet is created on our platform and an authentication key is generated by our SDK locally. Each person is the sole owner of their cloud wallet. They're meant to be easy to create so that you can create a cloud wallet for an end-user in your UX with very little friction.

The CLI offers an interactive way of creating wallets. For demo purposes, we'll create all three on the same machine. However, this demo could be done with all three wallets created on three separate machines.

When creating a wallet in the CLI, the wallet will store a private authentication token for the wallet in ~/.trinsic. If using the CLI with a real-world wallet, store this authentication token somewhere securely.

trinsic account login --description "Allison's Wallet" --alias allison && \
trinsic account login --description "Airline's wallet" --alias airline && \
trinsic account login --description "Vaccination Clinic" --alias clinic

// Create 3 different profiles for each participant in the scenario
const allison = await accountService.signIn();
const clinic = await accountService.signIn();
const airline = await accountService.signIn();
var allison = await accountService.SignInAsync(new() {EcosystemId = ecosystemId});
var clinic = await accountService.SignInAsync(new() {EcosystemId = ecosystemId});
var airline = await accountService.SignInAsync(new() {EcosystemId = ecosystemId});
# Create 3 different profiles for each participant in the scenario
allison = await account_service.sign_in(
    request=SignInRequest(ecosystem_id=ecosystem_id)
)
clinic = await account_service.sign_in(
    request=SignInRequest(ecosystem_id=ecosystem_id)
)
airline = await account_service.sign_in(
    request=SignInRequest(ecosystem_id=ecosystem_id)
)
// Create 3 different profiles for each participant in the scenario
var allison = accountService.signIn().get();
var clinic = accountService.signIn().get();
var airline = accountService.signIn().get();
allison, _, err := accountService.SignIn(context.Background(), &sdk.SignInRequest{})
failError(t, "error creating profile", err)
if !assert2.NotNil(allison) {
    return
}

clinic, _, err := accountService.SignIn(context.Background(), &sdk.SignInRequest{})
failError(t, "error creating profile", err)
if !assert2.NotNil(clinic) {
    return
}

airline, _, err := accountService.SignIn(context.Background(), &sdk.SignInRequest{})
failError(t, "error creating profile", err)
if !assert2.NotNil(airline) {
    return
}

If you would like to save the profile for future use, you can simply export the serialized profile to a local storage. Please note that the profiles contain sensitive key data, so they should be stored in a secure enclave.

// Serialize auth token by exporting it to file
File.WriteAllText("allison.txt", allison);
// Create auth token from existing data
allison = File.ReadAllText("allison.txt");
# Store profile for later use
with open("allison.txt", "wb") as fid:
    fid.write(allison.encode("utf-8"))

# Create profile from existing data
with open("allison.txt", "rb") as fid:
    allison = fid.readline()
var writeFile = new BufferedWriter(new FileWriter("allison.txt"));
writeFile.write(allison);
writeFile.flush();
writeFile.close();

// Create profile from existing data
var readFile = new BufferedReader(new FileReader("allison.txt"));
allison = readFile.readLine().strip();
readFile.close();
// Store profile for later use
// File.WriteAllBytes("allison.bin", allison.ToByteString().ToByteArray());

// Create profile from existing data
// var allison = WalletProfile.Parser.ParseFrom(File.ReadAllBytes("allison.bin"));

Note

References:


Define a Template

const credentialTemplateName = `My First Credential Template-${uuid()}`;
const nameField = TemplateField.fromPartial({
  description: "The name of the person",
  type: FieldType.STRING,
  optional: false,
});

const numberOfBags = TemplateField.fromPartial({
  type: FieldType.NUMBER,
  description: "The number of bags the person is taking on the trip",
  optional: false,
});

const dateOfBirth = TemplateField.fromPartial({
  type: FieldType.DATETIME,
  description: "The date of birth of the person",
  optional: false,
});

const isVaccinated = TemplateField.fromPartial({
  type: FieldType.BOOL,
  description: "Whether or not the person has been vaccinated",
  optional: false,
});
const templateService = new TemplateService(options);

let request = CreateCredentialTemplateRequest.fromPartial({
  name: credentialTemplateName,
  fields: {
    name: nameField,
    numberOfBags: numberOfBags,
    dateOfBirth: dateOfBirth,
    vaccinated: isVaccinated,
  },
});

let response = await templateService.createCredentialTemplate(request);
CreateCredentialTemplateRequest templateRequest = new() {
    Name = "An Example Credential",
    AllowAdditionalFields = false
};
templateRequest.Fields.Add("firstName", new() {Description = "Given name"});
templateRequest.Fields.Add("lastName", new());
templateRequest.Fields.Add("age", new() {Optional = true}); // TODO - use FieldType.NUMBER once schema validation is fixed.

var template = await templateService.CreateAsync(templateRequest);
template = await template_service.create(
    request=CreateCredentialTemplateRequest(
        name=f"An Example Credential: {uuid.uuid4()}",
        allow_additional_fields=False,
        fields={
            "firstName": TemplateField(description="Given name"),
            "lastName": TemplateField(),
            "age": TemplateField(optional=True, type=FieldType.NUMBER),
        },
    )
)
var fields = new HashMap<String, Templates.TemplateField>();
fields.put("firstName", Templates.TemplateField.newBuilder().setDescription("Given name").build());
fields.put("lastName", Templates.TemplateField.newBuilder().build());
fields.put("age", Templates.TemplateField.newBuilder().setType(Templates.FieldType.NUMBER).setOptional(true).build());
var templateRequest = Templates.CreateCredentialTemplateRequest.newBuilder().setName("My Example Credential-" + UUID.randomUUID()).setAllowAdditionalFields(false).putAllFields(fields).build();
var template = templateService.create(templateRequest).get();
templateRequest := &sdk.CreateCredentialTemplateRequest{Name: fmt.Sprintf("Example Template - %s", uuid.New()), AllowAdditionalFields: false, Fields: make(map[string]*sdk.TemplateField)}
templateRequest.Fields["firstName"] = &sdk.TemplateField{Description: "Given name"}
templateRequest.Fields["lastName"] = &sdk.TemplateField{}
templateRequest.Fields["age"] = &sdk.TemplateField{Type: sdk.FieldType_NUMBER, Optional: true}

template, err := templateService.Create(context.Background(), templateRequest)

Create an Ecosystem

Issue a Credential

Upon receiving her vaccine, Allison also receives a digital certificate from the clinic. This certificate is digitally signed by the clinic, acting as an issuer. The certificate is in a JSON form, and for this example, we will use the following JSON. Add this file to your project named vaccination-certificate-unsigned.jsonld.

{
    "@context": [
        "https://www.w3.org/2018/credentials/v1",
        "https://w3id.org/vaccination/v1",
        "https://w3id.org/security/bbs/v1"
    ],
    "id": "urn:uvci:af5vshde843jf831j128fj",
    "type": [
        "VaccinationCertificate",
        "VerifiableCredential"
    ],
    "description": "COVID-19 Vaccination Certificate",
    "name": "COVID-19 Vaccination Certificate",
    "expirationDate": "2029-12-03T12:19:52Z",
    "issuanceDate": "2019-12-03T12:19:52Z",
    "issuer": "did:key:zUC724vuGvHpnCGFG1qqpXb81SiBLu3KLSqVzenwEZNPoY35i2Bscb8DLaVwHvRFs6F2NkNNXRcPWvqnPDUd9ukdjLkjZd3u9zzL4wDZDUpkPAatLDGLEYVo8kkAzuAKJQMr7N2",
    "credentialSubject": {
        "id": "urn:uuid:c53e70f8-ce9a-4576-8744-e5f85c20a743",
        "type": "VaccinationEvent",
        "batchNumber": "1183738569",
        "countryOfVaccination": "US"
    }
}

Behind the scenes, each credential is a JSON document that is signed with a special digital signature to make each piece of data in the credential separately verifiable.

Signatures are a way to make sure that credentials are not forged or tampered with between getting issued and verified. They also are how a verifier can know that the credential was issued by who the credential says it was issued by.

To issue this credential we'll specify links to the json files, set the active profile to the clinic, and call the issuance endpoint:

trinsic  --profile clinic issuer issue --document data/vaccination-certificate-unsigned.json --out vaccination-certificate-signed.json
// Sign a credential as the clinic and send it to Allison
const credentialJson = getVaccineCertUnsignedJSON();
const credential = await credentialService.issueCredential(
  IssueRequest.fromPartial({ documentJson: credentialJson })
);
// Set active profile to 'clinic' so we can issue credential signed
// with the clinic's signing keys
walletService.Options.AuthToken = credentialsService.Options.AuthToken = clinic;

// Read the JSON credential data
var credentialJson = await File.ReadAllTextAsync(VaccinationCertificateUnsigned);
// Sign the credential using BBS+ signature scheme
// issueCredentialSample() {
var credential = await credentialsService.IssueCredentialAsync(new() {DocumentJson = credentialJson});
_testOutputHelper.WriteLine($"Credential:\n{credential.SignedDocumentJson}");
// }

We specify links to the jsonld files:

def _base_data_path() -> str:
    return abspath(join(dirname(__file__), "..", "..", "devops", "testdata"))


def _vaccine_cert_unsigned_path() -> str:
    return abspath(join(_base_data_path(), "vaccination-certificate-unsigned.jsonld"))


def _vaccine_cert_frame_path() -> str:
    return abspath(join(_base_data_path(), "vaccination-certificate-frame.jsonld"))

Let's set the active profile to the clinic, and call the issuance endpoint

issue_response = await credentials_service.issue_credential(
    request=IssueRequest(document_json=credential_json)
)

We specify links to the jsonld files:

public static String baseTestPath() {
    return Path.of(new File("").getAbsolutePath(), "..", "devops", "testdata").toAbsolutePath().toString();
}

public static Path vaccineCertUnsignedPath() {
    return Path.of(baseTestPath(), "vaccination-certificate-unsigned.jsonld");
}

public static Path vaccineCertFramePath() {
    return Path.of(baseTestPath(), "vaccination-certificate-frame.jsonld");
}

Let's set the active profile to the clinic, and call the issuance endpoint

// Sign a credential as the clinic and send it to Allison
var credentialJson = Files.readString(vaccineCertUnsignedPath());
// issueCredentialSample() {
var issueResult = credentialsService.issueCredential(VerifiableCredentials.IssueRequest.newBuilder().setDocumentJson(credentialJson).build()).get();
// }
var credential = issueResult.getSignedDocumentJson();
System.out.println("Credential: " + credential);
func GetBasePath() string {
    _, fileName, _, _ := runtime.Caller(1)
    path := filepath.Clean(filepath.Join(filepath.Dir(fileName), "..", "..", "devops", "testdata"))
    return path
}
func GetVaccineCertUnsignedPath() string {
    return filepath.Join(GetBasePath(), "vaccination-certificate-unsigned.jsonld")
}
func GetVaccineCertFramePath() string {
    return filepath.Join(GetBasePath(), "vaccination-certificate-frame.jsonld")
}
fileContent, err := ioutil.ReadFile(GetVaccineCertUnsignedPath())
failError(t, "error reading file", err)

credentialService.SetToken(clinic)
// issueCredentialSample() {
credential, err := credentialService.IssueCredential(context.Background(), &sdk.IssueRequest{DocumentJson: string(fileContent)})
// }
failError(t, "error issuing credential", err)
fmt.Printf("Credential:%s\n", credential)

Info

Reference:


Send Credential to Allison

At this point, the clinic can send the signed credential to Allison using any available methods. These methods can include any message exchange protocol, or a custom transport. In this case, we'll assume that the credential was delivered to Allison in an offline environment.

Sending credentials securely is an important part of maintaining the privacy of a credential holder. At this point we do not have a standard way of sending credentials to a wallet. There are a couple of options available.

First, if you've onboarded two wallets on the Trinsic platform there is a way to send credentials to a wallet via the wallet's email address. This method is a temporary fix. We recommend using a secure channel of communication within your application for sending and receiving credentials. Sending via https or another encrypted messaging protocol is critical to ensure the personal information inside the credential is not accidentally leaked. As Trinsic's platform develops, we will embed an encrypted messaging protocol called DIDComm (decentralized identifier communication) to exchange credentials between any wallet.

Info

Dive Deeper:


Store Credential in Wallet

Once Allison receives the credential, she or her wallet application can store it within her wallet. She can use any device that she's authorized to use with her wallet. Storing credentials securely is also important to maintaining Allison's privacy.

trinsic --profile allison wallet insert-item --item vaccination-certificate-signed.json
// Alice stores the credential in her cloud wallet.
walletService.options.authToken = allison;
const itemId = await walletService.insertItem(
  InsertItemRequest.fromPartial({ itemJson: credential.signedDocumentJson })
);
// Set active profile to 'allison' so we can manage her cloud wallet
walletService.Options.AuthToken = credentialsService.Options.AuthToken = allison;

var insertItemResponse = await walletService.InsertItemAsync(new() {ItemJson = credential.SignedDocumentJson});
var itemId = insertItemResponse.ItemId;
# Alice stores the credential in her cloud wallet.
wallet_service.service_options.auth_token = allison
# insertItemWallet() {
insert_response = await wallet_service.insert_item(
    request=InsertItemRequest(item_json=credential)
)
# }
item_id = insert_response.item_id
// Alice stores the credential in her cloud wallet.
// insertItemWallet() {
var insertItemResponse = walletService.insertItem(UniversalWalletOuterClass.InsertItemRequest.newBuilder().setItemJson(credential).build()).get();
// }
final var itemId = insertItemResponse.getItemId();
System.out.println("item id = " + itemId);
walletService.SetToken(allison)
failError(t, "error setting profile", err)
// insertItemWallet() {
itemID, err := walletService.InsertItem(context.Background(), &sdk.InsertItemRequest{ItemJson: credential.SignedDocumentJson})
// }
failError(t, "error inserting item", err)
fmt.Println("item id", itemID)

Note down the response item_id printed to the console for the next step.

Info

Reference: Insert Record


Create a Proof of Vaccination

Before boarding an airplane, Allison must show a proof of vaccination. The request for this proof also comes in a form of JSON, in this case a JSON-LD frame.

This request can be communicated using any exchange protocol. Again, we'll assume this was done offline.

Let's save this request in a file named vaccination-certificate-frame.jsonld

{
    "@context": [
        "https://www.w3.org/2018/credentials/v1",
        "https://w3id.org/vaccination/v1",
        "https://w3id.org/security/bbs/v1"
    ],
    "type": [
        "VerifiableCredential",
        "VaccinationCertificate"
    ],
    "@explicit": true,
    "issuer": {},
    "credentialSubject": {
        "@explicit": true,
        "@type": "VaccinationEvent",
        "batchNumber": {},
        "countryOfVaccination": {}
    }
}

This request asks Allison to provide proof of valid vaccination certificate, including the issuer, batchNumberand countryOfVaccination fields.

Allison can use the Create Proof functions to build a proof that will share only the requested fields.

Now let's create a proof for Allison. She may choose to generate this proof before going to the airport, or might generate it right as she boards.

Replace the <item_id> in the generate proof command below with the output from the insert_item above.

trinsic --profile allison issuer create-proof --document-id "<item-id>" --out vaccination-certificate-partial-proof.json --reveal-document data/vaccination-certificate-frame.json
// Allison shares the credential with the venue.
// The venue has communicated with Allison the details of the credential
// that they require expressed as a JSON-LD frame.
credentialService.options.authToken = allison;
const proofRequestJson = getVaccineCertFrameJSON();
const proof = await credentialService.createProof(
  CreateProofRequest.fromPartial({
    itemId: itemId.itemId,
    revealDocumentJson: proofRequestJson,
  })
);
// We'll read the request frame from a file and communicate this with Allison
walletService.Options.AuthToken = credentialsService.Options.AuthToken = allison;

var proofRequestJson = await File.ReadAllTextAsync(VaccinationCertificateFrame);

// Build a proof for the given request and the `itemId` we previously received
// which points to the stored credential
var credentialProof = await credentialsService.CreateProofAsync(new() {
    ItemId = itemId,
    RevealDocumentJson = proofRequestJson
});
_testOutputHelper.WriteLine("Proof:");
_testOutputHelper.WriteLine(credentialProof.ProofDocumentJson);
# Allison shares the credential with the venue.
# The venue has communicated with Allison the details of the credential
# that they require expressed as a JSON-LD frame.
credentials_service.service_options.auth_token = allison
wallet_service.service_options.auth_token = allison
with open(_vaccine_cert_frame_path(), "r") as fid2:
    proof_request_json = "\n".join(fid2.readlines())

# createProof() {
proof_response = await credentials_service.create_proof(
    request=CreateProofRequest(
        reveal_document_json=proof_request_json, item_id=item_id
    )
)
# }
credential_proof = proof_response.proof_document_json
print(f"Proof: {credential_proof}")
// Allison shares the credential with the venue.
// The venue has communicated with Allison the details of the credential
// that they require expressed as a JSON-LD frame.
credentialsService.setProfile(allison);
var proofRequestJson = Files.readString(vaccineCertFramePath());
// createProof() {
var createProofResponse = credentialsService.createProof(VerifiableCredentials.CreateProofRequest.newBuilder().setItemId(itemId).setRevealDocumentJson(proofRequestJson).build()).get();
// }
var credentialProof = createProofResponse.getProofDocumentJson();
System.out.println("Proof: " + credentialProof);
walletService.SetToken(allison)
failError(t, "error reading file", err)

fileContent2, err := ioutil.ReadFile(GetVaccineCertFramePath())
failError(t, "error reading file", err)

req := &sdk.CreateProofRequest{
    RevealDocumentJson: string(fileContent2),
    Proof:              &sdk.CreateProofRequest_ItemId{ItemId: itemID},
}

credentialService.SetToken(allison)
// createProof() {
credentialProof, err := credentialService.CreateProof(context.Background(), req)
// }
failError(t, "error creating proof", err)
fmt.Println("Credential proof", credentialProof)

Take a look at the proof. Notice how only the attributes included in the frame are included with the proof.

Allison sends this proof to the airline for them to verify.

Info

Reference: Create Proof


Verify Proof

Once the airline receives the proof, they can now verify it to ensure its authenticity. Because Allison sent a proof of her vaccination credential and not the credential itself, the airline only receives its required information.

trinsic --profile airline issuer verify-proof --proof-document vaccination-certificate-partial-proof.json
// The airline verifies the credential
credentialService.options.authToken = airline;
const verifyResponse = await credentialService.verifyProof(
  VerifyProofRequest.fromPartial({
    proofDocumentJson: proof.proofDocumentJson,
  })
);
// The airline verifies the credential
walletService.Options.AuthToken = credentialsService.Options.AuthToken = airline;

// Check for valid signature
var valid = await credentialsService.VerifyProofAsync(new() {
    ProofDocumentJson = credentialProof.ProofDocumentJson
});
_testOutputHelper.WriteLine($"Verification result: {valid.IsValid}");
Assert.True(valid.IsValid);
# The airline verifies the credential
credentials_service.service_options.auth_token = airline
wallet_service.service_options.auth_token = airline
# verifyProof() {
verify_result = await credentials_service.verify_proof(
    request=VerifyProofRequest(proof_document_json=credential_proof)
)
# }
valid = verify_result.is_valid
print(f"Verification result: {valid}")
assert valid is True
// The airline verifies the credential
credentialsService.setProfile(airline);
// verifyProof() {
var verifyProofResponse = credentialsService.verifyProof(VerifiableCredentials.VerifyProofRequest.newBuilder().setProofDocumentJson(credentialProof).build()).get();
// }
var isValid = verifyProofResponse.getIsValid();
System.out.println("Verification result: " + isValid);
assert isValid;
walletService.SetToken(airline)
failError(t, "error setting profile", err)
// verifyProof() {
valid, err := credentialService.VerifyProof(context.Background(), &sdk.VerifyProofRequest{ProofDocumentJson: credential.SignedDocumentJson})
// }
failError(t, "error verifying proof", err)
fmt.Println("Validation result", valid)
if valid != true {
    t.Fail()
}

Watch for the result of true to know that the credential successfully passed all of the verification processes.

Info

Reference: Verify Proof



Full Source Code

web

This sample is available in our dotnet directory.

This sample is available as ecosystem_demo.py

This sample is available in the Java directory.

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
  • Learn more about wallets, credentials, templates, and ecosystems
  • Review the SDK Reference

Sample Applications

We have language specific sample applications that you can run to understand how the Trinsic SDK works in a development environment.

This sample is available in our node directory

This sample is available in our dotnet directory.

This sample is available in the python directory.

This sample is available in our Github repo in the java directory.