Skip to content

Commit aab30e4

Browse files
author
Sumit Hotchandani
authored
Merge pull request #6 from impactility-dev/v2.1.0
V2.1.0
2 parents 2b118fc + b374a1c commit aab30e4

File tree

5 files changed

+169
-69
lines changed

5 files changed

+169
-69
lines changed

README.md

+69-6
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,101 @@
44
# ethersjs-azure-keyvault-signer
55
An Ethers.js compatible signer that connects to Azure Key Vault
66

7+
<br/>
8+
79
# Installation
810
Install the azure keyvault signer library using npm
911

1012
`npm install ethersjs-azure-keyvault-signer`
1113

14+
<br/>
15+
1216
# Background
1317
- Current web3 signers only support keys managed by the users directly in the form of browser wallets like Metamask, WalletConnect, Hardware wallets or self managed keys.
1418
- Enterprises prefer to maintain the private keys in a secured key store like Azure Key Vault rather than letting their employees handle their private keys.
1519
- Private keys generated and stored in key stores like Azure Key Vault/HSM are never exposed directly to the users. Interaction with such keys is done via SDKs developed by the respective key stores.
1620
- Our library allows enterprise users to interact with dapps without having to deal with browser wallets or the hassle of managing keys
1721
- It enables the user to perform cryptographic operations like signing messages and transactions stored in their enterprises' Azure Key Vault or Managed HSM
1822

19-
## Azure Key Vault Credentials Interface
23+
<br/>
2024

21-
Authentication to Azure Key Vault can be done either using client secret or client certificate.
25+
# Azure Key Vault Credentials Interface
2226

23-
> Note: The client certificate should be a .pem encoded file with unencrypted private key included.
27+
Authentication to Azure Key Vault can be done either using client secret, client certificate or access token(with the Key Vault scope).
2428

2529
```ts
2630
interface AzureKeyVaultCredentials {
2731
keyName: string;
2832
vaultName: string;
29-
clientId: string;
30-
tenantId: string;
33+
clientId?: string;
34+
tenantId?: string;
3135
clientSecret?: string;
3236
clientCertificatePath?: string;
37+
accessToken?: AccessToken;
3338
keyVersion?: string
3439
}
3540
```
3641

42+
<br/>
43+
44+
## Sample AzureKeyVaultCredentials objects
45+
46+
<br/>
47+
48+
- *Client Secret*
49+
```ts
50+
const keyVaultCredentials : AzureKeyVaultCredentials = {
51+
keyName: 'my-key',
52+
vaultUrl: 'https://my-vault.vault.azure.net',
53+
clientId: 'ACIXXXXXXXXXXXX',
54+
clientSecret: 'XXXXXXXXXXXXXXXXX',
55+
tenantId: 'ATIXXXXXXXXXXXXXXXX',
56+
keyVersion: '610f2XXXXXXXXXXX' //optional; if not included, latest version of the key is fetched
57+
};
58+
```
59+
60+
<br/>
61+
62+
- *Client Certificate*
63+
64+
```ts
65+
const keyVaultCredentials : AzureKeyVaultCredentials = {
66+
keyName: 'my-key',
67+
vaultUrl: 'https://my-vault.vault.azure.net',
68+
clientId: 'ACIXXXXXXXXXXXX',
69+
clientCertificatePath: './directory/cert.pem',
70+
tenantId: 'ATIXXXXXXXXXXXXXXXX',
71+
keyVersion: '610f2XXXXXXXXXXX' //optional; if not included, latest version of the key is fetched
72+
};
73+
```
74+
> Note: The client certificate should be a .pem encoded file with unencrypted private key included.
75+
76+
<br/>
77+
78+
- *Access Token*
79+
80+
```ts
81+
import { AccessToken } from "@azure/core-auth";
82+
83+
const accessTokenObject : AccessToken = {
84+
token: '<JWT-Access-Token>',
85+
expiresOnTimestamp: '<expiration-time-of-token>', // can be obtained from the accessToken object in the application
86+
};
87+
88+
const keyVaultCredentials : AzureKeyVaultCredentials = {
89+
keyName: 'my-key',
90+
vaultUrl: 'https://my-vault.vault.azure.net',
91+
accessToken: accessTokenObject,
92+
keyVersion: '610f2XXXXXXXXXXX' //optional; if not included, latest version of the key is fetched
93+
};
94+
```
95+
<br/>
3796

3897
# Sample Usage
3998

40-
You need to provide the Azure Key Vault credentials to instantiate an instance of `AzureKeyVaultSigner` shown below:
99+
You need to provide the Azure Key Vault credentials to instantiate an instance of `AzureKeyVaultSigner` shown below.
100+
101+
All examples below use client secret based authentication.
41102

42103

43104
```ts
@@ -61,6 +122,7 @@ azureKeyVaultSigner = azureKeyVaultSigner.connect(provider);
61122
const tx = await azureKeyVaultSigner.sendTransaction({ to: '0x19De7137aEba698D5970d0B2d41eB03e0F97fA56', value: 2 });
62123
console.log(tx);
63124
```
125+
<br/>
64126

