Skip to content

Commit

Permalink
feat(azure-signing-manager): add azure signing manager
Browse files Browse the repository at this point in the history
add azure signing manager that allows for keys to be stored in an Azure Key Vault and to be used
with the Polymesh SDK

DA-1322
  • Loading branch information
polymath-eric committed Oct 4, 2024
1 parent e9a191a commit 02730cc
Show file tree
Hide file tree
Showing 25 changed files with 1,513 additions and 6 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"polywallet",
"Resonse",
"satoshi",
"secp",
"sonarcloud",
"tscpaths",
"Unsub"
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# SigningManagers

Monorepo for Polymesh SDK compatible Signing Managers.
Monorepo for Polymesh SDK compatible Signing Managers. A signing manager abstracts key signing operations to provide a uniform interface for a variety of key storage solutions

## Projects

Expand All @@ -10,6 +10,7 @@ Monorepo for Polymesh SDK compatible Signing Managers.
| **Browser Extension Signing Manager** | [`@polymeshassociation/browser-extension-signing-manager`](https://npmjs.com/package/@polymeshassociation/browser-extension-signing-manager) | [![npm latest version](https://img.shields.io/npm/v/@polymeshassociation/browser-extension-signing-manager/latest.svg)](https://www.npmjs.com/package/@polymeshassociation/browser-extension-signing-manager) | [![README](https://img.shields.io/badge/README--green.svg)](/packages/browser-extension-signing-manager/README.md) [![CHANGELOG](https://img.shields.io/badge/CHANGELOG--orange.svg)](/packages/browser-extension-signing-manager/CHANGELOG.md) |
| **Hashicorp Vault Signing Manager** | [`@polymeshassociation/hashicorp-vault-signing-manager`](https://npmjs.com/package/@polymeshassociation/hashicorp-vault-signing-manager) | [![npm latest version](https://img.shields.io/npm/v/@polymeshassociation/hashicorp-vault-signing-manager/latest.svg)](https://www.npmjs.com/package/@polymeshassociation/hashicorp-vault-signing-manager) | [![README](https://img.shields.io/badge/README--green.svg)](/packages/hashicorp-vault-signing-manager/README.md) [![CHANGELOG](https://img.shields.io/badge/CHANGELOG--orange.svg)](/packages/hashicorp-vault-signing-manager/CHANGELOG.md) |
| **Fireblocks Signing Manager** | [`@polymeshassociation/fireblocks-signing-manager`](https://npmjs.com/package/@polymeshassociation/fireblocks-signing-manager) | [![npm latest version](https://img.shields.io/npm/v/@polymeshassociation/fireblocks-signing-manager/latest.svg)](https://www.npmjs.com/package/@polymeshassociation/fireblocks-signing-manager) | [![README](https://img.shields.io/badge/README--green.svg)](/packages/fireblocks-signing-manager/README.md) [![CHANGELOG](https://img.shields.io/badge/CHANGELOG--orange.svg)](/packages/fireblocks-signing-manager/CHANGELOG.md) |
| **Azure Signing Manager** | [`@polymeshassociation/azure-signing-manager`](https://npmjs.com/package/@polymeshassociation/azure-signing-manager) | [![npm latest version](https://img.shields.io/npm/v/@polymeshassociation/azure-signing-manager/latest.svg)](https://www.npmjs.com/package/@polymeshassociation/azure-signing-manager) | [![README](https://img.shields.io/badge/README--green.svg)](/packages/azure-signing-manager/README.md) [![CHANGELOG](https://img.shields.io/badge/CHANGELOG--orange.svg)](/packages/azure-signing-manager/CHANGELOG.md) |

## Scripts

Expand Down
1 change: 1 addition & 0 deletions commitlint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module.exports = {
'approval-signing-manager',
'fireblocks-signing-manager',
'walletconnect-signing-manager',
'azure-signing-manager',
],
],
},
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@babel/preset-env": "^7.16.11",
"@commitlint/cli": "^16.2.1",
"@commitlint/config-conventional": "^16.2.1",
"@golevelup/ts-jest": "^0.5.6",
"@ng-easy/builders": "^5.2.0",
"@nrwl/cli": "13.8.2",
"@nrwl/devkit": "13.8.2",
Expand All @@ -48,6 +49,8 @@
"@types/node": "16.11.7",
"@types/require-from-string": "^1.2.1",
"@typescript-eslint/eslint-plugin": "~5.10.0",
"@types/jest-when": "^3.5.2",
"jest-when": "^3.6.0",
"@typescript-eslint/parser": "~5.10.0",
"babel-loader": "^8.2.3",
"commitiquette": "^1.2.1",
Expand Down
3 changes: 3 additions & 0 deletions packages/azure-signing-manager/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]]
}
18 changes: 18 additions & 0 deletions packages/azure-signing-manager/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
53 changes: 53 additions & 0 deletions packages/azure-signing-manager/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# azure-signing-manager

