Skip to content

Commit

Permalink
Refactor @web5/dids to adopt Web5 Spec API changes (#406)
Browse files Browse the repository at this point in the history
* Break out BearerDid/PortableDid and add DidRegistrationResult
* Refactor DIDs package to new API design
* Bump @web5/dids to 0.4.0

---------

Signed-off-by: Frank Hinek <[email protected]>
Co-authored-by: nitro-neal <[email protected]>
  • Loading branch information
2 people authored and finn-block committed Mar 19, 2024
1 parent 809957c commit 3048ca8
Show file tree
Hide file tree
Showing 22 changed files with 2,991 additions and 3,075 deletions.
458 changes: 265 additions & 193 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/dids/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@web5/dids",
"version": "0.3.0",
"version": "0.4.0",
"description": "TBD DIDs library",
"type": "module",
"main": "./dist/cjs/index.js",
Expand Down
307 changes: 307 additions & 0 deletions packages/dids/src/bearer-did.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
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 { DidDocument } from './types/did-core.js';
import type { DidMetadata, PortableDid } from './types/portable-did.js';

import { DidError, DidErrorCode } from './did-error.js';
import { extractDidFragment, getVerificationMethods } from './utils.js';

/**
* A `BearerDidSigner` extends the {@link Signer} interface to include specific properties for
* signing with a Decentralized Identifier (DID). It encapsulates the algorithm and key identifier,
* which are often needed when signing JWTs, JWSs, JWEs, and other data structures.
*
* Typically, the algorithm and key identifier are used to populate the `alg` and `kid` fields of a
* JWT or JWS header.
*/
export interface BearerDidSigner extends Signer {
/**
* The cryptographic algorithm identifier used for signing operations.
*
* Typically, this value is used to populate the `alg` field of a JWT or JWS header. The
* registered algorithm names are defined in the
* {@link https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms | IANA JSON Web Signature and Encryption Algorithms registry}.
*
* @example
* "ES256" // ECDSA using P-256 and SHA-256
*/
algorithm: string;

/**
* The unique identifier of the key within the DID document that is used for signing and
* verification operations.
*
* This identifier must be a DID URI with a fragment (e.g., did:method:123#key-0) that references
* a specific verification method in the DID document. It allows users of a `BearerDidSigner` to
* determine the DID and key that will be used for signing and verification operations.
*
* @example
* "did:dht:123#key-1" // A fragment identifier referring to a key in the DID document
*/
keyId: string;
}

/**
* Represents a Decentralized Identifier (DID) along with its DID document, key manager, metadata,
* and convenience functions.
*/
export class BearerDid {
/** {@inheritDoc Did#uri} */
uri: string;

/**
* The DID document associated with this DID.
*
* @see {@link https://www.w3.org/TR/did-core/#dfn-diddocument | DID Core Specification, § DID Document}
*/
document: DidDocument;

/** {@inheritDoc DidMetadata} */
metadata: DidMetadata;

/**
* Key Management System (KMS) used to manage the DIDs keys and sign data.
*
* Each DID method requires at least one key be present in the provided `keyManager`.
*/
keyManager: CryptoApi;

constructor({ uri, document, metadata, keyManager }: {
uri: string,
document: DidDocument,
metadata: DidMetadata,
keyManager: CryptoApi
}) {
this.uri = uri;
this.document = document;
this.metadata = metadata;
this.keyManager = keyManager;
}

/**
* Converts a `BearerDid` object to a portable format containing the URI and verification methods
* associated with the DID.
*
* This method is useful when you need to represent the key material and metadata associated with
* a DID in format that can be used independently of the specific DID method implementation. It
* extracts both public and private keys from the DID's key manager and organizes them into a
* `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.
*
* 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.
*
* @example
* ```ts
* // Assuming `did` is an instance of BearerDid
* const portableDid = await did.export();
* // portableDid now contains the DID URI, document, metadata, and optionally, private keys.
* ```
*
* @returns A `PortableDid` containing the URI, DID document, metadata, and optionally private
* keys associated with the `BearerDid`.
* @throws An error if the DID document does not contain any verification methods or the keys for
* any verification method are missing in the key manager.
*/
public async export(): Promise<PortableDid> {
// Verify the DID document contains at least one verification method.
if (!(Array.isArray(this.document.verificationMethod) && this.document.verificationMethod.length > 0)) {
throw new Error(`DID document for '${this.uri}' is missing verification methods`);
}

// Create a new `PortableDid` object to store the exported data.
let portableDid: PortableDid = {
uri : this.uri,
document : this.document,
metadata : this.metadata
};

// If the BearerDid's key manager supports exporting private keys, add them to the portable DID.
if ('exportKey' in this.keyManager && typeof this.keyManager.exportKey === 'function') {
const privateKeys: Jwk[] = [];
for (let vm of this.document.verificationMethod) {
if (!vm.publicKeyJwk) {
throw new Error(`Verification method '${vm.id}' does not contain a public key in JWK format`);
}

// Compute the key URI of the verification method's public key.
const keyUri = await this.keyManager.getKeyUri({ key: vm.publicKeyJwk });

// Retrieve the private key from the key manager.
const privateKey = await this.keyManager.exportKey({ keyUri }) as Jwk;

// Add the verification method to the key set.
privateKeys.push({ ...privateKey });
}
portableDid.privateKeys = privateKeys;
}

return portableDid;
}

/**
* Return a {@link Signer} that can be used to sign messages, credentials, or arbitrary data.
*
* If given, the `methodId` parameter is used to select a key from the verification methods
* present in the DID Document.
*
* If `methodID` is not given, the first verification method intended for signing claims is used.
*
* @param params - The parameters for the `getSigner` operation.
* @param params.methodId - ID of the verification method key that will be used for sign and
* verify operations. Optional.
* @returns An instantiated {@link Signer} that can be used to sign and verify data.
*/
public async getSigner(params?: { methodId: string }): Promise<BearerDidSigner> {
// Attempt to find a verification method that matches the given method ID, or if not given,
// find the first verification method intended for signing claims.
const verificationMethod = this.document.verificationMethod?.find(
vm => extractDidFragment(vm.id) === (extractDidFragment(params?.methodId) ?? extractDidFragment(this.document.assertionMethod?.[0]))
);

if (!(verificationMethod && verificationMethod.publicKeyJwk)) {
throw new DidError(DidErrorCode.InternalError, 'A verification method intended for signing could not be determined from the DID Document');
}

// Compute the expected key URI of the signing key.
const keyUri = await this.keyManager.getKeyUri({ key: verificationMethod.publicKeyJwk });

// Get the public key to be used for verify operations, which also verifies that the key is
// present in the key manager's store.
const publicKey = await this.keyManager.getPublicKey({ keyUri });

// Bind the DID's key manager to the signer.
const keyManager = this.keyManager;

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

return {
algorithm : algorithm,
keyId : verificationMethod.id,

async sign({ data }: EnclosedSignParams): Promise<Uint8Array> {
const signature = await keyManager.sign({ data, keyUri: keyUri! }); // `keyUri` is guaranteed to be defined at this point.
return signature;
},

async verify({ data, signature }: EnclosedVerifyParams): Promise<boolean> {
const isValid = await keyManager.verify({ data, key: publicKey!, signature }); // `publicKey` is guaranteed to be defined at this point.
return isValid;
}
};
}

/**
* Instantiates a {@link BearerDid} object for the DID DHT method from a given {@link PortableDid}.
*
* This method allows for the creation of a `BearerDid` object using a previously created DID's
* key material, DID document, and metadata.
*
* @example
* ```ts
* // Export an existing BearerDid to PortableDid format.
* const portableDid = await did.export();
* // Reconstruct a BearerDid object from the PortableDid.
* const did = await DidDht.import({ portableDid });
* ```
*
* @param params - The parameters for the import operation.
* @param params.portableDid - The PortableDid object to import.
* @param params.keyManager - Optionally specify an external Key Management System (KMS) used to
* generate keys and sign data. If not given, a new
* {@link LocalKeyManager} instance will be created and
* used.
* @returns A Promise resolving to a `BearerDid` object representing the DID formed from the
* provided PortableDid.
* @throws An error if the PortableDid document does not contain any verification methods or the
* keys for any verification method are missing in the key manager.
*/
public static async import({ portableDid, keyManager = new LocalKeyManager() }: {
keyManager?: CryptoApi & KeyImporterExporter<KmsImportKeyParams, KeyIdentifier, KmsExportKeyParams>;
portableDid: PortableDid;
}): Promise<BearerDid> {
// Get all verification methods from the given DID document, including embedded methods.
const verificationMethods = getVerificationMethods({ didDocument: portableDid.document });

// Validate that the DID document contains at least one verification method.
if (verificationMethods.length === 0) {
throw new DidError(DidErrorCode.InvalidDidDocument, `At least one verification method is required but 0 were given`);
}

// If given, import the private key material into the key manager.
for (let key of portableDid.privateKeys ?? []) {
await keyManager.importKey({ key });
}

// Validate that the key material for every verification method in the DID document is present
// in the key manager.
for (let vm of verificationMethods) {
if (!vm.publicKeyJwk) {
throw new Error(`Verification method '${vm.id}' does not contain a public key in JWK format`);
}

// Compute the key URI of the verification method's public key.
const keyUri = await keyManager.getKeyUri({ key: vm.publicKeyJwk });

// Verify that the key is present in the key manager. If not, an error is thrown.
await keyManager.getPublicKey({ keyUri });
}

// Use the given PortableDid to construct the BearerDid object.
const did = new BearerDid({
uri : portableDid.uri,
document : portableDid.document,
metadata : portableDid.metadata,
keyManager
});

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}`);
}
}
9 changes: 6 additions & 3 deletions packages/dids/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
export * from './types/did-core.js';
export type * from './types/multibase.js';
export type * from './types/portable-did.js';

export * from './did.js';
export * from './did-error.js';
export * from './bearer-did.js';

export * from './methods/did-dht.js';
export * from './methods/did-ion.js';
Expand All @@ -12,6 +17,4 @@ export * from './resolver/did-resolver.js';
export * from './resolver/resolver-cache-level.js';
export * from './resolver/resolver-cache-noop.js';

export * as utils from './utils.js';

export * from './types/did-core.js';
export * as utils from './utils.js';
Loading

0 comments on commit 3048ca8

Please sign in to comment.