Skip to content

Commit

Permalink
Merge pull request #6 from impactility-dev/v2.1.0
Browse files Browse the repository at this point in the history
V2.1.0
  • Loading branch information
Sumit Hotchandani authored Jul 30, 2022
2 parents 2b118fc + b374a1c commit aab30e4
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 69 deletions.
75 changes: 69 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,101 @@
# ethersjs-azure-keyvault-signer
An Ethers.js compatible signer that connects to Azure Key Vault

<br/>

# Installation
Install the azure keyvault signer library using npm

`npm install ethersjs-azure-keyvault-signer`

<br/>

# Background
- 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.
- 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.
- 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.
- Our library allows enterprise users to interact with dapps without having to deal with browser wallets or the hassle of managing keys
- It enables the user to perform cryptographic operations like signing messages and transactions stored in their enterprises' Azure Key Vault or Managed HSM

## Azure Key Vault Credentials Interface
<br/>

Authentication to Azure Key Vault can be done either using client secret or client certificate.
# Azure Key Vault Credentials Interface

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

```ts
interface AzureKeyVaultCredentials {
keyName: string;
vaultName: string;
clientId: string;
tenantId: string;
clientId?: string;
tenantId?: string;
clientSecret?: string;
clientCertificatePath?: string;
accessToken?: AccessToken;
keyVersion?: string
}
```

<br/>

## Sample AzureKeyVaultCredentials objects

<br/>

- *Client Secret*
```ts
const keyVaultCredentials : AzureKeyVaultCredentials = {
keyName: 'my-key',
vaultUrl: 'https://my-vault.vault.azure.net',
clientId: 'ACIXXXXXXXXXXXX',
clientSecret: 'XXXXXXXXXXXXXXXXX',
tenantId: 'ATIXXXXXXXXXXXXXXXX',
keyVersion: '610f2XXXXXXXXXXX' //optional; if not included, latest version of the key is fetched
};
```

<br/>

- *Client Certificate*

```ts
const keyVaultCredentials : AzureKeyVaultCredentials = {
keyName: 'my-key',
vaultUrl: 'https://my-vault.vault.azure.net',
clientId: 'ACIXXXXXXXXXXXX',
clientCertificatePath: './directory/cert.pem',
tenantId: 'ATIXXXXXXXXXXXXXXXX',
keyVersion: '610f2XXXXXXXXXXX' //optional; if not included, latest version of the key is fetched
};
```
> Note: The client certificate should be a .pem encoded file with unencrypted private key included.

<br/>

- *Access Token*

```ts
import { AccessToken } from "@azure/core-auth";
const accessTokenObject : AccessToken = {
token: '<JWT-Access-Token>',
expiresOnTimestamp: '<expiration-time-of-token>', // can be obtained from the accessToken object in the application
};
const keyVaultCredentials : AzureKeyVaultCredentials = {
keyName: 'my-key',
vaultUrl: 'https://my-vault.vault.azure.net',
accessToken: accessTokenObject,
keyVersion: '610f2XXXXXXXXXXX' //optional; if not included, latest version of the key is fetched
};
```
<br/>

# Sample Usage

You need to provide the Azure Key Vault credentials to instantiate an instance of `AzureKeyVaultSigner` shown below:
You need to provide the Azure Key Vault credentials to instantiate an instance of `AzureKeyVaultSigner` shown below.

All examples below use client secret based authentication.


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

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

