Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ecdsa-multikey v1.0.0 #1

Merged
merged 51 commits into from
Feb 27, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
2e7f75e
added constants, helpers, and core/browser ecdsa lib files
kezike Feb 4, 2023
9d80c4c
added factory and serialization lib files and updated package.json
kezike Feb 4, 2023
ff755c8
updated index.js with proper functionality
kezike Feb 4, 2023
99bd482
added constants
kezike Feb 13, 2023
b087b90
added constants for ecdsa curves and hash algorithms
kezike Feb 13, 2023
b9fcffd
incorporated constants into helper code
kezike Feb 13, 2023
53b7467
added helper functions to encode key pair into multibase
kezike Feb 13, 2023
a885424
added file to convert key pair into multikey
kezike Feb 13, 2023
a60eebc
added id property to input of createSigner and createVerifier; handle…
kezike Feb 13, 2023
9b12905
included type and controller in exported key pairs; throw appropriate…
kezike Feb 13, 2023
027f74a
modified browser binding in package.json; added multiformats library
kezike Feb 13, 2023
4374a8f
fixed error message of stringToUint8Array function
kezike Feb 13, 2023
7ab64d4
include complete set of multikey properties in generated key pairs; f…
kezike Feb 13, 2023
0210145
removed unnecessary comma
kezike Feb 13, 2023
caa0089
added mock data; added initial set of tests for ecdsa multikey
kezike Feb 13, 2023
0c49d37
fixed constants import in ecdsa multikey test
kezike Feb 13, 2023
cd66010
added ecdsa sign and verify tests
kezike Feb 13, 2023
1dcae0a
replace all relevant instances of "private" to "secret"
kezike Feb 13, 2023
6d83cca
updated readme
kezike Feb 13, 2023
c05dacb
use ALGORITHM constant in remaining instances
kezike Feb 14, 2023
88bc648
added script to convert varint to hex
kezike Feb 14, 2023
85a9026
remove outdated node-browser compatibility test
kezike Feb 14, 2023
bc0751f
simplified multicodec header constants
kezike Feb 15, 2023
6d253ac
improved output of util/varint-conversions.js
kezike Feb 15, 2023
ef1a967
added useful utility script for key operations
kezike Feb 15, 2023
00b6ce3
fixed _ensurePublicKeyEncoding by using latest version of multicodec …
kezike Feb 15, 2023
22c15a8
fixes lint errors; removes unnecessary code and constants; adds funct…
kezike Feb 16, 2023
8d15b13
test github workflow on node version 18.x
kezike Feb 16, 2023
b07b8b5
use latest version github actions tools
kezike Feb 16, 2023
14b8b20
use latest version of codecov tool
kezike Feb 16, 2023
4338914
removed nonexistent test file reference
kezike Feb 16, 2023
41f5528
improved error messaging etiquette
kezike Feb 16, 2023
199bf35
set library version to pre-1.0.0 so that release tooling increments i…
kezike Feb 16, 2023
1fc4163
migrated ecdsa curve and hash enums to constants file
kezike Feb 16, 2023
962df20
renamed crypto source files
kezike Feb 16, 2023
a47de67
updated casing of ecdsa hash constant enum members
kezike Feb 21, 2023
16d5f42
minor comment improvement
kezike Feb 21, 2023
1c6d7e0
moved varint from dependencies to devDependencies in package.json
kezike Feb 21, 2023
73313f9
changed ecdsa type array to set and moved out of toMultikey function
kezike Feb 21, 2023
9022fcb
improved code coverage
kezike Feb 21, 2023
b665c4e
fixed lint errors
kezike Feb 22, 2023
4ae2fc9
removed outdated comment
kezike Feb 22, 2023
e6e93d9
fixed keys in readme
kezike Feb 22, 2023
9127aa0
remove codecov upload directive in github action ci worklow
kezike Feb 22, 2023
d73e775
fix copyright header in source code
kezike Feb 22, 2023
d2610fe
added minor style tweaks
kezike Feb 22, 2023
d67e7af
removed unnecessary ci run on node version 18 for karma, lint, and co…
kezike Feb 22, 2023
a0d13e0
refactored representation of ecdsa curve header constants from raw by…
kezike Feb 22, 2023
9a9e05a
removed unnecessary reference to webcrypto property in global crypto …
kezike Feb 22, 2023
ef1e22e
Update remaining ecdsa curve header constants.
kezike Feb 23, 2023
8716a20
Changed nested if statement to compound and statement.
kezike Feb 23, 2023
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
30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,16 @@ npm install

To generate a new public/secret key pair:

