diff --git a/json-schemas/interface-methods/messages-get.json b/json-schemas/interface-methods/messages-get.json index da1c3d962..3f0bf98a2 100644 --- a/json-schemas/interface-methods/messages-get.json +++ b/json-schemas/interface-methods/messages-get.json @@ -33,7 +33,7 @@ "type": "string" }, "messageTimestamp": { - "type": "string" + "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/definitions/date-time" }, "messageCids": { "type": "array", diff --git a/json-schemas/interface-methods/permissions-request.json b/json-schemas/interface-methods/permissions-request.json index a320870c4..6b6ec68fe 100644 --- a/json-schemas/interface-methods/permissions-request.json +++ b/json-schemas/interface-methods/permissions-request.json @@ -37,7 +37,7 @@ "type": "string" }, "messageTimestamp": { - "type": "string" + "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/definitions/date-time" }, "description": { "type": "string" diff --git a/json-schemas/interface-methods/permissions-revoke.json b/json-schemas/interface-methods/permissions-revoke.json index 64543e876..2b62e5edf 100644 --- a/json-schemas/interface-methods/permissions-revoke.json +++ b/json-schemas/interface-methods/permissions-revoke.json @@ -22,7 +22,7 @@ ], "properties": { "messageTimestamp": { - "type": "string" + "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/definitions/date-time" }, "permissionsGrantId": { "type": "string" diff --git a/json-schemas/interface-methods/protocols-query.json b/json-schemas/interface-methods/protocols-query.json index 6d9027a4d..88bfa113e 100644 --- a/json-schemas/interface-methods/protocols-query.json +++ b/json-schemas/interface-methods/protocols-query.json @@ -32,7 +32,7 @@ "type": "string" }, "messageTimestamp": { - "type": "string" + "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/definitions/date-time" }, "filter": { "type": "object", diff --git a/json-schemas/interface-methods/records-delete.json b/json-schemas/interface-methods/records-delete.json index 163a981ef..23b34383f 100644 --- a/json-schemas/interface-methods/records-delete.json +++ b/json-schemas/interface-methods/records-delete.json @@ -34,7 +34,7 @@ "type": "string" }, "messageTimestamp": { - "type": "string" + "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/definitions/date-time" }, "recordId": { "type": "string" diff --git a/json-schemas/interface-methods/records-read.json b/json-schemas/interface-methods/records-read.json index 0e43dd97a..3969d8bc1 100644 --- a/json-schemas/interface-methods/records-read.json +++ b/json-schemas/interface-methods/records-read.json @@ -33,7 +33,7 @@ "type": "string" }, "messageTimestamp": { - "type": "string" + "$ref": "https://identity.foundation/dwn/json-schemas/defs.json#/definitions/date-time" }, "filter": { "$ref": "https://identity.foundation/dwn/json-schemas/records-filter.json" diff --git a/src/core/dwn-error.ts b/src/core/dwn-error.ts index 71a92a7d8..65338686a 100644 --- a/src/core/dwn-error.ts +++ b/src/core/dwn-error.ts @@ -88,6 +88,7 @@ export enum DwnErrorCode { RecordsWriteSignAsOwnerUnknownAuthor = 'RecordsWriteSignAsOwnerUnknownAuthor', RecordsWriteValidateIntegrityEncryptionCidMismatch = 'RecordsWriteValidateIntegrityEncryptionCidMismatch', Secp256k1KeyNotValid = 'Secp256k1KeyNotValid', + TimestampInvalid = 'TimestampInvalid', UrlProtocolNotNormalized = 'UrlProtocolNotNormalized', UrlProtocolNotNormalizable = 'UrlProtocolNotNormalizable', UrlSchemaNotNormalized = 'UrlSchemaNotNormalized', diff --git a/src/interfaces/events-get.ts b/src/interfaces/events-get.ts index 90788f955..1f79dc6ff 100644 --- a/src/interfaces/events-get.ts +++ b/src/interfaces/events-get.ts @@ -1,9 +1,9 @@ import type { Signer } from '../types/signer.js'; import type { EventsGetDescriptor, EventsGetMessage } from '../types/event-types.js'; -import { getCurrentTimeInHighPrecision } from '../utils/time.js'; import { validateMessageSignatureIntegrity } from '../core/auth.js'; import { DwnInterfaceName, DwnMethodName, Message } from '../core/message.js'; +import { getCurrentTimeInHighPrecision, validateTimestamp } from '../utils/time.js'; export type EventsGetOptions = { watermark?: string; @@ -16,6 +16,7 @@ export class EventsGet extends Message { public static async parse(message: EventsGetMessage): Promise { Message.validateJsonSchema(message); await validateMessageSignatureIntegrity(message.authorization.authorSignature, message.descriptor); + validateTimestamp(message.descriptor.messageTimestamp); return new EventsGet(message); } diff --git a/src/interfaces/messages-get.ts b/src/interfaces/messages-get.ts index 0bdb5305f..8e461d6fd 100644 --- a/src/interfaces/messages-get.ts +++ b/src/interfaces/messages-get.ts @@ -2,9 +2,9 @@ import type { Signer } from '../types/signer.js'; import type { MessagesGetDescriptor, MessagesGetMessage } from '../types/messages-types.js'; import { Cid } from '../utils/cid.js'; -import { getCurrentTimeInHighPrecision } from '../utils/time.js'; import { validateMessageSignatureIntegrity } from '../core/auth.js'; import { DwnInterfaceName, DwnMethodName, Message } from '../core/message.js'; +import { getCurrentTimeInHighPrecision, validateTimestamp } from '../utils/time.js'; export type MessagesGetOptions = { messageCids: string[]; @@ -18,6 +18,7 @@ export class MessagesGet extends Message { this.validateMessageCids(message.descriptor.messageCids); await validateMessageSignatureIntegrity(message.authorization.authorSignature, message.descriptor); + validateTimestamp(message.descriptor.messageTimestamp); return new MessagesGet(message); } diff --git a/src/interfaces/permissions-grant.ts b/src/interfaces/permissions-grant.ts index b911feff6..c4f1837c4 100644 --- a/src/interfaces/permissions-grant.ts +++ b/src/interfaces/permissions-grant.ts @@ -3,11 +3,11 @@ import type { Signer } from '../types/signer.js'; import type { PermissionConditions, PermissionScope, RecordsPermissionScope } from '../types/permissions-types.js'; import type { PermissionsGrantDescriptor, PermissionsGrantMessage } from '../types/permissions-types.js'; -import { getCurrentTimeInHighPrecision } from '../utils/time.js'; import { removeUndefinedProperties } from '../utils/object.js'; import { validateMessageSignatureIntegrity } from '../core/auth.js'; import { DwnError, DwnErrorCode } from '../core/dwn-error.js'; import { DwnInterfaceName, DwnMethodName, Message } from '../core/message.js'; +import { getCurrentTimeInHighPrecision, validateTimestamp } from '../utils/time.js'; import { normalizeProtocolUrl, normalizeSchemaUrl } from '../utils/url.js'; export type PermissionsGrantOptions = { @@ -38,6 +38,8 @@ export class PermissionsGrant extends Message { public static async parse(message: PermissionsGrantMessage): Promise { await validateMessageSignatureIntegrity(message.authorization.authorSignature, message.descriptor); PermissionsGrant.validateScope(message); + validateTimestamp(message.descriptor.messageTimestamp); + validateTimestamp(message.descriptor.dateExpires); return new PermissionsGrant(message); } diff --git a/src/interfaces/permissions-request.ts b/src/interfaces/permissions-request.ts index cd16ca606..875116745 100644 --- a/src/interfaces/permissions-request.ts +++ b/src/interfaces/permissions-request.ts @@ -2,10 +2,10 @@ import type { Signer } from '../types/signer.js'; import type { PermissionConditions, PermissionScope } from '../types/permissions-types.js'; import type { PermissionsRequestDescriptor, PermissionsRequestMessage } from '../types/permissions-types.js'; -import { getCurrentTimeInHighPrecision } from '../utils/time.js'; import { removeUndefinedProperties } from '../utils/object.js'; import { validateMessageSignatureIntegrity } from '../core/auth.js'; import { DwnInterfaceName, DwnMethodName, Message } from '../core/message.js'; +import { getCurrentTimeInHighPrecision, validateTimestamp } from '../utils/time.js'; export type PermissionsRequestOptions = { messageTimestamp?: string; @@ -22,6 +22,7 @@ export class PermissionsRequest extends Message { public static async parse(message: PermissionsRequestMessage): Promise { await validateMessageSignatureIntegrity(message.authorization.authorSignature, message.descriptor); + validateTimestamp(message.descriptor.messageTimestamp); return new PermissionsRequest(message); } diff --git a/src/interfaces/permissions-revoke.ts b/src/interfaces/permissions-revoke.ts index c1206494a..459dbd2cc 100644 --- a/src/interfaces/permissions-revoke.ts +++ b/src/interfaces/permissions-revoke.ts @@ -1,10 +1,10 @@ import type { Signer } from '../types/signer.js'; import type { PermissionsGrantMessage, PermissionsRevokeDescriptor, PermissionsRevokeMessage } from '../types/permissions-types.js'; -import { getCurrentTimeInHighPrecision } from '../utils/time.js'; import { validateMessageSignatureIntegrity } from '../core/auth.js'; import { DwnError, DwnErrorCode } from '../core/dwn-error.js'; import { DwnInterfaceName, DwnMethodName, Message } from '../core/message.js'; +import { getCurrentTimeInHighPrecision, validateTimestamp } from '../utils/time.js'; export type PermissionsRevokeOptions = { messageTimestamp?: string; @@ -15,6 +15,7 @@ export type PermissionsRevokeOptions = { export class PermissionsRevoke extends Message { public static async parse(message: PermissionsRevokeMessage): Promise { await validateMessageSignatureIntegrity(message.authorization.authorSignature, message.descriptor); + validateTimestamp(message.descriptor.messageTimestamp); return new PermissionsRevoke(message); } diff --git a/src/interfaces/protocols-configure.ts b/src/interfaces/protocols-configure.ts index b19b9fbf9..8f631efad 100644 --- a/src/interfaces/protocols-configure.ts +++ b/src/interfaces/protocols-configure.ts @@ -1,10 +1,10 @@ import type { Signer } from '../types/signer.js'; import type { ProtocolDefinition, ProtocolRuleSet, ProtocolsConfigureDescriptor, ProtocolsConfigureMessage } from '../types/protocols-types.js'; -import { getCurrentTimeInHighPrecision } from '../utils/time.js'; import { validateMessageSignatureIntegrity } from '../core/auth.js'; import { DwnError, DwnErrorCode } from '../index.js'; import { DwnInterfaceName, DwnMethodName, Message } from '../core/message.js'; +import { getCurrentTimeInHighPrecision, validateTimestamp } from '../utils/time.js'; import { normalizeProtocolUrl, normalizeSchemaUrl, validateProtocolUrlNormalized, validateSchemaUrlNormalized } from '../utils/url.js'; export type ProtocolsConfigureOptions = { @@ -22,6 +22,7 @@ export class ProtocolsConfigure extends Message { Message.validateJsonSchema(message); ProtocolsConfigure.validateProtocolDefinition(message.descriptor.definition); await validateMessageSignatureIntegrity(message.authorization.authorSignature, message.descriptor); + validateTimestamp(message.descriptor.messageTimestamp); return new ProtocolsConfigure(message); } diff --git a/src/interfaces/protocols-query.ts b/src/interfaces/protocols-query.ts index ec76569f0..67baa8d47 100644 --- a/src/interfaces/protocols-query.ts +++ b/src/interfaces/protocols-query.ts @@ -3,11 +3,11 @@ import type { MessageStore } from '../types/message-store.js'; import type { Signer } from '../types/signer.js'; import type { ProtocolsQueryDescriptor, ProtocolsQueryFilter, ProtocolsQueryMessage } from '../types/protocols-types.js'; -import { getCurrentTimeInHighPrecision } from '../utils/time.js'; import { GrantAuthorization } from '../core/grant-authorization.js'; import { removeUndefinedProperties } from '../utils/object.js'; import { validateMessageSignatureIntegrity } from '../core/auth.js'; import { DwnInterfaceName, DwnMethodName, Message } from '../core/message.js'; +import { getCurrentTimeInHighPrecision, validateTimestamp } from '../utils/time.js'; import { normalizeProtocolUrl, validateProtocolUrlNormalized } from '../utils/url.js'; import { DwnError, DwnErrorCode } from '../core/dwn-error.js'; @@ -29,6 +29,7 @@ export class ProtocolsQuery extends Message { if (message.descriptor.filter !== undefined) { validateProtocolUrlNormalized(message.descriptor.filter.protocol); } + validateTimestamp(message.descriptor.messageTimestamp); return new ProtocolsQuery(message); } diff --git a/src/interfaces/records-delete.ts b/src/interfaces/records-delete.ts index 7d8188963..dd488cba5 100644 --- a/src/interfaces/records-delete.ts +++ b/src/interfaces/records-delete.ts @@ -1,11 +1,11 @@ import type { RecordsDeleteDescriptor, RecordsDeleteMessage } from '../types/records-types.js'; -import { getCurrentTimeInHighPrecision } from '../utils/time.js'; import { Message } from '../core/message.js'; import type { Signer } from '../types/signer.js'; import { authorize, validateMessageSignatureIntegrity } from '../core/auth.js'; import { DwnInterfaceName, DwnMethodName } from '../core/message.js'; +import { getCurrentTimeInHighPrecision, validateTimestamp } from '../utils/time.js'; export type RecordsDeleteOptions = { recordId: string; @@ -17,6 +17,7 @@ export class RecordsDelete extends Message { public static async parse(message: RecordsDeleteMessage): Promise { await validateMessageSignatureIntegrity(message.authorization.authorSignature, message.descriptor); + validateTimestamp(message.descriptor.messageTimestamp); const recordsDelete = new RecordsDelete(message); return recordsDelete; diff --git a/src/interfaces/records-query.ts b/src/interfaces/records-query.ts index 48e3265eb..0c552f248 100644 --- a/src/interfaces/records-query.ts +++ b/src/interfaces/records-query.ts @@ -2,13 +2,13 @@ import type { Pagination } from '../types/message-types.js'; import type { Signer } from '../types/signer.js'; import type { RecordsFilter, RecordsQueryDescriptor, RecordsQueryMessage } from '../types/records-types.js'; -import { getCurrentTimeInHighPrecision } from '../utils/time.js'; import { Message } from '../core/message.js'; import { Records } from '../utils/records.js'; import { removeUndefinedProperties } from '../utils/object.js'; import { validateMessageSignatureIntegrity } from '../core/auth.js'; import { DwnError, DwnErrorCode } from '../core/dwn-error.js'; import { DwnInterfaceName, DwnMethodName } from '../core/message.js'; +import { getCurrentTimeInHighPrecision, validateTimestamp } from '../utils/time.js'; import { validateProtocolUrlNormalized, validateSchemaUrlNormalized } from '../utils/url.js'; export enum DateSort { @@ -49,6 +49,7 @@ export class RecordsQuery extends Message { if (message.descriptor.filter.schema !== undefined) { validateSchemaUrlNormalized(message.descriptor.filter.schema); } + validateTimestamp(message.descriptor.messageTimestamp); return new RecordsQuery(message); } diff --git a/src/interfaces/records-read.ts b/src/interfaces/records-read.ts index 10374cff7..400617124 100644 --- a/src/interfaces/records-read.ts +++ b/src/interfaces/records-read.ts @@ -3,7 +3,6 @@ import type { RecordsWrite } from './records-write.js'; import type { Signer } from '../types/signer.js'; import type { RecordsFilter , RecordsReadDescriptor, RecordsReadMessage } from '../types/records-types.js'; -import { getCurrentTimeInHighPrecision } from '../utils/time.js'; import { Message } from '../core/message.js'; import { ProtocolAuthorization } from '../core/protocol-authorization.js'; import { Records } from '../utils/records.js'; @@ -11,6 +10,7 @@ import { RecordsGrantAuthorization } from '../core/records-grant-authorization.j import { removeUndefinedProperties } from '../utils/object.js'; import { validateMessageSignatureIntegrity } from '../core/auth.js'; import { DwnInterfaceName, DwnMethodName } from '../core/message.js'; +import { getCurrentTimeInHighPrecision, validateTimestamp } from '../utils/time.js'; export type RecordsReadOptions = { filter: RecordsFilter; @@ -30,6 +30,7 @@ export class RecordsRead extends Message { if (message.authorization !== undefined) { await validateMessageSignatureIntegrity(message.authorization.authorSignature, message.descriptor); } + validateTimestamp(message.descriptor.messageTimestamp); const recordsRead = new RecordsRead(message); return recordsRead; diff --git a/src/interfaces/records-write.ts b/src/interfaces/records-write.ts index 8e5dd7110..e61eb752f 100644 --- a/src/interfaces/records-write.ts +++ b/src/interfaces/records-write.ts @@ -18,7 +18,6 @@ import { Encoder } from '../utils/encoder.js'; import { Encryption } from '../utils/encryption.js'; import { EncryptionAlgorithm } from '../utils/encryption.js'; import { GeneralJwsBuilder } from '../jose/jws/general/builder.js'; -import { getCurrentTimeInHighPrecision } from '../utils/time.js'; import { Jws } from '../utils/jws.js'; import { KeyDerivationScheme } from '../utils/hd-key.js'; import { Message } from '../core/message.js'; @@ -29,6 +28,7 @@ import { Secp256k1 } from '../utils/secp256k1.js'; import { validateMessageSignatureIntegrity } from '../core/auth.js'; import { DwnError, DwnErrorCode } from '../core/dwn-error.js'; import { DwnInterfaceName, DwnMethodName } from '../core/message.js'; +import { getCurrentTimeInHighPrecision, validateTimestamp } from '../utils/time.js'; import { normalizeProtocolUrl, normalizeSchemaUrl, validateProtocolUrlNormalized, validateSchemaUrlNormalized } from '../utils/url.js'; export type RecordsWriteOptions = { @@ -541,6 +541,12 @@ export class RecordsWrite { if (this.message.descriptor.schema !== undefined) { validateSchemaUrlNormalized(this.message.descriptor.schema); } + + validateTimestamp(this.message.descriptor.messageTimestamp); + validateTimestamp(this.message.descriptor.dateCreated); + if (this.message.descriptor.datePublished){ + validateTimestamp(this.message.descriptor.datePublished); + } } /** diff --git a/src/utils/time.ts b/src/utils/time.ts index ae741be28..fab05d85a 100644 --- a/src/utils/time.ts +++ b/src/utils/time.ts @@ -1,4 +1,5 @@ import { Temporal } from '@js-temporal/polyfill'; +import { DwnError, DwnErrorCode } from '../index.js'; /** * sleeps for the desired duration @@ -24,3 +25,16 @@ export function getCurrentTimeInHighPrecision(): string { export async function minimalSleep(): Promise { await sleep(2); } + +/** + * Validates that the provided timestamp is a valid number + * @param timestamp the timestamp to validate + * @throws DwnError if timestamp is not a valid number + */ +export function validateTimestamp(timestamp: string): void { + try { + Temporal.Instant.from(timestamp); + } catch { + throw new DwnError(DwnErrorCode.TimestampInvalid,`Invalid timestamp: ${timestamp}`); + } +} diff --git a/tests/utils/time.spec.ts b/tests/utils/time.spec.ts new file mode 100644 index 000000000..b92d2170b --- /dev/null +++ b/tests/utils/time.spec.ts @@ -0,0 +1,29 @@ +import { DwnErrorCode } from '../../src/core/dwn-error.js'; +import { expect } from 'chai'; +import { TestDataGenerator } from '../utils/test-data-generator.js'; +import { validateTimestamp } from '../../src/utils/time.js'; + + +describe('time', () => { + describe('validateTimstamp', () => { + describe('invalid timestamps', () => { + const invalidTimstamps = [ + '2022-02-31T10:20:30.405060Z', // invalid day + '2022-01-36T90:20:30.405060Z', // invalid hour + '2022-01-36T25:99:30.405060Z', // invalid minute + '2022-14-18T10:30:00.123456Z', // invalid month + ]; + invalidTimstamps.forEach((timestamp) => { + it(`should throw an exception if an invalid timestamp is passed: ${timestamp}`, () => { + expect(() => validateTimestamp(timestamp)).to.throw(DwnErrorCode.TimestampInvalid); + }); + }); + }); + describe('valid timestamps', () => { + it('should pass if a valid timestamp is passed', () => { + expect(() => validateTimestamp('2022-04-29T10:30:00.123456Z')).to.not.throw(); + expect(() => validateTimestamp(TestDataGenerator.randomTimestamp())).to.not.throw(); + }); + }); + }); +}); \ No newline at end of file