-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(hashicorp-vault-signing-manager): implement signing manager
add unit tests and modify README
- Loading branch information
Showing
11 changed files
with
746 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; |
177 changes: 173 additions & 4 deletions
177
packages/hashicorp-vault-signing-manager/src/lib/hashicorp-vault-signing-manager.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); | ||
}); | ||
}); |
152 changes: 150 additions & 2 deletions
152
packages/hashicorp-vault-signing-manager/src/lib/hashicorp-vault-signing-manager.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.