* `{string} [id]` Optional id for the generated key.
* `{string} [controller]` Optional controller URI or DID to initialize the
generated key. (This will also init the key id.)
* `{string} [seed]` Optional deterministic seed value from which to generate the
key.
* `{string} [curve]` \[Required\] ECDSA curve used to generate the key:
\['P-256', 'P-384', 'P-521'\].
* `{string} [id]` \[Optional\] ID for the generated key.
* `{string} [controller]` \[Optional\] Controller URI or DID to initialize the
generated key. (This will be used to generate `id` if it is not explicitly defined.)

```js
import * as EcdsaMultikey from '@digitalbazaar/ecdsa-multikey';

const edKeyPair = await EcdsaMultikey.generate();
const keyPair = await EcdsaMultikey.generate({curve: 'P-384'});
```

### Importing a key pair from storage
Expand All @@ -82,9 +82,9 @@ await keyPair.export({publicKey: true});
// ->
{
type: 'Multikey',
id: 'did:example:1234#z6Mkon3Necd6NkkyfoGoHxid2znGc59LU3K7mubaRcFbLfLX',
id: 'did:example:1234#zynkWQ2LUf58H4VwLGjscwb9KJwGUmqBFXdJKnCXxDMciFXioY7Hq7MvEdVfsBiQSVa9k9',
kezike marked this conversation as resolved.
Show resolved Hide resolved
controller: 'did:example:1234',
publicKeyMultibase: 'z6Mkon3Necd6NkkyfoGoHxid2znGc59LU3K7mubaRcFbLfLX'
publicKeyMultibase: 'zynkWQ2LUf58H4VwLGjscwb9KJwGUmqBFXdJKnCXxDMciFXioY7Hq7MvEdVfsBiQSVa9k9'
}
```

Expand All @@ -98,10 +98,10 @@ await keyPair.export({publicKey: true, secretKey: true});
// ->
{
type: 'Multikey',
id: 'did:example:1234#z6Mkon3Necd6NkkyfoGoHxid2znGc59LU3K7mubaRcFbLfLX',
id: 'did:example:1234#zynkWQ2LUf58H4VwLGjscwb9KJwGUmqBFXdJKnCXxDMciFXioY7Hq7MvEdVfsBiQSVa9k9',
controller: 'did:example:1234',
publicKeyMultibase: 'z6Mkon3Necd6NkkyfoGoHxid2znGc59LU3K7mubaRcFbLfLX',
secretKeyMultibase: 'zruzf4Y29hDp7vLoV3NWzuymGMTtJcQfttAWzESod4wV2fbPvEp4XtzGp2VWwQSQAXMxDyqrnVurYg2sBiqiu1FHDDM'
publicKeyMultibase: 'zynkWQ2LUf58H4VwLGjscwb9KJwGUmqBFXdJKnCXxDMciFXioY7Hq7MvEdVfsBiQSVa9k9',
secretKeyMultibase: 'zEbCcd62anxNxioGCdP818UobJn1oEmgL5PHUvQ5AvdyVeLJxeJ4Kr73RqqvG5DN7Zq48'
kezike marked this conversation as resolved.
Show resolved Hide resolved
}
```

Expand All @@ -111,14 +111,14 @@ In order to perform a cryptographic signature, you need to create a `sign`
function, and then invoke it.

```js
const keyPair = EcdsaMultikey.generate();
const keyPair = EcdsaMultikey.generate({curve: 'P-256'});

const {sign} = keyPair.signer();

// data is a Uint8Array of bytes
const data = (new TextEncoder()).encode('test data goes here');
// Signing also outputs a Uint8Array, which you can serialize to text etc.
const signatureValueBytes = await sign({data});
const signature = await sign({data});
```

### Creating a verifier function
Expand All @@ -127,7 +127,7 @@ In order to verify a cryptographic signature, you need to create a `verify`
function, and then invoke it (passing it the data to verify, and the signature).

