Skip to content

Commit f6eb76c

Browse files
authored
#310 - added protocolPath validation
1 parent cd96f50 commit f6eb76c

File tree

7 files changed

+219
-16
lines changed

7 files changed

+219
-16
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-93.95%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-93.46%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-91.69%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-93.95%25-brightgreen.svg?style=flat)
6+
![Statements](https://img.shields.io/badge/statements-93.99%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-93.5%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-91.72%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-93.99%25-brightgreen.svg?style=flat)
77

88
## Introduction
99

json-schemas/records/records-write.json

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,8 @@
105105
"type": "string"
106106
},
107107
"protocolPath": {
108-
"type": "string"
108+
"type": "string",
109+
"pattern": "^[a-zA-Z]+(\/[a-zA-Z]+)*$"
109110
},
110111
"schema": {
111112
"type": "string"
@@ -208,7 +209,9 @@
208209
"descriptor": {
209210
"type": "object",
210211
"required": [
211-
"protocol"
212+
"protocol",
213+
"protocolPath",
214+
"schema"
212215
]
213216
}
214217
},
@@ -236,6 +239,18 @@
236239
}
237240
}
238241
}
242+
},
243+
{
244+
"properties": {
245+
"descriptor": {
246+
"type": "object",
247+
"not": {
248+
"required": [
249+
"protocolPath"
250+
]
251+
}
252+
}
253+
}
239254
}
240255
]
241256
}