65127
# Examples
66128
The following section provides code snippets that cover functionalities offered by ethersjs-azure-keyvault-signer package.
@@ -165,6 +227,7 @@ to: '0x19De7137aEba698D5970d0B2d41eB03e0F97fA56',
165227
const signedTransaction = await azureKeyVaultSigner.signTransaction(transaction);
166228
console.log(signedTransaction);
167229
```
230+
<br/>
168231

169232
# LICENSE
170233
MIT © [Impactility](https://github.com/impactility-dev)

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ethersjs-azure-keyvault-signer",
3-
"version": "2.0.0",
3+
"version": "2.1.0",
44
"description": "An Ethers.js compatible signer that connects to Azure Key Vault",
55
"main": "dist/index.js",
66
"scripts": {

src/index.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {AccessToken} from '@azure/identity';
12
import {ethers, UnsignedTransaction} from 'ethers';
23
import {getEthereumAddress,
34
getPublicKey,
@@ -12,10 +13,11 @@ import {getEthereumAddress,
1213
export interface AzureKeyVaultCredentials {
1314
keyName: string;
1415
vaultUrl: string;
15-
clientId: string;
16+
clientId?: string;
17+
tenantId?: string;
1618
clientSecret?: string;
1719
clientCertificatePath?: string;
18-
tenantId: string;
20+
accessToken?: AccessToken;
1921
keyVersion?: string
2022
}
2123

src/util/azure_utils.ts

+76-60
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ import {ethers} from 'ethers';
22
import {KeyClient, CryptographyClient, SignResult} from '@azure/keyvault-keys';
33
import {
44
ClientSecretCredential,
5-
ClientCertificateCredential} from '@azure/identity';
5+
ClientCertificateCredential,
6+
} from '@azure/identity';
67
import {BN} from 'bn.js';
78
import {AzureKeyVaultCredentials} from '../index';
9+
import {StaticTokenCredential} from './credentials';
810

911
/**
1012
* function to connect to Key Vault using either
@@ -13,22 +15,13 @@ import {AzureKeyVaultCredentials} from '../index';
1315
*/
1416
export async function keyVaultConnect(keyVaultCredentials:
1517
AzureKeyVaultCredentials): Promise<KeyClient> {
16-
const keyVaultUrl = keyVaultCredentials.vaultUrl;
17-
let credentials;
18-
19-
if (keyVaultCredentials.clientSecret) {
20-
credentials = new ClientSecretCredential(
21-
keyVaultCredentials.tenantId,
22-
keyVaultCredentials.clientId,
23-
keyVaultCredentials.clientSecret);
24-
} else {
25-
credentials = new ClientCertificateCredential(
26-
keyVaultCredentials.tenantId,
27-
keyVaultCredentials.clientId,
28-
keyVaultCredentials.clientCertificatePath);
18+
try {
19+
const keyVaultUrl = keyVaultCredentials.vaultUrl;
20+
const credentials = await getCredentials(keyVaultCredentials);
21+
return new KeyClient(keyVaultUrl, credentials);
22+
} catch (error) {
23+
throw new Error(error);
2924
}
30-
31-
return new KeyClient(keyVaultUrl, credentials);
3225
}
3326

3427
/**
@@ -37,22 +30,33 @@ export async function keyVaultConnect(keyVaultCredentials:
3730
*/
3831
export async function getCredentials(keyVaultCredentials:
3932
AzureKeyVaultCredentials):
40-
Promise<ClientCertificateCredential | ClientSecretCredential> {
41-
let credentials;
33+
Promise<
34+
ClientCertificateCredential | ClientSecretCredential | StaticTokenCredential
35+
> {
36+
try {
37+
let credentials;
4238

43-
if (keyVaultCredentials.clientSecret) {
44-
credentials = new ClientSecretCredential(
45-
keyVaultCredentials.tenantId,
46-
keyVaultCredentials.clientId,
47-
keyVaultCredentials.clientSecret);
48-
} else {
49-
credentials = new ClientCertificateCredential(
50-
keyVaultCredentials.tenantId,
51-
keyVaultCredentials.clientId,
52-
keyVaultCredentials.clientCertificatePath);
53-
}
39+
if (keyVaultCredentials.clientSecret) {
40+
credentials = new ClientSecretCredential(
41+
keyVaultCredentials.tenantId,
42+
keyVaultCredentials.clientId,
43+
keyVaultCredentials.clientSecret);
44+
} else if (keyVaultCredentials.clientCertificatePath) {
45+
credentials = new ClientCertificateCredential(
46+
keyVaultCredentials.tenantId,
47+
keyVaultCredentials.clientId,
48+
keyVaultCredentials.clientCertificatePath);
49+
} else if (keyVaultCredentials.accessToken) {
50+
credentials = new StaticTokenCredential(
51+
keyVaultCredentials.accessToken);
52+
} else {
53+
throw new Error('Credentials not found');
54+
}
5455

55-
return credentials;
56+
return credentials;
57+
} catch (error) {
58+
throw new Error(error);
59+
}
5660
}
5761

5862
/**
@@ -121,11 +125,15 @@ export async function sign(digest: Buffer,
121125
* @return {any}
122126
*/
123127
function recoverPubKeyFromSig(msg: Buffer, r: BN, s: BN, v: number) {
124-
return ethers.utils.recoverAddress(`0x${msg.toString('hex')}`, {
125-
r: `0x${r.toString('hex')}`,
126-
s: `0x${s.toString('hex')}`,
127-
v,
128-
});
128+
try {
129+
return ethers.utils.recoverAddress(`0x${msg.toString('hex')}`, {
130+
r: `0x${r.toString('hex')}`,
131+
s: `0x${s.toString('hex')}`,
132+
v,
133+
});
134+
} catch (error) {
135+
throw new Error(error);
136+
}
129137
}
130138

131139
/**
@@ -138,19 +146,23 @@ function recoverPubKeyFromSig(msg: Buffer, r: BN, s: BN, v: number) {
138146
*/
139147
export function determineCorrectV(
140148
msg: Buffer, r: BN, s: BN, expectedEthAddr: string) {
141-
// This is the wrapper function to find the right v value
142-
// There are two matching signatures on the elliptic curve
143-
// we need to find the one that matches to our public key
144-
// it can be v = 27 or v = 28
145-
let v = 27;
146-
let pubKey = recoverPubKeyFromSig(msg, r, s, v);
147-
if (pubKey.toLowerCase() !== expectedEthAddr.toLowerCase()) {
148-
// if the pub key for v = 27 does not match
149-
// it has to be v = 28
150-
v = 28;
151-
pubKey = recoverPubKeyFromSig(msg, r, s, v);
149+
try {
150+
// This is the wrapper function to find the right v value
151+
// There are two matching signatures on the elliptic curve
152+
// we need to find the one that matches to our public key
153+
// it can be v = 27 or v = 28
154+
let v = 27;
155+
let pubKey = recoverPubKeyFromSig(msg, r, s, v);
156+
if (pubKey.toLowerCase() !== expectedEthAddr.toLowerCase()) {
157+
// if the pub key for v = 27 does not match
158+
// it has to be v = 28
159+
v = 28;
160+
pubKey = recoverPubKeyFromSig(msg, r, s, v);
161+
}
162+
return {pubKey, v};
163+
} catch (error) {
164+
throw new Error(error);
152165
}
153-
return {pubKey, v};
154166
}
155167

156168
/**
@@ -159,20 +171,24 @@ export function determineCorrectV(
159171
* @return {any}
160172
*/
161173
export function recoverSignature(signature: Buffer) {
162-
const r = new BN(signature.slice(0, 32));
163-
const s = new BN(signature.slice(32, 64));
174+
try {
175+
const r = new BN(signature.slice(0, 32));
176+
const s = new BN(signature.slice(32, 64));
164177

165-
const secp256k1N = new BN(
166-
'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141',
167-
16,
168-
); // max value on the curve
169-
const secp256k1halfN = secp256k1N.div(new BN(2)); // half of the curve
170-
// Because of EIP-2 not all elliptic curve signatures are accepted
171-
// the value of s needs to be SMALLER than half of the curve
172-
// i.e. we need to flip s if it's greater than half of the curve
173-
// if s is less than half of the curve,
174-
// we're on the "good" side of the curve, we can just return
175-
return {r, s: s.gt(secp256k1halfN) ? secp256k1N.sub(s) : s};
178+
const secp256k1N = new BN(
179+
'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141',
180+
16,
181+
); // max value on the curve
182+
const secp256k1halfN = secp256k1N.div(new BN(2)); // half of the curve
183+
// Because of EIP-2 not all elliptic curve signatures are accepted
184+
// the value of s needs to be SMALLER than half of the curve
185+
// i.e. we need to flip s if it's greater than half of the curve
186+
// if s is less than half of the curve,
187+
// we're on the "good" side of the curve, we can just return
188+
return {r, s: s.gt(secp256k1halfN) ? secp256k1N.sub(s) : s};
189+
} catch (error) {
190+
throw new Error(error);
191+
}
176192
}
177193

178194
/**

src/util/credentials.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {TokenCredential, AccessToken} from '@azure/identity';
2+
3+
/**
4+
* class implementing StaticTokenCredential for AccessToken compatibility
5+
*/
6+
export class StaticTokenCredential implements TokenCredential {
7+
/**
8+
* @param {AccessToken} accessToken
9+
*/
10+
constructor(private accessToken: AccessToken) {}
11+
12+
/**
13+
* override getToken function from Token Credentials
14+
* to get the access token object
15+
*/
16+
async getToken(): Promise<AccessToken> {
17+
return this.accessToken;
18+
}
19+
}

0 commit comments

Comments
 (0)