```js
const keyPair = EcdsaMultikey.generate();
const keyPair = EcdsaMultikey.generate({curve: 'P-521'});

const {verify} = keyPair.verifier();

Expand All @@ -151,4 +151,4 @@ Digital Bazaar: [email protected]

## License

[New BSD License (3-clause)](LICENSE) © 2020 Digital Bazaar
[New BSD License (3-clause)](LICENSE) © 2023 Digital Bazaar
37 changes: 37 additions & 0 deletions lib/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*!
* Copyright (c) 2023 Digital Bazaar, Inc. All rights reserved.
*/

// Name of algorithm
export const ALGORITHM = 'ECDSA';
// Determines whether key pair is extractable
export const EXTRACTABLE = true;
// ECDSA curve P-256 type
export const ECDSA_2019_SECP_256_KEY_TYPE = 'EcdsaSecp256r1VerificationKey2019';
// ECDSA curve P-384 type
export const ECDSA_2019_SECP_384_KEY_TYPE = 'EcdsaSecp384r1VerificationKey2019';
// ECDSA curve P-521 type
export const ECDSA_2019_SECP_521_KEY_TYPE = 'EcdsaSecp521r1VerificationKey2019';
// ECDSA 2019 suite context v1 URL
export const ECDSA_2019_SUITE_CONTEXT_V1_URL = 'https://w3id.org/security/suites/ecdsa-2019/v1';
// Multikey context v1 URL
export const MULTIKEY_CONTEXT_V1_URL = 'https://w3id.org/security/multikey/v1';
export const MULTIBASE_BASE58_HEADER = 'z';

// Multicodec ECDSA public key header byte 1
export const MULTICODEC_ECDSA_PUBLIC_KEY_HEADER_BYTE_1 = 0x12;
// Multicodec p256-pub header byte 2
export const MULTICODEC_P256_PUBLIC_KEY_HEADER_BYTE_2 = 0x00;
// Multicodec p384-pub header byte 2
export const MULTICODEC_P384_PUBLIC_KEY_HEADER_BYTE_2 = 0x01;
// Multicodec p521-pub header byte 2
export const MULTICODEC_P521_PUBLIC_KEY_HEADER_BYTE_2 = 0x02;

// Multicodec ECDSA secret key header byte 1
export const MULTICODEC_ECDSA_SECRET_KEY_HEADER_BYTE_1 = 0x13;
// Multicodec p256-priv header byte 2
export const MULTICODEC_P256_SECRET_KEY_HEADER_BYTE_2 = 0x03;
// Multicodec p384-priv header byte 2
export const MULTICODEC_P384_SECRET_KEY_HEADER_BYTE_2 = 0x04;
// Multicodec p521-priv header byte 2
export const MULTICODEC_P521_SECRET_KEY_HEADER_BYTE_2 = 0x05;
7 changes: 7 additions & 0 deletions lib/ecdsa-browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*!
* Copyright (c) 2023 Digital Bazaar, Inc. All rights reserved.
*/
// eslint-disable-next-line no-undef
export const webcrypto = globalThis.crypto.webcrypto ?? globalThis.crypto;
// eslint-disable-next-line no-undef
export const CryptoKey = globalThis.CryptoKey ?? webcrypto.CryptoKey;
18 changes: 18 additions & 0 deletions lib/ecdsa.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*!
* Copyright (c) 2023 Digital Bazaar, Inc. All rights reserved.
*/
import {webcrypto} from 'node:crypto';
const {CryptoKey} = webcrypto;
export {CryptoKey, webcrypto};

export const EcdsaCurve = {
kezike marked this conversation as resolved.
Show resolved Hide resolved
P256: 'P-256',
P384: 'P-384',
P521: 'P-521'
};

export const EcdsaHash = {
Sha256: 'SHA-256',
Sha384: 'SHA-384',
Sha512: 'SHA-512'
};
48 changes: 48 additions & 0 deletions lib/factory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*!
* Copyright (c) 2023 Digital Bazaar, Inc. All rights reserved.
kezike marked this conversation as resolved.
Show resolved Hide resolved
*/
import {ALGORITHM} from './constants.js';
import {webcrypto, EcdsaCurve, EcdsaHash} from './ecdsa.js';

export function createSigner({id, secretKey}) {
if(!secretKey) {
throw new Error('A secret key is not available for signing.');
kezike marked this conversation as resolved.
Show resolved Hide resolved
}
const {namedCurve: curve} = secretKey.algorithm;
const algorithm = {name: ALGORITHM, hash: {name: _getEcdsaHash({curve})}};
return {
algorithm: ALGORITHM,
id,
async sign({data} = {}) {
return webcrypto.subtle.sign(algorithm, secretKey, data);
}
};
}

export function createVerifier({id, publicKey}) {
if(!publicKey) {
throw new Error('A public key is not available for verifying.');
}
const {namedCurve: curve} = publicKey.algorithm;
const algorithm = {name: 'ECDSA', hash: {name: _getEcdsaHash({curve})}};
return {
algorithm: ALGORITHM,
id,
async verify({data, signature} = {}) {
return webcrypto.subtle.verify(algorithm, publicKey, signature, data);
}
};
}

function _getEcdsaHash({curve}) {
if(curve === EcdsaCurve.P256) {
return EcdsaHash.Sha256;
}
if(curve === EcdsaCurve.P384) {
return EcdsaHash.Sha384;
}
if(curve === EcdsaCurve.P521) {
return EcdsaHash.Sha512;
}
throw Error(`Unsupported curve "${curve}".`);
}
106 changes: 106 additions & 0 deletions lib/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*!
* Copyright (c) 2023 Digital Bazaar, Inc. All rights reserved.
kezike marked this conversation as resolved.
Show resolved Hide resolved
*/
import * as base58 from 'base58-universal';
import {
MULTIBASE_BASE58_HEADER,
MULTICODEC_ECDSA_PUBLIC_KEY_HEADER_BYTE_1,
MULTICODEC_ECDSA_SECRET_KEY_HEADER_BYTE_1,
MULTICODEC_P256_PUBLIC_KEY_HEADER_BYTE_2,
MULTICODEC_P256_SECRET_KEY_HEADER_BYTE_2,
MULTICODEC_P384_PUBLIC_KEY_HEADER_BYTE_2,
MULTICODEC_P384_SECRET_KEY_HEADER_BYTE_2,
MULTICODEC_P521_PUBLIC_KEY_HEADER_BYTE_2,
MULTICODEC_P521_SECRET_KEY_HEADER_BYTE_2
} from './constants.js';
import {EcdsaCurve} from './ecdsa.js';

export function getNamedCurve({publicMultikey}) {
if(publicMultikey[0] === MULTICODEC_ECDSA_PUBLIC_KEY_HEADER_BYTE_1) {
if(publicMultikey[1] === MULTICODEC_P256_PUBLIC_KEY_HEADER_BYTE_2) {
return EcdsaCurve.P256;
}
if(publicMultikey[1] === MULTICODEC_P384_PUBLIC_KEY_HEADER_BYTE_2) {
return EcdsaCurve.P384;
}
if(publicMultikey[1] === MULTICODEC_P521_PUBLIC_KEY_HEADER_BYTE_2) {
return EcdsaCurve.P521;
}
}

// FIXME; also support P-256K/secp256k1
const err = new Error('Unsupported multikey header.');
err.name = 'UnsupportedError';
kezike marked this conversation as resolved.
Show resolved Hide resolved
throw err;
}

export function getSecretKeySize({keyPair}) {
const key = keyPair.secretKey || keyPair.publicKey;
const {namedCurve: curve} = key.algorithm;
if(curve === EcdsaCurve.P256) {
return 32;
}
if(curve === EcdsaCurve.P384) {
return 48;
}
if(curve === EcdsaCurve.P521) {
return 66;
}
throw new Error(`Unsupported curve "${curve}".`);
}

export function setSecretKeyHeader({keyPair, buffer}) {
const key = keyPair.secretKey || keyPair.publicKey;
const {namedCurve: curve} = key.algorithm;
// FIXME: these must be added to the multicodec table
Copy link
Member

Choose a reason for hiding this comment

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

Looks like this got done (thanks!):

Suggested change
// FIXME: these must be added to the multicodec table

if(curve === EcdsaCurve.P256) {
buffer[0] = MULTICODEC_ECDSA_SECRET_KEY_HEADER_BYTE_1;
buffer[1] = MULTICODEC_P256_SECRET_KEY_HEADER_BYTE_2;
} else if(curve === EcdsaCurve.P384) {
buffer[0] = MULTICODEC_ECDSA_SECRET_KEY_HEADER_BYTE_1;
buffer[1] = MULTICODEC_P384_SECRET_KEY_HEADER_BYTE_2;
} else if(curve === EcdsaCurve.P521) {
buffer[0] = MULTICODEC_ECDSA_SECRET_KEY_HEADER_BYTE_1;
buffer[1] = MULTICODEC_P521_SECRET_KEY_HEADER_BYTE_2;
} else {
throw new Error(`Unsupported curve "${curve}".`);
}
}

export function setPublicKeyHeader({keyPair, buffer}) {
const {namedCurve: curve} = keyPair.publicKey.algorithm;
if(curve === EcdsaCurve.P256) {
buffer[0] = MULTICODEC_ECDSA_PUBLIC_KEY_HEADER_BYTE_1;
buffer[1] = MULTICODEC_P256_PUBLIC_KEY_HEADER_BYTE_2;
} else if(curve === EcdsaCurve.P384) {
buffer[0] = MULTICODEC_ECDSA_PUBLIC_KEY_HEADER_BYTE_1;
buffer[1] = MULTICODEC_P384_PUBLIC_KEY_HEADER_BYTE_2;
} else if(curve === EcdsaCurve.P521) {
buffer[0] = MULTICODEC_ECDSA_PUBLIC_KEY_HEADER_BYTE_1;
buffer[1] = MULTICODEC_P521_PUBLIC_KEY_HEADER_BYTE_2;
} else {
throw new Error(`Unsupported curve "${curve}".`);
}
}

export function mbEncodeKeyPair({keyPair}) {
const publicKeyMultibase =
_encodeMbKey(MULTICODEC_PUB_HEADER, keyPair.publicKey);
const secretKeyMultibase =
_encodeMbKey(MULTICODEC_PRIV_HEADER, keyPair.secretKey);

return {
publicKeyMultibase,
secretKeyMultibase
};
}

// encode a multibase base58 multicodec key
function _encodeMbKey(header, key) {
const mbKey = new Uint8Array(header.length + key.length);

mbKey.set(header);
mbKey.set(key, header.length);

return MULTIBASE_BASE58_HEADER + base58.encode(mbKey);
}
76 changes: 71 additions & 5 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,83 @@
/*!
* Copyright (c) 2023 Digital Bazaar, Inc. All rights reserved.
kezike marked this conversation as resolved.
Show resolved Hide resolved
*/
import {MULTIKEY_CONTEXT_V1_URL} from './constants.js';

export async function generate({id, controller} = {}) {
import {EXTRACTABLE, MULTIKEY_CONTEXT_V1_URL} from './constants.js';
import {CryptoKey, EcdsaCurve, webcrypto} from './ecdsa.js';
import {createSigner, createVerifier} from './factory.js';
import {exportKeyPair, importKeyPair} from './serialize.js';
import {toMultikey} from './translators.js';

// FIXME: support `P-256K` via `@noble/secp256k1`
// Generate ECDSA key pair
export async function generate({id, controller, curve} = {}) {
if(!curve) {
throw new TypeError('"curve" must be one of the following values: ' +
`${Object.values(EcdsaCurve).map((v) => `'${v}'`).join(', ')}.`);
}
const algorithm = {name: 'ECDSA', namedCurve: curve};
const keyPair = await webcrypto.subtle.generateKey(
algorithm, EXTRACTABLE, ['sign', 'verify']
);
keyPair.secretKey = keyPair.privateKey;
delete keyPair.privateKey;
const keyPairInterface = await _createKeyPairInterface({keyPair});
const exportedKeyPair = await keyPairInterface.export({ publicKey: true });
const {publicKeyMultibase} = exportedKeyPair;
if(controller && !id) {
id = `${controller}#${publicKeyMultibase}`;
}
keyPairInterface.id = id;
keyPairInterface.controller = controller;
return keyPairInterface;
}

