Claims
Overview
At the most fundamental level, a claim is a statement about a subject. A claim is a component of a Verifiable Credential, which is the web3 standard for digital credentials in a decentralized ecosystem.
- See the W3 documentation for Claims here
- See the W3 documentation for Verifiable Credentials here
- Read more about Verifiable Credentials and their role in IAM Client Library and Switchboard here
In the context of IAM Client Library, a claim is submitted by a requester to an issuer for a subject, in order to prove that the subject has the correct credentials to either:
- Take on a role within an application or organization. This is known as a Role Claim.
- Obtain a temporary credential used to authenticate to the cache server. This is known as an Authentication Claim.
The issuer is responsible for verifying and issuing the claim.
Role Claim
The most common credential type in Switchboard is the role claim. A role claim is a presentation of a credential in order to take on a role within an application or an organization.
The subject of the claim can be the requester, or it can be an asset that the requester is requesting a claim on behalf of.
Roles
In the Claim interface, the role is defined in the claimType:
export interface IClaimRequest extends IMessage {
token: string;
claimType: string; //CLAIM TYPE
claimTypeVersion: string;
registrationTypes: RegistrationTypes[];
subjectAgreement?: string;
}
The claimType is a string composed of the role name and the namespace to which the role belongs to.
Example: "email.roles.verification.apps.energyweb.iam.ewc"
Namespaced roles are persisted in the Role Repository in the IAM Cache Server.
Role Claim Data Persistence
Blockchain
Depending on if the requester wants to register the claim On-Chain and/or Off-Chain, The IAM Client library's Claim Service saves claim data either to IPFS as an encoded JWT token, or in the ClaimManager smart contract's registry. This is discussed below.
Repository
Claim data is also persisted by the IAM Cache Server in the Role Claim Repository. The IAM Client library's Claim Service methods post claim data to the Cache Server, where the data is persisted by the Cache Server's Claims Service methods. View the Cache Server's Claim Service on GitHub.
1. Requesting Claims
A claim request is created by the signer and submitted to the Role issuer(s) using the createClaimRequest method.
The createClaimRequest method creates a claim request message (of type IClaimRequest), and sends the message to the IAM cache server:
const message: IClaimRequest = {
id: v4(),
token,
claimIssuer: issuer,
requester: this._signerService.did,
registrationTypes,
claimType: role,
claimTypeVersion: version.toString(),
};
if (registrationTypes.includes(RegistrationTypes.OnChain)) {
if (!version) {
throw new Error(ERROR_MESSAGES.ONCHAIN_ROLE_VERSION_NOT_SPECIFIED);
}
message.subjectAgreement = await this.approveRolePublishing({ subject, role, version });
}
//submit claim request to cacher server:
await this._cacheClient.requestClaim(subject, message);
The IAM cache server then:
- Verifies that enrolment preconditions are met
- Resolves the issuers of the claim from role definition
- Notifies issuer(s) of the claim request via NATS
See the Cache Server request handler here.
2. Issuing Claims
If the subject's enrolment request is valid, the Issuer can approve and issue the claim to the subject. If the claim has been requested by the signer, this is done by the issueClaimRequest method. If a claim is being directly issued without having been requested, this is done by the issueClaim method.
Registering Claims on the Blockchain
A claim request has an array of RegistrationTypes. A claim can be registered:
- On-Chain only
- Off-Chain only
- On-Chain and Off-Chain
In both On-Chain and Off-Chain registration, the claim is technically saved to the blockchain. However, Off-Chain registration is saved to IPFS and is linked to the user's DID Document, but this data is not able to be accessed by other smart contracts.
Off-Chain Registration
If a claim request requires Off-Chain registration, the publishPublicClaim method saves the claim in IPFS as an encoded JWT token. The user's DID document is updated with a link to this IPFS record in the DID Document's service array. To read more about storing Verifiable Credentials on IPFS and referencing them in a user's DID Document on the Energy Web Chain, see our documentation here.
async publishPublicClaim({ token }: { token: string }) {
const payload = (await this._didRegistry.decodeJWTToken({ token })) as {
iss: string;
sub: string;
claimData: ClaimData;
};
const { iss, claimData } = payload;
let sub = payload.sub;
// Initialy subject was ignored because it was requester
if (!sub || sub.length === 0 || !isValidDID(sub)) {
sub = this._signerService.did;
}
if (!(await this._didRegistry.verifyPublicClaim(token, iss))) {
throw new Error("Incorrect signature");
}
const url = await this._didRegistry.ipfsStore.save(token);
const data = {
type: DIDAttribute.ServicePoint,
value: {
id: await this.getClaimId({ claimData }),
serviceEndpoint: url,
hash: hashes.SHA256(token),
hashAlg: "SHA256",
},
};
await this._didRegistry.updateDocument({ didAttribute: DIDAttribute.ServicePoint, data, did: sub });
return url;
}
Note: While this data is public on the blockchain, it is not accessible to any external smart contracts.
On-Chain Registration
If a claim request requires On-Chain registration, the claim is persisted in the ClaimManager smart contract's registory. You can view the ClaimManager smart contract on GitHub here.
if (registrationTypes.includes(RegistrationTypes.OnChain)) {
const { claimType: role, claimTypeVersion: version } = claimData;
const expiry = defaultClaimExpiry;
const onChainProof = await this.createOnChainProof(role, version, expiry, sub);
message.onChainProof = onChainProof;
if (publishOnChain) {
await this.registerOnchain({
token,
subjectAgreement,
onChainProof,
acceptedBy: this._signerService.did,
});
}
}
The registerOnChain method registers the role with the ClaimManager smart contract using the smart contract's 'register' method:
async registerOnchain(claim: Pick<Claim, "token" | "subjectAgreement" | "onChainProof" | "acceptedBy">) {
if (!readyToBeRegisteredOnchain(claim)) {
throw new Error(ERROR_MESSAGES.CLAIM_WAS_NOT_ISSUED);
}
const { token, subjectAgreement, onChainProof, acceptedBy } = claim;
const { claimData, sub } = this._didRegistry.jwt.decode(token) as {
claimData: { claimType: string; claimTypeVersion: number };
sub: string;
};
const expiry = defaultClaimExpiry;
const { claimType: role, claimTypeVersion: version } = claimData;
const data = this._claimManagerInterface.encodeFunctionData("register", [
addressOf(sub),
namehash(role),
version,
expiry,
addressOf(acceptedBy),
subjectAgreement,
onChainProof,
]);
await this._signerService.send({
to: this._claimManager,
data,
});
}
In the ClaimManager contract's register method, the claim data is added to the 'roles' mapping, and can then be accessed and read by other smart contracts on the blockchain.
Note: An issuer can directly issue a claim directly without a request. This is done through the issueClaim method. This method does not handle On-Chain registration.
3. Alternatives to Claim Issuance
Reject Claim
The rejectClaimRequest method is used for an Issuer to reject a claim request:
async rejectClaimRequest({
id,
requesterDID,
rejectionReason,
}: {
id: string;
requesterDID: string;
rejectionReason?: string;
}) {
const message: IClaimRejection = {
id,
requester: requesterDID,
claimIssuer: [this._signerService.did],
isRejected: true,
rejectionReason,
};
return this._cacheClient.rejectClaim(this._signerService.did, message);
}
The rejection message (of type IClaimRejection) is sent to the Cache Server. The Cache Server handles the claim rejection and notifies the requester that the claim has been rejected via NATS.
Delete Claim
The deleteClaimRequest method is used to delete a claim request:
async deleteClaim({ id }: { id: string }) {
await this._cacheClient.deleteClaim(id);
}
The claim is deleted from the role claim repository in the Cache Server.
Claim Interface
Issued role claims are of type Claim
Example role claim:
{
"id": "a099...",
"requester": "did:ethr:volta:0xc56e...",
"subject": "did:ethr:volta:0xc56e...",
"claimType": "email.roles.verification.apps.energyweb.iam.ewc",
"registrationTypes": [
"RegistrationTypes::OnChain"
],
"claimTypeVersion": "1",
"token": "eyJhb...",
"subjectAgreement": "0xadb...",
"onChainProof": null,
"issuedToken": null,
"isAccepted": false,
"createdAt": "2021-12-08T07:52:32.456Z",
"acceptedBy": null,
"isRejected": true,
"rejectionReason": null,
"namespace": "verification.apps.energyweb.iam.ewc"
}
- claimType
The role that the claim is submitted in support of. The claimType is a string composed of the role name and the namespace to which the role belongs to.
- requester
The DID of the claim requester
- subject
The DID of the claim subject (though could be the requester or an asset or application of the requester)
- id
The UUID identifier for the claim
- registrationTypes
The claim's Registration Types, which can be On-Chain or Off-Chain, or both. These are explained in greater detail above.
- subjectAgreement
Signifies the agreement of claim subject to make the enrollment publically available on the blockchain through the ClaimManager smart contract. This exists only if the claim includes On-Chain registration.
if (registrationTypes.includes(RegistrationTypes.OnChain)) {
if (!version) {
throw new Error(ERROR_MESSAGES.ONCHAIN_ROLE_VERSION_NOT_SPECIFIED);
}
message.subjectAgreement = await this.approveRolePublishing({ subject, role, version });
}
- onChainProof
Provides on-chain proof of claim approval. This exists only if the claim includes On-Chain registration.
if (registrationTypes.includes(RegistrationTypes.OnChain)) {
const { claimType: role, claimTypeVersion: version } = claimData;
const expiry = defaultClaimExpiry;
const onChainProof = await this.createOnChainProof(role, version, expiry, sub);
...
}
- issuedToken
A signed representation of the role claim in the DID Registry. This exists only if the claim includes Off-Chain registration, or if a claim is issued without being requested.
if (registrationTypes.includes(RegistrationTypes.OffChain)) {
const publicClaim: IPublicClaim = {
did: sub,
signer: this._signerService.did,
claimData: { ...strippedClaimData, ...(issuerFields && { issuerFields }) },
};
message.issuedToken = await this._didRegistry.issuePublicClaim({
publicClaim,
});
}
- nameSpace
The namespace to which the role belongs to (an application or an organization).
- issuerFields
Additional data which issuer might need to add to enrolled claim. When publishOnChain
claim will be immediately registered after approvement, otherwise it will be stored on cache server and will be sent to chain later on with ClaimService.registerOnChain
.
Authentication Claim
An Authentication Claim is a temporary credential used to authenticate to the IAM Cache Server. If the user is not authenticated, they must authenticate using their pulic key:
private async _calculatePubKeyAndIdentityToken() {
const header = {
alg: "ES256",
typ: "JWT",
};
const encodedHeader = base64url(JSON.stringify(header));
const address = this._address;
const payload = {
iss: `did:${Methods.Erc1056}:${this.chainName()}:${address}`,
claimData: {
blockNumber: await this._signer.provider.getBlockNumber(),
},
};
const encodedPayload = base64url(JSON.stringify(payload));
const token = `0x${Buffer.from(`${encodedHeader}.${encodedPayload}`).toString("hex")}`;
// arrayification is necessary for WalletConnect signatures to work. eth_sign expects message in bytes: https://docs.walletconnect.org/json-rpc-api-methods/ethereum#eth_sign
// keccak256 hash is applied for Metamask to display a coherent hex value when signing
const message = arrayify(keccak256(token));
const sig = await this.signMessage(message);
const recoverValidatedPublicKey = (signedMessage: Uint8Array): string | undefined => {
const publicKey = recoverPublicKey(signedMessage, sig);
if (getAddress(address) === getAddress(computeAddress(publicKey))) {
return computePublicKey(publicKey, true).slice(2);
}
return undefined;
};
// Computation of the digest in order to recover the public key under the assumption
// that signature was performed as per the eth_sign spec (https://eth.wiki/json-rpc/API#eth_sign)
// In the event that the wallet isn't prefixing & hashing message as per spec, attempt recovery without digest
const digest = arrayify(hashMessage(message));
const publicKey = recoverValidatedPublicKey(digest) ?? recoverValidatedPublicKey(message);
if (publicKey) {
this._publicKey = publicKey;
this._identityToken = `${encodedHeader}.${encodedPayload}.${base64url(sig)}`;
} else {
throw new Error(ERROR_MESSAGES.PUBLIC_KEY_NOT_RECOVERED);
}
}
Example of authentication token:
{
iss: 'did:ethr:volta:0xc56e...',
claimData: { blockNumber: 15133056 }
}
Claims Service Public APIs
- getClaimsId
- getClaimsByIssuer
- getClaimsByRequester
- getClaimsBySubject
- getUserClaims
- issueClaimRequest
- issueClaim
- rejectClaimRequest
- issuePublicClaim
- publishPublicClaim
- registerOnChain
For detailed description of the enrolment flow process, see the Claims Service end-to-end tests.