Skip to content
This repository has been archived by the owner on Aug 2, 2022. It is now read-only.

Web Crypto Signature Provider and conversion methods #937

Merged
merged 22 commits into from
Sep 7, 2021
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9b880ef
Web Crypto Signature Provider
May 19, 2021
b8999f3
Updating package.json and yarn.lock
blockone-devops May 19, 2021
73e0c62
Hardening the Crypto Key Signature Provider
May 20, 2021
57ea202
Multiple changes and tests for web/node. More tests soon and docs
May 27, 2021
a3c7189
Updating nodejs version in ci/cd to resolve web crypto issue
May 27, 2021
10c4f26
Correct high-S to low-s method
May 28, 2021
561be1e
Switching to different method
May 28, 2021
adca5aa
Remaining tests for Web Crypto
May 28, 2021
1d98c3e
Updating package.json and yarn.lock
blockone-devops May 29, 2021
69385c4
Concise documentation for the WebCryptoSignatureProvider
May 29, 2021
3652757
Merge remote-tracking branch 'origin/develop' into subtleCrypto
May 29, 2021
132e8f5
Resolving a circular dependency issue
May 31, 2021
fd0d029
Updating package.json and yarn.lock
blockone-devops May 31, 2021
c18433b
Removing constructElliptic to avoid more circular dependency issues
May 31, 2021
9f8eef5
Removing the possibility of proving an ec object to avoid issues
Jun 23, 2021
4c1c1f0
Merge branch 'develop' into subtleCrypto
Jul 9, 2021
93532da
Merge remote-tracking branch 'origin/develop' into subtleCrypto
Aug 16, 2021
caa965a
Adding resources to generated account
Aug 16, 2021
3a7cca5
Updating package.json and yarn.lock
blockone-devops Aug 16, 2021
83b272e
Merge branch 'develop' into subtleCrypto
Aug 27, 2021
8cb9d6b
Removing documentation
Sep 7, 2021
dedc6e4
Updating package.json and yarn.lock
blockone-devops Sep 7, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cypress/integration/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ describe('eosjs web test', () => {
cy.get('#testWithReadOnlyFailureTrace').contains('Success', { timeout: 5000 });
});

it('test With Web Crypto Tx', () => {
cy.visit(('./src/tests/web.html'));
cy.wait(500);
cy.get('#testWithWebCryptoTx').click();
cy.get('#testWithWebCryptoTx').contains('Success', { timeout: 5000 });
});

