Skip to content

Commit

Permalink
fix: 943 certificate.ts verify explicits encoding chain from JSON/str… (
Browse files Browse the repository at this point in the history
#962)

* fix: 943 certificate.ts verify explicits encoding chain from JSON/string to binary hash

* fix: improve casts

* feat: release 15

---------

Co-authored-by: rodolfopietro97 <[email protected]>
  • Loading branch information
lucanicoladebiasi and rodolfopietro97 authored Jun 6, 2024
1 parent 3f49de6 commit 90aa623
Show file tree
Hide file tree
Showing 20 changed files with 1,021 additions and 954 deletions.
6 changes: 3 additions & 3 deletions apps/sdk-hardhat-integration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
"deploy-testnet": "npx hardhat run scripts/deploy.ts --network vechain_testnet"
},
"dependencies": {
"@vechain/sdk-core": "1.0.0-beta.14",
"@vechain/sdk-hardhat-plugin": "1.0.0-beta.14",
"@vechain/sdk-logging": "1.0.0-beta.14",
"@vechain/sdk-core": "1.0.0-beta.15",
"@vechain/sdk-hardhat-plugin": "1.0.0-beta.15",
"@vechain/sdk-logging": "1.0.0-beta.15",
"@openzeppelin/contracts": "^5.0.2"
},
"devDependencies": {
Expand Down
216 changes: 108 additions & 108 deletions apps/sdk-hardhat-integration/yarn.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions apps/sdk-nextjs-integration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"test:watch": "jest --watch"
},
"dependencies": {
"@vechain/sdk-core": "1.0.0-beta.14",
"@vechain/sdk-network": "1.0.0-beta.14",
"@vechain/sdk-core": "1.0.0-beta.15",
"@vechain/sdk-network": "1.0.0-beta.15",
"install": "^0.13.0",
"next": "14",
"react": "^18",
Expand Down
451 changes: 215 additions & 236 deletions apps/sdk-nextjs-integration/yarn.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/sdk-node-integration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"test": "jest"
},
"dependencies": {
"@vechain/sdk-network": "1.0.0-beta.14",
"@vechain/sdk-network": "1.0.0-beta.15",
"axios": "^1.6.8",
"typescript": "^5.3.3"
},
Expand Down
664 changes: 334 additions & 330 deletions apps/sdk-node-integration/yarn.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "docs",
"version": "1.0.0-beta.14",
"version": "1.0.0-beta.15",
"description": "Documentation for the SDK with examples",
"author": "vechain Foundation",
"license": "MIT",
Expand All @@ -20,7 +20,7 @@
"test:examples:solo": "(yarn start-thor-solo && yarn test:examples && yarn stop-thor-solo) || yarn stop-thor-solo"
},
"dependencies": {
"@vechain/sdk-core": "1.0.0-beta.14",
"@vechain/sdk-core": "1.0.0-beta.15",
"typescript": "^5.4.4"
},
"devDependencies": {
Expand Down
6 changes: 3 additions & 3 deletions packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@vechain/sdk-core",
"version": "1.0.0-beta.14",
"version": "1.0.0-beta.15",
"description": "Includes modules for fundamental operations like hashing and cryptography",
"author": "vechain Foundation",
"license": "MIT",
Expand Down Expand Up @@ -39,8 +39,8 @@
"@scure/bip32": "^1.4.0",
"@scure/bip39": "^1.3.0",
"@types/elliptic": "^6.4.18",
"@vechain/sdk-errors": "1.0.0-beta.14",
"@vechain/sdk-logging": "1.0.0-beta.14",
"@vechain/sdk-errors": "1.0.0-beta.15",
"@vechain/sdk-logging": "1.0.0-beta.15",
"bignumber.js": "^9.1.2",
"blakejs": "^1.2.1",
"elliptic": "^6.5.5",
Expand Down
91 changes: 70 additions & 21 deletions packages/core/src/certificate/certificate.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as utils from '@noble/curves/abstract/utils';
import fastJsonStableStringify from 'fast-json-stable-stringify';
import { Hex, Hex0x } from '../utils';
import { addressUtils } from '../address';
import { assert, buildError, CERTIFICATE } from '@vechain/sdk-errors';
import { blake2b256 } from '../hash';
import { hexToBytes } from '@noble/curves/abstract/utils';
import { secp256k1 } from '../secp256k1';
import { type Certificate } from './types';

Expand Down Expand Up @@ -33,7 +33,7 @@ function encode(cert: Certificate): string {
}

/**
* Verifies the validity of a certificate.
* Matches a certificate against a given address and signature.
*
* This method is insensitive to the case representation of the signer's address.
*
Expand All @@ -42,9 +42,67 @@ function encode(cert: Certificate): string {
*
* Secure audit function.
* - {@link blake2b256};
* - {@link certificate.encode};
* - {@link secp256k1.recover}.
*
* @param {Uint8Array} cert - The certificate to match. computed from the certificate without the `signature` property.
* @param {string} address - The address to match against, optionally prefixed with `0x`.
* @param {string} signature - The signature to verify expressed in hexadecimal form, optionally prefixed with `0x`.
*
* @returns {void} - No return value.
*
* @throws CertificateInvalidSignatureFormatError - If the certificate signature's is not a valid hexadecimal expression prefixed with `0x`.
* @throws CertificateNotSignedError - If the certificate is not signed.
* @throws CertificateInvalidSignerError - If the certificate's signature's doesn't match with the signer;s public key.
*
*/
function match(cert: Uint8Array, address: string, signature: string): void {
// Invalid hexadecimal as signature.
assert(
'certificate.match',
Hex0x.isValid(signature, true, true),
CERTIFICATE.CERTIFICATE_INVALID_SIGNATURE_FORMAT,
'Verification failed: signature format is invalid.',
{ signature }
);
try {
// The `encode` method could throw `InvalidAddressError`.
const signingHash = blake2b256(cert, 'buffer');
const signingPublicKey = secp256k1.recover(
signingHash,
hexToBytes(Hex.canon(signature))
);
const signingAddress = addressUtils.fromPublicKey(signingPublicKey);
// Signature does not match with the signer's public key.
assert(
'certificate.match',
signingAddress.toLowerCase() === address.toLowerCase(),
CERTIFICATE.CERTIFICATE_INVALID_SIGNER,
"Verification failed: signature does not correspond to the signer's public key.",
{ pubKey: signingPublicKey, cert }
);
} catch (e) {
throw buildError(
'certificate.match',
CERTIFICATE.CERTIFICATE_INVALID_SIGNER,
(e as Error).message,
{ address, certificate: cert, signature },
e
);
}
}

/**
* Verifies the validity of a certificate.
*
* This method is insensitive to the case representation of the signer's address.
*
* [EIP/ERC-55: Mixed-case checksum address encoding](https://eips.ethereum.org/EIPS/eip-55).
* is supported.
*
* Secure audit function.
* - {@link certificate.encode};
* - {@link match}.
*
* @param {Certificate} cert - The certificate to verify.
*
* @returns {void} - No return value.
Expand All @@ -54,12 +112,13 @@ function encode(cert: Certificate): string {
* @throws CertificateInvalidSignerError - If the certificate's signature's doesn't match with the signer;s public key.
*
* @remark This methods {@link certificate.encode} the `cert` instance
* to extract its signer 's address and compare it with the address derioved from the public key recovered from the
* to extract its signer 's address and compare it with the address computed from the public key recovered from the
* certificate using the
* [BLAKE2](https://en.wikipedia.org/wiki/BLAKE_(hash_function)#BLAKE2)
* hash of its JSON encoded representation.
*
* @see {encode}
* @see {match}
*/
function verify(cert: Certificate): void {
// No signature.
Expand All @@ -78,24 +137,14 @@ function verify(cert: Certificate): void {
'Verification failed: signature format is invalid.',
{ cert }
);
// Encode the certificate without the signature and get signing hash.
try {
// The `encode` method could throw `InvalidAddressError`.
const encoded = encode({ ...cert, signature: undefined });
const signingHash = blake2b256(encoded);
const pubKey = secp256k1.recover(
signingHash,
utils.hexToBytes(Hex.canon(cert.signature as string))
);
// Signature does not match with the signer's public key.
assert(
'certificate.verify',
addressUtils.fromPublicKey(pubKey).toLowerCase() ===
cert.signer.toLowerCase(),
CERTIFICATE.CERTIFICATE_INVALID_SIGNER,
"Verification failed: signature does not correspond to the signer's public key.",
{ pubKey, cert }
// Encode the certificate without the signature.
const encoded = new TextEncoder().encode(
certificate
.encode({ ...cert, signature: undefined })
.normalize('NFC')
);
match(new Uint8Array(encoded), cert.signer, cert.signature as string);
} catch (e) {
throw buildError(
'certificate.verify',
Expand All @@ -110,4 +159,4 @@ function verify(cert: Certificate): void {
/**
* Exposes the certificate encoding and verification functions.
*/
export const certificate = { encode, verify };
export const certificate = { encode, match, verify };
52 changes: 42 additions & 10 deletions packages/core/tests/certificate/certificate.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, test } from '@jest/globals';
import { certificate } from '../../src';
import { cert, cert2, invalidSignature, sig, sig2 } from './fixture';
import { cert1, cert2, invalidSignature, sig1, sig2 } from './fixture';
import {
CertificateInvalidSignatureFormatError,
CertificateInvalidSignerError,
Expand All @@ -15,13 +15,13 @@ import {
describe('certificate', () => {
describe('encode', () => {
test('consistent between two certificates - before signature', () => {
expect(certificate.encode(cert)).toStrictEqual(
expect(certificate.encode(cert1)).toStrictEqual(
certificate.encode(cert2)
);
});

test('consistent between two certificates - after signature', () => {
expect(certificate.encode({ ...cert, signature: sig })).toEqual(
expect(certificate.encode({ ...cert1, signature: sig1 })).toEqual(
certificate.encode({
...cert2,
signature: sig2
Expand All @@ -30,42 +30,74 @@ describe('certificate', () => {
});
});

describe('match', () => {
const cert = new TextEncoder().encode(
certificate
.encode({ ...cert1, signature: undefined })
.normalize('NFC')
);

test('valid - because signature', () => {
expect(() => {
certificate.match(cert, cert1.signer, sig1);
}).not.toThrowError();
});

test('valid - because signature - uppercase', () => {
expect(() => {
certificate.match(cert, cert1.signer, sig1.toUpperCase());
}).not.toThrowError();
});

test('invalid - because signer address', () => {
expect(() => {
certificate.match(cert, '0x', sig1);
}).toThrowError(CertificateInvalidSignerError);
});

test('invalid - because invalid signature format', () => {
expect(() => {
certificate.match(cert, cert1.signer, invalidSignature);
}).toThrowError(CertificateInvalidSignatureFormatError);
});
});

describe('verify', () => {
test('valid - because signature', () => {
expect(() => {
certificate.verify({ ...cert, signature: sig });
certificate.verify({ ...cert1, signature: sig1 });
}).not.toThrowError();
});

test('valid - because signature - uppercase', () => {
expect(() => {
certificate.verify({
...cert,
signature: sig.toUpperCase()
...cert1,
signature: sig1.toUpperCase()
});
}).not.toThrowError();
});

test('invalid - because signer address', () => {
expect(() => {
certificate.verify({
...cert,
signature: sig,
...cert1,
signature: sig1,
signer: '0x'
});
}).toThrowError(CertificateInvalidSignerError);
});

test('invalid - because missing signature', () => {
expect(() => {
certificate.verify({ ...cert, signer: '0x' });
certificate.verify({ ...cert1, signer: '0x' });
}).toThrowError(CertificateNotSignedError);
});

test('invalid - because invalid signature format', () => {
expect(() => {
certificate.verify({
...cert,
...cert1,
signature: invalidSignature,
signer: '0x'
});
Expand Down
12 changes: 6 additions & 6 deletions packages/core/tests/certificate/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const privKey = utils.hexToBytes(
/**
* Certificate n.1 to be used for testing, mostly for encoding and verify functions
*/
const cert = {
const cert1 = {
purpose: 'identification',
payload: {
type: 'text',
Expand All @@ -36,7 +36,7 @@ const cert2 = {
domain: 'localhost',
timestamp: 1545035330,
purpose: 'identification',
signer: cert.signer,
signer: cert1.signer,
payload: {
content: 'fyi',
type: 'text'
Expand All @@ -46,8 +46,8 @@ const cert2 = {
/**
* Signature of Certificate n.1
*/
const sig = Hex0x.of(
secp256k1.sign(blake2b256(certificate.encode(cert)), privKey)
const sig1 = Hex0x.of(
secp256k1.sign(blake2b256(certificate.encode(cert1)), privKey)
);

/**
Expand All @@ -62,6 +62,6 @@ const sig2 = Hex0x.of(
*/
const invalidSignature =
'0xBAD' +
Hex.of(secp256k1.sign(blake2b256(certificate.encode(cert)), privKey));
Hex.of(secp256k1.sign(blake2b256(certificate.encode(cert1)), privKey));

export { privKey, cert, cert2, sig, sig2, invalidSignature };
export { privKey, cert1, cert2, sig1, sig2, invalidSignature };
4 changes: 2 additions & 2 deletions packages/core/tests/hash/hash.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { InvalidDataReturnTypeError } from '@vechain/sdk-errors';
import { Buffer } from 'buffer';
import { ZERO_BYTES } from '../../src';

import { cert } from '../certificate/fixture';
import { cert1 } from '../certificate/fixture';
import { bytesToHex } from '@noble/ciphers/utils';

/**
Expand All @@ -14,7 +14,7 @@ import { bytesToHex } from '@noble/ciphers/utils';
*/
describe('Hash', () => {
test('thordevkit', () => {
const json = JSON.stringify(cert);
const json = JSON.stringify(cert1);
console.log(json);
// const blake_dev_dir = ThorDevKit.blake2b256(json);
// console.log('dir', blake_dev_dir);
Expand Down
2 changes: 1 addition & 1 deletion packages/errors/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@vechain/sdk-errors",
"version": "1.0.0-beta.14",
"version": "1.0.0-beta.15",
"description": "This module is dedicated to managing and customizing errors within the SDK",
"author": "vechain Foundation",
"license": "MIT",
Expand Down
6 changes: 3 additions & 3 deletions packages/ethers-adapter/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@vechain/sdk-ethers-adapter",
"version": "1.0.0-beta.14",
"version": "1.0.0-beta.15",
"description": "This module serves as a crucial bridge between the standard Ethereum tooling provided by Hardhat and the unique features of the vechain thor blockchain",
"author": "vechain Foundation",
"license": "MIT",
Expand Down Expand Up @@ -34,8 +34,8 @@
"test": "rm -rf ./coverage && jest --coverage --coverageDirectory=coverage --group=integration --group=unit"
},
"dependencies": {
"@vechain/sdk-core": "1.0.0-beta.14",
"@vechain/sdk-network": "1.0.0-beta.14"
"@vechain/sdk-core": "1.0.0-beta.15",
"@vechain/sdk-network": "1.0.0-beta.15"
},
"devDependencies": {
"@nomicfoundation/hardhat-ethers": "^3.0.6"
Expand Down
Loading

1 comment on commit 90aa623

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Coverage

Summary

Lines Statements Branches Functions
Coverage: 100%
100% (3412/3412) 100% (794/794) 100% (705/705)
Title Tests Skipped Failures Errors Time
core 498 0 💤 0 ❌ 0 🔥 1m 4s ⏱️
network 658 0 💤 0 ❌ 0 🔥 3m 59s ⏱️
errors 48 0 💤 0 ❌ 0 🔥 10.017s ⏱️

Please sign in to comment.