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

Add crypto utility function and use in BearerDid #430

Merged
merged 2 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
54 changes: 54 additions & 0 deletions packages/crypto/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Jwk } from './jose/jwk.js';
jiyoonie9 marked this conversation as resolved.
Show resolved Hide resolved

import { crypto } from '@noble/hashes/crypto';
import { randomBytes as nobleRandomBytes } from '@noble/hashes/utils';

Expand Down Expand Up @@ -63,6 +65,58 @@ export function checkValidProperty(params: {
}
}

/**
* Determines the JOSE algorithm identifier of the digital signature algorithm based on the `alg` or
* `crv` property of a {@link Jwk | JWK}.
*
* If the `alg` property is present, its value takes precedence and is returned. Otherwise, the
* `crv` property is used to determine the algorithm.
*
* @see {@link https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms | JOSE Algorithms}
* @see {@link https://datatracker.ietf.org/doc/draft-ietf-jose-fully-specified-algorithms/ | Fully-Specified Algorithms for JOSE and COSE}
*
* @example
* ```ts
* const publicKey: Jwk = {
* "kty": "OKP",
* "crv": "Ed25519",
* "x": "FEJG7OakZi500EydXxuE8uMc8uaAzEJkmQeG8khXANw"
* }
* const algorithm = getJoseSignatureAlgorithmFromPublicKey(publicKey);
* console.log(algorithm); // Output: "EdDSA"
* ```
*
* @param publicKey - A JWK containing the `alg` and/or `crv` properties.
* @returns The name of the algorithm associated with the key.
* @throws Error if the algorithm cannot be determined from the provided input.
*/
export function getJoseSignatureAlgorithmFromPublicKey(publicKey: Jwk): string {
const curveToJoseAlgorithm: Record<string, string> = {
jiyoonie9 marked this conversation as resolved.
Show resolved Hide resolved
'Ed25519' : 'EdDSA',
'P-256' : 'ES256',
'P-384' : 'ES384',
'P-521' : 'ES512',
'secp256k1' : 'ES256K',
};

// If the key contains an `alg` property that matches a JOSE registered algorithm identifier,
// return its value.
if (publicKey.alg && Object.values(curveToJoseAlgorithm).includes(publicKey.alg)) {
Copy link

Choose a reason for hiding this comment

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

Nit clarification: Previously, we didn't check that the alg is in curveToJoseAlgorithm values. This seems correct to me, but also took me a second to notice while reviewing. Did we previously want to return "unsupported" algorithms or was that just an oversight that we're not correcting?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I may be missing the question. Unsupported algorithms won't be returned and an error will be thrown.

return publicKey.alg;
}

// If the key contains a `crv` property, return the corresponding algorithm.
if (publicKey.crv && Object.keys(curveToJoseAlgorithm).includes(publicKey.crv)) {
return curveToJoseAlgorithm[publicKey.crv];
}

throw new Error(
`Unable to determine algorithm based on provided input: alg=${publicKey.alg}, crv=${publicKey.crv}. ` +
`Supported 'alg' values: ${Object.values(curveToJoseAlgorithm).join(', ')}. ` +
`Supported 'crv' values: ${Object.keys(curveToJoseAlgorithm).join(', ')}.`
);
}