// import key pair from JSON Multikey
// Import ECDSA key pair from JSON Multikey
export async function from(key) {
if(key.type && key.type !== 'Multikey') {
key = await toMultikey({keyPair: key});
return _createKeyPairInterface({keyPair: key});
}
if(!key.type) {
key.type = 'Multikey';
}
if(!key['@context']) {
key['@context'] = MULTIKEY_CONTEXT_V1_URL;
}
if(key.controller && !key.id) {
key.id = `${key.controller}#${key.publicKeyMultibase}`;
}

_assertMultikey(key);
return _createKeyPairInterface({keyPair: key});
}

async function _createKeyPairInterface({keyPair}) {
if(!(keyPair?.publicKey instanceof CryptoKey)) {
keyPair = await importKeyPair(keyPair);
}
const exportFn = async ({
publicKey = true, secretKey = false, includeContext = true
} = {}) => {
return exportKeyPair({keyPair, publicKey, secretKey, includeContext});
};
const {publicKeyMultibase, secretKeyMultibase} = await exportFn({
publicKey: true, secretKey: true, includeContext: true
});
keyPair = {
...keyPair,
publicKeyMultibase,
secretKeyMultibase,
export: exportFn,
signer() {
const {id, secretKey} = keyPair;
return createSigner({id, secretKey});
},
verifier() {
const {id, publicKey} = keyPair;
return createVerifier({id, publicKey});
}
};

return keyPair;
}

Expand All @@ -25,6 +90,7 @@ function _assertMultikey(key) {
}
if(key['@context'] !== MULTIKEY_CONTEXT_V1_URL) {
throw new Error('"key" must be a Multikey with context ' +
`"${MULTIKEY_CONTEXT_V1_URL}".`);
`"${MULTIKEY_CONTEXT_V1_URL}".`
);
}
}
Loading