Skip to content

Commit

Permalink
Add raw key import API fromRaw().
Browse files Browse the repository at this point in the history
  • Loading branch information
dlongley committed Nov 7, 2023
1 parent 753720a commit c6e9d93
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 31 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# @digitalbazaar/ecdsa-multikey ChangeLog

## 1.6.0 - 2023-11-dd

### Added
- Add `fromRaw()` to import a key pair from a named `curve`, `secretKey`,
and `publicKey`.
- Reformat `keyAgreement` param in `from()` to `options` to enable named
usage (`{keyAgreement: true|false}`) for better API.

## 1.5.0 - 2023-11-05

### Added
Expand Down
28 changes: 27 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import {CryptoKey, webcrypto} from './crypto.js';
import {createSigner, createVerifier} from './factory.js';
import {
cryptoKeyfromRaw,
exportKeyPair, importKeyPair,
toPublicKeyBytes, toSecretKeyBytes,
toPublicKeyMultibase, toSecretKeyMultibase
Expand Down Expand Up @@ -47,7 +48,13 @@ export async function generate({
}

// imports P-256 key pair from JSON Multikey
export async function from(key, keyAgreement = false) {
export async function from(key, options = {}) {
// backwards compatibility
if(typeof options === 'boolean') {
options = {keyAgreement: options};
}
const {keyAgreement} = options;

let multikey = {...key};
if(multikey.type && multikey.type !== 'Multikey') {
multikey = await toMultikey({keyPair: multikey});
Expand Down Expand Up @@ -92,6 +99,25 @@ export async function toJwk({keyPair, secretKey = false} = {}) {
return jwk;
}

// raw import from secretKey and publicKey bytes
export async function fromRaw({
curve, secretKey, publicKey, keyAgreement = false
} = {}) {
if(typeof curve !== 'string') {
throw new TypeError('"curve" must be a string.');
}
if(secretKey && !(secretKey instanceof Uint8Array)) {
throw new TypeError('"secretKey" must be a Uint8Array.');
}
if(!(publicKey instanceof Uint8Array)) {
throw new TypeError('"publicKey" must be a Uint8Array.');
}
const cryptoKey = await cryptoKeyfromRaw(
{curve, secretKey, publicKey, keyAgreement});
const jwk = await webcrypto.subtle.exportKey('jwk', cryptoKey);
return fromJwk({jwk, secretKey: !!secretKey, keyAgreement});
}

// augments key pair with useful metadata and utilities
async function _createKeyPairInterface({keyPair, keyAgreement = false}) {
if(!(keyPair?.publicKey instanceof CryptoKey)) {
Expand Down
90 changes: 60 additions & 30 deletions lib/serialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,38 @@ const SPKI_PREFIXES = new Map([
])]
]);

// converts key pair to PKCS #8 format
export async function cryptoKeyfromRaw({
curve, secretKey, publicKey, keyAgreement
} = {}) {
const algorithm = {
name: keyAgreement ? 'ECDH' : ALGORITHM,
namedCurve: curve
};

let cryptoKey;
if(secretKey) {
const pkcs8 = _rawToPkcs8({curve, secretKey, publicKey});
const secretUsage = keyAgreement ? ['deriveBits'] : ['sign'];
cryptoKey = await webcrypto.subtle.importKey(
'pkcs8', pkcs8, algorithm, EXTRACTABLE, secretUsage);
} else {
const spki = _rawToSpki({curve, publicKey});
// must be empty usage for importing a public key
const publicUsage = keyAgreement ? [] : ['verify'];
cryptoKey = await webcrypto.subtle.importKey(
'spki', spki, algorithm, EXTRACTABLE, publicUsage);
}
return cryptoKey;
}

// exports key pair
export async function exportKeyPair({
keyPair, secretKey, publicKey, includeContext
} = {}) {
if(!(publicKey || secretKey)) {
throw new TypeError(
'Export requires specifying either "publicKey" or "secretKey".'
);
'Export requires specifying either "publicKey" or "secretKey".');
}

// get JWK
Expand Down Expand Up @@ -113,8 +137,7 @@ export async function importKeyPair({
if(!(publicKeyMultibase && typeof publicKeyMultibase === 'string' &&
publicKeyMultibase[0] === MULTIBASE_BASE58_HEADER)) {
throw new TypeError(
'"publicKeyMultibase" must be a multibase, base58-encoded string.'
);
'"publicKeyMultibase" must be a multibase, base58-encoded string.');
}
const publicMultikey = base58.decode(publicKeyMultibase.slice(1));

Expand All @@ -126,7 +149,7 @@ export async function importKeyPair({

// import public key; convert to `spki` format because `jwk` doesn't handle
// compressed public keys
const spki = _toSpki({publicMultikey});
const spki = _multikeyToSpki({publicMultikey});
// must be empty usage for importing a public key
const publicUsage = keyAgreement ? [] : ['verify'];
keyPair.publicKey = await webcrypto.subtle.importKey(
Expand All @@ -147,7 +170,7 @@ export async function importKeyPair({

// convert to `pkcs8` format for import because `jwk` doesn't support
// compressed keys
const pkcs8 = _toPkcs8({secretMultikey, publicMultikey});
const pkcs8 = _multikeyToPkcs8({secretMultikey, publicMultikey});
const secretUsage = keyAgreement ? ['deriveBits'] : ['sign'];
keyPair.secretKey = await webcrypto.subtle.importKey(
'pkcs8', pkcs8, algorithm, EXTRACTABLE, secretUsage);
Expand Down Expand Up @@ -235,13 +258,28 @@ function _ensureMultikeyHeadersMatch({secretMultikey, publicMultikey}) {
if(publicCurve !== secretCurve) {
throw new Error(
`Public key curve ('${publicCurve}') does not match ` +
`secret key curve ('${secretCurve}').`
);
`secret key curve ('${secretCurve}').`);
}
}

// converts key pair to PKCS #8 format
function _toPkcs8({secretMultikey, publicMultikey}) {
function _multikeyToPkcs8({secretMultikey, publicMultikey}) {
const curve = getNamedCurveFromSecretMultikey({secretMultikey});
// omit multikey headers
const secretKey = secretMultikey.subarray(2);
const publicKey = publicMultikey.subarray(2);
return _rawToPkcs8({curve, secretKey, publicKey});
}

function _multikeyToSpki({publicMultikey}) {
const curve = getNamedCurveFromPublicMultikey({publicMultikey});
// omit multikey header
const publicKey = publicMultikey.subarray(2);
return _rawToSpki({curve, publicKey});
}

// converts key pair to PKCS #8 format
export function _rawToPkcs8({curve, secretKey, publicKey}) {
/* Format:
SEQUENCE (3 elem)
INTEGER 0
Expand All @@ -262,42 +300,34 @@ function _toPkcs8({secretMultikey, publicMultikey}) {
RAW SECRET KEY BYTES
PKCS #8 DER PUBLIC KEY HEADER
COMPRESSED / UNCOMPRESSED PUBLIC KEY BYTES */
const headers = PKCS8_PREFIXES.get(
getNamedCurveFromPublicMultikey({publicMultikey}));
const headers = PKCS8_PREFIXES.get(curve);
if(!headers) {
throw new Error(`Unsupported curve "${curve}".`);
}
const pkcs8 = new Uint8Array(
headers.secret.length +
// do not include multikey 2-byte header
secretMultikey.length - 2 +
headers.public.length +
// do not include multikey 2-byte header
publicMultikey.length - 2
);
headers.secret.length + secretKey.length +
headers.public.length + publicKey.length);
let offset = 0;
pkcs8.set(headers.secret, offset);
offset += headers.secret.length;
pkcs8.set(secretMultikey.subarray(2), offset);
offset += secretMultikey.length - 2;
pkcs8.set(secretKey, offset);
offset += secretKey.length;
pkcs8.set(headers.public, offset);
offset += headers.public.length;
pkcs8.set(publicMultikey.subarray(2), offset);
pkcs8.set(publicKey, offset);
return pkcs8;
}

// converts public key to SubjectPublicKeyInfo format
function _toSpki({publicMultikey}) {
function _rawToSpki({curve, publicKey}) {
/* Format:
SPKI DER PUBLIC KEY HEADER (w/algorithm OID for specific key type)
COMPRESSED / UNCOMPRESSED PUBLIC KEY BYTES */
const header = SPKI_PREFIXES.get(
getNamedCurveFromPublicMultikey({publicMultikey}));
const spki = new Uint8Array(
header.length +
// do not include multikey 2-byte header
publicMultikey.length - 2
);
const header = SPKI_PREFIXES.get(curve);
const spki = new Uint8Array(header.length + publicKey.length);
let offset = 0;
spki.set(header, offset);
offset += header.length;
spki.set(publicMultikey.subarray(2), offset);
spki.set(publicKey, offset);
return spki;
}
42 changes: 42 additions & 0 deletions test/EcdsaMultikey.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,48 @@ describe('EcdsaMultikey', () => {
});
});

describe('fromRaw', () => {
it('should import raw public key', async () => {
const curve = 'P-256';
const keyPair = await EcdsaMultikey.generate({curve});

// first export
const expectedPublicKey = base58.decode(
keyPair.publicKeyMultibase.slice(1)).slice(2);
const {publicKey} = await keyPair.export({publicKey: true, raw: true});
expect(expectedPublicKey).to.deep.equal(publicKey);

// then import
const imported = await EcdsaMultikey.fromRaw({curve, publicKey});

// then re-export to confirm
const {publicKey: publicKey2} = await imported.export(
{publicKey: true, raw: true});
expect(expectedPublicKey).to.deep.equal(publicKey2);
});

it('should import raw secret key', async () => {
const curve = 'P-256';
const keyPair = await EcdsaMultikey.generate({curve});

// first export
const expectedSecretKey = base58.decode(
keyPair.secretKeyMultibase.slice(1)).slice(2);
const {secretKey, publicKey} = await keyPair.export(
{secretKey: true, raw: true});
expect(expectedSecretKey).to.deep.equal(secretKey);

// then import
const imported = await EcdsaMultikey.fromRaw(
{curve, secretKey, publicKey});

// then re-export to confirm
const {secretKey: secreyKey2} = await imported.export(
{secretKey: true, raw: true});
expect(expectedSecretKey).to.deep.equal(secreyKey2);
});
});

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

0 comments on commit c6e9d93

Please sign in to comment.