Skip to content

Commit

Permalink
Merge pull request #169 from identity-com/CIV-3254_merkletree_signature
Browse files Browse the repository at this point in the history
CIV-3154 Merkletree Sign and Verify
  • Loading branch information
jpsantosbh authored May 18, 2021
2 parents 7816103 + c0149d1 commit f9e114c
Show file tree
Hide file tree
Showing 5 changed files with 266 additions and 11 deletions.
146 changes: 146 additions & 0 deletions __test__/CredentialSignerVerifier.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/* eslint-disable max-len */

const { HDNode } = require('bitcoinjs-lib');
const CredentialSignerVerifier = require('../src/creds/CredentialSignerVerifier');

const SEED = 'f6d466fd58c20ff964673522083efebf';
const prvBase58 = 'xprv9s21ZrQH143K4aBUwUW6GVec7Y6oUEBqrt2WWaXyxjh2pjofNc1of44BLufn4p1t7Jq4EPzm5C9sRxCuBYJdHu62jhgfyPm544sNjtH7x8S';

const pubBase58 = 'xpub661MyMwAqRbcH4Fx3W36ddbLfZwHsguhE6x7JxwbX5E1hY8ov9L4CrNfCCQpV8pVK64CVqkhYQ9QLFgkVAUqkRThkTY1R4GiWHNZtAFSVpD';