src/core/dwn-error.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export enum DwnErrorCode {
2020
MessageStoreDataCidMismatch = 'MessageStoreDataCidMismatch',
2121
MessageStoreDataNotFound = 'MessageStoreDataNotFound',
2222
MessageStoreDataSizeMismatch = 'MessageStoreDataSizeMismatch',
23+
ProtocolAuthorizationIncorrectProtocolPath = 'ProtocolAuthorizationIncorrectProtocolPath',
24+
ProtocolAuthorizationInvalidSchema = 'ProtocolAuthorizationInvalidSchema',
2325
RecordsDecryptNoMatchingKeyDerivationScheme = 'RecordsDecryptNoMatchingKeyDerivationScheme',
2426
RecordsDeriveLeafPrivateKeyUnSupportedCurve = 'RecordsDeriveLeafPrivateKeyUnSupportedCurve',
2527
RecordsDeriveLeafPublicKeyUnSupportedCurve = 'RecordsDeriveLeafPublicKeyUnSupportedCurve',

src/core/protocol-authorization.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { ProtocolDefinition, ProtocolRuleSet, ProtocolsConfigureMessage } f
55
import type { RecordsReadMessage, RecordsWriteMessage } from '../interfaces/records/types.js';
66

77
import { RecordsWrite } from '../interfaces/records/messages/records-write.js';
8+
import { DwnError, DwnErrorCode } from './dwn-error.js';
89
import { DwnInterfaceName, DwnMethodName, Message } from './message.js';
910

1011
const methodToAllowedActionMap: Record<string, string> = {
@@ -43,6 +44,9 @@ export class ProtocolAuthorization {
4344
recordSchemaToLabelMap.set(schema, schemaLabel);
4445
}
4546

47+
// validate `protocolPath`
48+
ProtocolAuthorization.verifyProtocolPath(incomingMessage, ancestorMessageChain, recordSchemaToLabelMap);
49+
4650
// get the rule set for the inbound message
4751
const inboundMessageRuleSet = ProtocolAuthorization.getRuleSet(
4852
incomingMessage.message,
@@ -176,12 +180,8 @@ export class ProtocolAuthorization {
176180
let allowedRecordsAtCurrentLevel: { [key: string]: ProtocolRuleSet} | undefined = protocolDefinition.records;
177181
let currentMessageIndex = 0;
178182
while (true) {
179-
const currentRecordSchema = messageChain[currentMessageIndex].descriptor.schema;
180-
const currentRecordType = recordSchemaToLabelMap.get(currentRecordSchema!);
181-
182-
if (currentRecordType === undefined) {
183-
throw new Error(`record with schema '${currentRecordSchema}' not allowed in protocol`);
184-
}
183+
const currentRecordSchema = messageChain[currentMessageIndex].descriptor.schema!;
184+
const currentRecordType = recordSchemaToLabelMap.get(currentRecordSchema)!;
185185

186186
if (allowedRecordsAtCurrentLevel === undefined || !(currentRecordType in allowedRecordsAtCurrentLevel)) {
187187
throw new Error(`record with schema: '${currentRecordSchema}' not allowed in structure level ${currentMessageIndex}`);
@@ -199,6 +199,43 @@ export class ProtocolAuthorization {
199199
}
200200
}
201201

202+
/**
203+
* Verifies the `protocolPath` declared in the given message (if it is a RecordsWrite) matches the path of actual ancestor chain.
204+
* @throws {DwnError} if fails verification.
205+
*/
206+
private static verifyProtocolPath(
207+
inboundMessage: RecordsRead | RecordsWrite,
208+
ancestorMessageChain: RecordsWriteMessage[],
209+
recordSchemaToLabelMap: Map<string, string>
210+
): void {
211+
// skip verification if this is not a RecordsWrite
212+
if (inboundMessage.message.descriptor.method !== DwnMethodName.Write) {
213+
return;
214+
}
215+
216+
const currentRecordSchema = inboundMessage.message.descriptor.schema!;
217+
const currentRecordSchemaLabel = recordSchemaToLabelMap.get(currentRecordSchema);
218+
if (currentRecordSchemaLabel === undefined) {
219+
throw new DwnError(DwnErrorCode.ProtocolAuthorizationInvalidSchema, `record with schema '${currentRecordSchema}' not allowed in protocol`);
220+
}
221+
222+
const declaredProtocolPath = (inboundMessage as RecordsWrite).message.descriptor.protocolPath!;
223+
let ancestorProtocolPath: string = '';
224+
for (const ancestor of ancestorMessageChain) {
225+
const ancestorSchemaLabel = recordSchemaToLabelMap.get(ancestor.descriptor.schema!);
226+
ancestorProtocolPath += `${ancestorSchemaLabel}/`; // e.g. `foo/bar/`, notice the trailing slash
227+
}
228+
229+
const actualProtocolPath = ancestorProtocolPath + currentRecordSchemaLabel; // e.g. `foo/bar/baz`
230+
231+
if (declaredProtocolPath !== actualProtocolPath) {
232+
throw new DwnError(
233+
DwnErrorCode.ProtocolAuthorizationIncorrectProtocolPath,
234+
`Declared protocol path '${declaredProtocolPath}' is not the same as actual protocol path '${actualProtocolPath}'.`
235+
);
236+
}
237+
}
238+
202239
/**
203240
* Verifies the actions specified in the given message matches the allowed actions in the rule set.
204241
* @throws {Error} if action not allowed.

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -998,7 +998,35 @@ describe('RecordsWriteHandler.handle()', () => {
998998

999999
const reply = await dwn.processMessage(alice.did, credentialApplication.message, credentialApplication.dataStream);
10001000
expect(reply.status.code).to.equal(401);
1001-
expect(reply.status.detail).to.equal('record with schema \'unexpectedSchema\' not allowed in protocol');
1001+
expect(reply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationInvalidSchema);
1002+
});
1003+
1004+
it('should fail authorization if given `protocolPath` is mismatching with actual path', async () => {
1005+
const alice = await DidKeyResolver.generate();
1006+
1007+
const protocol = 'https://identity.foundation/decentralized-web-node/protocols/credential-issuance';
1008+
const protocolConfig = await TestDataGenerator.generateProtocolsConfigure({
1009+
requester : alice,
1010+
protocol,
1011+
protocolDefinition : credentialIssuanceProtocolDefinition
1012+
});
1013+
1014+
const protocolConfigureReply = await dwn.processMessage(alice.did, protocolConfig.message, protocolConfig.dataStream);
1015+
expect(protocolConfigureReply.status.code).to.equal(202);
1016+
1017+
const data = Encoder.stringToBytes('any data');
1018+
const credentialApplication = await TestDataGenerator.generateRecordsWrite({
1019+
requester : alice,
1020+
recipientDid : alice.did,
1021+
protocol,
1022+
protocolPath : 'incorrect/protocol/path',
1023+
schema : credentialIssuanceProtocolDefinition.labels.credentialApplication.schema,
1024+
data
1025+
});
1026+
1027+
const reply = await dwn.processMessage(alice.did, credentialApplication.message, credentialApplication.dataStream);
1028+
expect(reply.status.code).to.equal(401);
1029+
expect(reply.status.detail).to.contain(DwnErrorCode.ProtocolAuthorizationIncorrectProtocolPath);
10021030
});
10031031

10041032
it('should fail authorization if record schema is not allowed at the hierarchical level attempted for the RecordsWrite', async () => {

tests/interfaces/records/messages/records-write.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ describe('RecordsWrite', () => {
133133
dataFormat : 'application/json',
134134
authorizationSignatureInput : Jws.createSignatureInput(alice),
135135
protocol : 'example.com/',
136-
protocolPath : 'example'
136+
protocolPath : 'example',
137+
schema : 'http://foo.bar/schema'
137138
};
138139
const recordsWrite = await RecordsWrite.create(options);
139140

tests/validation/json-schemas/records/records-write.spec.ts

Lines changed: 125 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,44 @@ describe('RecordsWrite schema definition', () => {
123123
}).throws('must NOT have additional properties');
124124
});
125125

126-
it('should pass if `contextId` and `protocol` are both present', () => {
126+
it('should pass if `protocol` exists and its related properties are all present', () => {
127+
const validMessage = {
128+
recordId : 'anyRecordId',
129+
contextId : 'someContext', // must exist because `protocol` exists
130+
descriptor : {
131+
interface : 'Records',
132+
method : 'Write',
133+
protocol : 'someProtocolId',
134+
protocolPath : 'foo/bar', // must exist because `protocol` exists
135+
schema : 'http://foo.bar/schema', // must exist because `protocol` exists
136+
dataCid : 'anyCid',
137+
dataFormat : 'application/json',
138+
dataSize : 123,
139+
dateCreated : '2022-12-19T10:20:30.123456',
140+
dateModified : '2022-12-19T10:20:30.123456'
141+
},
142+
authorization: {
143+
payload : 'anyPayload',
144+
signatures : [{
145+
protected : 'anyProtectedHeader',
146+
signature : 'anySignature'
147+
}]
148+
}
149+
};
150+
151+
Message.validateJsonSchema(validMessage);
152+
});
153+
154+
it('should throw if `protocolPath` contains invalid characters', () => {
127155
const invalidMessage = {
128156
recordId : 'anyRecordId',
129-
contextId : 'someContext', // protocol must exist
157+
contextId : 'someContext',
130158
descriptor : {
131159
interface : 'Records',
132160
method : 'Write',
133-
protocol : 'someProtocolId', // contextId must exist
161+
protocol : 'http://foo.bar',
162+
protocolPath : 'invalid:path', // `:` is not a valid char in `protocolPath`
163+
schema : 'http://foo.bar/schema',
134164
dataCid : 'anyCid',
135165
dataFormat : 'application/json',
136166
dataSize : 123,
@@ -146,10 +176,12 @@ describe('RecordsWrite schema definition', () => {
146176
}
147177
};
148178

149-
Message.validateJsonSchema(invalidMessage);
179+
expect(() => {
180+
Message.validateJsonSchema(invalidMessage);
181+
}).throws('protocolPath: must match pattern "^[a-zA-Z]+(/[a-zA-Z]+)*$');
150182
});
151183

152-
it('should pass if `contextId` and `protocol` are both not present', () => {
184+
it('should pass if none of `protocol` related properties are present', () => {
153185
const invalidMessage = {
154186
recordId : 'anyRecordId',
155187
descriptor : {
@@ -227,6 +259,94 @@ describe('RecordsWrite schema definition', () => {
227259
}).throws('must have required property \'contextId\'');
228260
});
229261

262+
it('should throw if `protocol` is set but `protocolPath` is missing', () => {
263+
const invalidMessage = {
264+
recordId : 'anyRecordId',
265+
contextId : 'anyContextId', // required by protocol-based message
266+
descriptor : {
267+
interface : 'Records',
268+
method : 'Write',
269+
protocol : 'http://foo.bar',
270+
// protocolPath : 'foo/bar', // intentionally missing
271+
dataCid : 'anyCid',
272+
dataFormat : 'application/json',
273+
dataSize : 123,
274+
dateCreated : '2022-12-19T10:20:30.123456',
275+
dateModified : '2022-12-19T10:20:30.123456'
276+
},
277+
authorization: {
278+
payload : 'anyPayload',
279+
signatures : [{
280+
protected : 'anyProtectedHeader',
281+
signature : 'anySignature'
282+
}]
283+
}
284+
};
285+
286+
expect(() => {
287+
Message.validateJsonSchema(invalidMessage);
288+
}).throws('descriptor: must have required property \'protocolPath\'');
289+
});
290+
291+
it('should throw if `protocolPath` is set but `protocol` is missing', () => {
292+
const invalidMessage = {
293+
recordId : 'anyRecordId',
294+
contextId : 'anyContextId',
295+
descriptor : {
296+
interface : 'Records',
297+
method : 'Write',
298+
// protocol : 'http://foo.bar', // intentionally missing
299+
protocolPath : 'foo/bar',
300+
dataCid : 'anyCid',
301+
dataFormat : 'application/json',
302+
dataSize : 123,
303+
dateCreated : '2022-12-19T10:20:30.123456',
304+
dateModified : '2022-12-19T10:20:30.123456'
305+
},
306+
authorization: {
307+
payload : 'anyPayload',
308+
signatures : [{
309+
protected : 'anyProtectedHeader',
310+
signature : 'anySignature'
311+
}]
312+
}
313+
};
314+
315+
expect(() => {
316+
Message.validateJsonSchema(invalidMessage);
317+
}).throws('descriptor: must have required property \'protocol\'');
318+
});
319+
320+
it('should throw if `protocol` is set but `schema` is missing', () => {
321+
const invalidMessage = {
322+
recordId : 'anyRecordId',
323+
contextId : 'anyContextId', // required by protocol-based message
324+
descriptor : {
325+
interface : 'Records',
326+
method : 'Write',
327+
protocol : 'http://foo.bar',
328+
protocolPath : 'foo/bar',
329+
// schema : 'http://foo.bar/schema', // intentionally missing
330+
dataCid : 'anyCid',
331+
dataFormat : 'application/json',
332+
dataSize : 123,
333+
dateCreated : '2022-12-19T10:20:30.123456',
334+
dateModified : '2022-12-19T10:20:30.123456'
335+
},
336+
authorization: {
337+
payload : 'anyPayload',
338+
signatures : [{
339+
protected : 'anyProtectedHeader',
340+
signature : 'anySignature'
341+
}]
342+
}
343+
};
344+
345+
expect(() => {
346+
Message.validateJsonSchema(invalidMessage);
347+
}).throws('descriptor: must have required property \'schema\'');
348+
});
349+
230350
it('should throw if published is false but datePublished is present', () => {
231351
const invalidMessage = {
232352
recordId : 'anyRecordId',

0 commit comments

Comments
 (0)