Skip to content

Commit

Permalink
Merge pull request #52 from zksecurity/feature/verification-2
Browse files Browse the repository at this point in the history
Verification of presentations
  • Loading branch information
mitschabaude authored Oct 28, 2024
2 parents f192fb5 + 2d4c573 commit ef98449
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 120 deletions.
52 changes: 34 additions & 18 deletions examples/unique-hash.eg.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Bytes } from 'o1js';
import { Bytes, Field } from 'o1js';
import {
Spec,
Operation,
Expand All @@ -9,8 +9,13 @@ import {
assert,
type InferSchema,
} from '../src/index.ts';
import { issuerKey, owner, ownerKey } from '../tests/test-utils.ts';
import { validateCredential } from '../src/credential-index.ts';
import {
issuer,
issuerKey,
owner,
ownerKey,
randomPublicKey,
} from '../tests/test-utils.ts';
import { array } from '../src/o1js-missing.ts';

// example schema of the credential, which has enough entropy to be hashed into a unique id
Expand All @@ -36,7 +41,7 @@ console.log('✅ ISSUER: issued credential:', credentialJson);

let storedCredential = Credential.fromJSON(credentialJson);

await validateCredential(storedCredential);
await Credential.validate(storedCredential);

console.log('✅ WALLET: imported and validated credential');

Expand All @@ -46,29 +51,36 @@ console.log('✅ WALLET: imported and validated credential');
const spec = Spec(
{
signedData: Credential.Simple(Schema), // schema needed here!
targetNationalities: Claim(array(Bytes32, 3)), // TODO would make more sense as dynamic array
targetNations: Claim(array(Bytes32, 3)), // TODO would make more sense as dynamic array
targetIssuers: Claim(array(Field, 3)),
appId: Claim(Bytes32),
},
({ signedData, targetNationalities, appId }) => ({
// we assert that the owner has the target nationality
// TODO: add a one-of-many operation to make this more interesting
assert: Operation.equalsOneOf(
Operation.property(signedData, 'nationality'),
targetNationalities
({ signedData, targetNations, targetIssuers, appId }) => ({
// we assert that:
// 1. the owner has one of the accepted nationalities
// 2. the credential was issued by one of the accepted issuers
assert: Operation.and(
Operation.equalsOneOf(
Operation.property(signedData, 'nationality'),
targetNations
),
Operation.equalsOneOf(Operation.issuer(signedData), targetIssuers)
),
// we expose a unique hash of the credential data, as nullifier
// we expose a unique hash of the credential data, to be used as nullifier
ouputClaim: Operation.record({
nullifier: Operation.hash(signedData, appId),
}),
})
);

const targetNationalities = ['United States of America', 'Canada', 'Mexico'];
const targetNations = ['United States of America', 'Canada', 'Mexico'];
const targetIssuers = [issuer, randomPublicKey(), randomPublicKey()];

let request = PresentationRequest.https(
spec,
{
targetNationalities: targetNationalities.map((s) => Bytes32.fromString(s)),
targetNations: targetNations.map((s) => Bytes32.fromString(s)),
targetIssuers: targetIssuers.map((pk) => Credential.Simple.issuer(pk)),
appId: Bytes32.fromString('my-app-id:123'),
},
{ action: 'my-app-id:123:authenticate' }
Expand All @@ -92,21 +104,25 @@ let presentation = await Presentation.create(ownerKey, {
context: { verifierIdentity: 'my-app.xyz' },
});
console.timeEnd('create');

// TODO: to send the presentation back we need to serialize it as well

console.log('✅ WALLET: created presentation:', presentation);

// ---------------------------------------------
// VERIFIER: verify the presentation, and check that the nullifier was not used yet
// VERIFIER: verify the presentation against the request we submitted, and check that the nullifier was not used yet

let outputClaim = await Presentation.verify(request, presentation, {
verifierIdentity: 'my-app.xyz',
});
console.log('✅ VERIFIER: verified presentation');

let existingNullifiers = new Set([0x13c43f30n, 0x370f3473n, 0xe1fe0cdan]);

// TODO: claims and other I/O values should be plain JS types
let { nullifier } = presentation.outputClaim;
let { nullifier } = outputClaim;
assert(
!existingNullifiers.has(nullifier.toBigInt()),
'Nullifier should be unique'
);
console.log('✅ VERIFIER: checked nullifier uniqueness');

// TODO: implement verification
5 changes: 5 additions & 0 deletions src/credential-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ const Credential = {
credential: deserializeNestedProvableValue(obj.credential),
};
},

/**
* Validate a credential.
*/
validate: validateCredential,
};

// validating generic credential
Expand Down
49 changes: 28 additions & 21 deletions src/credential-signed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,29 +25,36 @@ type Metadata = undefined;

type Signed<Data> = StoredCredential<Data, Witness, Metadata>;

const Signed = defineCredential({
credentialType: 'simple',
witness: {
type: ProvableType.constant('simple' as const),
issuer: PublicKey,
issuerSignature: Signature,
},
const Signed = Object.assign(
defineCredential({
credentialType: 'simple',
witness: {
type: ProvableType.constant('simple' as const),
issuer: PublicKey,
issuerSignature: Signature,
},

// verify the signature
verify({ issuer, issuerSignature }, credHash) {
let ok = issuerSignature.verify(issuer, [credHash.hash]);
ok.assertTrue('Invalid signature');
},
async verifyOutsideCircuit({ issuer, issuerSignature }, credHash) {
let ok = issuerSignature.verify(issuer, [credHash.hash]);
ok.assertTrue('Invalid signature');
},
// verify the signature
verify({ issuer, issuerSignature }, credHash) {
let ok = issuerSignature.verify(issuer, [credHash.hash]);
ok.assertTrue('Invalid signature');
},
async verifyOutsideCircuit({ issuer, issuerSignature }, credHash) {
let ok = issuerSignature.verify(issuer, [credHash.hash]);
ok.assertTrue('Invalid signature');
},

// issuer == issuer public key
issuer({ issuer }) {
return Poseidon.hashWithPrefix(prefixes.issuerSimple, issuer.toFields());
},
});
// issuer == issuer public key
issuer({ issuer }) {
return Poseidon.hashWithPrefix(prefixes.issuerSimple, issuer.toFields());
},
}),
{
issuer(issuer: PublicKey) {
return Poseidon.hashWithPrefix(prefixes.issuerSimple, issuer.toFields());
},
}
);

function createSigned<Data>(
issuerPrivateKey: PrivateKey,
Expand Down
55 changes: 54 additions & 1 deletion src/presentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import {
Poseidon,
PrivateKey,
Proof,
Provable,
PublicKey,
Struct,
VerificationKey,
verify,
} from 'o1js';
import {
Spec,
Expand Down Expand Up @@ -112,7 +114,7 @@ const PresentationRequest = {
spec,
claims,
program: createProgram(spec),
inputContext: { ...context, type: 'zk-app', serverNonce },
inputContext: { type: 'zk-app', ...context, serverNonce },
});
},

Expand Down Expand Up @@ -210,7 +212,19 @@ const Presentation = {
return { ...request, program, verificationKey };
},

/**
* Create a presentation, given the request, context, and credentials.
*
* The first argument is the private key of the credential's owner, which is needed to sign credentials.
*/
create: createPresentation,

/**
* Verify a presentation against a request and context.
*
* Returns the verified output claim of the proof, to be consumed by application-specific logic.
*/
verify: verifyPresentation,
};

async function createPresentation<R extends PresentationRequest>(
Expand Down Expand Up @@ -272,6 +286,45 @@ async function createPresentation<R extends PresentationRequest>(
};
}

async function verifyPresentation<R extends PresentationRequest>(
request: R,
presentation: Presentation<any, Record<string, any>>,
context: WalletContext<R>
): Promise<Output<R>> {
// make sure request is compiled
let { program, verificationKey } = await Presentation.compile(request);

// rederive context
let contextHash = request.deriveContext(request.inputContext, context, {
clientNonce: presentation.clientNonce,
vkHash: verificationKey.hash,
claims: hashClaims(request.claims),
});

// assert the correct claims were used, and claims match the proof public inputs
let { proof } = presentation;
let claimType = NestedProvable.get(NestedProvable.fromValue(request.claims));
let outputType = program.program.publicOutputType;
let claims = request.claims;
Provable.assertEqual(claimType, proof.publicInput.claims, claims);
Provable.assertEqual(claimType, presentation.claims, claims);
Provable.assertEqual(
outputType,
proof.publicOutput,
presentation.outputClaim
);

// assert that the correct context was used
proof.publicInput.context.assertEquals(contextHash, 'Invalid context');

// verify the proof against our verification key
let ok = await verify(proof, verificationKey);
assert(ok, 'Invalid proof');

// return the verified outputClaim
return proof.publicOutput;
}

function pickCredentials(
credentialsNeeded: string[],
[...credentials]: (StoredCredential & { key?: string })[]
Expand Down
24 changes: 11 additions & 13 deletions src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
Bytes,
Field,
Hash,
Poseidon,
Proof,
Signature,
VerificationKey,
Expand Down Expand Up @@ -43,23 +42,16 @@ type Program<Output, Inputs extends Record<string, Input>> = {
>;
};

function deriveCircuitName(spec: Spec): string {
const serializedSpec = JSON.stringify(convertSpecToSerializable(spec));
const specBytes = Bytes.fromString(serializedSpec);
const hashBytes = Hash.Keccak256.hash(specBytes);
return `credential-${hashBytes.toString()}`;
}

function createProgram<S extends Spec>(
spec: S
): Program<GetSpecData<S>, S['inputs']> {
// 1. split spec inputs into public and private inputs
// split spec inputs into public and private inputs
let PublicInput = NestedProvable.get(publicInputTypes(spec));
let PublicOutput = publicOutputType(spec);
let PrivateInput = NestedProvable.get(privateInputTypes(spec));

let program = ZkProgram({
name: deriveCircuitName(spec),
name: programName(spec),
publicInput: PublicInput,
publicOutput: PublicOutput,
methods: {
Expand All @@ -77,7 +69,6 @@ function createProgram<S extends Spec>(
publicInput,
privateInput
);
// TODO return issuers from this function and pass it to app logic
let credentialOutputs = verifyCredentials(credentials);

let root = recombineDataInputs(
Expand All @@ -87,9 +78,9 @@ function createProgram<S extends Spec>(
credentialOutputs
);
let assertion = Node.eval(root, spec.logic.assert);
let output = Node.eval(root, spec.logic.ouputClaim);
let outputClaim = Node.eval(root, spec.logic.ouputClaim);
assertion.assertTrue('Program assertion failed!');
return { publicOutput: output };
return { publicOutput: outputClaim };
},
},
},
Expand Down Expand Up @@ -117,6 +108,13 @@ function createProgram<S extends Spec>(

// helper

function programName(spec: Spec): string {
const serializedSpec = JSON.stringify(convertSpecToSerializable(spec));
const specBytes = Bytes.fromString(serializedSpec);
const hashBytes = Hash.Keccak256.hash(specBytes);
return `credential-${hashBytes.toHex().slice(0, 16)}`;
}

type GetSpecData<S extends Spec> = S extends Spec<infer Data, any>
? Data
: never;
Loading

0 comments on commit ef98449

Please sign in to comment.