Link credentials
In many cases, it is necessary to create relationships between different credentials. For example, you might link a diploma credential to a professional license, or a national ID to a work visa. The Truvity SDK allows you to link credentials together to form a hierarchy of related data.
Why link credentials?
Linking credentials provides several benefits:
- Data integrity: Maintain the integrity of related credentials, ensuring they are connected in a meaningful way.
- Hierarchical relationships: Create relationships between credentials, such as educational qualifications and work history, to form a complete identity profile.
- Simplified verification: Verifiers can easily check related credentials in one verification request.
Example: Link credential
To link one credential to another, use the LinkedCredential
custom claim type available as part of the user-defined types (UDTs):
- TypeScript
- Java
@VcContext({ name: "Passport", namespace: "https://www.w3.org/ns/credentials/issuer-dependent" })
class Passport {
@VcNotEmptyClaim
name!: string;
}
@VcContext({ name: "ClaimsModelExample", namespace: "https://www.w3.org/ns/credentials/issuer-dependent" })
class ClaimsModelExample {
@VcNotEmptyClaim
passport!: LinkedCredential<Passport>;
}
const key = await client.keys.keyGenerate({
data: {
type: 'P256',
},
});
const passport = client.createVcDecorator(Passport);
const passportVc = await passport.issue(key.id, {
claims: {
name: "John Smith"
}
});
const claimsModelExample = client.createVcDecorator(ClaimsModelExample);
const claimsModelExampleVc = await claimsModelExample.issue(key.id, {
claims: {
passport: passportVc.toLinkedCredential()
}
});
const claimsModelExampleClaims = await claimsModelExampleVc.getClaims();
const linkedPassport = await claimsModelExampleClaims.passport.dereferenceAs(PassportNL);
const linkedPassportClaims = await linkedPassport.getClaims();
linkedPassportClaims.name; // "John Smith"
@VcContext(name = "Passport", namespace = "https://www.w3.org/ns/credentials/issuer-dependent")
class Passport {
@NotEmpty
private String name;
Passport(String name) {
this.name = name;
}
}
@VcContext(name = "ClaimsModelExample", namespace = "https://www.w3.org/ns/credentials/issuer-dependent")
class ClaimsModelExample {
@NotEmpty
private LinkedCredential<Passport> passport;
ClaimsModelExample(LinkedCredential<Passport> passport) {
this.passport = passport;
}
}
ResourceKeyPublic key = client.keys()
.keyGenerate(KeyGenerateInput.builder()
.data(KeyGenerate.builder().type(KeyGenerateType.P_256).build())
.build());
VcDecorator<Passport> passport = client.vcDecorator(Passport.class);
VerifiableCredentialWithClaims<Passport> passportVc = passport.issue(
key.getId(), new Passport("John Smith")
);
VcDecorator<ClaimsModelExample> claimsModelExample = client.vcDecorator(ClaimsModelExample.class);
VerifiableCredentialWithClaims<Passport> claimsModelExampleVc = claimsModelExample.issue(
key.getId(),
new ClaimsModelExample(passportVc.toLinkedCredential())
);
LinkedCredential<Passport> passportLinkedCredential = claimsModelExampleVc.getClaims().passport;
VerifiableCredentialWithClaims<Passport> passportVc = passportLinkedCredential.dereference();
passportVc.getClaims().name; // "John Smith"
Example: Link multiple credential types
A single claim is able to support multiple different types of linked credentials (one at a time). This can be useful when more than one type of document can be used for the same purpose. For example, different certificates, like birth or marriage certificates, can often fulfill the same role or, as another example, passports of different nationalities.
- TypeScript
- Java
There are two approaches to linking multiple credential types on a single claim:
- Listing the different types, such as
LinkedCredential<PassportNL | PassportDE>
, like the example below. This approach would be used in cases where the set of different linked credential types is statically known. - Using the
LinkedCredential<unknown>
type. This approach would be used when you're handling an arbitrary set of types that is not statically known.
Both approaches provide the same programming interface. The first approach, listing the types, provides better static type checking.
@VcContext({ name: "PassportNL", namespace: "https://www.w3.org/ns/credentials/issuer-dependent" })
class PassportNL {
@VcNotEmptyClaim
nameNL!: string;
}
@VcContext({ name: "PassportDE", namespace: "https://www.w3.org/ns/credentials/issuer-dependent" })
class PassportDE {
@VcNotEmptyClaim
nameDE!: string;
}
@VcContext({ name: "ClaimsModelExample", namespace: "https://www.w3.org/ns/credentials/issuer-dependent" })
class ClaimsModelExample {
@VcNotEmptyClaim
passport!: LinkedCredential<PassportNL | PassportDE>;
}
const key = await client.keys.keyGenerate({
data: {
type: 'P256',
},
});
const passport = client.createVcDecorator(PassportNL);
const passportVc = await passport.issue(key.id, { claims: { nameNL: "John Smith" } });
const claimsModelExample = client.createVcDecorator(ClaimsModelExample);
const claimsModelExampleVc = await claimsModelExample.issue(key.id, {
claims: {
passport: passportVc.toLinkedCredential()
}
});
const claimsModelExampleClaims = await claimsModelExampleVc.getClaims();
const passportLinkedCredential = await claimsModelExampleClaims.passport.dereference();
if (await passportLinkedCredential.canMap(PassportNL)) {
const linkedPassport = await passportLinkedCredential.map(PassportNL);
const linkedPassportClaims = await passportVc.getClaims();
linkedPassportClaims.nameNL; // "John Smith"
} else {
const linkedPassport = await passportLinkedCredential.map(PassportDE);
const linkedPassportClaims = await passportVc.getClaims();
linkedPassportClaims.nameDE;
}
@VcContext(name = "PassportNL", namespace = "https://www.w3.org/ns/credentials/issuer-dependent")
class PassportNL {
@NotEmpty
private String nameNL;
PassportNL(String name) {
this.nameNL = name;
}
}
@VcContext(name = "PassportDE", namespace = "https://www.w3.org/ns/credentials/issuer-dependent")
class PassportDE {
@NotEmpty
private String nameDE;
PassportDE(String name) {
this.nameDE = name;
}
}
@VcContext(name = "ClaimsModelExample", namespace = "https://www.w3.org/ns/credentials/issuer-dependent")
class ClaimsModelExample {
@NotEmpty
private LinkedCredentialUnknown passport;
ClaimsModelExample(LinkedCredentialUnknown passport) {
this.passport = passport;
}
}
ResourceKeyPublic key = client.keys()
.keyGenerate(KeyGenerateInput.builder()
.data(KeyGenerate.builder().type(KeyGenerateType.P_256).build())
.build());
VcDecorator<PassportNL> passport = client.vcDecorator(PassportNL.class);
VerifiableCredentialWithClaims<PassportNL> passportVc = passport.issue(
key.getId(), new PassportNL("John Smith")
);
VcDecorator<ClaimsModelExample> claimsModelExample = client.vcDecorator(ClaimsModelExample.class);
VerifiableCredentialWithClaims<ClaimsModelExample> claimsModelExampleVc = claimsModelExample.issue(
key.getId(),
new ClaimsModelExample(passportVc.toLinkedCredentialUnknown())
);
LinkedCredentialUnknown passportLinkedCredential = claimsModelExampleVc.getClaims().passport;
UnknownVerifiableCredential passportUnknownVc = passportLinkedCredential.dereference();
if (passportUnknownVc.canMap(PassportNL.class)) {
VerifiableCredentialWithClaims<PassportNL> passportVc = passportUnknownVc.map(PassportNL.class);
passportVc.getClaims().nameNL; // "John Smith"
} else {
VerifiableCredentialWithClaims<PassportDE> passportVc = passportUnknownVc.map(PassportDE.class);
passportVc.getClaims().nameDE;
}