Skip to content

Commit

Permalink
feat(hashicorp-vault-signing-manager): implement signing manager
Browse files Browse the repository at this point in the history
add unit tests and modify README
  • Loading branch information
monitz87 committed Mar 4, 2022
1 parent e9bd9e2 commit f811dbc
Show file tree
Hide file tree
Showing 11 changed files with 746 additions and 10 deletions.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"@polkadot/api": "5.1.1",
"@polkadot/extension-dapp": "^0.42.7",
"@polkadot/util": "^7.0.2",
"@polkadot/util-crypto": "^7.0.2"
"@polkadot/util-crypto": "^7.0.2",
"cross-fetch": "^3.1.5",
"lodash": "^4.17.21"
},
"devDependencies": {
"@angular-devkit/build-angular": "13.2.3",
Expand All @@ -42,6 +44,7 @@
"@nrwl/workspace": "13.8.2",
"@open-wc/webpack-import-meta-loader": "^0.4.7",
"@types/jest": "27.0.2",
"@types/lodash": "^4.14.179",
"@types/node": "16.11.7",
"@types/require-from-string": "^1.2.1",
"@typescript-eslint/eslint-plugin": "~5.10.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/hashicorp-vault-signing-manager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { HashicorpVaultSigningManager } from '@polymathnetwork/hashicorp-vault-s
import { Polymesh } from '@polymathnetwork/polymesh-sdk';

// setup
const signingManager = await HashicorpVaultSigningManager.create({
const signingManager = new HashicorpVaultSigningManager({
// URL where the vault is hosted
url: 'https://my-hosted-vault.io',
// authentication token
Expand Down
4 changes: 3 additions & 1 deletion packages/hashicorp-vault-signing-manager/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './lib/hashicorp-vault-signing-manager';
/* istanbul ignore file */

export { HashicorpVaultSigningManager } from './lib/hashicorp-vault-signing-manager';
Original file line number Diff line number Diff line change
@@ -1,7 +1,176 @@
import { hashicorpVaultSigningManager } from './hashicorp-vault-signing-manager';
import { TypeRegistry } from '@polkadot/types';
import { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types';
import { stringToU8a, u8aToHex } from '@polkadot/util';

describe('hashicorpVaultSigningManager', () => {
it('should work', () => {
expect(hashicorpVaultSigningManager()).toEqual('hashicorp-vault-signing-manager');
import { HashicorpVault } from './hashicorp-vault';
import { mockHashicorpVault } from './hashicorp-vault/mocks';
import { HashicorpVaultSigningManager, VaultSigner } from './hashicorp-vault-signing-manager';

jest.mock('./hashicorp-vault', () => ({
HashicorpVault: function () {
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require('./hashicorp-vault/mocks').mockHashicorpVault;
},
}));

const url = 'http://vault/v1/transit';
const token = 'someAuthToken';
const accounts = [
{
name: 'Alice',
address: '5Ef2XHepJvTUJLhhx39Nf5iqu6AACrfFAmc6AW8a3hKF4Rdc',
publicKey: '0x72a5a53f6a04459a8e8ed266cc048db7f8c8d3faac0204f99ed593400bad636c',
version: 1,
},
{
name: 'Bob',
address: '5HQLVKFYkytr9HisQRWoUArUWw8YNWUmhLdXztRFjqysiNUx',
publicKey: '0xec2624ca769be5bc57cd23f0f1d8c06a0f68ac06a57e00355361d45000af7c28',
version: 2,
},
{
name: 'Charlie',
address: '5Cg3MNhhuPD5UUjXjjKNszzXic5KMDwMpUansgPVqb9KoE54',
publicKey: '0x1af337073aac07c2622ba393854850341cff112d5d6380def23ee323b0d48802',
version: 1,
},
];

beforeEach(() => {
mockHashicorpVault.fetchAllKeys.mockResolvedValue(
accounts.map(({ name, publicKey, version }) => ({ name, publicKey, version }))
);
});

afterEach(() => {
jest.resetAllMocks();
});

describe('HashicorpVaultSigningManager Class', () => {
let signingManager: HashicorpVaultSigningManager;

beforeEach(async () => {
signingManager = new HashicorpVaultSigningManager({
url,
token,
});

signingManager.setSs58Format(42);
});

describe('method: getAccounts', () => {
it('should return all Accounts held in the Vault', async () => {
const result = await signingManager.getAccounts();

expect(result).toEqual(accounts.map(({ address }) => address));
});

it("should throw an error if the Signing Manager doesn't have a SS58 format", async () => {
signingManager = new HashicorpVaultSigningManager({
url,
token,
});

expect(signingManager.getAccounts()).rejects.toThrow(
"Cannot call 'getAccounts' before calling 'setSs58Format'. Did you forget to use this Signing Manager to connect with the Polymesh SDK?"
);
});
});

describe('method: getExternalSigner', () => {
it('should return a Vault Signer', () => {
const signer = signingManager.getExternalSigner();
expect(signer instanceof VaultSigner).toBe(true);
});
});
});

describe('class VaultSigner', () => {
const expectedSignature = 'signature';

let signer: VaultSigner;
let registry: TypeRegistry;

beforeEach(() => {
registry = new TypeRegistry();
signer = new VaultSigner(mockHashicorpVault as unknown as HashicorpVault, registry);
mockHashicorpVault.signData.mockResolvedValue(expectedSignature);
});

describe('method signPayload', () => {
it('should return a signed payload and an incremental ID', async () => {
const payload = {
specVersion: '0x00000bb9',
transactionVersion: '0x00000002',
address: accounts[0].address,
blockHash: '0xdf06dca982acacbd5f0bcd7a8a062465b8441d569813561ed13ab81883bc08e7',
blockNumber: '0x00000280',
era: '0x0500',
genesisHash: '0x44748824f9798715435c421b5db9af2beae537974d192fab5fb6fc12e1523765',
method: '0x1a005041594c4f41445f54455354',
nonce: '0x00000001',
signedExtensions: [
'CheckSpecVersion',
'CheckTxVersion',
'CheckGenesis',
'CheckMortality',
'CheckNonce',
'CheckWeight',
'ChargeTransactionPayment',
],
tip: '0x00000000000000000000000000000000',
version: 4,
};

let result = await signer.signPayload(payload);

expect(result.id).toBe(0);
expect(result.signature).toBe(expectedSignature);

result = await signer.signPayload(payload);

expect(result.id).toBe(1);
expect(result.signature).toBe(expectedSignature);
});

it('should throw an error if the payload address is not present in the Vault', () => {
mockHashicorpVault.fetchAllKeys.mockResolvedValue([]);
return expect(
signer.signPayload({
address: '5Ef2XHepJvTUJLhhx39Nf5iqu6AACrfFAmc6AW8a3hKF4Rdc',
} as SignerPayloadJSON)
).rejects.toThrow('The signer cannot sign transactions on behalf of the calling Account');
});
});

describe('method signRaw', () => {
it('should return signed raw data and an incremental ID', async () => {
const address = accounts[0].address;
const data = u8aToHex(stringToU8a('Hello, my name is Alice'));
const raw = {
address,
data,
type: 'bytes' as const,
};

let result = await signer.signRaw(raw);

expect(result.id).toBe(0);
expect(result.signature).toBe(expectedSignature);

result = await signer.signRaw(raw);

expect(result.id).toBe(1);
expect(result.signature).toBe(expectedSignature);
});

it('should throw an error if the payload address is not present in the Vault', () => {
mockHashicorpVault.fetchAllKeys.mockResolvedValue([]);
return expect(
signer.signRaw({
address: '5Ef2XHepJvTUJLhhx39Nf5iqu6AACrfFAmc6AW8a3hKF4Rdc',
} as SignerPayloadRaw)
).rejects.toThrow('The signer cannot sign transactions on behalf of the calling Account');
});
});
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,151 @@
export function hashicorpVaultSigningManager(): string {
return 'hashicorp-vault-signing-manager';
import { TypeRegistry } from '@polkadot/types';
import { SignerPayloadJSON, SignerPayloadRaw, SignerResult } from '@polkadot/types/types';
import { hexToU8a, u8aToHex } from '@polkadot/util';
import { decodeAddress, encodeAddress } from '@polkadot/util-crypto';
import { PolkadotSigner, SigningManager } from '@polymathnetwork/signing-manager-types';

import { HashicorpVault, VaultKey } from './hashicorp-vault';

export class VaultSigner implements PolkadotSigner {
private currentId = -1;

/**
* @hidden
*/
constructor(private readonly vault: HashicorpVault, private readonly registry: TypeRegistry) {}

/**
* Sign a payload
*/
public async signPayload(payload: SignerPayloadJSON): Promise<SignerResult> {
const { registry } = this;
const { address, signedExtensions, version } = payload;

const { name, version: keyVersion } = await this.getVaultKey(address);

registry.setSignedExtensions(signedExtensions);

const signablePayload = registry.createType('ExtrinsicPayload', payload, {
version,
});

return this.signData(name, keyVersion, signablePayload.toU8a(true));
}

/**
* Sign raw data
*/
public async signRaw(raw: SignerPayloadRaw): Promise<SignerResult> {
const { address, data } = raw;

const { name, version } = await this.getVaultKey(address);

return this.signData(name, version, hexToU8a(data));
}

/**
* @hidden
*
* Use the Vault to sign raw data and return the signature + update ID
*/
private async signData(name: string, version: number, data: Uint8Array): Promise<SignerResult> {
const body = {
input: Buffer.from(data).toString('base64'),
key_version: version,
};

const signature = await this.vault.signData(name, body);

const id = (this.currentId += 1);

return {
signature,
id,
};
}

/**
* @hidden
*
* Get a key from the Vault
*
* @param address - SS58 formatted address
*
* @throws if there is no key with that address
*/
private async getVaultKey(address: string): Promise<VaultKey> {
const payloadPublicKey = u8aToHex(decodeAddress(address));

const allKeys = await this.vault.fetchAllKeys();

const foundKey = allKeys.find(({ publicKey }) => publicKey === payloadPublicKey);

if (!foundKey) {
throw new Error('The signer cannot sign transactions on behalf of the calling Account');
}

return foundKey;
}
}
export class HashicorpVaultSigningManager implements SigningManager {
private externalSigner: VaultSigner;
private vault: HashicorpVault;
private _ss58Format?: number;

/**
* Create an instance of the Hashicorp Vault Signing Manager
*
* @param args.url - points to where the vault is hosted
* @param args.token - authentication token used for signing
*/
public constructor(args: { url: string; token: string }) {
const { url, token } = args;

this.vault = new HashicorpVault(url, token);
this.externalSigner = new VaultSigner(this.vault, new TypeRegistry());
}

/**
* Set the SS58 format in which returned addresses will be encoded
*/
public setSs58Format(ss58Format: number): void {
this._ss58Format = ss58Format;
}

/**
* Return the addresses of all Accounts in the Hashicorp Vault
*
* @throws if called before calling `setSs58Format`. Normally, `setSs58Format` will be called by the SDK when instantiated
*/
public async getAccounts(): Promise<string[]> {
const ss58Format = this.getSs58Format('getAccounts');

const keys = await this.vault.fetchAllKeys();

return keys.map(({ publicKey }) => encodeAddress(publicKey, ss58Format));
}

/**
* Return a signer object that uses the underlying keyring pairs to sign
*/
public getExternalSigner(): PolkadotSigner {
return this.externalSigner;
}

/**
* @hidden
*
* @throws if the SS58 format hasn't been set yet
*/
private getSs58Format(methodName: string): number {
const { _ss58Format } = this;

if (_ss58Format === undefined) {
throw new Error(
`Cannot call '${methodName}' before calling 'setSs58Format'. Did you forget to use this Signing Manager to connect with the Polymesh SDK?`
);
}

return _ss58Format;
}
}

0 comments on commit f811dbc

Please sign in to comment.