Skip to content

Commit

Permalink
Add fromJwk()/toJwk().
Browse files Browse the repository at this point in the history
  • Loading branch information
dlongley committed Oct 30, 2023
1 parent 5c13147 commit db7086a
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 43 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# @digitalbazaar/ecdsa-multikey ChangeLog

## 1.2.0 - 2023-10-dd

### Added
- Add `fromJwk()` and `toJwk()` for importing / exporting key pairs using JWK.

## 1.1.3 - 2023-05-19

### Fixed
Expand Down
11 changes: 3 additions & 8 deletions lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,7 @@ export function getNamedCurveFromSecretMultikey({secretMultikey}) {
}

// retrieves byte size of secret key
export function getSecretKeySize({keyPair}) {
const key = keyPair.secretKey || keyPair.publicKey;
const {namedCurve: curve} = key.algorithm;
export function getSecretKeySize({curve}) {
if(curve === ECDSA_CURVE.P256) {
return 32;
}
Expand All @@ -64,9 +62,7 @@ export function getSecretKeySize({keyPair}) {
}

// sets secret key header bytes on key pair
export function setSecretKeyHeader({keyPair, buffer}) {
const key = keyPair.secretKey || keyPair.publicKey;
const {namedCurve: curve} = key.algorithm;
export function setSecretKeyHeader({curve, buffer}) {
if(curve === ECDSA_CURVE.P256) {
buffer[0] = MULTICODEC_P256_SECRET_KEY_HEADER[0];
buffer[1] = MULTICODEC_P256_SECRET_KEY_HEADER[1];
Expand All @@ -82,8 +78,7 @@ export function setSecretKeyHeader({keyPair, buffer}) {
}

// sets public key header bytes on key pair
export function setPublicKeyHeader({keyPair, buffer}) {
const {namedCurve: curve} = keyPair.publicKey.algorithm;
export function setPublicKeyHeader({curve, buffer}) {
if(curve === ECDSA_CURVE.P256) {
buffer[0] = MULTICODEC_P256_PUBLIC_KEY_HEADER[0];
buffer[1] = MULTICODEC_P256_PUBLIC_KEY_HEADER[1];
Expand Down
30 changes: 28 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
} from './constants.js';
import {CryptoKey, webcrypto} from './crypto.js';
import {createSigner, createVerifier} from './factory.js';
import {exportKeyPair, importKeyPair} from './serialize.js';
import {
exportKeyPair, importKeyPair, toPublicKeyMultibase, toSecretKeyMultibase
} from './serialize.js';
import {toMultikey} from './translators.js';

// FIXME: support `P-256K` via `@noble/secp256k1`
Expand Down Expand Up @@ -38,7 +40,7 @@ export async function generate({id, controller, curve} = {}) {
return keyPairInterface;
}

// imports ECDSA key pair from JSON Multikey
// imports P-256 key pair from JSON Multikey
export async function from(key) {
let multikey = {...key};
if(multikey.type && multikey.type !== 'Multikey') {
Expand All @@ -59,6 +61,30 @@ export async function from(key) {
return _createKeyPairInterface({keyPair: multikey});
}

// imports key pair from JWK
export async function fromJwk({jwk, secretKey = false} = {}) {
const multikey = {
'@context': MULTIKEY_CONTEXT_V1_URL,
type: 'Multikey',
publicKeyMultibase: toPublicKeyMultibase({jwk})
};
if(jwk.d) {
multikey.secretKeyMultibase = toSecretKeyMultibase({jwk});
}
return from(multikey);
}

// converts key pair to JWK
export async function toJwk({keyPair, secretKey = false} = {}) {
if(!(keyPair?.publicKey instanceof CryptoKey)) {
keyPair = await importKeyPair(keyPair);
}
const useSecretKey = secretKey && !!keyPair.secretKey;
const cryptoKey = useSecretKey ? keyPair.secretKey : keyPair.publicKey;
const jwk = await webcrypto.subtle.exportKey('jwk', cryptoKey);
return jwk;
}

// augments key pair with useful metadata and utilities
async function _createKeyPairInterface({keyPair}) {
if(!(keyPair?.publicKey instanceof CryptoKey)) {
Expand Down
77 changes: 45 additions & 32 deletions lib/serialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,8 @@ export async function exportKeyPair({
);
}

const useSecretKey = secretKey && !!keyPair.secretKey;
const secretKeySize = getSecretKeySize({keyPair});

// get JWK
const useSecretKey = secretKey && !!keyPair.secretKey;
const cryptoKey = useSecretKey ? keyPair.secretKey : keyPair.publicKey;
const jwk = await webcrypto.subtle.exportKey('jwk', cryptoKey);

Expand All @@ -91,32 +89,11 @@ export async function exportKeyPair({
exported.controller = keyPair.controller;

if(publicKey) {
// convert `x` coordinate to compressed public key
const x = base64url.decode(jwk.x);
const y = base64url.decode(jwk.y);
// public key size is always secret key size + 1
const publicKeySize = secretKeySize + 1;
// leave room for multicodec header (2 bytes)
const multikey = new Uint8Array(2 + publicKeySize);
setPublicKeyHeader({keyPair, buffer: multikey});
// use even / odd status of `y` coordinate for compressed header
const even = y[y.length - 1] % 2 === 0;
multikey[2] = even ? 2 : 3;
// write `x` coordinate at end of multikey buffer to zero-fill it
multikey.set(x, multikey.length - x.length);
exported.publicKeyMultibase = MULTIBASE_BASE58_HEADER +
base58.encode(multikey);
exported.publicKeyMultibase = toPublicKeyMultibase({jwk});
}

if(useSecretKey) {
const d = base64url.decode(jwk.d);
// leave room for multicodec header (2 bytes)
const multikey = new Uint8Array(2 + secretKeySize);
setSecretKeyHeader({keyPair, buffer: multikey});
// write `d` at end of multikey buffer to zero-fill it
multikey.set(d, multikey.length - d.length);
exported.secretKeyMultibase = MULTIBASE_BASE58_HEADER +
base58.encode(multikey);
exported.secretKeyMultibase = toSecretKeyMultibase({jwk});
}

return exported;
Expand Down Expand Up @@ -151,16 +128,14 @@ export async function importKeyPair({
// compressed public keys
const spki = _toSpki({publicMultikey});
keyPair.publicKey = await webcrypto.subtle.importKey(
'spki', spki, algorithm, EXTRACTABLE, ['verify']
);
'spki', spki, algorithm, EXTRACTABLE, ['verify']);

// import secret key if given
if(secretKeyMultibase) {
if(!(typeof secretKeyMultibase === 'string' &&
secretKeyMultibase[0] === MULTIBASE_BASE58_HEADER)) {
throw new TypeError(
'"secretKeyMultibase" must be a multibase, base58-encoded string.'
);
'"secretKeyMultibase" must be a multibase, base58-encoded string.');
}
const secretMultikey = base58.decode(secretKeyMultibase.slice(1));

Expand All @@ -172,13 +147,51 @@ export async function importKeyPair({
// compressed keys
const pkcs8 = _toPkcs8({secretMultikey, publicMultikey});
keyPair.secretKey = await webcrypto.subtle.importKey(
'pkcs8', pkcs8, algorithm, EXTRACTABLE, ['sign']
);
'pkcs8', pkcs8, algorithm, EXTRACTABLE, ['sign']);
}

return keyPair;
}

export function toPublicKeyMultibase({jwk} = {}) {
if(jwk?.kty !== 'EC') {
throw new TypeError('"jwk.kty" must be "EC".');
}
const {crv: curve} = jwk;
const secretKeySize = getSecretKeySize({curve});
// convert `x` coordinate to compressed public key
const x = base64url.decode(jwk.x);
const y = base64url.decode(jwk.y);
// public key size is always secret key size + 1
const publicKeySize = secretKeySize + 1;
// leave room for multicodec header (2 bytes)
const multikey = new Uint8Array(2 + publicKeySize);
setPublicKeyHeader({curve, buffer: multikey});
// use even / odd status of `y` coordinate for compressed header
const even = y[y.length - 1] % 2 === 0;
multikey[2] = even ? 2 : 3;
// write `x` coordinate at end of multikey buffer to zero-fill it
multikey.set(x, multikey.length - x.length);
const publicKeyMultibase = MULTIBASE_BASE58_HEADER + base58.encode(multikey);
return publicKeyMultibase;
}

export function toSecretKeyMultibase({jwk} = {}) {
if(jwk?.kty !== 'EC') {
throw new TypeError('"jwk.kty" must be "EC".');
}
const {crv: curve} = jwk;
const secretKeySize = getSecretKeySize({curve});
const d = base64url.decode(jwk.d);
// leave room for multicodec header (2 bytes)
const multikey = new Uint8Array(2 + secretKeySize);
setSecretKeyHeader({curve: jwk.crv, buffer: multikey});
// write `d` at end of multikey buffer to zero-fill it
multikey.set(d, multikey.length - d.length);
const secretKeyMultibase = MULTIBASE_BASE58_HEADER + base58.encode(multikey);
return secretKeyMultibase;
}

// ensures that public key header matches secret key header
function _ensureMultikeyHeadersMatch({secretMultikey, publicMultikey}) {
const publicCurve = getNamedCurveFromPublicMultikey({publicMultikey});
Expand Down
32 changes: 31 additions & 1 deletion test/EcdsaMultikey.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ const {expect} = chai;

describe('EcdsaMultikey', () => {
describe('module', () => {
it('should have "generate" and "from" properties', async () => {
it('should have proper exports', async () => {
expect(EcdsaMultikey).to.have.property('generate');
expect(EcdsaMultikey).to.have.property('from');
expect(EcdsaMultikey).to.have.property('fromJwk');
expect(EcdsaMultikey).to.have.property('toJwk');
});
});

Expand Down Expand Up @@ -161,6 +163,34 @@ describe('EcdsaMultikey', () => {
});
});

describe('fromJwk/toJwk', () => {
it('should round-trip secret JWKs', async () => {
const keyPair = await EcdsaMultikey.generate({
id: '4e0db4260c87cc200df3',
curve: 'P-256'
});
const jwk1 = await EcdsaMultikey.toJwk({keyPair, secretKey: true});
should.exist(jwk1.d);
const keyPairImported = await EcdsaMultikey.fromJwk(
{jwk: jwk1, secretKey: true});
const jwk2 = await EcdsaMultikey.toJwk(
{keyPair: keyPairImported, secretKey: true});
expect(jwk1).to.eql(jwk2);
});

it('should round-trip public JWKs', async () => {
const keyPair = await EcdsaMultikey.generate({
id: '4e0db4260c87cc200df3',
curve: 'P-256'
});
const jwk1 = await EcdsaMultikey.toJwk({keyPair});
should.not.exist(jwk1.d);
const keyPairImported = await EcdsaMultikey.fromJwk({jwk: jwk1});
const jwk2 = await EcdsaMultikey.toJwk({keyPair: keyPairImported});
expect(jwk1).to.eql(jwk2);
});
});

describe('Backwards compat with EcdsaSecp256r1VerificationKey2019', () => {
it('Multikey should import properly', async () => {
const keyPair = await EcdsaMultikey.from(mockKeyEcdsaSecp256);
Expand Down

0 comments on commit db7086a

Please sign in to comment.