-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(azure-signing-manager): add azure signing manager
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
1 parent
e9a191a
commit 02730cc
Showing
25 changed files
with
1,513 additions
and
6 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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ | |
"polywallet", | ||
"Resonse", | ||
"satoshi", | ||
"secp", | ||
"sonarcloud", | ||
"tscpaths", | ||
"Unsub" | ||
|
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
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 |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"presets": [["@nrwl/web/babel", { "useBuiltIns": "usage" }]] | ||
} |
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 |
---|---|---|
@@ -0,0 +1,18 @@ | ||
{ | ||
"extends": ["../../.eslintrc.json"], | ||
"ignorePatterns": ["!**/*"], | ||
"overrides": [ | ||
{ | ||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"], | ||
"rules": {} | ||
}, | ||
{ | ||
"files": ["*.ts", "*.tsx"], | ||
"rules": {} | ||
}, | ||
{ | ||
"files": ["*.js", "*.jsx"], | ||
"rules": {} | ||
} | ||
] | ||
} |
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 |
---|---|---|
@@ -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). |
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 |
---|---|---|
@@ -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', | ||
}; |
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 |
---|---|---|
@@ -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" | ||
} | ||
} |
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 |
---|---|---|
@@ -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": [] | ||
} |
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 |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './lib/azure-signing-manager'; | ||
export * from './types'; |
154 changes: 154 additions & 0 deletions
154
packages/azure-signing-manager/src/lib/azure-hsm/azure-hsm.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 |
---|---|---|
@@ -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' | ||
); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.