Skip to content

Commit

Permalink
Added support for owner-delegated grant (#707)
Browse files Browse the repository at this point in the history
1. Added support for owner-delegated grant
2. Added missing checks for author-deleted grant:
   a. Disallowed invocation of non-delegated grant as delegated grant
   b. Disallowed mismatching of grant ID referenced and CID of actual given grant
3. Added numerous tests to catch up on missing scenario tests for delegated grants in general
  • Loading branch information
thehenrytsai committed Mar 18, 2024
1 parent 8134078 commit 9198817
Show file tree
Hide file tree
Showing 24 changed files with 1,682 additions and 271 deletions.
3 changes: 3 additions & 0 deletions json-schemas/authorization-owner.json
Expand Up @@ -12,6 +12,9 @@
},
"ownerSignature": {
"$ref": "https://identity.foundation/dwn/json-schemas/general-jws.json"
},
"ownerDelegatedGrant": {
"$ref": "https://identity.foundation/dwn/json-schemas/permissions-grant.json"
}
},
"description": "`signature` can exist by itself. But if `ownerSignature` is present, then `signature` must also exist",
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion src/core/auth.ts
Expand Up @@ -24,10 +24,16 @@ export async function authenticate(authorizationModel: AuthorizationModel | unde
}

if (authorizationModel.authorDelegatedGrant !== undefined) {
// verify the signature of the grantor of the delegated grant
// verify the signature of the grantor of the author-delegated grant
const authorDelegatedGrant = await PermissionsGrant.parse(authorizationModel.authorDelegatedGrant);
await GeneralJwsVerifier.verifySignatures(authorDelegatedGrant.message.authorization.signature, didResolver);
}

if (authorizationModel.ownerDelegatedGrant !== undefined) {
// verify the signature of the grantor of the owner-delegated grant
const ownerDelegatedGrant = await PermissionsGrant.parse(authorizationModel.ownerDelegatedGrant);
await GeneralJwsVerifier.verifySignatures(ownerDelegatedGrant.message.authorization.signature, didResolver);
}
}