/**
* Checks if the Web Crypto API is supported in the current runtime environment.
*
Expand Down
45 changes: 45 additions & 0 deletions packages/crypto/tests/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { expect } from 'chai';
import * as sinon from 'sinon';

import type { Jwk } from '../src/jose/jwk.js';

import {
randomUuid,
randomBytes,
checkValidProperty,
isWebCryptoSupported,
checkRequiredProperty,
getJoseSignatureAlgorithmFromPublicKey,
} from '../src/utils.js';

// TODO: Remove this polyfill once Node.js v18 is no longer supported by @web5/crypto.
Expand Down Expand Up @@ -65,6 +68,48 @@ describe('Crypto Utils', () => {
});
});

describe('getJoseSignatureAlgorithmFromPublicKey()', () => {
it('returns the algorithm specified by the alg property regardless of the crv property', () => {
const publicKey: Jwk = { kty: 'OKP', alg: 'EdDSA', crv: 'P-256' };
expect(getJoseSignatureAlgorithmFromPublicKey(publicKey)).to.equal('EdDSA');
});

it('returns the correct algorithm for Ed25519 curve', () => {
const publicKey: Jwk = { kty: 'OKP', crv: 'Ed25519' };
expect(getJoseSignatureAlgorithmFromPublicKey(publicKey)).to.equal('EdDSA');
});

it('returns the correct algorithm for P-256 curve', () => {
const publicKey: Jwk = { kty: 'EC', crv: 'P-256' };
expect(getJoseSignatureAlgorithmFromPublicKey(publicKey)).to.equal('ES256');
});

it('returns the correct algorithm for P-384 curve', () => {
const publicKey: Jwk = { kty: 'EC', crv: 'P-384' };
expect(getJoseSignatureAlgorithmFromPublicKey(publicKey)).to.equal('ES384');
});

it('returns the correct algorithm for P-521 curve', () => {
const publicKey: Jwk = { kty: 'EC', crv: 'P-521' };
expect(getJoseSignatureAlgorithmFromPublicKey(publicKey)).to.equal('ES512');
});

it('throws an error for unsupported algorithms', () => {
const publicKey: Jwk = { kty: 'EC', alg: 'UnsupportedAlgorithm' };
expect(() => getJoseSignatureAlgorithmFromPublicKey(publicKey)).to.throw();
});

it('throws an error for unsupported curves', () => {
const publicKey: Jwk = { kty: 'EC', crv: 'UnsupportedCurve' };
expect(() => getJoseSignatureAlgorithmFromPublicKey(publicKey)).to.throw();
});

it('throws an error when neither alg nor crv is provided', () => {
const publicKey: Jwk = { kty: 'EC' };
expect(() => getJoseSignatureAlgorithmFromPublicKey(publicKey)).to.throw();
});
});

describe('isWebCryptoSupported()', () => {
afterEach(() => {
// Restore the original state after each test
Expand Down
71 changes: 22 additions & 49 deletions packages/dids/src/bearer-did.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
import { LocalKeyManager, type CryptoApi, type EnclosedSignParams, type EnclosedVerifyParams, type Jwk, type KeyIdentifier, type KeyImporterExporter, type KmsExportKeyParams, type KmsImportKeyParams, type Signer } from '@web5/crypto';
import type {
Jwk,
Signer,
CryptoApi,
KeyIdentifier,
EnclosedSignParams,
KmsExportKeyParams,
KmsImportKeyParams,
KeyImporterExporter,
EnclosedVerifyParams,
} from '@web5/crypto';

import { LocalKeyManager, utils as cryptoUtils } from '@web5/crypto';

import type { DidDocument } from './types/did-core.js';
import type { DidMetadata, PortableDid } from './types/portable-did.js';
Expand Down Expand Up @@ -88,13 +100,15 @@ export class BearerDid {
* `PortableDid` structure.
*
* @remarks
* This method requires that the DID's key manager supports the `exportKey` operation. If the DID
* document does not contain any verification methods, or if the key manager does not support key
* export, an error is thrown.
* If the DID's key manager does not allow private keys to be exported, the `PortableDid` returned
* will not contain a `privateKeys` property. This enables the importing and exporting DIDs that
* use the same underlying KMS even if the KMS does not support exporting private keys. Examples
* include hardware security modules (HSMs) and cloud-based KMS services like AWS KMS.
*
* The resulting `PortableDid` will contain the same number of verification methods as the DID
* document, each with its associated public and private keys and the purposes for which the key
* can be used.
* If the DID's key manager does support exporting private keys, the resulting `PortableDid` will
* include a `privateKeys` property which contains the same number of entries as there are
* verification methods as the DID document, each with its associated private key and the
* purpose(s) for which the key can be used (e.g., `authentication`, `assertionMethod`, etc.).
*
* @example
* ```ts
Expand Down Expand Up @@ -179,7 +193,7 @@ export class BearerDid {
const keyManager = this.keyManager;

// Determine the signing algorithm.
const algorithm = BearerDid.getAlgorithmFromPublicKey(publicKey);
const algorithm = cryptoUtils.getJoseSignatureAlgorithmFromPublicKey(publicKey);

return {
algorithm : algorithm,
Expand Down Expand Up @@ -263,45 +277,4 @@ export class BearerDid {

return did;
}

/**
* Determines the name of the algorithm based on the key's curve property.
*
* @remarks
* This method facilitates the identification of the correct algorithm for cryptographic
* operations based on the `alg` or `crv` properties of a {@link Jwk | JWK}.
*
* @example
* ```ts
* const publicKey = { ... }; // Public key in JWK format
* const algorithm = BearerDid.getAlgorithmFromPublicKey({ key: publicKey });
* ```
*
* @param publicKey - A JWK containing the `alg` and/or `crv` properties.
*
* @returns The name of the algorithm associated with the key.
*
* @throws Error if the algorithm cannot be determined from the provided input.
*/
private static getAlgorithmFromPublicKey(publicKey: Jwk): string {
const registeredSigningAlgorithms: Record<string, string> = {
'Ed25519' : 'EdDSA',
'P-256' : 'ES256',
'P-384' : 'ES384',
'P-521' : 'ES512',
'secp256k1' : 'ES256K',
};

// If the key contains an `alg` property, return its value.
if (publicKey.alg) {
return publicKey.alg;
}

// If the key contains a `crv` property, return the corresponding algorithm.
if (publicKey.crv && Object.keys(registeredSigningAlgorithms).includes(publicKey.crv)) {
return registeredSigningAlgorithms[publicKey.crv];
}

throw new Error(`Unable to determine algorithm based on provided input: alg=${publicKey.alg}, crv=${publicKey.crv}`);
}
}
Loading