describe('CredentialSignerVerifier Tests', () => {
describe('Using a ECKeyPair', () => {
let keyPair;
let signerVerifier;

beforeAll(() => {
keyPair = HDNode.fromSeedHex(SEED);
signerVerifier = new CredentialSignerVerifier({ keyPair });
});

it('Should sign and verify', () => {
const toSign = { merkleRoot: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' };
const signature = signerVerifier.sign(toSign);
expect(signature).toBeDefined();
const toVerify = {
proof: {
...toSign,
merkleRootSignature: signature,
},
};
expect(signerVerifier.isSignatureValid(toVerify)).toBeTruthy();
});

it('Should verify', () => {
const toVerify = {
proof: {
merkleRoot: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b666',
merkleRootSignature: {
algo: 'ec256k1',
pubBase58: 'xpub661MyMwAqRbcH4Fx3W36ddbLfZwHsguhE6x7JxwbX5E1hY8ov9L4CrNfCCQpV8pVK64CVqkhYQ9QLFgkVAUqkRThkTY1R4GiWHNZtAFSVpD',
signature: '3045022100e7f0921491e8da2759b24047443325483ac023795683dc3b91c78d0566a1159602206fd4e80982fd83705932543d02bc6abd079446bf4ec7b5d9fba4f7f5363bd6fa',
},
},
};

expect(signerVerifier.isSignatureValid(toVerify)).toBeTruthy();
});

it('Should not verify', () => {
const toVerify = {
proof: {
merkleRoot: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b666',
merkleRootSignature: {
algo: 'ec256k1',
pubBase58: 'xpub661MyMwAqRbcH4Fx3W36ddbLfZwHsguhE6x7JxwbX5E1hY8ov9L4CrNfCCQpV8pVK64CVqkhYQ9QLFgkVAUqkRThkTY1R4GiWHNZtAFSVpD',
signature: 'fa3e022100e7f0921491e8da2759b24047443325483ac023795683dc3b91c78d0566a1159602206fd4e80982fd83705932543d02bc6abd079446bf4ec7b5d9fba4f7f5363bd6fa',
},
},
};
expect(signerVerifier.isSignatureValid(toVerify)).toBeFalsy();
});
});
describe('Using a prvBase58', () => {
let signerVerifier;

beforeAll(() => {
signerVerifier = new CredentialSignerVerifier({ prvBase58 });
});

it('Should sign and verify', () => {
const toSign = { merkleRoot: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' };
const signature = signerVerifier.sign(toSign);
expect(signature).toBeDefined();
const toVerify = {
proof: {
...toSign,
merkleRootSignature: signature,
},
};
expect(signerVerifier.isSignatureValid(toVerify)).toBeTruthy();
});

it('Should verify', () => {
const toVerify = {
proof: {
merkleRoot: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b666',
merkleRootSignature: {
algo: 'ec256k1',
pubBase58: 'xpub661MyMwAqRbcH4Fx3W36ddbLfZwHsguhE6x7JxwbX5E1hY8ov9L4CrNfCCQpV8pVK64CVqkhYQ9QLFgkVAUqkRThkTY1R4GiWHNZtAFSVpD',
signature: '3045022100e7f0921491e8da2759b24047443325483ac023795683dc3b91c78d0566a1159602206fd4e80982fd83705932543d02bc6abd079446bf4ec7b5d9fba4f7f5363bd6fa',
},
},
};
expect(signerVerifier.isSignatureValid(toVerify)).toBeTruthy();
});

it('Should not verify', () => {
const toVerify = {
proof: {
merkleRoot: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b666',
merkleRootSignature: {
algo: 'ec256k1',
pubBase58: 'xpub661MyMwAqRbcH4Fx3W36ddbLfZwHsguhE6x7JxwbX5E1hY8ov9L4CrNfCCQpV8pVK64CVqkhYQ9QLFgkVAUqkRThkTY1R4GiWHNZtAFSVpD',
signature: 'fa3e022100e7f0921491e8da2759b24047443325483ac023795683dc3b91c78d0566a1159602206fd4e80982fd83705932543d02bc6abd079446bf4ec7b5d9fba4f7f5363bd6fa',
},
},
};
expect(signerVerifier.isSignatureValid(toVerify)).toBeFalsy();
});
});
describe('Using a pubBase58', () => {
let signerVerifier;

beforeAll(() => {
signerVerifier = new CredentialSignerVerifier({ pubBase58 });
});

it('Should verify', () => {
const toVerify = {
proof: {
merkleRoot: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b666',
merkleRootSignature: {
algo: 'ec256k1',
pubBase58: 'xpub661MyMwAqRbcH4Fx3W36ddbLfZwHsguhE6x7JxwbX5E1hY8ov9L4CrNfCCQpV8pVK64CVqkhYQ9QLFgkVAUqkRThkTY1R4GiWHNZtAFSVpD',
signature: '3045022100e7f0921491e8da2759b24047443325483ac023795683dc3b91c78d0566a1159602206fd4e80982fd83705932543d02bc6abd079446bf4ec7b5d9fba4f7f5363bd6fa',
},
},
};
expect(signerVerifier.isSignatureValid(toVerify)).toBeTruthy();
});

it('Should not verify', () => {
const toVerify = {
proof: {
merkleRoot: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b666',
merkleRootSignature: {
algo: 'ec256k1',
pubBase58: 'xpub661MyMwAqRbcH4Fx3W36ddbLfZwHsguhE6x7JxwbX5E1hY8ov9L4CrNfCCQpV8pVK64CVqkhYQ9QLFgkVAUqkRThkTY1R4GiWHNZtAFSVpD',
signature: 'fa3e022100e7f0921491e8da2759b24047443325483ac023795683dc3b91c78d0566a1159602206fd4e80982fd83705932543d02bc6abd079446bf4ec7b5d9fba4f7f5363bd6fa',
},
},
};
expect(signerVerifier.isSignatureValid(toVerify)).toBeFalsy();
});
});
});
40 changes: 36 additions & 4 deletions __test__/creds/VerifiableCredential.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ const VC = require('../../src/creds/VerifiableCredential');
const credentialDefinitions = require('../../src/creds/definitions');
const SchemaGenerator = require('../../src/schemas/generator/SchemaGenerator');
const MiniCryptoManagerImpl = require('../../src/services/MiniCryptoManagerImpl');
const CredentialSignerVerifier = require('../../src/creds/CredentialSignerVerifier');

// eslint-disable-next-line max-len
const prvBase58 = 'xprv9s21ZrQH143K4aBUwUW6GVec7Y6oUEBqrt2WWaXyxjh2pjofNc1of44BLufn4p1t7Jq4EPzm5C9sRxCuBYJdHu62jhgfyPm544sNjtH7x8S';
// eslint-disable-next-line max-len
const pubBase58 = 'xpub661MyMwAqRbcH4Fx3W36ddbLfZwHsguhE6x7JxwbX5E1hY8ov9L4CrNfCCQpV8pVK64CVqkhYQ9QLFgkVAUqkRThkTY1R4GiWHNZtAFSVpD';


jest.setTimeout(150000);

Expand Down Expand Up @@ -779,7 +786,7 @@ describe('Unit tests for Verifiable Credentials', () => {
const cred = VC.fromJSON(credentialJson);
expect(cred).toBeDefined();
expect(cred.proof.anchor).toBeDefined();
expect(await cred.verifySignature()).toBeTruthy();
expect(await cred.verifyAnchorSignature()).toBeTruthy();
done();
});

Expand All @@ -789,7 +796,7 @@ describe('Unit tests for Verifiable Credentials', () => {
const cred = VC.fromJSON(credentialJson);
expect(cred).toBeDefined();
expect(cred.proof.anchor).toBeDefined();
expect(await cred.verifySignature(XPUB1)).toBeTruthy();
expect(await cred.verifyAnchorSignature(XPUB1)).toBeTruthy();
done();
});

Expand All @@ -799,7 +806,7 @@ describe('Unit tests for Verifiable Credentials', () => {
const cred = VC.fromJSON(credentialJson);
expect(cred).toBeDefined();
expect(cred.proof.anchor).toBeDefined();
expect(() => cred.verifySignature(XPUB1.replace('9', '6'))).toThrow();
expect(() => cred.verifyAnchorSignature(XPUB1.replace('9', '6'))).toThrow();
done();
});

Expand All @@ -811,7 +818,7 @@ describe('Unit tests for Verifiable Credentials', () => {
cred.proof.merkleRoot = 'gfdagfagfda';
expect(cred).toBeDefined();
expect(cred.proof.anchor).toBeDefined();
expect(await cred.verifySignature()).toBeFalsy();
expect(await cred.verifyAnchorSignature()).toBeFalsy();
done();
});

Expand Down Expand Up @@ -1765,3 +1772,28 @@ describe('Transient Credential Tests', () => {
expect(proved).toBeTruthy();
});
});

