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

#206 - Introduced support for custom signer #502

Merged
merged 11 commits into from
Sep 18, 2023
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,31 @@ const tenantGate = new CustomTenantGate();
const dwn = await Dwn.create({ messageStore, dataStore, eventLog, tenantGate });
```

### Custom Signature Signer
If you have the private key readily available, it is recommended to use the built-in `PrivateKeySigner`. Otherwise, you can implement a customer signer to interface with external signing service, API, HSM, TPM etc and use it for signing your DWN messages:

```ts
// create a custom signer
class CustomSigner implements Signer {
public async sign (content: Uint8Array): Promise<Uint8Array> {
... // custom signing logic
}
}

const signer = new CustomSigner();

const options: RecordsWriteOptions = {
...
authorizationSignatureInput : {
signer,
protectedHeader: { alg: 'EdDSA', kid: 'did:example:alice#key1' } // see https://www.iana.org/assignments/jose/jose.xhtml for valid signature `alg` values
}
};

const recordsWrite = await RecordsWrite.create(options);
```


## Release/Build Process

The DWN JS SDK releases builds to [npmjs.com](https://www.npmjs.com/package/@tbd54566975/dwn-sdk-js). There are two build types: stable build and unstable build.
Expand Down
1 change: 1 addition & 0 deletions src/core/dwn-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export enum DwnErrorCode {
PermissionsGrantUnauthorizedGrant = 'PermissionsGrantUnauthorizedGrant',
PermissionsRevokeMissingPermissionsGrant = 'PermissionsRevokeMissingPermissionsGrant',
PermissionsRevokeUnauthorizedRevoke = 'PermissionsRevokeUnauthorizedRevoke',
PrivateKeySignerUnsupportedCurve = 'PrivateKeySignerUnsupportedCurve',
ProtocolAuthorizationActionNotAllowed = 'ProtocolAuthorizationActionNotAllowed',
ProtocolAuthorizationIncorrectDataFormat = 'ProtocolAuthorizationIncorrectDataFormat',
ProtocolAuthorizationIncorrectProtocolPath = 'ProtocolAuthorizationIncorrectProtocolPath',
Expand Down
6 changes: 3 additions & 3 deletions src/core/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { SignatureInput } from '../types/jws-types.js';
import type { BaseAuthorizationPayload, Descriptor, GenericMessage } from '../types/message-types.js';

import { Cid } from '../utils/cid.js';
import { GeneralJwsSigner } from '../jose/jws/general/signer.js';
import { GeneralJwsBuilder } from '../jose/jws/general/builder.js';
import { Jws } from '../utils/jws.js';
import { lexicographicalCompare } from '../utils/string.js';
import { removeUndefinedProperties } from '../utils/object.js';
Expand Down Expand Up @@ -148,9 +148,9 @@ export abstract class Message<M extends GenericMessage> {
const authPayloadStr = JSON.stringify(authPayload);
const authPayloadBytes = new TextEncoder().encode(authPayloadStr);

const signer = await GeneralJwsSigner.create(authPayloadBytes, [signatureInput]);
const builder = await GeneralJwsBuilder.create(authPayloadBytes, [signatureInput]);

return signer.getJws();
return builder.getJws();
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export { MessageStore, MessageStoreOptions } from './types/message-store.js';
export { PermissionsGrant, PermissionsGrantOptions } from './interfaces/permissions-grant.js';
export { PermissionsRequest, PermissionsRequestOptions } from './interfaces/permissions-request.js';
export { PermissionsRevoke, PermissionsRevokeOptions } from './interfaces/permissions-revoke.js';
export { PrivateKeySigner } from './utils/private-key-signer.js';
export { Protocols } from './utils/protocols.js';
export { ProtocolsConfigure, ProtocolsConfigureOptions } from './interfaces/protocols-configure.js';
export { ProtocolsQuery, ProtocolsQueryOptions } from './interfaces/protocols-query.js';
Expand All @@ -47,6 +48,7 @@ export { RecordsRead, RecordsReadOptions } from './interfaces/records-read.js';
export { SnapshotsCreate, SnapshotsCreateOptions } from './interfaces/snapshots-create.js';
export { Secp256k1 } from './utils/secp256k1.js';
export { SignatureInput } from './types/jws-types.js';
export { Signer } from './types/signer.js';
export { DataStoreLevel } from './store/data-store-level.js';
export { EventLogLevel } from './event-log/event-log-level.js';
export { MessageStoreLevel } from './store/message-store-level.js';
10 changes: 5 additions & 5 deletions src/interfaces/records-write.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { Cid } from '../utils/cid.js';
import { Encoder } from '../utils/encoder.js';
import { Encryption } from '../utils/encryption.js';
import { EncryptionAlgorithm } from '../utils/encryption.js';
import { GeneralJwsSigner } from '../jose/jws/general/signer.js';
import { GeneralJwsBuilder } from '../jose/jws/general/builder.js';
import { getCurrentTimeInHighPrecision } from '../utils/time.js';
import { Jws } from '../utils/jws.js';
import { KeyDerivationScheme } from '../index.js';
Expand Down Expand Up @@ -635,8 +635,8 @@ export class RecordsWrite {
const attestationPayload: RecordsWriteAttestationPayload = { descriptorCid };
const attestationPayloadBytes = Encoder.objectToBytes(attestationPayload);

const signer = await GeneralJwsSigner.create(attestationPayloadBytes, signatureInputs);
return signer.getJws();
const builder = await GeneralJwsBuilder.create(attestationPayloadBytes, signatureInputs);
return builder.getJws();
}

/**
Expand Down Expand Up @@ -666,8 +666,8 @@ export class RecordsWrite {

const authorizationPayloadBytes = Encoder.objectToBytes(authorizationPayload);

const signer = await GeneralJwsSigner.create(authorizationPayloadBytes, [signatureInput]);
return signer.getJws();
const builder = await GeneralJwsBuilder.create(authorizationPayloadBytes, [signatureInput]);
return builder.getJws();
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/jose/algorithms/signing/ed25519.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as Ed25519 from '@noble/ed25519';
import type { PrivateJwk, PublicJwk, Signer } from '../../../types/jose-types.js';
import type { PrivateJwk, PublicJwk, SignatureAlgorithm } from '../../../types/jose-types.js';

import { Encoder } from '../../../utils/encoder.js';

Expand All @@ -22,7 +22,7 @@ function publicKeyToJwk(publicKeyBytes: Uint8Array): PublicJwk {
return publicJwk;
}

export const ed25519: Signer = {
export const ed25519: SignatureAlgorithm = {
sign: async (content: Uint8Array, privateJwk: PrivateJwk): Promise<Uint8Array> => {
validateKey(privateJwk);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { Signer } from '../../../types/jose-types.js';
import type { SignatureAlgorithm } from '../../../types/jose-types.js';

import { ed25519 } from './ed25519.js';
import { Secp256k1 } from '../../../utils/secp256k1.js';

// the key should be the appropriate `crv` value
export const signers: Record<string, Signer> = {
export const signatureAlgorithms: Record<string, SignatureAlgorithm> = {
'Ed25519' : ed25519,
'secp256k1' : {
sign : Secp256k1.sign,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,45 +1,39 @@
import type { GeneralJws, SignatureInput } from '../../../types/jws-types.js';

import { Encoder } from '../../../utils/encoder.js';
import { signers } from '../../algorithms/signing/signers.js';

export class GeneralJwsSigner {
export class GeneralJwsBuilder {
private jws: GeneralJws;

constructor(jws: GeneralJws) {
private constructor(jws: GeneralJws) {
this.jws = jws;
}

static async create(payload: Uint8Array, signatureInputs: SignatureInput[] = []): Promise<GeneralJwsSigner> {
static async create(payload: Uint8Array, signatureInputs: SignatureInput[] = []): Promise<GeneralJwsBuilder> {
const jws: GeneralJws = {
payload : Encoder.bytesToBase64Url(payload),
signatures : []
};

const signer = new GeneralJwsSigner(jws);
const builder = new GeneralJwsBuilder(jws);

for (const signatureInput of signatureInputs) {
await signer.addSignature(signatureInput);
await builder.addSignature(signatureInput);
}

return signer;
return builder;
}

async addSignature(signatureInput: SignatureInput): Promise<void> {
const { privateJwk, protectedHeader } = signatureInput;
const signer = signers[privateJwk.crv];

if (!signer) {
throw new Error(`unsupported crv. crv must be one of ${Object.keys(signers)}`);
}
const { signer, protectedHeader } = signatureInput;

const protectedHeaderString = JSON.stringify(protectedHeader);
const protectedHeaderBase64UrlString = Encoder.stringToBase64Url(protectedHeaderString);

const signingInputString = `${protectedHeaderBase64UrlString}.${this.jws.payload}`;
const signingInputBytes = Encoder.stringToBytes(signingInputString);

const signatureBytes = await signer.sign(signingInputBytes, privateJwk);
const signatureBytes = await signer.sign(signingInputBytes);
const signature = Encoder.bytesToBase64Url(signatureBytes);

this.jws.signatures.push({ protected: protectedHeaderBase64UrlString, signature });
Expand Down
2 changes: 1 addition & 1 deletion src/types/jose-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export type PrivateJwk = PublicJwk & {
d: string;
};

export interface Signer {
export interface SignatureAlgorithm {
/**
* signs the provided payload using the provided JWK
* @param content - the content to sign
Expand Down
26 changes: 22 additions & 4 deletions src/types/jws-types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { PrivateJwk } from './jose-types.js';
import type { Signer } from '../types/signer.js';
/**
* General JWS definition. Payload is returned as an empty
* string when JWS Unencoded Payload Option
Expand Down Expand Up @@ -29,9 +29,22 @@ export type SignatureEntry = {
};

export type JwsHeaderParameters = {
/** JWS "alg" (Algorithm) Header Parameter. */
/**
* JWS "alg" (Algorithm) Header Parameter.
*
* This parameter is not used by the DWN but is unfortunately a required header property for a JWS as per:
* https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.1
*
* Valid signature algorithm values can be found at https://www.iana.org/assignments/jose/jose.xhtml
*/
alg: string
/** JWS "kid" (Key ID) Parameter. */

/**
* JWS "kid" (Key ID) Parameter.
*
* This property is not a required property per JWS specification, but is required for DWN authentication.
* This needs to be a fully-qualified ID (ie. prefixed with DID) so that author can be parsed out for processing such as `recordId` computation.
thehenrytsai marked this conversation as resolved.
Show resolved Hide resolved
*/
kid: string
};

Expand All @@ -40,5 +53,10 @@ export type JwsHeaderParameters = {
*/
export type SignatureInput = {
protectedHeader: JwsHeaderParameters
privateJwk: PrivateJwk

/**
* Signer used to produce the signature.
* You can use `PrivateKeySigner` if you have the private key readily available.
*/
signer: Signer
};
9 changes: 9 additions & 0 deletions src/types/signer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* A signer interface that can signing over arbitrary bytes.
*/
export interface Signer {
/**
* Signs the given content and returns the signature as bytes.
*/
sign (content: Uint8Array): Promise<Uint8Array>;
}
13 changes: 7 additions & 6 deletions src/utils/jws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import type { KeyMaterial, PublicJwk } from '../types/jose-types.js';
import isPlainObject from 'lodash/isPlainObject.js';

import { Encoder } from './encoder.js';
import { signers as verifiers } from '../jose/algorithms/signing/signers.js';
import { PrivateKeySigner } from './private-key-signer.js';
import { signatureAlgorithms } from '../jose/algorithms/signing/signature-algorithms.js';


/**
Expand Down Expand Up @@ -34,16 +35,16 @@ export class Jws {
* @returns `true` if signature is valid; `false` otherwise
*/
public static async verifySignature(base64UrlPayload: string, signatureEntry: SignatureEntry, jwkPublic: PublicJwk): Promise<boolean> {
const verifier = verifiers[jwkPublic.crv];
const signatureAlgorithm = signatureAlgorithms[jwkPublic.crv];

if (!verifier) {
throw new Error(`unsupported crv. crv must be one of ${Object.keys(verifiers)}`);
if (!signatureAlgorithm) {
throw new Error(`unsupported crv. crv must be one of ${Object.keys(signatureAlgorithms)}`);
}

const payload = Encoder.stringToBytes(`${signatureEntry.protected}.${base64UrlPayload}`);
const signatureBytes = Encoder.base64UrlToBytes(signatureEntry.signature);

return await verifier.verify(payload, signatureBytes, jwkPublic);
return await signatureAlgorithm.verify(payload, signatureBytes, jwkPublic);
}

/**
Expand Down Expand Up @@ -85,7 +86,7 @@ export class Jws {
*/
public static createSignatureInput(keyMaterial: KeyMaterial): SignatureInput {
const signatureInput = {
privateJwk : keyMaterial.keyPair.privateJwk,
signer : new PrivateKeySigner(keyMaterial.keyPair.privateJwk),
protectedHeader : {
alg : keyMaterial.keyPair.privateJwk.alg as string,
kid : keyMaterial.keyId
Expand Down
31 changes: 31 additions & 0 deletions src/utils/private-key-signer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { PrivateJwk } from '../types/jose-types.js';
import type { Signer } from '../types/signer.js';

import { signatureAlgorithms } from '../jose/algorithms/signing/signature-algorithms.js';
import { DwnError, DwnErrorCode } from '../index.js';

/**
* A signer that signs using a private key.
*/
export class PrivateKeySigner implements Signer {
private signatureAlgorithm;

public constructor(private privateJwk: PrivateJwk) {
diehuxx marked this conversation as resolved.
Show resolved Hide resolved
this.signatureAlgorithm = signatureAlgorithms[privateJwk.crv];

if (!this.signatureAlgorithm) {
throw new DwnError(
DwnErrorCode.PrivateKeySignerUnsupportedCurve,
`Unsupported crv ${privateJwk.crv}, crv must be one of ${Object.keys(signatureAlgorithms)}`
);
}
}

/**
* Signs the given content and returns the signature as bytes.
*/
public async sign (content: Uint8Array): Promise<Uint8Array> {
const signatureBytes = await this.signatureAlgorithm.sign(content, this.privateJwk);
return signatureBytes;
}
}
6 changes: 3 additions & 3 deletions tests/handlers/protocols-configure.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import dexProtocolDefinition from '../vectors/protocol-definitions/dex.json' ass
import minimalProtocolDefinition from '../vectors/protocol-definitions/minimal.json' assert { type: 'json' };

import { DidKeyResolver } from '../../src/did/did-key-resolver.js';
import { GeneralJwsSigner } from '../../src/jose/jws/general/signer.js';
import { GeneralJwsBuilder } from '../../src/jose/jws/general/builder.js';
import { lexicographicalCompare } from '../../src/utils/string.js';
import { Message } from '../../src/core/message.js';
import { minimalSleep } from '../../src/utils/time.js';
Expand Down Expand Up @@ -85,8 +85,8 @@ export function testProtocolsConfigureHandler(): void {

const authorizationPayloadBytes = Encoder.objectToBytes(protocolsConfigure.authorizationPayload!);

const signer = await GeneralJwsSigner.create(authorizationPayloadBytes, [signatureInput1, signatureInput2]);
message.authorization = signer.getJws();
const jwsBuilder = await GeneralJwsBuilder.create(authorizationPayloadBytes, [signatureInput1, signatureInput2]);
message.authorization = jwsBuilder.getJws();

TestStubGenerator.stubDidResolver(didResolver, [author]);

Expand Down
6 changes: 3 additions & 3 deletions tests/handlers/protocols-query.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import sinon from 'sinon';
import chai, { expect } from 'chai';

import { DidKeyResolver } from '../../src/did/did-key-resolver.js';
import { GeneralJwsSigner } from '../../src/jose/jws/general/signer.js';
import { GeneralJwsBuilder } from '../../src/jose/jws/general/builder.js';
import { TestDataGenerator } from '../utils/test-data-generator.js';
import { TestStores } from '../test-stores.js';
import { TestStubGenerator } from '../utils/test-stub-generator.js';
Expand Down Expand Up @@ -170,8 +170,8 @@ export function testProtocolsQueryHandler(): void {
authorizationPayload.descriptorCid = incorrectDescriptorCid;
const authorizationPayloadBytes = Encoder.objectToBytes(authorizationPayload);
const signatureInput = Jws.createSignatureInput(author);
const signer = await GeneralJwsSigner.create(authorizationPayloadBytes, [signatureInput]);
message.authorization = signer.getJws();
const jwsBuilder = await GeneralJwsBuilder.create(authorizationPayloadBytes, [signatureInput]);
message.authorization = jwsBuilder.getJws();

const reply = await dwn.processMessage(tenant, message);

Expand Down
Loading