# LICENSE
MIT © [Impactility](https://github.com/impactility-dev)
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ethersjs-azure-keyvault-signer",
"version": "2.0.0",
"version": "2.1.0",
"description": "An Ethers.js compatible signer that connects to Azure Key Vault",
"main": "dist/index.js",
"scripts": {
Expand Down
6 changes: 4 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {AccessToken} from '@azure/identity';
import {ethers, UnsignedTransaction} from 'ethers';
import {getEthereumAddress,
getPublicKey,
Expand All @@ -12,10 +13,11 @@ import {getEthereumAddress,
export interface AzureKeyVaultCredentials {
keyName: string;
vaultUrl: string;
clientId: string;
clientId?: string;
tenantId?: string;
clientSecret?: string;
clientCertificatePath?: string;
tenantId: string;
accessToken?: AccessToken;
keyVersion?: string
}

Expand Down
136 changes: 76 additions & 60 deletions src/util/azure_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import {ethers} from 'ethers';
import {KeyClient, CryptographyClient, SignResult} from '@azure/keyvault-keys';
import {
ClientSecretCredential,
ClientCertificateCredential} from '@azure/identity';
ClientCertificateCredential,
} from '@azure/identity';
import {BN} from 'bn.js';
import {AzureKeyVaultCredentials} from '../index';
import {StaticTokenCredential} from './credentials';

/**
* function to connect to Key Vault using either
Expand All @@ -13,22 +15,13 @@ import {AzureKeyVaultCredentials} from '../index';
*/
export async function keyVaultConnect(keyVaultCredentials:
AzureKeyVaultCredentials): Promise<KeyClient> {
const keyVaultUrl = keyVaultCredentials.vaultUrl;
let credentials;

if (keyVaultCredentials.clientSecret) {
credentials = new ClientSecretCredential(
keyVaultCredentials.tenantId,
keyVaultCredentials.clientId,
keyVaultCredentials.clientSecret);
} else {
credentials = new ClientCertificateCredential(
keyVaultCredentials.tenantId,
keyVaultCredentials.clientId,
keyVaultCredentials.clientCertificatePath);
try {
const keyVaultUrl = keyVaultCredentials.vaultUrl;
const credentials = await getCredentials(keyVaultCredentials);
return new KeyClient(keyVaultUrl, credentials);
} catch (error) {
throw new Error(error);
}

return new KeyClient(keyVaultUrl, credentials);
}

/**
Expand All @@ -37,22 +30,33 @@ export async function keyVaultConnect(keyVaultCredentials:
*/
export async function getCredentials(keyVaultCredentials:
AzureKeyVaultCredentials):
Promise<ClientCertificateCredential | ClientSecretCredential> {
let credentials;
Promise<
ClientCertificateCredential | ClientSecretCredential | StaticTokenCredential
> {
try {
let credentials;

if (keyVaultCredentials.clientSecret) {
credentials = new ClientSecretCredential(
keyVaultCredentials.tenantId,
keyVaultCredentials.clientId,
keyVaultCredentials.clientSecret);
} else {
credentials = new ClientCertificateCredential(
keyVaultCredentials.tenantId,
keyVaultCredentials.clientId,
keyVaultCredentials.clientCertificatePath);
}
if (keyVaultCredentials.clientSecret) {
credentials = new ClientSecretCredential(
keyVaultCredentials.tenantId,
keyVaultCredentials.clientId,
keyVaultCredentials.clientSecret);
} else if (keyVaultCredentials.clientCertificatePath) {
credentials = new ClientCertificateCredential(
keyVaultCredentials.tenantId,
keyVaultCredentials.clientId,
keyVaultCredentials.clientCertificatePath);
} else if (keyVaultCredentials.accessToken) {
credentials = new StaticTokenCredential(
keyVaultCredentials.accessToken);
} else {
throw new Error('Credentials not found');
}

return credentials;
return credentials;
} catch (error) {
throw new Error(error);
}
}

/**
Expand Down Expand Up @@ -121,11 +125,15 @@ export async function sign(digest: Buffer,
* @return {any}
*/
function recoverPubKeyFromSig(msg: Buffer, r: BN, s: BN, v: number) {
return ethers.utils.recoverAddress(`0x${msg.toString('hex')}`, {
r: `0x${r.toString('hex')}`,
s: `0x${s.toString('hex')}`,
v,
});
try {
return ethers.utils.recoverAddress(`0x${msg.toString('hex')}`, {
r: `0x${r.toString('hex')}`,
s: `0x${s.toString('hex')}`,
v,
});
} catch (error) {
throw new Error(error);
}
}

/**
Expand All @@ -138,19 +146,23 @@ function recoverPubKeyFromSig(msg: Buffer, r: BN, s: BN, v: number) {
*/
export function determineCorrectV(
msg: Buffer, r: BN, s: BN, expectedEthAddr: string) {
// This is the wrapper function to find the right v value
// There are two matching signatures on the elliptic curve
// we need to find the one that matches to our public key
// it can be v = 27 or v = 28
let v = 27;
let pubKey = recoverPubKeyFromSig(msg, r, s, v);
if (pubKey.toLowerCase() !== expectedEthAddr.toLowerCase()) {
// if the pub key for v = 27 does not match
// it has to be v = 28
v = 28;
pubKey = recoverPubKeyFromSig(msg, r, s, v);
try {
// This is the wrapper function to find the right v value
// There are two matching signatures on the elliptic curve
// we need to find the one that matches to our public key
// it can be v = 27 or v = 28
let v = 27;
let pubKey = recoverPubKeyFromSig(msg, r, s, v);
if (pubKey.toLowerCase() !== expectedEthAddr.toLowerCase()) {
// if the pub key for v = 27 does not match
// it has to be v = 28
v = 28;
pubKey = recoverPubKeyFromSig(msg, r, s, v);
}
return {pubKey, v};
} catch (error) {
throw new Error(error);
}
return {pubKey, v};
}

/**
Expand All @@ -159,20 +171,24 @@ export function determineCorrectV(
* @return {any}
*/
export function recoverSignature(signature: Buffer) {
const r = new BN(signature.slice(0, 32));
const s = new BN(signature.slice(32, 64));
try {
const r = new BN(signature.slice(0, 32));
const s = new BN(signature.slice(32, 64));

const secp256k1N = new BN(
'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141',
16,
); // max value on the curve
const secp256k1halfN = secp256k1N.div(new BN(2)); // half of the curve
// Because of EIP-2 not all elliptic curve signatures are accepted
// the value of s needs to be SMALLER than half of the curve
// i.e. we need to flip s if it's greater than half of the curve
// if s is less than half of the curve,
// we're on the "good" side of the curve, we can just return
return {r, s: s.gt(secp256k1halfN) ? secp256k1N.sub(s) : s};
const secp256k1N = new BN(
'fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141',
16,
); // max value on the curve
const secp256k1halfN = secp256k1N.div(new BN(2)); // half of the curve
// Because of EIP-2 not all elliptic curve signatures are accepted
// the value of s needs to be SMALLER than half of the curve
// i.e. we need to flip s if it's greater than half of the curve
// if s is less than half of the curve,
// we're on the "good" side of the curve, we can just return
return {r, s: s.gt(secp256k1halfN) ? secp256k1N.sub(s) : s};
} catch (error) {
throw new Error(error);
}
}

/**
Expand Down
19 changes: 19 additions & 0 deletions src/util/credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {TokenCredential, AccessToken} from '@azure/identity';

/**
* class implementing StaticTokenCredential for AccessToken compatibility
*/
export class StaticTokenCredential implements TokenCredential {
/**
* @param {AccessToken} accessToken
*/
constructor(private accessToken: AccessToken) {}

/**
* override getToken function from Token Credentials
* to get the access token object
*/
async getToken(): Promise<AccessToken> {
return this.accessToken;
}
}

0 comments on commit aab30e4

Please sign in to comment.