describe('Signned Verifiable Credentials', () => {
test('Should create a verifiable credential instance', () => {
const name = new Claim.IdentityName(identityName);
const dob = new Claim.IdentityDateOfBirth(identityDateOfBirth);
const cred = new VC('credential-cvc:Identity-v1', uuidv4(), null, [name, dob], '1', null,
new CredentialSignerVerifier({ prvBase58 }));
expect(cred).toBeDefined();
expect(cred.proof.merkleRootSignature).toBeDefined();
expect(cred.verifyMerkletreeSignature(pubBase58)).toBeTruthy();
});

test('Should verify credential(data only) signature', () => {
const name = new Claim.IdentityName(identityName);
const dob = new Claim.IdentityDateOfBirth(identityDateOfBirth);
const signerVerifier = new CredentialSignerVerifier({ prvBase58 });
const cred = new VC('credential-cvc:Identity-v1', uuidv4(), null, [name, dob], '1', null,
signerVerifier);
expect(cred).toBeDefined();
expect(cred.proof.merkleRootSignature).toBeDefined();

const dataOnlyCredential = JSON.parse(JSON.stringify(cred));
expect(signerVerifier.isSignatureValid(dataOnlyCredential)).toBeTruthy();
});
});
61 changes: 61 additions & 0 deletions src/creds/CredentialSignerVerifier.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const _ = require('lodash');
const { HDNode, ECSignature } = require('bitcoinjs-lib');

const SIGNATURE_ALGO = 'ec256k1';
class CredentialSignerVerifier {
/**
* Creates a new instance of a CredentialSignerVerifier
*
* @param options.keyPair any instace that implements sign and verify interface
* or
* @param options.prvBase58 bse58 serialized private key
* or for verification only
* @param options.pubBase58 bse58 serialized public key
*/
constructor(options) {
if (_.isEmpty(options.keyPair) && _.isEmpty(options.prvBase58) && _.isEmpty(options.pubBase58)) {
throw new Error('Either a keyPair, prvBase58 or pubBase58(to verify only) is required');
}
this.keyPair = options.keyPair || HDNode.fromBase58(options.prvBase58 || options.pubBase58);
}

/**
* Verify is a credential has a valid merkletree signature, using a pinned pubkey
* @param credential
* @returns {*|boolean}
*/
isSignatureValid(credential) {
if (_.isEmpty(credential.proof)
|| _.isEmpty(credential.proof.merkleRoot)
|| _.isEmpty(credential.proof.merkleRootSignature)) {
throw Error('Invalid Credential Proof Schema');
}

try {
const signatureHex = _.get(credential, 'proof.merkleRootSignature.signature');
const signature = signatureHex ? ECSignature.fromDER(Buffer.from(signatureHex, 'hex')) : null;
const merkleRoot = _.get(credential, 'proof.merkleRoot');
return (signature && merkleRoot) ? this.keyPair.verify(Buffer.from(merkleRoot, 'hex'), signature) : false;
} catch (error) {
// verify throws in must cases but we want to return false
return false;
}
}

/**
* Create a merkleRootSignature object by signing with a pinned private key
* @param proof
* @returns {{signature, pubBase58: *, algo: string}}
*/
sign(proof) {
const hash = Buffer.from(proof.merkleRoot, 'hex');
const signature = this.keyPair.sign(hash);
return {
algo: SIGNATURE_ALGO,
pubBase58: this.keyPair.neutered().toBase58(),
signature: signature.toDER().toString('hex'),
};
}
}

