Skip to content

Commit bb246a2

Browse files
authored
added multi-attester block plus additional attestation validations and tests
1 parent 0d8d89d commit bb246a2

File tree

9 files changed

+188
-81
lines changed

9 files changed

+188
-81
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Decentralized Web Node (DWN) SDK
44

55
Code Coverage
6-
![Statements](https://img.shields.io/badge/statements-94.56%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-93.93%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-91.51%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-94.56%25-brightgreen.svg?style=flat)
6+
![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)
77

88
## Introduction
99

src/core/auth.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ export async function canonicalAuth(
2828
}
2929

3030
/**
31-
* Validates the data integrity of the `authorization` property.
32-
* NOTE signature is not verified.
31+
* Validates the structural integrity of the `authorization` property.
32+
* NOTE: signature is not verified.
3333
*/
3434
export async function validateAuthorizationIntegrity(
3535
message: BaseMessage,

src/interfaces/records/handlers/records-write.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,10 @@ export async function constructRecordsWriteIndexes(
111111
...descriptor
112112
};
113113

114-
// add `contextId` to additional index if given
114+
// add additional indexes to optional values if given
115+
// TODO: only indexing 1 attester, multi-attesters to be unblocked by
116+
// #205 - Revisit database interfaces (https://github.com/TBD54566975/dwn-sdk-js/issues/205)
117+
if (recordsWrite.attesters.length > 0) { indexes.attester = recordsWrite.attesters[0]; }
115118
if (message.contextId !== undefined) { indexes.contextId = message.contextId; }
116119

117120
// add `published` index

src/interfaces/records/messages/records-write.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { ProtocolAuthorization } from '../../../core/protocol-authorization.js';
1111
import { removeUndefinedProperties } from '../../../utils/object.js';
1212

1313
import { authorize, validateAuthorizationIntegrity } from '../../../core/auth.js';
14-
import { computeCid, getDagPbCid } from '../../../utils/cid.js';
14+
import { computeCid, getDagPbCid, parseCid } from '../../../utils/cid.js';
1515
import { DwnInterfaceName, DwnMethodName } from '../../../core/message.js';
1616
import { GeneralJws, SignatureInput } from '../../../jose/jws/general/types.js';
1717

@@ -47,15 +47,20 @@ export class RecordsWrite extends Message {
4747
* RecordsWrite message adhering to the DWN specification.
4848
*/
4949
readonly message: RecordsWriteMessage;
50+
readonly attesters: string[];
5051

5152
private constructor(message: RecordsWriteMessage) {
5253
super(message);
5354

55+
this.attesters = RecordsWrite.getAttesters(message);
56+
5457
// consider converting isInitialWrite() & getEntryId() into properties for performance and convenience
5558
}
5659

5760
public static async parse(message: RecordsWriteMessage): Promise<RecordsWrite> {
61+
// asynchronous checks that are required by the constructor to initialize members properly
5862
await validateAuthorizationIntegrity(message, { allowedProperties: new Set(['recordId', 'contextId', 'attestationCid']) });
63+
await RecordsWrite.validateAttestationIntegrity(message);
5964

6065
const recordsWrite = new RecordsWrite(message);
6166

@@ -278,6 +283,37 @@ export class RecordsWrite extends Message {
278283
}
279284
}
280285

286+
/**
287+
* Validates the structural integrity of the `attestation` property.
288+
* NOTE: signature is not verified.
289+
*/
290+
private static async validateAttestationIntegrity(message: RecordsWriteMessage): Promise<void> {
291+
if (message.attestation === undefined) {
292+
return;
293+
}
294+
295+
// TODO: multi-attesters to be unblocked by #205 - Revisit database interfaces (https://github.com/TBD54566975/dwn-sdk-js/issues/205)
296+
if (message.attestation.signatures.length !== 1) {
297+
throw new Error(`Currently implementation only supports 1 attester, but got ${message.attestation.signatures.length}`);
298+
}
299+
300+
const payloadJson = GeneralJwsVerifier.decodePlainObjectPayload(message.attestation);
301+
const { descriptorCid } = payloadJson;
302+
303+
// `descriptorCid` validation - ensure that the provided descriptorCid matches the CID of the actual message
304+
const providedDescriptorCid = parseCid(descriptorCid); // parseCid throws an exception if parsing fails
305+
const expectedDescriptorCid = await computeCid(message.descriptor);
306+
if (!providedDescriptorCid.equals(expectedDescriptorCid)) {
307+
throw new Error(`descriptorCid ${providedDescriptorCid} does not match expected descriptorCid ${expectedDescriptorCid}`);
308+
}
309+
310+
// check to ensure that no other unexpected properties exist in payload.
311+
const propertyCount = Object.keys(payloadJson).length;
312+
if (propertyCount > 1) {
313+
throw new Error(`Only 'descriptorCid' is allowed in attestation payload, but got ${propertyCount} properties.`);
314+
}
315+
};
316+
281317
/**
282318
* Computes the deterministic Entry ID of this message.
283319
*/
@@ -403,4 +439,13 @@ export class RecordsWrite extends Message {
403439

404440
return true;
405441
}
442+
443+
/**
444+
* Gets the DID of the author of the given message.
445+
*/
446+
public static getAttesters(message: RecordsWriteMessage): string[] {
447+
const attestationSignatures = message.attestation?.signatures ?? [];
448+
const attesters = attestationSignatures.map((signature) => GeneralJwsVerifier.getDid(signature));
449+
return attesters;
450+
}
406451
}

tests/interfaces/protocols/handlers/protocols-configure.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ describe('handleProtocolsQuery()', () => {
4242
await messageStore.close();
4343
});
4444

45-
it('should return 400 if failed to parse the message', async () => {
45+
it('should return 400 if more than 1 signature is provided in `authorization`', async () => {
4646
const { requester, message, protocolsConfigure } = await TestDataGenerator.generateProtocolsConfigure();
4747
const tenant = requester.did;
4848

tests/interfaces/protocols/handlers/protocols-query.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ describe('handleProtocolsQuery()', () => {
7777
expect(reply2.entries?.length).to.equal(3); // expecting all 3 entries written above match the query
7878
});
7979

80-
it('should return 400 if failed to parse the message', async () => {
80+
it('should fail with 400 if `authorization` is referencing a different message (`descriptorCid`)', async () => {
8181
const { requester, message, protocolsQuery } = await TestDataGenerator.generateProtocolsQuery();
8282
const tenant = requester.did;
8383

tests/interfaces/records/handlers/records-query.spec.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,7 @@ describe('handleRecordsQuery()', () => {
109109
// scenario: alice and bob attest to a message alice authored
110110

111111
const alice = await DidKeyResolver.generate();
112-
const bob = await DidKeyResolver.generate();
113-
const { message } = await TestDataGenerator.generateRecordsWrite({ requester: alice, attesters: [alice, bob] });
112+
const { message } = await TestDataGenerator.generateRecordsWrite({ requester: alice, attesters: [alice] });
114113

115114
const writeReply = await handleRecordsWrite(alice.did, message, messageStore, didResolver);
116115
expect(writeReply.status.code).to.equal(202);
@@ -125,8 +124,7 @@ describe('handleRecordsQuery()', () => {
125124
expect(queryReply.entries?.length).to.equal(1);
126125

127126
const recordsWriteMessage = queryReply.entries[0] as any;
128-
expect(recordsWriteMessage.attestation?.signatures?.length).to.equal(2);
129-
expect(recordsWriteMessage.attestation.signatures[0]).to.not.equal(recordsWriteMessage.attestation.signatures[1]);
127+
expect(recordsWriteMessage.attestation?.signatures?.length).to.equal(1);
130128
});
131129

132130
it('should omit records that are not published if `dateSort` sorts on `datePublished`', async () => {

0 commit comments

Comments
 (0)