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

Commit

Permalink
Merge pull request #577 from EOSIO/wa-experiment
Browse files Browse the repository at this point in the history
web-authn support
  • Loading branch information
tbfleming authored Aug 8, 2019
2 parents cbcef42 + 6070a2e commit d85dbee
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 77 deletions.
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
},
"devDependencies": {
"@blockone/tslint-config-blockone": "3.0.0",
"@types/elliptic": "^6.4.9",
"@types/jest": "24.0.6",
"@types/node": "11.9.4",
"@types/text-encoding": "0.0.35",
Expand All @@ -42,6 +43,7 @@
"babel-preset-env": "1.7.0",
"babel-preset-stage-1": "6.24.1",
"cypress": "3.1.5",
"elliptic": "^6.5.0",
"jest": "23.5.0",
"jest-fetch-mock": "2.1.1",
"json-loader": "0.5.7",
Expand All @@ -55,6 +57,11 @@
"webpack": "4.29.5",
"webpack-cli": "3.2.3"
},
"resolutions": {
"braces": "2.3.1",
"handlebars": "4.1.2",
"js-yaml": "3.13.1"
},
"jest": {
"automock": false,
"setupFiles": [
Expand Down
47 changes: 43 additions & 4 deletions src/eosjs-numeric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,41 @@ export function signedBinaryToDecimal(bignum: Uint8Array, minDigits = 1) {
return binaryToDecimal(bignum, minDigits);
}

function base58ToBinaryVarSize(s: string) {
const result = [] as number[];
for (let i = 0; i < s.length; ++i) {
let carry = base58Map[s.charCodeAt(i)];
if (carry < 0) {
throw new Error('invalid base-58 value');
}
for (let j = 0; j < result.length; ++j) {
const x = result[j] * 58 + carry;
result[j] = x & 0xff;
carry = x >> 8;
}
if (carry) {
result.push(carry);
}
}
for (const ch of s) {
if (ch === '1') {
result.push(0);
} else {
break;
}
}
result.reverse();
return new Uint8Array(result);
}

/**
* Convert an unsigned base-58 number in `s` to a bignum
* @param size bignum size (bytes)
*/
export function base58ToBinary(size: number, s: string) {
if (!size) {
return base58ToBinaryVarSize(s);
}
const result = new Uint8Array(size);
for (let i = 0; i < s.length; ++i) {
let carry = base58Map[s.charCodeAt(i)];
Expand Down Expand Up @@ -217,6 +247,7 @@ export function base64ToBinary(s: string) {
export enum KeyType {
k1 = 0,
r1 = 1,
wa = 2,
}

/** Public key data size, excluding type field */
Expand Down Expand Up @@ -246,11 +277,11 @@ function digestSuffixRipemd160(data: Uint8Array, suffix: string) {
}

function stringToKey(s: string, type: KeyType, size: number, suffix: string): Key {
const whole = base58ToBinary(size + 4, s);
const result = { type, data: new Uint8Array(whole.buffer, 0, size) };
const whole = base58ToBinary(size ? size + 4 : 0, s);
const result = { type, data: new Uint8Array(whole.buffer, 0, whole.length - 4) };
const digest = new Uint8Array(digestSuffixRipemd160(result.data, suffix));
if (digest[0] !== whole[size + 0] || digest[1] !== whole[size + 1]
|| digest[2] !== whole[size + 2] || digest[3] !== whole[size + 3]) {
if (digest[0] !== whole[whole.length - 4] || digest[1] !== whole[whole.length - 3]
|| digest[2] !== whole[whole.length - 2] || digest[3] !== whole[whole.length - 1]) {
throw new Error('checksum doesn\'t match');
}
return result;
Expand Down Expand Up @@ -289,6 +320,8 @@ export function stringToPublicKey(s: string): Key {
return stringToKey(s.substr(7), KeyType.k1, publicKeyDataSize, 'K1');
} else if (s.substr(0, 7) === 'PUB_R1_') {
return stringToKey(s.substr(7), KeyType.r1, publicKeyDataSize, 'R1');
} else if (s.substr(0, 7) === 'PUB_WA_') {
return stringToKey(s.substr(7), KeyType.wa, 0, 'WA');
} else {
throw new Error('unrecognized public key format');
}
Expand All @@ -300,6 +333,8 @@ export function publicKeyToString(key: Key) {
return keyToString(key, 'K1', 'PUB_K1_');
} else if (key.type === KeyType.r1 && key.data.length === publicKeyDataSize) {
return keyToString(key, 'R1', 'PUB_R1_');
} else if (key.type === KeyType.wa) {
return keyToString(key, 'WA', 'PUB_WA_');
} else {
throw new Error('unrecognized public key format');
}
Expand Down Expand Up @@ -352,6 +387,8 @@ export function stringToSignature(s: string): Key {
return stringToKey(s.substr(7), KeyType.k1, signatureDataSize, 'K1');
} else if (s.substr(0, 7) === 'SIG_R1_') {
return stringToKey(s.substr(7), KeyType.r1, signatureDataSize, 'R1');
} else if (s.substr(0, 7) === 'SIG_WA_') {
return stringToKey(s.substr(7), KeyType.wa, 0, 'WA');
} else {
throw new Error('unrecognized signature format');
}
Expand All @@ -363,6 +400,8 @@ export function signatureToString(signature: Key) {
return keyToString(signature, 'K1', 'SIG_K1_');
} else if (signature.type === KeyType.r1) {
return keyToString(signature, 'R1', 'SIG_R1_');
} else if (signature.type === KeyType.wa) {
return keyToString(signature, 'WA', 'SIG_WA_');
} else {
throw new Error('unrecognized signature format');
}
Expand Down
29 changes: 27 additions & 2 deletions src/eosjs-serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,14 @@ export class SerialBuffer { // tslint:disable-line max-classes-per-file
return result;
}

/** Skip `len` bytes */
public skip(len: number) {
if (this.readPos + len > this.length) {
throw new Error('Read past end of buffer');
}
this.readPos += len;
}

/** Append a `uint16` */
public pushUint16(v: number) {
this.push((v >> 0) & 0xff, (v >> 8) & 0xff);
Expand Down Expand Up @@ -489,7 +497,15 @@ export class SerialBuffer { // tslint:disable-line max-classes-per-file
/** Get a public key */
public getPublicKey() {
const type = this.get();
const data = this.getUint8Array(numeric.publicKeyDataSize);
let data: Uint8Array;
if (type === numeric.KeyType.wa) {
const begin = this.readPos;
this.skip(34);
this.skip(this.getVaruint32());
data = new Uint8Array(this.array.buffer, this.array.byteOffset + begin, this.readPos - begin);
} else {
data = this.getUint8Array(numeric.publicKeyDataSize);
}
return numeric.publicKeyToString({ type, data });
}

Expand Down Expand Up @@ -517,7 +533,16 @@ export class SerialBuffer { // tslint:disable-line max-classes-per-file
/** Get a signature */
public getSignature() {
const type = this.get();
const data = this.getUint8Array(numeric.signatureDataSize);
let data: Uint8Array;
if (type === numeric.KeyType.wa) {
const begin = this.readPos;
this.skip(65);
this.skip(this.getVaruint32());
this.skip(this.getVaruint32());
data = new Uint8Array(this.array.buffer, this.array.byteOffset + begin, this.readPos - begin);
} else {
data = this.getUint8Array(numeric.signatureDataSize);
}
return numeric.signatureToString({ type, data });
}
} // SerialBuffer
Expand Down
104 changes: 104 additions & 0 deletions src/eosjs-webauthn-sig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* @module WebAuthn-Sig
*/
// copyright defined in eosjs/LICENSE.txt

import { SignatureProvider, SignatureProviderArgs } from './eosjs-api-interfaces';
import * as ser from './eosjs-serialize';
import * as numeric from './eosjs-numeric';
import { ec } from 'elliptic';

/** Signs transactions using WebAuthn */
export class WebAuthnSignatureProvider implements SignatureProvider {
/** Map public key to credential ID (hex). User must populate this. */
public keys = new Map<string, string>();

/** Public keys that the `SignatureProvider` holds */
public async getAvailableKeys() {
return Array.from(this.keys.keys());
}

/** Sign a transaction */
public async sign(
{ chainId, requiredKeys, serializedTransaction, serializedContextFreeData }:
SignatureProviderArgs,
) {
const signBuf = new ser.SerialBuffer();
signBuf.pushArray(ser.hexToUint8Array(chainId));
signBuf.pushArray(serializedTransaction);
if (serializedContextFreeData) {
signBuf.pushArray(new Uint8Array(await crypto.subtle.digest('SHA-256', serializedContextFreeData.buffer)));
} else {
signBuf.pushArray(new Uint8Array(32));
}
const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', signBuf.asUint8Array().slice().buffer));

const signatures = [] as string[];
for (const key of requiredKeys) {
const id = ser.hexToUint8Array(this.keys.get(key));
const assertion = await (navigator as any).credentials.get({
publicKey: {
timeout: 60000,
allowCredentials: [{
id,
type: 'public-key',
}],
challenge: digest.buffer,
},
});
const e = new ec('p256') as any;
const pubKey = e.keyFromPublic(numeric.stringToPublicKey(key).data.subarray(0, 33)).getPublic();

const fixup = (x: Uint8Array) => {
const a = Array.from(x);
while (a.length < 32) {
a.unshift(0);
}
while (a.length > 32) {
if (a.shift() !== 0) {
throw new Error('Signature has an r or s that is too big');
}
}
return new Uint8Array(a);
};

const der = new ser.SerialBuffer({ array: new Uint8Array(assertion.response.signature) });
if (der.get() !== 0x30) {
throw new Error('Signature missing DER prefix');
}
if (der.get() !== der.array.length - 2) {
throw new Error('Signature has bad length');
}
if (der.get() !== 0x02) {
throw new Error('Signature has bad r marker');
}
const r = fixup(der.getUint8Array(der.get()));
if (der.get() !== 0x02) {
throw new Error('Signature has bad s marker');
}
const s = fixup(der.getUint8Array(der.get()));

const whatItReallySigned = new ser.SerialBuffer();
whatItReallySigned.pushArray(new Uint8Array(assertion.response.authenticatorData));
whatItReallySigned.pushArray(new Uint8Array(
await crypto.subtle.digest('SHA-256', assertion.response.clientDataJSON)));
const hash = new Uint8Array(
await crypto.subtle.digest('SHA-256', whatItReallySigned.asUint8Array().slice()));
const recid = e.getKeyRecoveryParam(hash, new Uint8Array(assertion.response.signature), pubKey);

const sigData = new ser.SerialBuffer();
sigData.push(recid + 27 + 4);
sigData.pushArray(r);
sigData.pushArray(s);
sigData.pushBytes(new Uint8Array(assertion.response.authenticatorData));
sigData.pushBytes(new Uint8Array(assertion.response.clientDataJSON));

const sig = numeric.signatureToString({
type: numeric.KeyType.wa,
data: sigData.asUint8Array().slice(),
});
signatures.push(sig);
}
return { signatures, serializedTransaction, serializedContextFreeData };
}
}
Loading

0 comments on commit d85dbee

Please sign in to comment.