diff --git a/README.md b/README.md index 2284a8339..c6815c29e 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,11 @@ # Decentralized Web Node (DWN) SDK Code Coverage -![Statements](https://img.shields.io/badge/statements-94.63%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-94.12%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-91.61%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-94.63%25-brightgreen.svg?style=flat) +![Statements](https://img.shields.io/badge/statements-94.74%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-94.12%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-91.61%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-94.74%25-brightgreen.svg?style=flat) ## Introduction -This repository contains a reference implementation of Decentralized Web Node (DWN) as per the [specification](https://identity.foundation/decentralized-web-node/spec/). This specification is in a draft state and very much so a WIP. For the foreseeable future, a lot of the work on DWN will be split across this repo and the repo that houses the specification, which you can find [here](https://github.com/decentralized-identity/decentralized-web-node). The current goal is to produce a [beta implementation](https://github.com/TBD54566975/dwn-sdk-js/milestone/1) by Q4 2022. This won't include all interfaces described in the spec, but enough to begin building applications. +This repository contains a reference implementation of Decentralized Web Node (DWN) as per the [specification](https://identity.foundation/decentralized-web-node/spec/). This specification is in a draft state and very much so a WIP. For the foreseeable future, a lot of the work on DWN will be split across this repo and the repo that houses the specification, which you can find [here](https://github.com/decentralized-identity/decentralized-web-node). The current goal is to produce a beta implementation by March 2023. This won't include all interfaces described in the DWN spec, but will be enough to begin building applications. Proposals and issues for the specification itself should be submitted as pull requests to the [spec repo](https://github.com/decentralized-identity/decentralized-web-node). diff --git a/json-schemas/records/records-query.json b/json-schemas/records/records-query.json index 3699b56f0..a61dd6175 100644 --- a/json-schemas/records/records-query.json +++ b/json-schemas/records/records-query.json @@ -44,6 +44,9 @@ "protocol": { "type": "string" }, + "attester": { + "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/definitions/did" + }, "recipient": { "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/definitions/did" }, diff --git a/src/core/auth.ts b/src/core/auth.ts index 48d21d971..655b9d895 100644 --- a/src/core/auth.ts +++ b/src/core/auth.ts @@ -4,6 +4,7 @@ import { CID } from 'multiformats'; import { DidResolver } from '../did/did-resolver.js'; import { GeneralJws } from '../jose/jws/general/types.js'; import { GeneralJwsVerifier } from '../jose/jws/general/verifier.js'; +import { Jws } from '../utils/jws.js'; import { Message } from './message.js'; import { computeCid, parseCid } from '../utils/cid.js'; @@ -40,14 +41,13 @@ export async function validateAuthorizationIntegrity( throw new Error('expected no more than 1 signature for authorization'); } - const payloadJson = GeneralJwsVerifier.decodePlainObjectPayload(message.authorization); + const payloadJson = Jws.decodePlainObjectPayload(message.authorization); const { descriptorCid } = payloadJson; // `descriptorCid` validation - ensure that the provided descriptorCid matches the CID of the actual message - const providedDescriptorCid = parseCid(descriptorCid); // parseCid throws an exception if parsing fails const expectedDescriptorCid = await computeCid(message.descriptor); - if (!providedDescriptorCid.equals(expectedDescriptorCid)) { - throw new Error(`provided descriptorCid ${providedDescriptorCid} does not match expected CID ${expectedDescriptorCid}`); + if (descriptorCid !== expectedDescriptorCid) { + throw new Error(`provided descriptorCid ${descriptorCid} does not match expected CID ${expectedDescriptorCid}`); } // check to ensure that no other unexpected properties exist in payload. diff --git a/src/core/message.ts b/src/core/message.ts index de25e6a83..d4bb14dde 100644 --- a/src/core/message.ts +++ b/src/core/message.ts @@ -1,14 +1,13 @@ import type { SignatureInput } from '../jose/jws/general/types.js'; import type { BaseDecodedAuthorizationPayload, BaseMessage, Descriptor, TimestampedMessage } from './types.js'; -import { CID } from 'multiformats/cid'; import { computeCid } from '../utils/cid.js'; import { GeneralJws } from '../jose/jws/general/types.js'; import { GeneralJwsSigner } from '../jose/jws/general/signer.js'; -import { GeneralJwsVerifier } from '../jose/jws/general/verifier.js'; +import { Jws } from '../utils/jws.js'; import { lexicographicalCompare } from '../utils/string.js'; import { RecordsWriteMessage } from '../interfaces/records/types.js'; -import { validateJsonSchema } from '../validator.js'; +import { validateJsonSchema } from '../schema-validator.js'; export enum DwnInterfaceName { Hooks = 'Hooks', @@ -35,7 +34,7 @@ export abstract class Message { constructor(message: BaseMessage) { this.message = message; - this.authorizationPayload = GeneralJwsVerifier.decodePlainObjectPayload(message.authorization); + this.authorizationPayload = Jws.decodePlainObjectPayload(message.authorization); this.author = Message.getAuthor(message); } @@ -64,7 +63,7 @@ export abstract class Message { * Gets the DID of the author of the given message. */ public static getAuthor(message: BaseMessage): string { - const author = GeneralJwsVerifier.getDid(message.authorization.signatures[0]); + const author = Jws.getSignerDid(message.authorization.signatures[0]); return author; } @@ -72,7 +71,7 @@ export abstract class Message { * Gets the CID of the given message. * NOTE: `encodedData` is ignored when computing the CID of message. */ - public static async getCid(message: BaseMessage): Promise { + public static async getCid(message: BaseMessage): Promise { const messageCopy = { ...message }; if (messageCopy['encodedData'] !== undefined) { @@ -130,7 +129,7 @@ export abstract class Message { ): Promise { const descriptorCid = await computeCid(descriptor); - const authPayload: BaseDecodedAuthorizationPayload = { descriptorCid: descriptorCid.toString() }; + const authPayload: BaseDecodedAuthorizationPayload = { descriptorCid }; const authPayloadStr = JSON.stringify(authPayload); const authPayloadBytes = new TextEncoder().encode(authPayloadStr); diff --git a/src/interfaces/permissions/messages/permissions-grant.ts b/src/interfaces/permissions/messages/permissions-grant.ts index 9d2b6ef8c..9d0419139 100644 --- a/src/interfaces/permissions/messages/permissions-grant.ts +++ b/src/interfaces/permissions/messages/permissions-grant.ts @@ -2,7 +2,6 @@ import type { SignatureInput } from '../../../jose/jws/general/types'; import type { PermissionConditions, PermissionScope } from '../types'; import type { PermissionsGrantDescriptor, PermissionsGrantMessage } from '../types'; -import { CID } from 'multiformats/cid'; import { computeCid } from '../../../utils/cid'; import { getCurrentTimeInHighPrecision } from '../../../utils/time'; import { v4 as uuidv4 } from 'uuid'; @@ -131,8 +130,8 @@ export class PermissionsGrant extends Message { return this.message.descriptor.scope; } - private set delegatedFrom(cid: CID) { - this.message.descriptor.delegatedFrom = cid.toString(); + private set delegatedFrom(cid: string) { + this.message.descriptor.delegatedFrom = cid; } private set delegationChain(parentGrant: PermissionsGrantMessage) { diff --git a/src/interfaces/records/messages/records-query.ts b/src/interfaces/records/messages/records-query.ts index 9d3b4e9c5..44cdf5e93 100644 --- a/src/interfaces/records/messages/records-query.ts +++ b/src/interfaces/records/messages/records-query.ts @@ -1,5 +1,5 @@ import type { SignatureInput } from '../../../jose/jws/general/types.js'; -import type { RecordsQueryDescriptor, RecordsQueryMessage } from '../types.js'; +import type { RecordsQueryDescriptor, RecordsQueryFilter, RecordsQueryMessage } from '../types.js'; import { getCurrentTimeInHighPrecision } from '../../../utils/time.js'; import { Message } from '../../../core/message.js'; @@ -16,15 +16,7 @@ export enum DateSort { export type RecordsQueryOptions = { dateCreated?: string; - filter: { - recipient?: string; - protocol?: string; - contextId?: string; - schema?: string; - recordId?: string; - parentId?: string; - dataFormat?: string; - }, + filter: RecordsQueryFilter; dateSort?: DateSort; authorizationSignatureInput: SignatureInput; }; diff --git a/src/interfaces/records/messages/records-write.ts b/src/interfaces/records/messages/records-write.ts index b1f53a047..dfd56d921 100644 --- a/src/interfaces/records/messages/records-write.ts +++ b/src/interfaces/records/messages/records-write.ts @@ -3,15 +3,15 @@ import type { RecordsWriteAttestationPayload, RecordsWriteAuthorizationPayload, import { Encoder } from '../../../utils/encoder.js'; import { GeneralJwsSigner } from '../../../jose/jws/general/signer.js'; -import { GeneralJwsVerifier } from '../../../jose/jws/general/verifier.js'; import { getCurrentTimeInHighPrecision } from '../../../utils/time.js'; +import { Jws } from '../../../utils/jws.js'; import { Message } from '../../../core/message.js'; import { MessageStore } from '../../../store/message-store.js'; import { ProtocolAuthorization } from '../../../core/protocol-authorization.js'; import { removeUndefinedProperties } from '../../../utils/object.js'; import { authorize, validateAuthorizationIntegrity } from '../../../core/auth.js'; -import { computeCid, getDagPbCid, parseCid } from '../../../utils/cid.js'; +import { computeCid, computeDagPbCid } from '../../../utils/cid.js'; import { DwnInterfaceName, DwnMethodName } from '../../../core/message.js'; import { GeneralJws, SignatureInput } from '../../../jose/jws/general/types.js'; @@ -78,7 +78,7 @@ export class RecordsWrite extends Message { public static async create(options: RecordsWriteOptions): Promise { const currentTime = getCurrentTimeInHighPrecision(); - const dataCid = await getDagPbCid(options.data); + const dataCid = await computeDagPbCid(options.data); const descriptor: RecordsWriteDescriptor = { interface : DwnInterfaceName.Records, method : DwnMethodName.Write, @@ -104,7 +104,7 @@ export class RecordsWrite extends Message { // Error: `undefined` is not supported by the IPLD Data Model and cannot be encoded removeUndefinedProperties(descriptor); - const author = GeneralJwsVerifier.extractDid(options.authorizationSignatureInput.protectedHeader.kid); + const author = Jws.extractDid(options.authorizationSignatureInput.protectedHeader.kid); // `recordId` computation const recordId = options.recordId ?? await RecordsWrite.getEntryId(author, descriptor); @@ -121,7 +121,7 @@ export class RecordsWrite extends Message { } // `attestation` generation - const descriptorCid = (await computeCid(descriptor)).toString(); + const descriptorCid = await computeCid(descriptor); const attestation = await RecordsWrite.createAttestation(descriptorCid, options.attestationSignatureInputs); // `authorization` generation @@ -229,7 +229,7 @@ export class RecordsWrite extends Message { // verify dataCid matches given data if (this.message.encodedData !== undefined) { const rawData = Encoder.base64UrlToBytes(this.message.encodedData); - const actualDataCid = (await getDagPbCid(rawData)).toString(); + const actualDataCid = (await computeDagPbCid(rawData)).toString(); if (actualDataCid !== this.message.descriptor.dataCid) { throw new Error('actual CID of data and `dataCid` in descriptor mismatch'); @@ -273,7 +273,7 @@ export class RecordsWrite extends Message { // if `attestation` is given in message, make sure the correct `attestationCid` is in the `authorization` if (this.message.attestation !== undefined) { - const expectedAttestationCid = (await computeCid(this.message.attestation)).toString(); + const expectedAttestationCid = await computeCid(this.message.attestation); const actualAttestationCid = this.authorizationPayload.attestationCid; if (actualAttestationCid !== expectedAttestationCid) { throw new Error( @@ -297,14 +297,13 @@ export class RecordsWrite extends Message { throw new Error(`Currently implementation only supports 1 attester, but got ${message.attestation.signatures.length}`); } - const payloadJson = GeneralJwsVerifier.decodePlainObjectPayload(message.attestation); + const payloadJson = Jws.decodePlainObjectPayload(message.attestation); const { descriptorCid } = payloadJson; // `descriptorCid` validation - ensure that the provided descriptorCid matches the CID of the actual message - const providedDescriptorCid = parseCid(descriptorCid); // parseCid throws an exception if parsing fails const expectedDescriptorCid = await computeCid(message.descriptor); - if (!providedDescriptorCid.equals(expectedDescriptorCid)) { - throw new Error(`descriptorCid ${providedDescriptorCid} does not match expected descriptorCid ${expectedDescriptorCid}`); + if (descriptorCid !== expectedDescriptorCid) { + throw new Error(`descriptorCid ${descriptorCid} does not match expected descriptorCid ${expectedDescriptorCid}`); } // check to ensure that no other unexpected properties exist in payload. @@ -330,8 +329,7 @@ export class RecordsWrite extends Message { (entryIdInput as any).author = author; const cid = await computeCid(entryIdInput); - const cidString = cid.toString(); - return cidString; + return cid; }; /** @@ -388,7 +386,7 @@ export class RecordsWrite extends Message { descriptorCid }; - const attestationCid = attestation ? (await computeCid(attestation)).toString() : undefined; + const attestationCid = attestation ? await computeCid(attestation) : undefined; if (contextId !== undefined) { authorizationPayload.contextId = contextId; } // assign `contextId` only if it is defined if (attestationCid !== undefined) { authorizationPayload.attestationCid = attestationCid; } // assign `attestationCid` only if it is defined @@ -445,7 +443,7 @@ export class RecordsWrite extends Message { */ public static getAttesters(message: RecordsWriteMessage): string[] { const attestationSignatures = message.attestation?.signatures ?? []; - const attesters = attestationSignatures.map((signature) => GeneralJwsVerifier.getDid(signature)); + const attesters = attestationSignatures.map((signature) => Jws.getSignerDid(signature)); return attesters; } } diff --git a/src/interfaces/records/types.ts b/src/interfaces/records/types.ts index 4c7a004d5..543e46720 100644 --- a/src/interfaces/records/types.ts +++ b/src/interfaces/records/types.ts @@ -40,18 +40,21 @@ export type RecordsQueryDescriptor = { interface: DwnInterfaceName.Records; method: DwnMethodName.Query; dateCreated: string; - filter: { - recipient?: string; - protocol?: string; - contextId?: string; - schema?: string; - recordId?: string; - parentId?: string; - dataFormat?: string; - } + filter: RecordsQueryFilter; dateSort?: DateSort; }; +export type RecordsQueryFilter = { + attester?: string; + recipient?: string; + protocol?: string; + contextId?: string; + schema?: string; + recordId?: string; + parentId?: string; + dataFormat?: string; +}; + export type RecordsWriteAttestationPayload = { descriptorCid: string; }; diff --git a/src/jose/jws/general/verifier.ts b/src/jose/jws/general/verifier.ts index d1f5c8dcb..3c9f46e2e 100644 --- a/src/jose/jws/general/verifier.ts +++ b/src/jose/jws/general/verifier.ts @@ -1,15 +1,12 @@ import type { Cache } from '../../../utils/types.js'; +import type { GeneralJws } from './types.js'; import type { PublicJwk } from '../../types.js'; import type { VerificationMethod } from '../../../did/did-resolver.js'; -import type { GeneralJws, SignatureEntry } from './types.js'; - -import isPlainObject from 'lodash/isPlainObject.js'; import { DidResolver } from '../../../did/did-resolver.js'; -import { Encoder } from '../../../utils/encoder.js'; +import { Jws } from '../../../utils/jws.js'; import { MemoryCache } from '../../../utils/memory-cache.js'; -import { validateJsonSchema } from '../../../validator.js'; -import { signers as verifiers } from '../../algorithms/signing/signers.js'; +import { validateJsonSchema } from '../../../schema-validator.js'; type VerificationResult = { /** DIDs of all signers */ @@ -31,20 +28,20 @@ export class GeneralJwsVerifier { for (const signatureEntry of this.jws.signatures) { let isVerified: boolean; const cacheKey = `${signatureEntry.protected}.${this.jws.payload}.${signatureEntry.signature}`; - const kid = GeneralJwsVerifier.getKid(signatureEntry); + const kid = Jws.getKid(signatureEntry); const publicJwk = await GeneralJwsVerifier.getPublicKey(kid, didResolver); const cachedValue = await this.cache.get(cacheKey); // explicit strict equality check to avoid potential buggy cache implementation causing incorrect truthy compare e.g. "false" if (cachedValue === undefined) { - isVerified = await GeneralJwsVerifier.verifySignature(this.jws.payload, signatureEntry, publicJwk); + isVerified = await Jws.verifySignature(this.jws.payload, signatureEntry, publicJwk); await this.cache.set(cacheKey, isVerified); } else { isVerified = cachedValue; } - const did = GeneralJwsVerifier.extractDid(kid); + const did = Jws.extractDid(kid); if (isVerified) { signers.push(did); @@ -56,30 +53,13 @@ export class GeneralJwsVerifier { return { signers }; } - /** - * Gets the `kid` from a general JWS signature entry. - */ - private static getKid(signatureEntry: SignatureEntry): string { - const { kid } = Encoder.base64UrlToObject(signatureEntry.protected); - return kid; - } - - /** - * Gets the DID from a general JWS signature entry. - */ - public static getDid(signatureEntry: SignatureEntry): string { - const kid = GeneralJwsVerifier.getKid(signatureEntry); - const did = GeneralJwsVerifier.extractDid(kid); - return did; - } - /** * Gets the public key given a fully qualified key ID (`kid`). */ public static async getPublicKey(kid: string, didResolver: DidResolver): Promise { // `resolve` throws exception if DID is invalid, DID method is not supported, // or resolving DID fails - const did = GeneralJwsVerifier.extractDid(kid); + const did = Jws.extractDid(kid); const { didDocument } = await didResolver.resolve(did); const { verificationMethod: verificationMethods = [] } = didDocument || {}; @@ -105,40 +85,4 @@ export class GeneralJwsVerifier { return publicJwk as PublicJwk; } - - public static async verifySignature(base64UrlPayload: string, signatureEntry: SignatureEntry, jwkPublic: PublicJwk): Promise { - const verifier = verifiers[jwkPublic.crv]; - - if (!verifier) { - throw new Error(`unsupported crv. crv must be one of ${Object.keys(verifiers)}`); - } - - const payload = Encoder.stringToBytes(`${signatureEntry.protected}.${base64UrlPayload}`); - const signatureBytes = Encoder.base64UrlToBytes(signatureEntry.signature); - - return await verifier.verify(payload, signatureBytes, jwkPublic); - } - - public static decodePlainObjectPayload(jws: GeneralJws): any { - let payloadJson; - try { - payloadJson = Encoder.base64UrlToObject(jws.payload); - } catch { - throw new Error('authorization payload is not a JSON object'); - } - - if (!isPlainObject(payloadJson)) { - throw new Error('auth payload must be a valid JSON object'); - } - - return payloadJson; - } - - /** - * Extracts the DID from the given `kid` string. - */ - public static extractDid(kid: string): string { - const [ did ] = kid.split('#'); - return did; - } } \ No newline at end of file diff --git a/src/validator.ts b/src/schema-validator.ts similarity index 100% rename from src/validator.ts rename to src/schema-validator.ts diff --git a/src/store/message-store-level.ts b/src/store/message-store-level.ts index 7dd33330c..d53be84c8 100644 --- a/src/store/message-store-level.ts +++ b/src/store/message-store-level.ts @@ -60,7 +60,8 @@ export class MessageStoreLevel implements MessageStore { await this.index.INDEX.STORE.close(); // MUST close index-search DB, else `searchIndex()` triggered in a different instance will hang indefinitely } - async get(cid: CID): Promise { + async get(cidString: string): Promise { + const cid = CID.parse(cidString); const bytes = await this.db.get(cid); if (!bytes) { @@ -102,8 +103,7 @@ export class MessageStoreLevel implements MessageStore { const { RESULT: indexResults } = await this.index.QUERY({ AND: queryTerms }); for (const result of indexResults) { - const cid = CID.parse(result._id); - const message = await this.get(cid); + const message = await this.get(result._id); messages.push(message); } @@ -112,10 +112,11 @@ export class MessageStoreLevel implements MessageStore { } - async delete(cid: CID): Promise { + async delete(cidString: string): Promise { // TODO: Implement data deletion in Records - https://github.com/TBD54566975/dwn-sdk-js/issues/84 + const cid = CID.parse(cidString); await this.db.delete(cid); - await this.index.DELETE(cid.toString()); + await this.index.DELETE(cidString); return; } diff --git a/src/store/message-store.ts b/src/store/message-store.ts index 13afcc73d..ea8462a94 100644 --- a/src/store/message-store.ts +++ b/src/store/message-store.ts @@ -1,7 +1,5 @@ import type { BaseMessage } from '../core/types.js'; -import { CID } from 'multiformats/cid'; - export interface MessageStore { /** * opens a connection to the underlying store @@ -22,9 +20,8 @@ export interface MessageStore { /** * fetches a single message by `cid` from the underlying store. Returns `undefined` * if no message was found - * @param cid */ - get(cid: CID): Promise; + get(cid: string): Promise; /** * queries the underlying store for messages that match the query provided. @@ -35,7 +32,6 @@ export interface MessageStore { /** * deletes the message associated to the id provided - * @param cid */ - delete(cid: CID): Promise; + delete(cid: string): Promise; } \ No newline at end of file diff --git a/src/utils/cid.ts b/src/utils/cid.ts index 036ab4bcf..f32db3f67 100644 --- a/src/utils/cid.ts +++ b/src/utils/cid.ts @@ -20,7 +20,7 @@ const codecs = { /** * @returns V1 CID of the DAG comprised by chunking data into unixfs dag-pb encoded blocks */ -export async function getDagPbCid(content: Uint8Array): Promise { +export async function computeDagPbCid(content: Uint8Array): Promise { const chunk = importer([{ content }], undefined, { onlyHash: true, cidVersion: 1 }); let root; @@ -39,7 +39,7 @@ export async function getDagPbCid(content: Uint8Array): Promise { * @throws {Error} encoding fails * @throws {Error} if hasher is not supported */ -export async function computeCid(payload: any, codecCode = cbor.code, multihashCode = sha256.code): Promise { +export async function computeCid(payload: any, codecCode = cbor.code, multihashCode = sha256.code): Promise { const codec = codecs[codecCode]; if (!codec) { throw new Error(`codec [${codecCode}] not supported`); @@ -53,11 +53,12 @@ export async function computeCid(payload: any, codecCode = cbor.code, multihashC const payloadBytes = codec.encode(payload); const payloadHash = await hasher.digest(payloadBytes); - return await CID.createV1(codec.code, payloadHash); + const cid = await CID.createV1(codec.code, payloadHash); + return cid.toString(); } export function parseCid(str: string): CID { - const cid = CID.parse(str).toV1(); + const cid: CID = CID.parse(str).toV1(); if (!codecs[cid.code]) { throw new Error(`codec [${cid.code}] not supported`); diff --git a/src/utils/jws.ts b/src/utils/jws.ts new file mode 100644 index 000000000..a03101079 --- /dev/null +++ b/src/utils/jws.ts @@ -0,0 +1,72 @@ +import isPlainObject from 'lodash/isPlainObject.js'; + +import { Encoder } from './encoder.js'; +import { GeneralJws } from '../jose/jws/general/types.js'; +import { PublicJwk } from '../jose/types.js'; +import { SignatureEntry } from '../jose/jws/general/types.js'; +import { signers as verifiers } from '../jose/algorithms/signing/signers.js'; + +/** + * Utility class for JWS related operations. + */ +export class Jws { + /** + * Gets the `kid` from a general JWS signature entry. + */ + public static getKid(signatureEntry: SignatureEntry): string { + const { kid } = Encoder.base64UrlToObject(signatureEntry.protected); + return kid; + } + + /** + * Gets the signer DID from a general JWS signature entry. + */ + public static getSignerDid(signatureEntry: SignatureEntry): string { + const kid = Jws.getKid(signatureEntry); + const did = Jws.extractDid(kid); + return did; + } + + /** + * Verifies the signature against the given payload. + * @returns `true` if signature is valid; `false` otherwise + */ + public static async verifySignature(base64UrlPayload: string, signatureEntry: SignatureEntry, jwkPublic: PublicJwk): Promise { + const verifier = verifiers[jwkPublic.crv]; + + if (!verifier) { + throw new Error(`unsupported crv. crv must be one of ${Object.keys(verifiers)}`); + } + + const payload = Encoder.stringToBytes(`${signatureEntry.protected}.${base64UrlPayload}`); + const signatureBytes = Encoder.base64UrlToBytes(signatureEntry.signature); + + return await verifier.verify(payload, signatureBytes, jwkPublic); + } + + /** + * Decodes the payload of the given JWS object as a plain object. + */ + public static decodePlainObjectPayload(jws: GeneralJws): any { + let payloadJson; + try { + payloadJson = Encoder.base64UrlToObject(jws.payload); + } catch { + throw new Error('payload is not a JSON object'); + } + + if (!isPlainObject(payloadJson)) { + throw new Error('signed payload must be a plain object'); + } + + return payloadJson; + } + + /** + * Extracts the DID from the given `kid` string. + */ + public static extractDid(kid: string): string { + const [ did ] = kid.split('#'); + return did; + } +} diff --git a/tests/interfaces/permissions/messages/permissions-request.spec.ts b/tests/interfaces/permissions/messages/permissions-request.spec.ts index ee643e2ef..c7ef5da6c 100644 --- a/tests/interfaces/permissions/messages/permissions-request.spec.ts +++ b/tests/interfaces/permissions/messages/permissions-request.spec.ts @@ -34,7 +34,7 @@ describe('PermissionsRequest', () => { const testVectors = [ { input: 'dookie', expectedError: 'payload is not a JSON object' }, - { input: JSON.stringify([]), expectedError: 'must be a valid JSON object' } + { input: JSON.stringify([]), expectedError: 'signed payload must be a plain object' } ]; const { privateJwk } = await secp256k1.generateKeyPair(); diff --git a/tests/interfaces/records/handlers/records-query.spec.ts b/tests/interfaces/records/handlers/records-query.spec.ts index 13669e5ea..67be464c7 100644 --- a/tests/interfaces/records/handlers/records-query.spec.ts +++ b/tests/interfaces/records/handlers/records-query.spec.ts @@ -3,15 +3,16 @@ import sinon from 'sinon'; import chai, { expect } from 'chai'; import { DidKeyResolver } from '../../../../src/did/did-key-resolver.js'; -import { DidResolver } from '../../../../src/index.js'; import { Encoder } from '../../../../src/utils/encoder.js'; import { handleRecordsQuery } from '../../../../src/interfaces/records/handlers/records-query.js'; +import { Jws } from '../../../../src/utils/jws.js'; import { MessageStoreLevel } from '../../../../src/store/message-store-level.js'; import { TestDataGenerator } from '../../../utils/test-data-generator.js'; import { TestStubGenerator } from '../../../utils/test-stub-generator.js'; import { constructRecordsWriteIndexes, handleRecordsWrite } from '../../../../src/interfaces/records/handlers/records-write.js'; import { DateSort, RecordsQuery } from '../../../../src/interfaces/records/messages/records-query.js'; +import { DidResolver, RecordsWriteMessage } from '../../../../src/index.js'; chai.use(chaiAsPromised); @@ -84,6 +85,43 @@ describe('handleRecordsQuery()', () => { expect(reply2.entries?.length).to.equal(1); // only 1 entry should match the query }); + it('should be able to query by attester', async () => { + // scenario: 2 records authored by alice, 1st attested by alice, 2nd attested by bob + const alice = await DidKeyResolver.generate(); + const bob = await DidKeyResolver.generate(); + const recordsWrite1 = await TestDataGenerator.generateRecordsWrite({ requester: alice, attesters: [alice] }); + const recordsWrite2 = await TestDataGenerator.generateRecordsWrite({ requester: alice, attesters: [bob] }); + + // insert data + const writeReply1 = await handleRecordsWrite(alice.did, recordsWrite1.message, messageStore, didResolver); + const writeReply2 = await handleRecordsWrite(alice.did, recordsWrite2.message, messageStore, didResolver); + expect(writeReply1.status.code).to.equal(202); + expect(writeReply2.status.code).to.equal(202); + + // testing attester filter + const recordsQuery1 = await TestDataGenerator.generateRecordsQuery({ requester: alice, filter: { attester: alice.did } }); + const reply1 = await handleRecordsQuery(alice.did, recordsQuery1.message, messageStore, didResolver); + expect(reply1.entries?.length).to.equal(1); + const reply1Attester = Jws.getSignerDid((reply1.entries[0] as RecordsWriteMessage).attestation.signatures[0]); + expect(reply1Attester).to.equal(alice.did); + + // testing attester + another filter + const recordsQuery2 = await TestDataGenerator.generateRecordsQuery({ + requester : alice, + filter : { attester: bob.did, schema: recordsWrite2.message.descriptor.schema } + }); + const reply2 = await handleRecordsQuery(alice.did, recordsQuery2.message, messageStore, didResolver); + expect(reply2.entries?.length).to.equal(1); + const reply2Attester = Jws.getSignerDid((reply2.entries[0] as RecordsWriteMessage).attestation.signatures[0]); + expect(reply2Attester).to.equal(bob.did); + + // testing attester filter that yields no results + const carol = await DidKeyResolver.generate(); + const recordsQuery3 = await TestDataGenerator.generateRecordsQuery({ requester: alice, filter: { attester: carol.did } }); + const reply3 = await handleRecordsQuery(alice.did, recordsQuery3.message, messageStore, didResolver); + expect(reply3.entries?.length).to.equal(0); + }); + it('should not include `authorization` in returned records', async () => { const alice = await TestDataGenerator.generatePersona(); const { message } = await TestDataGenerator.generateRecordsWrite({ requester: alice }); diff --git a/tests/interfaces/records/handlers/records-write.spec.ts b/tests/interfaces/records/handlers/records-write.spec.ts index 952ec23fd..fc9a137be 100644 --- a/tests/interfaces/records/handlers/records-write.spec.ts +++ b/tests/interfaces/records/handlers/records-write.spec.ts @@ -1257,7 +1257,7 @@ describe('handleRecordsWrite()', () => { // recreate the `authorization` based on the new` attestationCid` const authorizationPayload = { ...recordsWrite.authorizationPayload }; - authorizationPayload.attestationCid = (await computeCid(attestationPayload)).toString(); + authorizationPayload.attestationCid = await computeCid(attestationPayload); const authorizationPayloadBytes = Encoder.objectToBytes(authorizationPayload); const authorizationSigner = await GeneralJwsSigner.create(authorizationPayloadBytes, [signatureInput]); message.authorization = authorizationSigner.getJws(); @@ -1299,7 +1299,7 @@ describe('handleRecordsWrite()', () => { // replace valid attestation (the one signed by `authorization` with another attestation to the same message (descriptorCid) const bob = await DidKeyResolver.generate(); - const descriptorCid = (await computeCid(message.descriptor)).toString(); + const descriptorCid = await computeCid(message.descriptor); const attestationNotReferencedByAuthorization = await RecordsWrite['createAttestation'](descriptorCid, TestDataGenerator.createSignatureInputsFromPersonas([bob])); message.attestation = attestationNotReferencedByAuthorization; diff --git a/tests/jose/jws/general.spec.ts b/tests/jose/jws/general.spec.ts index 86a81d749..49d0f5b71 100644 --- a/tests/jose/jws/general.spec.ts +++ b/tests/jose/jws/general.spec.ts @@ -4,6 +4,7 @@ import chai, { expect } from 'chai'; import { DidResolver } from '../../../src/did/did-resolver.js'; import { GeneralJwsSigner } from '../../../src/jose/jws/general/signer.js'; import { GeneralJwsVerifier } from '../../../src/jose/jws/general/verifier.js'; +import { Jws } from '../../../src/utils/jws.js'; import { signers } from '../../../src/jose/algorithms/signing/signers.js'; import sinon from 'sinon'; @@ -195,7 +196,7 @@ describe('General JWS Sign/Verify', () => { const verifier = new GeneralJwsVerifier(jws); - const verifySignatureSpy = sinon.spy(GeneralJwsVerifier, 'verifySignature'); + const verifySignatureSpy = sinon.spy(Jws, 'verifySignature'); const cacheSetSpy = sinon.spy(verifier.cache, 'set'); await verifier.verify(resolverStub); diff --git a/tests/jose/jws/verifier.spec.ts b/tests/jose/jws/verifier.spec.ts index 0307fa7d8..99ab4a1be 100644 --- a/tests/jose/jws/verifier.spec.ts +++ b/tests/jose/jws/verifier.spec.ts @@ -6,8 +6,4 @@ describe('GeneralJwsVerifier', () => { xit('throws an exception if verificationMethod type isn\'t JsonWebKey2020', () => {}); xit('returns public key', () => {}); }); - describe('verifySignature', () => { - xit('throws an exception if signature does not match', () => {}); - xit('returns true if signature is successfully verified', () => {}); - }); }); \ No newline at end of file diff --git a/tests/store/message-store.spec.ts b/tests/store/message-store.spec.ts index 45b3eb10d..dbb8d40b2 100644 --- a/tests/store/message-store.spec.ts +++ b/tests/store/message-store.spec.ts @@ -83,7 +83,7 @@ describe('MessageStoreLevel Tests', () => { const jsonMessage = await messageStore.get(expectedCid); const resultCid = await computeCid(jsonMessage); - expect(resultCid.equals(expectedCid)).to.be.true; + expect(resultCid).to.equal(expectedCid); }); // https://github.com/TBD54566975/dwn-sdk-js/issues/170 diff --git a/tests/utils/cid.spec.ts b/tests/utils/cid.spec.ts index 9ae11fd27..06fd4bdda 100644 --- a/tests/utils/cid.spec.ts +++ b/tests/utils/cid.spec.ts @@ -26,7 +26,7 @@ describe('CID', () => { const generatedCid = await computeCid(anyTestData); const encodedBlock = await block.encode({ value: anyTestData, codec: cbor, hasher: sha256 }); - expect(generatedCid.toString()).to.equal(encodedBlock.cid.toString()); + expect(generatedCid).to.equal(encodedBlock.cid.toString()); }); it('should canonicalize JSON input before hashing', async () => { @@ -44,7 +44,7 @@ describe('CID', () => { const cid1 = await computeCid(data1); const cid2 = await computeCid(data2); - expect(cid1.toString()).to.equal(cid2.toString()); + expect(cid1).to.equal(cid2); }); }); diff --git a/tests/utils/jws.spec.ts b/tests/utils/jws.spec.ts new file mode 100644 index 000000000..8bcbd0b2e --- /dev/null +++ b/tests/utils/jws.spec.ts @@ -0,0 +1,6 @@ +describe('Jws', () => { + describe('verifySignature', () => { + xit('throws an exception if signature does not match', () => {}); + xit('returns true if signature is successfully verified', () => {}); + }); +}); \ No newline at end of file diff --git a/tests/utils/test-data-generator.ts b/tests/utils/test-data-generator.ts index c962d4540..e88043222 100644 --- a/tests/utils/test-data-generator.ts +++ b/tests/utils/test-data-generator.ts @@ -6,6 +6,7 @@ import { DidResolutionResult } from '../../src/did/did-resolver.js'; import { ed25519 } from '../../src/jose/algorithms/signing/ed25519.js'; import { getCurrentTimeInHighPrecision } from '../../src/utils/time.js'; import { PermissionsRequest } from '../../src/interfaces/permissions/messages/permissions-request.js'; +import { RecordsQueryFilter } from '../../src/interfaces/records/types.js'; import { removeUndefinedProperties } from '../../src/utils/object.js'; import { secp256k1 } from '../../src/jose/algorithms/signing/secp256k1.js'; import { sha256 } from 'multiformats/hashes/sha2'; @@ -105,15 +106,7 @@ export type GenerateRecordsWriteOutput = { export type GenerateRecordsQueryInput = { requester?: Persona; dateCreated?: string; - filter?: { - recipient?: string; - protocol?: string; - contextId?: string; - schema?: string; - recordId?: string; - parentId?: string; - dataFormat?: string; - } + filter?: RecordsQueryFilter; dateSort?: DateSort; }; diff --git a/tests/validation/json-schemas/jwk-verification-method.spec.ts b/tests/validation/json-schemas/jwk-verification-method.spec.ts index 0c14eb402..c8334d293 100644 --- a/tests/validation/json-schemas/jwk-verification-method.spec.ts +++ b/tests/validation/json-schemas/jwk-verification-method.spec.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { signers } from '../../../src/jose/algorithms/signing/signers.js'; -import { validateJsonSchema } from '../../../src/validator.js'; +import { validateJsonSchema } from '../../../src/schema-validator.js'; const { secp256k1 } = signers; diff --git a/tests/validation/json-schemas/jwk/general-jwk.spec.ts b/tests/validation/json-schemas/jwk/general-jwk.spec.ts index 10c8f08f0..3493a27cf 100644 --- a/tests/validation/json-schemas/jwk/general-jwk.spec.ts +++ b/tests/validation/json-schemas/jwk/general-jwk.spec.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { signers } from '../../../../src/jose/algorithms/signing/signers.js'; -import { validateJsonSchema } from '../../../../src/validator.js'; +import { validateJsonSchema } from '../../../../src/schema-validator.js'; const { Ed25519, secp256k1 } = signers; diff --git a/tests/validation/json-schemas/jwk/public-jwk.spec.ts b/tests/validation/json-schemas/jwk/public-jwk.spec.ts index 863800426..345d9f3d5 100644 --- a/tests/validation/json-schemas/jwk/public-jwk.spec.ts +++ b/tests/validation/json-schemas/jwk/public-jwk.spec.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { signers } from '../../../../src/jose/algorithms/signing/signers.js'; -import { validateJsonSchema } from '../../../../src/validator.js'; +import { validateJsonSchema } from '../../../../src/schema-validator.js'; const { Ed25519, secp256k1 } = signers; diff --git a/tests/validation/json-schemas/protocols/protocols-configure.spec.ts b/tests/validation/json-schemas/protocols/protocols-configure.spec.ts index 58817c4c5..610f37d81 100644 --- a/tests/validation/json-schemas/protocols/protocols-configure.spec.ts +++ b/tests/validation/json-schemas/protocols/protocols-configure.spec.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { Message } from '../../../../src/core/message.js'; -import { validateJsonSchema } from '../../../../src/validator.js'; +import { validateJsonSchema } from '../../../../src/schema-validator.js'; describe('ProtocolsConfigure schema definition', () => { it('should throw if unknown allow rule is encountered', async () => {