Skip to content

Commit

Permalink
chore(improve args and return types)
Browse files Browse the repository at this point in the history
  • Loading branch information
teddyjfpender committed Aug 23, 2023
1 parent 37aa25b commit 84073c3
Show file tree
Hide file tree
Showing 14 changed files with 317 additions and 160 deletions.
12 changes: 8 additions & 4 deletions apps/docs/pages/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { Callout } from 'nextra-theme-docs'

# The Herald Stack

<Callout type="default" emoji="⚠️">
Note: These are outdated and need to be updated.
</Callout>

Herald is a thin wrapper around SnarkyJS. Herald currently has three layers:
- [**@herald-sdk/data-model**](#herald-sdkdata-model) - this package provides classes and utilities for managing claims, signed claims, and other data structures relevant to Zero-Knowledge Proofs and their associated cryptographic functions.
- [**@herald-sdk/provable-programs**](#herald-sdkprovable-programs) - this package provides a single `ZkProgram` that allows users to attest to their credentials per some `Rule` provided by a challenger.
Expand Down Expand Up @@ -84,7 +88,7 @@ The return type for the prove method in the `Credential` class. Contains:
- `verificationKey`: A string that represents the key used for verification.

##### Class: `Credential`
This class represents a digital credential containing a `MerkleMap` of claims (claim), a signed version of that map (`signedClaim`), and a dictionary of claims (`credential`).
This class represents a digital credential containing a `MerkleMap` of claims (claim), a signed version of that map (`signedClaim`), a flat dictionary of claims (`credential`), and a verifiable credential (`verifiableCredential`).

###### Constructor:
- Parameters:
Expand All @@ -94,14 +98,14 @@ This class represents a digital credential containing a `MerkleMap` of claims (c


###### Static Method: `create`
Description: An Issuer can construct a `Credential` object by taking in a dictionary of claims and a private key from the issuer.
Description: An Issuer can construct a `Credential` object by taking in a string credential object of claims and a private key from the issuer.
- Parameters:
- `claims`: A dictionary representing multiple claims.
- `claims`: A credential string representing multiple claims.
- `issuerPrvKey`: The private key of the issuer.


###### Method: `verify`
Description: Verifies the authenticity of the `Credential` object based on the given issuer and subject public keys.
Description: a test utility method that verifies the authenticity of the `Credential` object based on the given issuer and subject public keys.
- Parameters:
- `issuerPubKey`: The public key of the issuer.
- `subjectPubKey`: The public key of the subject (credential holder).
Expand Down
39 changes: 26 additions & 13 deletions apps/docs/pages/build.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@ import { ClaimType } from "@herald-sdk/data-model";
const issuerPrvKey = PrivateKey.random();

// construct a claim about the subject
const claims: {[key: string]: ClaimType} = {
const claimsObject: {[key: string]: ClaimType} = {
age: 21,
subject: "B62qkAqbeE4h1M5hop288jtVYxK1MsHVMMcBpaWo8qdsAztgXaHH1xq"
};
const claims = JSON.stringify(claimsObject);

// construct a credential using the claim and the issuer private key
const credential = Credential.create(claims, issuerPrvKey);
Expand Down Expand Up @@ -134,7 +135,8 @@ const credential = {
const flattenedCredential = flattenObject(credential);

// construct a claim about the subject
const claims = constructClaim(flattenedCredential)
const claimsObject = constructClaim(flattenedCredential)
const claims = JSON.stringify(claimsObject);

// construct a credential using the claim and the issuer private key
const credential = Credential.create(claims, issuerPrvKey);
Expand Down Expand Up @@ -205,23 +207,34 @@ The directory path to save the artifacts can be customized to the Issuers' desir

### Recreating a Credential

If a subject has been issued credentials by an issuer, they can use Herlad to reconstruct their Herald Credential.
If a subject has been issued verifiable credentials by an issuer, they can use Herlad to reconstruct their Herald Credential.

```ts
import { Credential } from @herald-sdk/credentials

// import the signed claim json object
const signedClaimJson = {
"claimRoot": "17432993283620653071265435513179338119184224288853623637901748948712012685273",
"signatureIssuer": {
"r": "21180312343368474591313734714969702118628675012610691774369339245240369832750",
"s": "22433867616914427848102713952217876822077837855966763038377768324812887427287"
const verifiableCredential = {
'@context': [
'https://www.w3.org/2018/credentials/v1',
'https://www.w3.org/2018/credentials/examples/v1'
],
id: 'http://example.edu/credentials/3732',
type: [ 'VerifiableCredential', 'UniversityDegreeCredential' ],
issuer: 'https://example.edu/issuers/565049',
issuanceDate: '2023-08-22T20:06:39.503Z',
credentialSubject: {
id: 'B62qqtuHton6vLuZYpKPcLGcieGsY74bwpgdVCgcduxJzkFSB5f3dLq',
degree: { type: 'BachelorDegree', name: 'Bachelor of Science and Arts' }
},
proof: {
claimRoot: '18686264541840123169250711918699166061016410941286208490859254528392281918734',
signatureIssuer: {
r: '3596861521036865055394265050093977169786039770907103121330640776332567591728',
s: '14071103483069538123785234764250401151906657100424343386132301701213333391103'
}
};

// reconstruct the SignedClaim Struct
const signedClaim = new SignedClaim(undefined, undefined, signedClaimJson);
}
};

// construct a credential using the claim and the issuer private key
const credential = Credential.recreate(claims, signedClaim);
const credential = Credential.recreate(JSON.stringify(verifiableCredential));
```
3 changes: 1 addition & 2 deletions apps/docs/pages/why.mdx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# Why Herald ?

# Why Does the World Need Herald?

While existing libraries also replicate the general SSI model, Herald can generate proofs that can be used as one of several arguments to other `ZkProgram`s or smart contracts; this is were the powers of recursive proofs come alive. This is a critical feature to reduce the amount of interaction necessary with smart contracts and off-chain `ZkProgram`s that are identity aware.
15 changes: 5 additions & 10 deletions packages/credentials/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { PrivateKey } from 'snarkyjs';
import { Credential } from './credential';
import { isClaimsObject } from './utils';
import fs from 'fs';
import path from 'path';
import { flattenObject } from '@herald-sdk/data-model';

yargs(hideBin(process.argv))
.command(
Expand All @@ -33,20 +31,16 @@ yargs(hideBin(process.argv))
}
},
(argv) => {
const parsedClaims = JSON.parse(argv.claims);
let claims;
if (!isClaimsObject(parsedClaims)) {
claims = flattenObject(parsedClaims)
} else {
claims = parsedClaims;
}
const parsedClaims = JSON.parse(argv.claims);
const claimsString = JSON.stringify(parsedClaims)
const issuerPrvKey = PrivateKey.fromBase58(argv.issuerPrvKey);
const credential = Credential.create(claims, issuerPrvKey);
const credential = Credential.create(claimsString, issuerPrvKey);

// save credential to file
//const claim = credential.claim; // merkle tree, doesn't need to be stored, can be reconstructed from flatCredentials
const signedClaim = credential.signedClaim.toJSON(); // this is a Struct
const flatCredentials = credential.credential; // flat dictionary of claims
const verifiableCredential = credential.verifiableCredential;

// Determine the save directory
let saveDir: string = (typeof argv.save === "string") ? argv.save : './artifacts';
Expand All @@ -60,6 +54,7 @@ yargs(hideBin(process.argv))
fs.writeFileSync(path.join(saveDir, 'signedClaim.json'), JSON.stringify(signedClaim, null, 2));
fs.writeFileSync(path.join(saveDir, 'flatCredentials.json'), JSON.stringify(flatCredentials, null, 2));
fs.writeFileSync(path.join(saveDir, 'credential.json'), JSON.stringify(parsedClaims, null, 2));
fs.writeFileSync(path.join(saveDir, 'verifiableCredential.json'), JSON.stringify(verifiableCredential, null, 2));

console.log(`Data saved in ${saveDir}`);
}
Expand Down
112 changes: 87 additions & 25 deletions packages/credentials/src/credential.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,82 @@
import { Poseidon, PrivateKey, Proof, PublicKey } from "snarkyjs";
import { Claim, ClaimType, CredentialPresentation, SignedClaim, constructClaim, constructSignedClaim } from "@herald-sdk/data-model";
import { PrivateKey, Proof, PublicKey } from "snarkyjs";
import { Claim, ClaimType, CredentialPresentation, SignedClaim, constructClaim, constructSignedClaim, flattenObject, stringToField } from "@herald-sdk/data-model";
import { PublicInputArgs, ZkProgramsDetails } from "@herald-sdk/provable-programs";
import { handleOperation, isClaimsObject } from "./utils";

export class Credential {
public claim: Claim; // A MerkleMap
public signedClaim: SignedClaim; // Signed MerkleMap Root by issuer
public credential: {[key: string]: ClaimType} // A dictionary of claims
public verifiableCredential: string // A verifiable credenital

constructor(claim: Claim, signedClaim: SignedClaim, credential: {[key: string]: ClaimType}) {
constructor(claim: Claim, signedClaim: SignedClaim, credential: {[key: string]: ClaimType}, verifiableCredential: string) {
this.claim = claim;
this.signedClaim = signedClaim;
this.credential = credential;
this.verifiableCredential = verifiableCredential;
}
// TODO: change `create` to `issue`
public static create(claims: {[key: string]: ClaimType}, issuerPrvKey: PrivateKey | string): Credential {

public static create(stringClaims: string, issuerPrvKey: PrivateKey | string): Credential {
if (typeof issuerPrvKey === "string") {
issuerPrvKey = PrivateKey.fromBase58(issuerPrvKey);
}
const parsedClaims = JSON.parse(stringClaims)
let claims;
if (!isClaimsObject(parsedClaims)) {
claims = flattenObject(parsedClaims)
} else {
claims = parsedClaims;
}
// constructs a Claim (MerkleMap) from a dictionary of claims
const claim = constructClaim(claims);
// construct a signed claim from a claim and a private key
const signedClaim = constructSignedClaim(claim, issuerPrvKey);
return new Credential(claim, signedClaim, claims);
// create verifiable credential - can be used to make proofs
// this could also be the proof from a zkProgram
const verifiableCredentialProofFields = {
proof: signedClaim.toJSON()
}
// Add verifiableCredentialProofFields to parsedClaims
const verifiableCredential = {
...parsedClaims,
...verifiableCredentialProofFields
};
return new Credential(claim, signedClaim, claims, verifiableCredential);
}

/**
*
* @param claims a JSON object of claims
* @param signedClaim the issuer's signature of the claim
* @param parsedVerifiableCredential a JSON object of a verifiable credential
* @returns a new Credential object
*
* @remarks the claims argument should be a flattened JSON object of claims (i.e. no nested objects). Use the `flattenObject` function from `@herald-sdk/data-model` to flatten a JSON object.
* @remarks the signedClaim argument should be a SignedClaim Struct, subjects can simply use `const signedClaim = new SignedClaim(undefined, undefined, signedClaimJson);`
*/
public static recreate(claims: {[key: string]: ClaimType}, signedClaim: SignedClaim): Credential {
// constructs a Claim (MerkleMap) from a dictionary of claims
const claim = constructClaim(claims);
return new Credential(claim, signedClaim, claims);
public static recreate(verifiableCredential: string): Credential {
const parsedVerifiableCredential = JSON.parse(verifiableCredential)
const {proof, ...claims} = parsedVerifiableCredential;
const claim = constructClaim(flattenObject(claims)) // do not include `proof` field
const signedClaim = new SignedClaim(undefined, undefined, proof)

return new Credential(claim, signedClaim, flattenObject(claims), parsedVerifiableCredential);
}

public verify(issuerPubKey: PublicKey, subjectPubKey: PublicKey): boolean {
public verify(issuerPubKey: PublicKey, subjectPubKey: PublicKey, subjectField: string): boolean {
// verify the Credential is about the expected subject and was signed by the expected issuer
const isValid = this.signedClaim.signatureIssuer.verify(
issuerPubKey,
[this.signedClaim.claimRoot],
).toBoolean();

if (!isValid) {
return false;
}
const subject = this.claim.getField("subject")?.equals(Poseidon.hash(subjectPubKey.toFields())).toBoolean();
const subject = this.claim.getField(subjectField)?.equals(stringToField(subjectPubKey.toBase58())).toBoolean();
if (!subject) {
return false;
}
return true;
}

// should take arguments that include those from a challenge object provided to the owner of the credentials
// A challenge can include asserting e.g. the claim "age" is greater than 18, the claim is signed by an expected issuer, etc.
// this should be a ZkProgram & must include the signature of the subject
// TODO: provide the verification key as an argument to the function, so that the prover knows which
// ZkProgram the challenger/verifier expects. This is necessary to constrain the prover to a specific ZkProgram.
public async prove(claimKey: string, challenge: PublicInputArgs, subjectPrvKey: PrivateKey, ZkProgram: string): Promise<Proof<PublicInputArgs, void>> {
public async prove(claimKey: string, challenge: PublicInputArgs, subjectPrvKey: PrivateKey, ZkProgram: string): Promise<{proof: Proof<PublicInputArgs, void>, verifiablePresentation: any}> {
const claimWitness = this.claim.getWitness(claimKey);
const claimValue = this.claim.getField(claimKey);

Expand All @@ -72,7 +88,7 @@ export class Credential {
if (!programDetails) {
throw new Error("ZkProgram not found");
}

// TODO: change name of credentialPresentation
const credentialPresentation = new CredentialPresentation(this.signedClaim, subjectPrvKey);

console.log("compiling...");
Expand All @@ -82,6 +98,52 @@ export class Credential {
console.log("proving...");
const proof = await programDetails.attest(challenge, claimWitness, claimValue, this.signedClaim, credentialPresentation);
console.log("proving complete");
return proof.attestationProof;
/*
we need to return a verifiable presentation
- Presentation metadata
- verifiable credentials
- proof(s)
criteria:
- Each derived verifiable credential within a verifiable presentation MUST contain all information
necessary to verify the verifiable credential, either by including it directly within the credential,
or by referencing the necessary information.
- A verifiable presentation MUST NOT leak information that would enable the verifier to correlate the
holder across multiple verifiable presentations.
- The verifiable presentation SHOULD contain a proof property to enable the verifier to check that all
derived verifiable credentials in the verifiable presentation were issued to the same holder without
leaking personally identifiable information that the holder did not intend to share.
*/

const verifiablePresentation = {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"type": "VerifiablePresentation",
"verifiableCredential": [
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://www.w3.org/2018/credentials/examples/v1"
],
"type": ["VerifiableCredential", "UniversityDegreeCredential"],
"credentialSchema": {
// TODO: replace
"id": "did:example:cdf:35LB7w9ueWbagPL94T9bMLtyXDj9pX5o",
"type": "did:example:schema:22KpkXgecryx9k7N6XN1QoN3gXwBkSU8SfyyYQG"
},
"issuer": "did:example:Wz4eUg7SetGfaUVCn8U9d62oDYrUJLuUtcy619",
"credentialSubject": {
// describes the public inputs
"field": claimKey,
"operation:" : handleOperation(challenge.provingRule.operation),
"value": challenge.provingRule.value
},
}],
"proof" : proof.attestationProof.toJSON()
}
return { proof: proof.attestationProof, verifiablePresentation: verifiablePresentation };
}
}
}
22 changes: 20 additions & 2 deletions packages/credentials/src/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ClaimType } from "@herald-sdk/data-model";
import { ClaimType, stringToField } from "@herald-sdk/data-model";
import { Field } from "snarkyjs";

export function isClaimsObject(obj: any): obj is {[key: string]: ClaimType} {
if (typeof obj !== 'object' || obj === null) {
Expand All @@ -21,4 +22,21 @@ function isClaimType(obj: any): obj is ClaimType {
return false;
}
return true;
}
}

export function handleOperation(operation: Field): string | undefined {
switch (true) {
case stringToField('lt').equals(operation).toBoolean():
return "lt";
case stringToField('lte').equals(operation).toBoolean():
return "lte";
case stringToField('eq').equals(operation).toBoolean():
return "eq";
case stringToField('gte').equals(operation).toBoolean():
return "gte";
case stringToField('gt').equals(operation).toBoolean():
return "gt";
default:
return undefined;
}
}
Loading

0 comments on commit 84073c3

Please sign in to comment.