Polymesh SDK compatible signing manager. This allows Polymesh transactions to be signed with keys in an Microsoft Azure [key vault](https://azure.microsoft.com/en-us/products/key-vault). The keys must be "EC"
type and use curve "P-256K". The signing manager will ignore any other type.

## Usage

```typescript
import { AzureSigningManager } from '@polymeshassociation/azure-signing-manager';
import { Polymesh } from '@polymeshassociation/polymesh-sdk';

// defaults to constructing `new DefaultAzureCredential()` for credential
const signingManager = new AzureSigningManager({
keyVaultUrl: 'https://somekeyvault.vault.azure.net/',
});

const polymesh = await Polymesh.connect({
nodeUrl,
signingManager,
});

const newKey = await signingManager.createKey('myKey') // keys can be created in the Azure UI or CLI as well
console.log('created key with address: ', newKey.address) // address is the primary way of specifying public keys on Polymesh
```

Details about the default credential behavior can be found [here](https://learn.microsoft.com/en-us/javascript/api/@azure/identity/defaultazurecredential?view=azure-node-latest#@azure-identity-defaultazurecredential-constructor). You can optionally pass in your own credential object and it will be used instead.


## Performance Note (for 1000+ keys)

The current implementation enumerates all possible keys and their versions to construct an index of public key to key name lookup. As an integrator you will likely have this data already indexed. If N+1 style performance issues are a concern the constructor can be extended where a lookup you provide can be called. e.g.

```ts
interface {
getKeyName(address: string): Promise<{ name: string; version: string }>
}
```

For now it is recommended to have a key vault dedicated to Polymesh keys. Please open an issue if you performing the additional work is worth having thousands of keys stored.

For Reference - Azure key vault pricing ($USD), October 2024:
```
First 250 keys $5 per key per month
From 251 – 1,500 keys $2.50 per key per month
From 1,501 – 4,000 keys $0.90 per key per month
4,001+ keys $0.40 per key per month
```

## Running unit tests

Run `nx test azure-signing-manager` to execute the unit tests via [Jest](https://jestjs.io).

This library was generated with [Nx](https://nx.dev).
12 changes: 12 additions & 0 deletions packages/azure-signing-manager/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = {
displayName: 'azure-signing-manager',
preset: '../../jest.preset.js',
globals: {
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json',
},
},
testEnvironment: 'node',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/packages/azure-signing-manager',
};
25 changes: 25 additions & 0 deletions packages/azure-signing-manager/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@polymeshassociation/azure-signing-manager",
"version": "0.0.1",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@polymeshassociation/signing-manager-types": "^3.3.0",
"@azure/keyvault-keys": "4.8.0",
"@azure/identity": "4.4.1",
"secp256k1": "5.0.0"
},
"devDependencies": {
"@types/secp256k1": "4.0.6"
},
"peerDependencies": {
"@polymeshassociation/polymesh-sdk": ">=15.0.0"
},
"main": "./index.js",
"typings": "./index.d.ts",
"repository": {
"type": "git",
"url": "https://github.com/PolymeshAssociation/signing-managers.git"
}
}
51 changes: 51 additions & 0 deletions packages/azure-signing-manager/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"root": "packages/azure-signing-manager",
"sourceRoot": "packages/azure-signing-manager/src",
"projectType": "library",
"targets": {
"build": {
"executor": "@nrwl/node:package",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/packages/azure-signing-manager",
"tsConfig": "packages/azure-signing-manager/tsconfig.lib.json",
"packageJson": "packages/azure-signing-manager/package.json",
"main": "packages/azure-signing-manager/src/index.ts",
"assets": ["packages/azure-signing-manager/*.md"],
"srcRootForCompilationRoot": "packages/azure-signing-manager/src"
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["packages/azure-signing-manager/**/*.ts"]
}
},
"test": {
"executor": "@nrwl/jest:jest",
"outputs": ["coverage/packages/azure-signing-manager"],
"options": {
"jestConfig": "packages/azure-signing-manager/jest.config.js",
"passWithNoTests": true
}
},
"release": {
"executor": "@ng-easy/builders:semantic-release",
"configurations": {
"local": {
"force": true
}
}
},
"run-local": {
"executor": "./tools/executors/run-local:run-local",
"options": {
"runInBrowser": false,
"path": "packages/azure-signing-manager/sandbox/index.ts",
"port": 9000
}
}
},
"tags": []
}
2 changes: 2 additions & 0 deletions packages/azure-signing-manager/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './lib/azure-signing-manager';
export * from './types';
154 changes: 154 additions & 0 deletions packages/azure-signing-manager/src/lib/azure-hsm/azure-hsm.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import {
CryptographyClient,
KeyClient,
KeyProperties,
KeyVaultKey,
KnownSignatureAlgorithms,
} from '@azure/keyvault-keys';
import { createMock, DeepMocked } from '@golevelup/ts-jest';
import { when } from 'jest-when';

import { AzureHsm } from './azure-hsm';
import { AzureSignerError } from './types';
import { createPagedAsyncIterableIterator } from './util';

const mockKeyClient = createMock<KeyClient>();
const mockCryptographyClient = createMock<CryptographyClient>();
const mockKeyVaultKey = createMock<KeyVaultKey>();