it('test Transact Should Fail', () => {
cy.visit(('./src/tests/web.html'));
cy.wait(500);
Expand Down
3 changes: 3 additions & 0 deletions docs/basic-usage/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ The SignatureProvider object must contain the private keys corresponding to the
The Api constructor requires a SignatureProvider. SignatureProviders implement the `dist/eosjs-api-interfaces.SignatureProvider` interface. For development purpose only, a `JsSignatureProvider` object is also provided via the `dist/eosjs-jssig` import to stand-in for an easy option for a signature provider during development, but should only be used in development, as it is not secure.

In production code, it is recommended that you use a secure vault outside of the context of the webpage (which will also implement the `eosjs-api-interfaces.SignatureProvider` interface) to ensure security when signing transactions.

## WebCryptoSignatureProvider
Additionally, `WebCryptoSignatureProvider` is available in `eosjs` as a more secure built-in `SignatureProvider` than `JsSignatureProvider`. Utilizing the [SubtleCrypto interface of the Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto), the `WebCryptoSignatureProvider` provides functionality for signing transactions utilizing CryptoKeys created by Web Crypto. There are several methods available as well to convert extractable CryptoKeys into PrivateKey/PublicKey `eosjs` formats, which can then be converted into string or `elliptic` keypairs. The method `generateWebCryptoKeyPair` is also available to create a CryptoKeyPair where the private CryptoKey is non-extractable and the public CryptoKey is extractable. This is intentional as an extractable private key is more insecure than a non-extractable private key. There are several more security concerns that you will need to address when it comes to utilizing the Web Crypto API, such as secure key management, so only utilize the `WebCryptoSignatureProvider` if you are aware of these security requirements and the risks involved in insecure environments.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this section and provide a stub in its place and once the documentation is created and signed off by Product and Developer Relations - they can PR those changes in via a separate PR. Other than that this PR is now ready to merge per BLU-20451

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
"@types/node": "^14.17.9",
"@types/node-fetch": "^2.5.12",
"@types/pako": "^1.0.2",
"atob": "^2.1.2",
"btoa": "^1.2.1",
"buffer": "^6.0.3",
"clean-webpack-plugin": "^3.0.0",
"crypto-browserify": "^3.12.0",
Expand Down
47 changes: 47 additions & 0 deletions src/KeyUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {ec as EC} from 'elliptic';
import { KeyType } from './eosjs-numeric';
import { PublicKey } from './PublicKey';
import { PrivateKey } from './PrivateKey';

const crypto = (typeof(window) !== 'undefined' ? window.crypto : require('crypto').webcrypto);

export const generateKeyPair = (
type: KeyType, options: { secureEnv?: boolean, ecOptions?: EC.GenKeyPairOptions } = {}
): { publicKey: PublicKey, privateKey: PrivateKey } => {
if (!options.secureEnv) {
throw new Error('Key generation is completely INSECURE in production environments in the browser. ' +
'If you are absolutely certain this does NOT describe your environment, set `secureEnv` in your ' +
'options to `true`. If this does describe your environment and you set `secureEnv` to `true`, ' +
'YOU DO SO AT YOUR OWN RISK AND THE RISK OF YOUR USERS.');
}
let ec;
if (type === KeyType.k1) {
ec = new EC('secp256k1');
} else {
ec = new EC('p256');
}
const ellipticKeyPair = ec.genKeyPair(options.ecOptions);
const publicKey = PublicKey.fromElliptic(ellipticKeyPair, type, ec);
const privateKey = PrivateKey.fromElliptic(ellipticKeyPair, type, ec);
return {publicKey, privateKey};
};

/** Construct a p256/secp256r1 CryptoKeyPair from Web Crypto
* Note: While creating a key that is not extractable means that it would not be possible
* to convert the private key to string, it is not necessary to have the key extractable
* for the Web Crypto Signature Provider. Additionally, creating a key that is extractable
* introduces security concerns. For this reason, this function only creates CryptoKeyPairs
* where the private key is not extractable and the public key is extractable.
*/
export const generateWebCryptoKeyPair = async (
keyUsage: KeyUsage[] = ['sign', 'verify']
): Promise<CryptoKeyPair> => {
return await crypto.subtle.generateKey(
{
name: 'ECDSA',
namedCurve: 'P-256'
},
false,
keyUsage
);
};
77 changes: 74 additions & 3 deletions src/PrivateKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,14 @@ import {
privateKeyToLegacyString,
privateKeyToString,
stringToPrivateKey,
arrayToString,
stringToArray,
} from './eosjs-numeric';
import { constructElliptic, PublicKey, Signature } from './eosjs-key-conversions';
import { PublicKey } from './PublicKey';
import { Signature } from './Signature';

const crypto = (typeof(window) !== 'undefined' ? window.crypto : require('crypto').webcrypto);
type WebCryptoSignatureData = Int8Array | Int16Array | Int32Array | Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray | Float32Array | Float64Array | DataView | ArrayBuffer;

/** Represents/stores a private key and provides easy conversion for use with `elliptic` lib */
export class PrivateKey {
Expand All @@ -15,19 +21,43 @@ export class PrivateKey {
/** Instantiate private key from an `elliptic`-format private key */
public static fromElliptic(privKey: EC.KeyPair, keyType: KeyType, ec?: EC): PrivateKey {
if (!ec) {
ec = constructElliptic(keyType);
if (keyType === KeyType.k1) {
ec = new EC('secp256k1');
} else {
ec = new EC('p256');
}
}
return new PrivateKey({
type: keyType,
data: privKey.getPrivate().toArrayLike(Buffer, 'be', 32),
}, ec);
}

/** Instantiate private key from a `CryptoKey`-format private key */
public static async fromWebCrypto(privKey: CryptoKey): Promise<PrivateKey> {
if (privKey.extractable === false) {
throw new Error('Crypto Key is not extractable');
}
const ec = new EC('p256');

const extractedArrayBuffer = await crypto.subtle.exportKey('pkcs8', privKey);
const extractedDecoded = arrayToString(extractedArrayBuffer);
const derHex = Buffer.from(extractedDecoded, 'binary').toString('hex');
let privateKeyHex = derHex.replace('308187020100301306072a8648ce3d020106082a8648ce3d030107046d306b0201010420', '');
privateKeyHex = privateKeyHex.substring(0, privateKeyHex.indexOf('a144034200'));
const privateKeyEc = ec.keyFromPrivate(privateKeyHex, 'hex');
return PrivateKey.fromElliptic(privateKeyEc, KeyType.r1, ec);
}

/** Instantiate private key from an EOSIO-format private key */
public static fromString(keyString: string, ec?: EC): PrivateKey {
const privateKey = stringToPrivateKey(keyString);
if (!ec) {
ec = constructElliptic(privateKey.type);
if (privateKey.type === KeyType.k1) {
ec = new EC('secp256k1');
} else {
ec = new EC('p256');
}
}
return new PrivateKey(privateKey, ec);
}
Expand All @@ -37,6 +67,29 @@ export class PrivateKey {
return this.ec.keyFromPrivate(this.key.data);
}

/** Export private key as `CryptoKey`-format private key */
public async toWebCrypto(extractable: boolean = false): Promise<CryptoKey> {
const privateKeyEc = this.toElliptic();
const privateKeyHex = privateKeyEc.getPrivate('hex');
const publicKey = this.getPublicKey();
const publicKeyEc = publicKey.toElliptic();
const publicKeyHex = publicKeyEc.getPublic('hex');

const derHex = `308187020100301306072a8648ce3d020106082a8648ce3d030107046d306b0201010420${privateKeyHex}a144034200${publicKeyHex}`;
const derBinary = Buffer.from(derHex, 'hex').toString('binary');
const pkcs8ArrayBuffer = stringToArray(derBinary);
return await crypto.subtle.importKey(
'pkcs8',
pkcs8ArrayBuffer,
{
name: 'ECDSA',
namedCurve: 'P-256'
},
extractable,
['sign']
);
}

public toLegacyString(): string {
return privateKeyToLegacyString(this.key);
}
Expand Down Expand Up @@ -86,6 +139,24 @@ export class PrivateKey {
return signature;
}

/** Use Web Crypto to sign data (that matches types) with private CryptoKey */
public async webCryptoSign(data: WebCryptoSignatureData): Promise<Signature> {
const publicKey = this.getPublicKey();
const privWebCrypto = await this.toWebCrypto();
const webCryptoSig = await crypto.subtle.sign(
{
name: 'ECDSA',
hash: {
name: 'SHA-256'
}
},
privWebCrypto,
data
);

return Signature.fromWebCrypto(data, webCryptoSig, publicKey);
}

/** Validate a private key */
public isValid(): boolean {
try {
Expand Down
52 changes: 49 additions & 3 deletions src/PublicKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import {
publicKeyToLegacyString,
publicKeyToString,
stringToPublicKey,
arrayToString,
stringToArray,
} from './eosjs-numeric';
import { constructElliptic } from './eosjs-key-conversions';

const crypto = (typeof(window) !== 'undefined' ? window.crypto : require('crypto').webcrypto);

/** Represents/stores a public key and provides easy conversion for use with `elliptic` lib */
export class PublicKey {
Expand All @@ -16,7 +19,11 @@ export class PublicKey {
public static fromString(publicKeyStr: string, ec?: EC): PublicKey {
const key = stringToPublicKey(publicKeyStr);
if (!ec) {
ec = constructElliptic(key.type);
if (key.type === KeyType.k1) {
ec = new EC('secp256k1');
} else {
ec = new EC('p256');
}
}
return new PublicKey(key, ec);
}
Expand All @@ -26,14 +33,33 @@ export class PublicKey {
const x = publicKey.getPublic().getX().toArray('be', 32);
const y = publicKey.getPublic().getY().toArray('be', 32);
if (!ec) {
ec = constructElliptic(keyType);
if (keyType === KeyType.k1) {
ec = new EC('secp256k1');
} else {
ec = new EC('p256');
}
}
return new PublicKey({
type: keyType,
data: new Uint8Array([(y[31] & 1) ? 3 : 2].concat(x)),
}, ec);
}

/** Instantiate public key from a `CryptoKey`-format public key */
public static async fromWebCrypto(publicKey: CryptoKey): Promise<PublicKey> {
if (publicKey.extractable === false) {
throw new Error('Crypto Key is not extractable');
}
const ec = new EC('p256');

const extractedArrayBuffer = await crypto.subtle.exportKey('spki', publicKey);
const extractedDecoded = arrayToString(extractedArrayBuffer);
const derHex = Buffer.from(extractedDecoded, 'binary').toString('hex');
const publicKeyHex = derHex.replace('3059301306072a8648ce3d020106082a8648ce3d030107034200', '');
const publicKeyEc = ec.keyFromPublic(publicKeyHex, 'hex');
return PublicKey.fromElliptic(publicKeyEc, KeyType.r1, ec);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to confirm, could ec be initially passed non-empty and of type secp256k1, or only p256 is supported here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only p256 is supported for the Web Crypto API but it is possible that someone could put a k1 ec object and the code would break. I think a check is likely needed, I'll add it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, turns out the elliptic library does not easily label the curve utilized in the object. It would not be as simple as checking ec.curve.label = 'p256';. I will remove the ability of providing an ec object to the functions.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! This looks more clear too me.

}

/** Export public key as EOSIO-format public key */
public toString(): string {
return publicKeyToString(this.key);
Expand All @@ -51,6 +77,26 @@ export class PublicKey {
});
}

/** Export public key as `CryptoKey`-format public key */
public async toWebCrypto(extractable: boolean = false): Promise<CryptoKey> {
const publicKeyEc = this.toElliptic();
const publicKeyHex = publicKeyEc.getPublic('hex');

const derHex = `3059301306072a8648ce3d020106082a8648ce3d030107034200${publicKeyHex}`;
const derBase64 = Buffer.from(derHex, 'hex').toString('binary');
const spkiArrayBuffer = stringToArray(derBase64);
return await crypto.subtle.importKey(
'spki',
spkiArrayBuffer,
{
name: 'ECDSA',
namedCurve: 'P-256'
},
extractable,
['verify']
);
}

/** Get key type from key */
public getType(): KeyType {
return this.key.type;
Expand Down
68 changes: 63 additions & 5 deletions src/Signature.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { BNInput, ec as EC } from 'elliptic';
import BN = require('bn.js');

import {
Key,
KeyType,
signatureToString,
stringToSignature,
} from './eosjs-numeric';
import { constructElliptic, PublicKey } from './eosjs-key-conversions';
import { PublicKey } from './PublicKey';

const crypto = (typeof(window) !== 'undefined' ? window.crypto : require('crypto').webcrypto);
type WebCryptoSignatureData = Int8Array | Int16Array | Int32Array | Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray | Float32Array | Float64Array | DataView | ArrayBuffer;

/** Represents/stores a Signature and provides easy conversion for use with `elliptic` lib */
export class Signature {
Expand All @@ -17,13 +19,17 @@ export class Signature {
public static fromString(sig: string, ec?: EC): Signature {
const signature = stringToSignature(sig);
if (!ec) {
ec = constructElliptic(signature.type);
if (signature.type === KeyType.k1) {
ec = new EC('secp256k1');
} else {
ec = new EC('p256');
}
}
return new Signature(signature, ec);
}

/** Instantiate Signature from an `elliptic`-format Signature */
public static fromElliptic(ellipticSig: EC.Signature, keyType: KeyType, ec?: EC): Signature {
public static fromElliptic(ellipticSig: EC.Signature | {r: BN, s: BN, recoveryParam: number | null}, keyType: KeyType, ec?: EC): Signature {
const r = ellipticSig.r.toArray('be', 32);
const s = ellipticSig.s.toArray('be', 32);
let eosioRecoveryParam;
Expand All @@ -37,14 +43,50 @@ export class Signature {
}
const sigData = new Uint8Array([eosioRecoveryParam].concat(r, s));
if (!ec) {
ec = constructElliptic(keyType);
if (keyType === KeyType.k1) {
ec = new EC('secp256k1');
} else {
ec = new EC('p256');
}
}
return new Signature({
type: keyType,
data: sigData,
}, ec);
}

/** Instantiate Signature from a Web Crypto Signature */
public static async fromWebCrypto(data: WebCryptoSignatureData, webCryptoSig: ArrayBuffer, publicKey: PublicKey) {
const ec = new EC('p256');

const hash = await crypto.subtle.digest('SHA-256', data);
const r = new BN(new Uint8Array(webCryptoSig.slice(0, 32)), 32);
let s = new BN(new Uint8Array(webCryptoSig.slice(32)), 32);
const halforder = ec.curve.n.ushrn(1); // shift right 1 bit -- division by two
if (s.ucmp(halforder) === 1) {
s = ec.curve.n.sub(s);
}
const recoveryParam = this.getRecoveryParam(Buffer.from(hash), {r, s}, publicKey.toString(), ec);
return Signature.fromElliptic({r, s, recoveryParam}, KeyType.r1, ec);
}

/** Replaced version of getRecoveryParam from `elliptic` library */
private static getRecoveryParam = (digest: BNInput, signature: EC.SignatureOptions, publicKey: string, ec: EC) => {
let recoveredKey: any;
for (let i = 0; i < 4; i++) {
try {
const keyPair = ec.recoverPubKey(digest, signature, i);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name keyPair is a bit confusing as private key is not here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually the recoverPubKey function will return an ec keyPair object, although the private will be empty and the public will be non-empty.

recoveredKey = PublicKey.fromElliptic(ec.keyFromPublic(keyPair), KeyType.r1, ec).toString();
} catch (e) {
continue;
}
if (recoveredKey === publicKey) {
return i;
}
}
throw new Error('Unable to find valid recovery factor');
};

/** Export Signature as `elliptic`-format Signature
* NOTE: This isn't an actual elliptic-format Signature, as ec.Signature is not exported by the library.
* That's also why the return type is `any`. We're *actually* returning an object with the 3 params
Expand Down Expand Up @@ -98,6 +140,22 @@ export class Signature {
return this.ec.verify(data, ellipticSignature, ellipticPublicKey, encoding);
}

/** Verify a Web Crypto signature with data (that matches types) and public key */
public async webCryptoVerify(data: WebCryptoSignatureData, webCryptoSig: ArrayBuffer, publicKey: PublicKey): Promise<boolean> {
const webCryptoPub = await publicKey.toWebCrypto();
return await crypto.subtle.verify(
{
name: 'ECDSA',
hash: {
name: 'SHA-256'
}
},
webCryptoPub,
webCryptoSig,
data
);
}

/** Recover a public key from a message or hashed message digest and signature */
public recover(data: BNInput, shouldHash: boolean = true, encoding: BufferEncoding = 'utf8'): PublicKey {
if (shouldHash) {
Expand Down
Loading