/**
Expand Down
11 changes: 9 additions & 2 deletions src/core/dwn-error.ts
Expand Up @@ -90,6 +90,10 @@ export enum DwnErrorCode {
ProtocolsConfigureRoleDoesNotExistAtGivenPath = 'ProtocolsConfigureRoleDoesNotExistAtGivenPath',
ProtocolsConfigureUnauthorized = 'ProtocolsConfigureUnauthorized',
ProtocolsQueryUnauthorized = 'ProtocolsQueryUnauthorized',
RecordsAuthorDelegatedGrantAndIdExistenceMismatch = 'RecordsAuthorDelegatedGrantAndIdExistenceMismatch',
RecordsAuthorDelegatedGrantCidMismatch = 'RecordsAuthorDelegatedGrantCidMismatch',
RecordsAuthorDelegatedGrantGrantedToAndOwnerSignatureMismatch = 'RecordsAuthorDelegatedGrantGrantedToAndOwnerSignatureMismatch',
RecordsAuthorDelegatedGrantNotADelegatedGrant = 'RecordsAuthorDelegatedGrantNotADelegatedGrant',
RecordsDecryptNoMatchingKeyEncryptedFound = 'RecordsDecryptNoMatchingKeyEncryptedFound',
RecordsDeleteAuthorizationFailed = 'RecordsDeleteAuthorizationFailed',
RecordsQueryCreateFilterPublishedSortInvalid = 'RecordsQueryCreateFilterPublishedSortInvalid',
Expand All @@ -105,6 +109,10 @@ export enum DwnErrorCode {
RecordsGrantAuthorizationScopeSchema = 'RecordsGrantAuthorizationScopeSchema',
RecordsDerivePrivateKeyUnSupportedCurve = 'RecordsDerivePrivateKeyUnSupportedCurve',
RecordsInvalidAncestorKeyDerivationSegment = 'RecordsInvalidAncestorKeyDerivationSegment',
RecordsOwnerDelegatedGrantAndIdExistenceMismatch = 'RecordsOwnerDelegatedGrantAndIdExistenceMismatch',
RecordsOwnerDelegatedGrantCidMismatch = 'RecordsOwnerDelegatedGrantCidMismatch',
RecordsOwnerDelegatedGrantGrantedToAndOwnerSignatureMismatch = 'RecordsOwnerDelegatedGrantGrantedToAndOwnerSignatureMismatch',
RecordsOwnerDelegatedGrantNotADelegatedGrant = 'RecordsOwnerDelegatedGrantNotADelegatedGrant',
RecordsProtocolContextDerivationSchemeMissingContextId = 'RecordsProtocolContextDerivationSchemeMissingContextId',
RecordsProtocolPathDerivationSchemeMissingProtocol = 'RecordsProtocolPathDerivationSchemeMissingProtocol',
RecordsQueryFilterMissingRequiredProperties = 'RecordsQueryFilterMissingRequiredProperties',
Expand All @@ -113,8 +121,6 @@ export enum DwnErrorCode {
RecordsSubscribeEventStreamUnimplemented = 'RecordsSubscribeEventStreamUnimplemented',
RecordsSubscribeFilterMissingRequiredProperties = 'RecordsSubscribeFilterMissingRequiredProperties',
RecordsSchemasDerivationSchemeMissingSchema = 'RecordsSchemasDerivationSchemeMissingSchema',
RecordsValidateIntegrityDelegatedGrantAndIdExistenceMismatch = 'RecordsValidateIntegrityDelegatedGrantAndIdExistenceMismatch',
RecordsValidateIntegrityGrantedToAndSignerMismatch = 'RecordsValidateIntegrityGrantedToAndSignerMismatch',
RecordsWriteAttestationIntegrityMoreThanOneSignature = 'RecordsWriteAttestationIntegrityMoreThanOneSignature',
RecordsWriteAttestationIntegrityDescriptorCidMismatch = 'RecordsWriteAttestationIntegrityDescriptorCidMismatch',
RecordsWriteAttestationIntegrityInvalidPayloadProperty = 'RecordsWriteAttestationIntegrityInvalidPayloadProperty',
Expand All @@ -135,6 +141,7 @@ export enum DwnErrorCode {
RecordsWriteMissingProtocol = 'RecordsWriteMissingProtocol',
RecordsWriteMissingSchema = 'RecordsWriteMissingSchema',
RecordsWriteOwnerAndTenantMismatch = 'RecordsWriteOwnerAndTenantMismatch',
RecordsWriteSignAsOwnerDelegateUnknownAuthor = 'RecordsWriteSignAsOwnerDelegateUnknownAuthor',
RecordsWriteSignAsOwnerUnknownAuthor = 'RecordsWriteSignAsOwnerUnknownAuthor',
RecordsWriteValidateIntegrityAttestationMismatch = 'RecordsWriteValidateIntegrityAttestationMismatch',
RecordsWriteValidateIntegrityContextIdMismatch = 'RecordsWriteValidateIntegrityContextIdMismatch',
Expand Down
12 changes: 9 additions & 3 deletions src/core/message.ts
Expand Up @@ -172,12 +172,19 @@ export class Message {
}

/**
* See if the given message is signed by a delegate
* See if the given message is signed by an author-delegate.
*/
public static isSignedByDelegate(message: GenericMessage): boolean {
public static isSignedByAuthorDelegate(message: GenericMessage): boolean {
return message.authorization?.authorDelegatedGrant !== undefined;
}

/**
* See if the given message is signed by an owner-delegate.
*/
public static isSignedByOwnerDelegate(message: GenericMessage): boolean {
return message.authorization?.ownerDelegatedGrant !== undefined;
}

/**
* Compares the `messageTimestamp` of the given messages with a fallback to message CID according to the spec.
* @returns 1 if `a` is larger/newer than `b`; -1 if `a` is smaller/older than `b`; 0 otherwise (same age)
Expand All @@ -194,7 +201,6 @@ export class Message {
return Message.compareCid(a, b);
}


/**
* Validates the structural integrity of the message signature given:
* 1. The message signature must contain exactly 1 signature
Expand Down
2 changes: 1 addition & 1 deletion src/handlers/records-delete.ts
Expand Up @@ -118,7 +118,7 @@ export class RecordsDeleteHandler implements MethodHandler {
messageStore: MessageStore
): Promise<void> {

if (Message.isSignedByDelegate(recordsDelete.message)) {
if (Message.isSignedByAuthorDelegate(recordsDelete.message)) {
await recordsDelete.authorizeDelegate(recordsWriteToDelete.message, messageStore);
}

Expand Down
2 changes: 1 addition & 1 deletion src/handlers/records-query.ts
Expand Up @@ -247,7 +247,7 @@ export class RecordsQueryHandler implements MethodHandler {
messageStore: MessageStore
): Promise<void> {

if (Message.isSignedByDelegate(recordsQuery.message)) {
if (Message.isSignedByAuthorDelegate(recordsQuery.message)) {
await recordsQuery.authorizeDelegate(messageStore);
}

Expand Down
2 changes: 1 addition & 1 deletion src/handlers/records-read.ts
Expand Up @@ -117,7 +117,7 @@ export class RecordsReadHandler implements MethodHandler {
matchedRecordsWrite: RecordsWrite,
messageStore: MessageStore
): Promise<void> {
if (Message.isSignedByDelegate(recordsRead.message)) {
if (Message.isSignedByAuthorDelegate(recordsRead.message)) {
await recordsRead.authorizeDelegate(matchedRecordsWrite.message, messageStore);
}

Expand Down
2 changes: 1 addition & 1 deletion src/handlers/records-subscribe.ts
Expand Up @@ -203,7 +203,7 @@ export class RecordsSubscribeHandler implements MethodHandler {
messageStore: MessageStore
): Promise<void> {

if (Message.isSignedByDelegate(recordsSubscribe.message)) {
if (Message.isSignedByAuthorDelegate(recordsSubscribe.message)) {
await recordsSubscribe.authorizeDelegate(messageStore);
}

Expand Down
12 changes: 8 additions & 4 deletions src/handlers/records-write.ts
Expand Up @@ -43,7 +43,7 @@ export class RecordsWriteHandler implements MethodHandler {
try {
recordsWrite = await RecordsWrite.parse(message);

// Protocol record specific validation
// Protocol-authorized record specific validation
if (message.descriptor.protocol !== undefined) {
await ProtocolAuthorization.validateReferentialIntegrity(tenant, recordsWrite, this.messageStore);
}
Expand Down Expand Up @@ -280,16 +280,20 @@ export class RecordsWriteHandler implements MethodHandler {
}

private static async authorizeRecordsWrite(tenant: string, recordsWrite: RecordsWrite, messageStore: MessageStore): Promise<void> {
// if owner DID is specified, it must be the same as the tenant DID
// if owner signature is given (`owner` is not `undefined`), it must be the same as the tenant DID
if (recordsWrite.owner !== undefined && recordsWrite.owner !== tenant) {
throw new DwnError(
DwnErrorCode.RecordsWriteOwnerAndTenantMismatch,
`Owner ${recordsWrite.owner} must be the same as tenant ${tenant} when specified.`
);
}

if (recordsWrite.isSignedByDelegate) {
await recordsWrite.authorizeDelegate(messageStore);
if (recordsWrite.isSignedByAuthorDelegate) {
await recordsWrite.authorizeAuthorDelegate(messageStore);
}

if (recordsWrite.isSignedByOwnerDelegate) {
await recordsWrite.authorizeOwnerDelegate(messageStore);
}

if (recordsWrite.owner !== undefined) {
Expand Down
4 changes: 4 additions & 0 deletions src/interfaces/permissions-grant.ts
Expand Up @@ -14,6 +14,10 @@ import { normalizeProtocolUrl, normalizeSchemaUrl } from '../utils/url.js';

export type PermissionsGrantOptions = {
messageTimestamp?: string;

/**
* Expire time in UTC ISO-8601 format with microsecond precision.
*/
dateExpires: string;
description?: string;
grantedTo: string;
Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/records-delete.ts
Expand Up @@ -32,7 +32,7 @@ export class RecordsDelete extends AbstractMessage<RecordsDeleteMessage> {
signaturePayload = await Message.validateSignatureStructure(message.authorization.signature, message.descriptor);
}

Records.validateDelegatedGrantReferentialIntegrity(message, signaturePayload);
await Records.validateDelegatedGrantReferentialIntegrity(message, signaturePayload);

Time.validateTimestamp(message.descriptor.messageTimestamp);

Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/records-query.ts
Expand Up @@ -50,7 +50,7 @@ export class RecordsQuery extends AbstractMessage<RecordsQueryMessage> {
signaturePayload = await Message.validateSignatureStructure(message.authorization.signature, message.descriptor);
}

Records.validateDelegatedGrantReferentialIntegrity(message, signaturePayload);
await Records.validateDelegatedGrantReferentialIntegrity(message, signaturePayload);

if (signaturePayload?.protocolRole !== undefined) {
if (message.descriptor.filter.protocolPath === undefined) {
Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/records-read.ts
Expand Up @@ -36,7 +36,7 @@ export class RecordsRead extends AbstractMessage<RecordsReadMessage> {
signaturePayload = await Message.validateSignatureStructure(message.authorization.signature, message.descriptor);
}

Records.validateDelegatedGrantReferentialIntegrity(message, signaturePayload);
await Records.validateDelegatedGrantReferentialIntegrity(message, signaturePayload);

Time.validateTimestamp(message.descriptor.messageTimestamp);

Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/records-subscribe.ts
Expand Up @@ -36,7 +36,7 @@ export class RecordsSubscribe extends AbstractMessage<RecordsSubscribeMessage> {
signaturePayload = await Message.validateSignatureStructure(message.authorization.signature, message.descriptor);
}

Records.validateDelegatedGrantReferentialIntegrity(message, signaturePayload);
await Records.validateDelegatedGrantReferentialIntegrity(message, signaturePayload);

if (signaturePayload?.protocolRole !== undefined) {
if (message.descriptor.filter.protocolPath === undefined) {
Expand Down

0 comments on commit 9198817

Please sign in to comment.