jest.mock('@azure/keyvault-keys', () => ({
...jest.requireActual('@azure/keyvault-keys'),
KeyClient: jest.fn().mockImplementation(() => mockKeyClient),
CryptographyClient: jest.fn().mockImplementation(() => mockCryptographyClient),
KeyVaultKey: jest.fn().mockImplementation(() => mockKeyVaultKey),
}));

describe('AzureHsm', () => {
let azure: AzureHsm;
let mockKey: DeepMocked<KeyVaultKey>;
let mockNewKey: DeepMocked<KeyVaultKey>;
let mockPartialKey: DeepMocked<KeyVaultKey>;

const x = Buffer.from('d6eb9ae6b44331da8c92a0bd88850b94c7f32981aaf42d1c1345e05af810470a', 'hex');
const y = Buffer.from('7b4603950822dd8806494a3aff5d6bc2e437b71f7c7de64e9d84a60411dc1293', 'hex');
const publicKey = '0x63eb973f33ad5737a1ea2e4100c04e806c403b5994d986f0c71a2fd80dbbd179';

beforeEach(() => {
azure = new AzureHsm({ keyVaultUrl: 'https://example.com' });

mockKey = createMock<KeyVaultKey>({
key: { x, y, crv: 'P-256K' },
properties: { version: 'v1' },
});
mockNewKey = createMock<KeyVaultKey>({
key: { x, y, crv: 'P-256K' },
properties: { version: 'v1' },
});
mockPartialKey = createMock<KeyVaultKey>({
key: { x: undefined, y: undefined, crv: 'P-256K' },
properties: { version: 'v1' },
});

const mockKeyProperties: KeyProperties = {
name: 'someKey',
vaultUrl: 'http://example.com/keyOne',
version: 'v1',
};

const mockIterator = createPagedAsyncIterableIterator([mockKeyProperties]);

mockKeyClient.listPropertiesOfKeys.mockReturnValue(mockIterator);
mockKeyClient.listPropertiesOfKeyVersions.mockReturnValue(
createPagedAsyncIterableIterator([mockKey.properties])
);

mockKeyClient.getKey.mockRejectedValue({ code: 'KeyNotFound' });

mockKeyClient.createKey.mockImplementation(async name => {
when(mockKeyClient.getKey).calledWith(name, { version: 'v1' }).mockResolvedValue(mockKey);
when(mockKeyClient.listPropertiesOfKeyVersions)
.calledWith(name)
.mockReturnValue(createPagedAsyncIterableIterator([{ ...mockKeyProperties, name }]));

return mockKey;
});
when(mockKeyClient.getKey).calledWith('someKey', { version: 'v1' }).mockResolvedValue(mockKey);

when(mockKeyClient.getKey)
.calledWith('partialKey', { version: 'v1' })
.mockResolvedValue(mockPartialKey);

mockCryptographyClient.sign.mockResolvedValue({
result: Buffer.from(
'b4b24959854a6afdba21e1007955c6c728725009feab9886408d6dc4fb9712e3ab5f30be4f06d2f12f01a10ccd374278d44ab133ed8ee5998341034f9347f156',
'hex'
),
algorithm: KnownSignatureAlgorithms.ES256K,
});

mockKeyClient.getCryptographyClient.mockReturnValue(mockCryptographyClient);
});

describe('createKey', () => {
it('should create a key', async () => {
const result = await azure.createKey('newKey');

expect(result).toEqual(
expect.objectContaining({
name: 'newKey',
keyVaultKey: mockNewKey,
})
);
});
});

describe('fetchAllKeys', () => {
it('should return all of the keys', async () => {
const result = await azure.fetchAllKeys();

expect(result).toEqual(
expect.arrayContaining([expect.objectContaining({ name: 'someKey', keyVaultKey: mockKey })])
);
});
});

describe('getAzureKeyByPubKey', () => {
it('should return the key', async () => {
const key = await azure.getAzureKeyByPubKey(publicKey);

expect(key).toEqual(expect.objectContaining({ name: 'someKey', keyVaultKey: mockKey }));
});
});

describe('getAzureKey', () => {
it('should return the azure key', async () => {
const result = await azure.getAzureKeyVersions('someKey');

expect(result).toEqual(
expect.arrayContaining([expect.objectContaining({ name: 'someKey', keyVaultKey: mockKey })])
);
});

it('should return empty array if key is not found', () => {
return expect(azure.getAzureKeyVersions('unknownKey')).resolves.toEqual([]);
});

it('should throw an error if the key is missing details', async () => {
const expectedError = new AzureSignerError({ message: 'essential key details missing' });

return expect(azure.getAzureKeyVersions('partialKey')).rejects.toThrow(expectedError);
});
});

describe('signData', () => {
it('should sign the data', async () => {
const bytes = Buffer.from('00', 'hex');

const result = await azure.signData(publicKey, bytes);

expect(result).toEqual(
'0x02b4b24959854a6afdba21e1007955c6c728725009feab9886408d6dc4fb9712e3ab5f30be4f06d2f12f01a10ccd374278d44ab133ed8ee5998341034f9347f15601'
);
});
});
});
Loading

0 comments on commit 02730cc

Please sign in to comment.