Skip to content

Commit 9b37d06

Browse files
author
Diane Huxley
authored
Reject messages with nonnormalized schema URIs (#325)
* Reject messages with nonnormalized schema URIs * URI -> URL * Fix README
1 parent 93a8eaa commit 9b37d06

File tree

16 files changed

+214
-64
lines changed

16 files changed

+214
-64
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.07%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-93.6%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-91.8%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-94.07%25-brightgreen.svg?style=flat)
6+
![Statements](https://img.shields.io/badge/statements-94.1%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-93.71%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-91.94%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-94.1%25-brightgreen.svg?style=flat)
77

88
## Introduction
99

src/core/dwn-error.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,7 @@ export enum DwnErrorCode {
3232
RecordsWriteValidateIntegrityEncryptionCidMismatch = 'RecordsWriteValidateIntegrityEncryptionCidMismatch',
3333
Secp256k1KeyNotValid = 'Secp256k1KeyNotValid',
3434
UrlProtocolNotNormalized = 'UrlProtocolNotNormalized',
35-
UrlProtocolNotNormalizable = 'UrlProtocolNotNormalizable'
35+
UrlProtocolNotNormalizable = 'UrlProtocolNotNormalizable',
36+
UrlSchemaNotNormalized = 'UrlSchemaNotNormalized',
37+
UrlSchemaNotNormalizable = 'UrlSchemaNotNormalizable'
3638
};

src/interfaces/protocols/messages/protocols-configure.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { getCurrentTimeInHighPrecision } from '../../../utils/time.js';
55
import { validateAuthorizationIntegrity } from '../../../core/auth.js';
66

77
import { DwnInterfaceName, DwnMethodName, Message } from '../../../core/message.js';
8-
import { normalizeProtocolUri, validateProtocolUriNormalized } from '../../../utils/url.js';
8+
import { normalizeProtocolUrl, normalizeSchemaUrl, validateProtocolUrlNormalized, validateSchemaUrlNormalized } from '../../../utils/url.js';
99

1010
export type ProtocolsConfigureOptions = {
1111
dateCreated? : string;
@@ -18,7 +18,8 @@ export class ProtocolsConfigure extends Message<ProtocolsConfigureMessage> {
1818

1919
public static async parse(message: ProtocolsConfigureMessage): Promise<ProtocolsConfigure> {
2020
await validateAuthorizationIntegrity(message);
21-
validateProtocolUriNormalized(message.descriptor.protocol);
21+
validateProtocolUrlNormalized(message.descriptor.protocol);
22+
ProtocolsConfigure.validateDefinitionNormalized(message.descriptor.definition);
2223

2324
return new ProtocolsConfigure(message);
2425
}
@@ -28,8 +29,9 @@ export class ProtocolsConfigure extends Message<ProtocolsConfigureMessage> {
2829
interface : DwnInterfaceName.Protocols,
2930
method : DwnMethodName.Configure,
3031
dateCreated : options.dateCreated ?? getCurrentTimeInHighPrecision(),
31-
protocol : normalizeProtocolUri(options.protocol),
32-
definition : options.definition // TODO: #139 - move definition out of the descriptor - https://github.com/TBD54566975/dwn-sdk-js/issues/139
32+
protocol : normalizeProtocolUrl(options.protocol),
33+
// TODO: #139 - move definition out of the descriptor - https://github.com/TBD54566975/dwn-sdk-js/issues/139
34+
definition : ProtocolsConfigure.normalizeDefinition(options.definition)
3335
};
3436

3537
Message.validateJsonSchema({ descriptor, authorization: { } });
@@ -40,4 +42,23 @@ export class ProtocolsConfigure extends Message<ProtocolsConfigureMessage> {
4042
const protocolsConfigure = new ProtocolsConfigure(message);
4143
return protocolsConfigure;
4244
}
45+
46+
private static validateDefinitionNormalized(definition: ProtocolDefinition): void {
47+
// validate schema url normalized
48+
for (const labelKey in definition.labels) {
49+
const schema = definition.labels[labelKey].schema;
50+
validateSchemaUrlNormalized(schema);
51+
}
52+
}
53+
54+
private static normalizeDefinition(definition: ProtocolDefinition): ProtocolDefinition {
55+
const definitionCopy = { ...definition };
56+
57+
// Normalize schema url
58+
for (const labelKey in definition.labels) {
59+
definitionCopy.labels[labelKey].schema = normalizeSchemaUrl(definitionCopy.labels[labelKey].schema);
60+
}
61+
62+
return definitionCopy;
63+
}
4364
}

src/interfaces/protocols/messages/protocols-query.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { getCurrentTimeInHighPrecision } from '../../../utils/time.js';
55
import { removeUndefinedProperties } from '../../../utils/object.js';
66
import { validateAuthorizationIntegrity } from '../../../core/auth.js';
77
import { DwnInterfaceName, DwnMethodName, Message } from '../../../core/message.js';
8-
import { normalizeProtocolUri, validateProtocolUriNormalized } from '../../../utils/url.js';
8+
import { normalizeProtocolUrl, validateProtocolUrlNormalized } from '../../../utils/url.js';
99

1010
export type ProtocolsQueryOptions = {
1111
dateCreated?: string;
@@ -19,7 +19,7 @@ export class ProtocolsQuery extends Message<ProtocolsQueryMessage> {
1919
await validateAuthorizationIntegrity(message);
2020

2121
if (message.descriptor.filter !== undefined) {
22-
validateProtocolUriNormalized(message.descriptor.filter.protocol);
22+
validateProtocolUrlNormalized(message.descriptor.filter.protocol);
2323
}
2424

2525
return new ProtocolsQuery(message);
@@ -53,7 +53,7 @@ export class ProtocolsQuery extends Message<ProtocolsQueryMessage> {
5353

5454
return {
5555
...filter,
56-
protocol: normalizeProtocolUri(filter.protocol),
56+
protocol: normalizeProtocolUrl(filter.protocol),
5757
};
5858
}
5959
}

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

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Message } from '../../../core/message.js';
77
import { removeUndefinedProperties } from '../../../utils/object.js';
88
import { validateAuthorizationIntegrity } from '../../../core/auth.js';
99
import { DwnInterfaceName, DwnMethodName } from '../../../core/message.js';
10-
import { normalizeProtocolUri, validateProtocolUriNormalized } from '../../../utils/url.js';
10+
import { normalizeProtocolUrl, normalizeSchemaUrl, validateProtocolUrlNormalized, validateSchemaUrlNormalized } from '../../../utils/url.js';
1111

1212
export enum DateSort {
1313
CreatedAscending = 'createdAscending',
@@ -28,8 +28,11 @@ export class RecordsQuery extends Message<RecordsQueryMessage> {
2828
public static async parse(message: RecordsQueryMessage): Promise<RecordsQuery> {
2929
await validateAuthorizationIntegrity(message);
3030

31-
if (message.descriptor.filter?.protocol !== undefined) {
32-
validateProtocolUriNormalized(message.descriptor.filter.protocol);
31+
if (message.descriptor.filter.protocol !== undefined) {
32+
validateProtocolUrlNormalized(message.descriptor.filter.protocol);
33+
}
34+
if (message.descriptor.filter.schema !== undefined) {
35+
validateSchemaUrlNormalized(message.descriptor.filter.schema);
3336
}
3437

3538
return new RecordsQuery(message);
@@ -40,7 +43,7 @@ export class RecordsQuery extends Message<RecordsQueryMessage> {
4043
interface : DwnInterfaceName.Records,
4144
method : DwnMethodName.Query,
4245
dateCreated : options.dateCreated ?? getCurrentTimeInHighPrecision(),
43-
filter : RecordsQuery.normalizerFilter(options.filter),
46+
filter : RecordsQuery.normalizeFilter(options.filter),
4447
dateSort : options.dateSort
4548
};
4649

@@ -101,17 +104,25 @@ export class RecordsQuery extends Message<RecordsQueryMessage> {
101104
return filterCopy as Filter;
102105
}
103106

104-
public static normalizerFilter(filter: RecordsQueryFilter): RecordsQueryFilter {
107+
public static normalizeFilter(filter: RecordsQueryFilter): RecordsQueryFilter {
105108
let protocol;
106109
if (filter.protocol === undefined) {
107110
protocol = undefined;
108111
} else {
109-
protocol = normalizeProtocolUri(filter.protocol);
112+
protocol = normalizeProtocolUrl(filter.protocol);
113+
}
114+
115+
let schema;
116+
if (filter.schema === undefined) {
117+
schema = undefined;
118+
} else {
119+
schema = normalizeSchemaUrl(filter.schema);
110120
}
111121

112122
return {
113123
...filter,
114124
protocol,
125+
schema,
115126
};
116127
}
117128
}

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { authorize, validateAuthorizationIntegrity } from '../../../core/auth.js
2828
import { Cid, computeCid } from '../../../utils/cid.js';
2929
import { DwnError, DwnErrorCode } from '../../../core/dwn-error.js';
3030
import { DwnInterfaceName, DwnMethodName } from '../../../core/message.js';
31-
import { normalizeProtocolUri, validateProtocolUriNormalized } from '../../../utils/url.js';
31+
import { normalizeProtocolUrl, normalizeSchemaUrl, validateProtocolUrlNormalized, validateSchemaUrlNormalized } from '../../../utils/url.js';
3232

3333
export type RecordsWriteOptions = {
3434
recipient?: string;
@@ -166,10 +166,10 @@ export class RecordsWrite extends Message<RecordsWriteMessage> {
166166
const descriptor: RecordsWriteDescriptor = {
167167
interface : DwnInterfaceName.Records,
168168
method : DwnMethodName.Write,
169-
protocol : options.protocol !== undefined ? normalizeProtocolUri(options.protocol) : undefined,
169+
protocol : options.protocol !== undefined ? normalizeProtocolUrl(options.protocol) : undefined,
170170
protocolPath : options.protocolPath,
171171
recipient : options.recipient!,
172-
schema : options.schema,
172+
schema : options.schema !== undefined ? normalizeSchemaUrl(options.schema) : undefined,
173173
parentId : options.parentId,
174174
dataCid,
175175
dataSize,
@@ -375,7 +375,10 @@ export class RecordsWrite extends Message<RecordsWriteMessage> {
375375
}
376376

377377
if (this.message.descriptor.protocol !== undefined) {
378-
validateProtocolUriNormalized(this.message.descriptor.protocol);
378+
validateProtocolUrlNormalized(this.message.descriptor.protocol);
379+
}
380+
if (this.message.descriptor.schema !== undefined) {
381+
validateSchemaUrlNormalized(this.message.descriptor.schema);
379382
}
380383
}
381384

src/utils/url.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,44 @@
11
import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
22

3+
export function validateProtocolUrlNormalized(url: string): void {
4+
let normalized: string | undefined;
5+
try {
6+
normalized = normalizeProtocolUrl(url);
7+
} catch {
8+
normalized = undefined;
9+
}
10+
11+
if (url !== normalized) {
12+
throw new DwnError(DwnErrorCode.UrlProtocolNotNormalized, `Protocol URI ${url} must be normalized.`);
13+
}
14+
}
15+
16+
export function normalizeProtocolUrl(url: string): string {
17+
// Keeping protocol normalization as a separate function in case
18+
// protocol and schema normalization diverge in the future
19+
return normalizeUrl(url);
20+
}
321

4-
export function validateProtocolUriNormalized(protocol: string): void {
22+
export function validateSchemaUrlNormalized(url: string): void {
523
let normalized: string | undefined;
624
try {
7-
normalized = normalizeProtocolUri(protocol);
25+
normalized = normalizeSchemaUrl(url);
826
} catch {
927
normalized = undefined;
1028
}
1129

12-
if (protocol !== normalized) {
13-
throw new DwnError(DwnErrorCode.UrlProtocolNotNormalized, 'Protocol URI must be normalized.');
30+
if (url !== normalized) {
31+
throw new DwnError(DwnErrorCode.UrlSchemaNotNormalized, `Schema URI ${url} must be normalized.`);
1432
}
1533
}
1634

17-
export function normalizeProtocolUri(url: string): string {
35+
export function normalizeSchemaUrl(url: string): string {
36+
// Keeping schema normalization as a separate function in case
37+
// protocol and schema normalization diverge in the future
38+
return normalizeUrl(url);
39+
}
40+
41+
function normalizeUrl(url: string): string {
1842
let fullUrl: string;
1943
if (/^[^:]+:\/\/./.test(url)) {
2044
fullUrl = url;

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import chaiAsPromised from 'chai-as-promised';
44
import sinon from 'sinon';
55
import chai, { expect } from 'chai';
66

7+
import dexProtocolDefinition from '../../../vectors/protocol-definitions/dex.json' assert { type: 'json' };
8+
79
import { DataStoreLevel } from '../../../../src/store/data-store-level.js';
810
import { DidKeyResolver } from '../../../../src/did/did-key-resolver.js';
911
import { EventLogLevel } from '../../../../src/event-log/event-log-level.js';
@@ -164,6 +166,30 @@ describe('ProtocolsConfigureHandler.handle()', () => {
164166
expect(reply.status.detail).to.contain(DwnErrorCode.UrlProtocolNotNormalized);
165167
});
166168

169+
it('should return 400 if schema is not normalized', async () => {
170+
const alice = await DidKeyResolver.generate();
171+
172+
const protocolsConfig = await TestDataGenerator.generateProtocolsConfigure({
173+
requester : alice,
174+
protocol : 'example.com/',
175+
protocolDefinition : dexProtocolDefinition,
176+
});
177+
178+
// overwrite schema because #create auto-normalizes schema
179+
protocolsConfig.message.descriptor.definition.labels.ask.schema = 'ask';
180+
181+
// Re-create auth because we altered the descriptor after signing
182+
protocolsConfig.message.authorization = await Message.signAsAuthorization(
183+
protocolsConfig.message.descriptor,
184+
Jws.createSignatureInput(alice)
185+
);
186+
187+
// Send records write message
188+
const reply = await dwn.processMessage(alice.did, protocolsConfig.message);
189+
expect(reply.status.code).to.equal(400);
190+
expect(reply.status.detail).to.contain(DwnErrorCode.UrlSchemaNotNormalized);
191+
});
192+
167193
describe('event log', () => {
168194
it('should add event for ProtocolsConfigure', async () => {
169195
const alice = await DidKeyResolver.generate();

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,27 @@ describe('ProtocolsConfigure', () => {
4343

4444
expect(message.descriptor.protocol).to.eq('http://example.com');
4545
});
46+
47+
it('should auto-normalize schema URIs', async () => {
48+
const alice = await TestDataGenerator.generatePersona();
49+
50+
const nonnormalizedDexProtocol = { ...dexProtocolDefinition };
51+
nonnormalizedDexProtocol.labels.ask.schema = 'ask';
52+
53+
const options = {
54+
recipient : alice.did,
55+
data : TestDataGenerator.randomBytes(10),
56+
dataFormat : 'application/json',
57+
authorizationSignatureInput : Jws.createSignatureInput(alice),
58+
protocol : 'example.com/',
59+
definition : nonnormalizedDexProtocol
60+
};
61+
const protocolsConfig = await ProtocolsConfigure.create(options);
62+
63+
const message = protocolsConfig.message as ProtocolsConfigureMessage;
64+
65+
expect(message.descriptor.definition.labels.ask.schema).to.eq('http://ask');
66+
});
4667
});
4768
});
4869

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,30 @@ describe('RecordsQueryHandler.handle()', () => {
616616
expect(reply.status.detail).to.contain(DwnErrorCode.UrlProtocolNotNormalized);
617617
});
618618

619+
it('should return 400 if schema is not normalized', async () => {
620+
const alice = await DidKeyResolver.generate();
621+
622+
// query for non-normalized schema
623+
const recordsQuery = await TestDataGenerator.generateRecordsQuery({
624+
requester : alice,
625+
filter : { schema: 'example.com/' },
626+
});
627+
628+
// overwrite schema because #create auto-normalizes schema
629+
recordsQuery.message.descriptor.filter.schema = 'example.com/';
630+
631+
// Re-create auth because we altered the descriptor after signing
632+
recordsQuery.message.authorization = await Message.signAsAuthorization(
633+
recordsQuery.message.descriptor,
634+
Jws.createSignatureInput(alice)
635+
);
636+
637+
// Send records write message
638+
const reply = await dwn.processMessage(alice.did, recordsQuery.message);
639+
expect(reply.status.code).to.equal(400);
640+
expect(reply.status.detail).to.contain(DwnErrorCode.UrlSchemaNotNormalized);
641+
});
642+
619643
describe('encryption scenarios', () => {
620644
it('should only be able to decrypt record with a correct derived private key - protocols derivation scheme', async () => {
621645
// scenario, Bob writes into Alice's DWN an encrypted "email", alice is able to decrypt it

0 commit comments

Comments
 (0)