module.exports = CredentialSignerVerifier;
10 changes: 7 additions & 3 deletions src/creds/CvcMerkleProof.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,16 @@ class CvcMerkleProof {
return 16;
}

constructor(ucas) {
constructor(ucas, credentialSigner = null) {
const withRandomUcas = CvcMerkleProof.padTree(ucas);
this.type = 'CvcMerkleProof2018';
this.merkleRoot = null;
this.anchor = 'TBD (Civic Blockchain Attestation)';
this.leaves = CvcMerkleProof.getAllAttestableValue(withRandomUcas);
this.buildMerkleTree();
this.buildMerkleTree(credentialSigner);
}

buildMerkleTree() {
buildMerkleTree(credentialSigner = null) {
const merkleTools = new MerkleTools();
const hashes = _.map(this.leaves, n => sha256(n.value));
merkleTools.addLeaves(hashes);
Expand All @@ -34,6 +34,10 @@ class CvcMerkleProof {
});
this.leaves = _.filter(this.leaves, el => !(el.identifier === 'cvc:Random:node'));
this.merkleRoot = merkleTools.getMerkleRoot().toString('hex');

if (credentialSigner) {
this.merkleRootSignature = credentialSigner.sign(this);
}
}

static padTree(nodes) {
Expand Down
20 changes: 16 additions & 4 deletions src/creds/VerifiableCredential.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const { services } = require('../services');
const time = require('../timeHelper');
const { CvcMerkleProof } = require('./CvcMerkleProof');
const { ClaimModel } = require('./ClaimModel');
const CredentialSignerVerifier = require('./CredentialSignerVerifier');

// convert a time delta to a timestamp
const convertDeltaToTimestamp = delta => time.applyDeltaToDate(delta).getTime() / 1000;
Expand Down Expand Up @@ -371,7 +372,8 @@ function getCredentialDefinition(identifier, version) {
* @param {*} version
* @param {*} [evidence]
*/
function VerifiableCredentialBaseConstructor(identifier, issuer, expiryIn, ucas, version, evidence) {
function VerifiableCredentialBaseConstructor(identifier, issuer, expiryIn, ucas,
version, evidence, signerVerifier = null) {
this.id = uuidv4();
this.issuer = issuer;
const issuerUCA = new Claim('cvc:Meta:issuer', this.issuer);
Expand Down Expand Up @@ -401,7 +403,7 @@ function VerifiableCredentialBaseConstructor(identifier, issuer, expiryIn, ucas,
if (!_.isEmpty(ucas)) {
verifyRequiredClaims(definition, ucas);
this.claim = new ClaimModel(ucas);
this.proof = new CvcMerkleProof(proofUCAs);
this.proof = new CvcMerkleProof(proofUCAs, signerVerifier);
if (!_.isEmpty(definition.excludes)) {
const removed = _.remove(this.proof.leaves, el => _.includes(definition.excludes, el.identifier));
_.forEach(removed, (r) => {
Expand Down Expand Up @@ -538,13 +540,23 @@ function VerifiableCredentialBaseConstructor(identifier, issuer, expiryIn, ucas,
* This method checks if the signature matches for the root of the Merkle Tree
* @return true or false for the validation
*/
this.verifySignature = (pinnedPubKey) => {
this.verifyAnchorSignature = (pinnedPubKey) => {
if (this.proof.anchor.type === 'transient') {
return true;
}
return services.container.AnchorService.verifySignature(this.proof, pinnedPubKey);
};

/**
* This methods check the stand alone merkletreeSiganture
* return true or false for the validation
*/
this.verifyMerkletreeSignature = (pubBase58) => {
if (_.isEmpty(pubBase58)) return false;
const verifier = new CredentialSignerVerifier({ pubBase58 });
return verifier.isSignatureValid(this);
};

/**
* This method checks that the attestation / anchor exists on the BC
*/
Expand Down Expand Up @@ -619,7 +631,7 @@ function VerifiableCredentialBaseConstructor(identifier, issuer, expiryIn, ucas,
if (_.isEmpty(_.get(this.proof, 'anchor.subject.label')) || _.isEmpty(_.get(this.proof, 'anchor.subject.data'))) {
throw new Error('Invalid credential attestation/anchor');
}
if (!this.verifySignature()) {
if (!this.verifyAnchorSignature()) {
throw new Error('Invalid credential attestation/anchor signature');
}
if (!requestorId || !requestId || !(keyName || pvtKey)) {
Expand Down

0 comments on commit f9e114c

Please sign in to comment.