Skip to content

Commit

Permalink
feat(ref-imp): #766 - updated API, resolver, batch writer to support …
Browse files Browse the repository at this point in the history
…revealValue in requests
  • Loading branch information
thehenrytsai committed Dec 4, 2020
1 parent 13ae338 commit 5025c70
Show file tree
Hide file tree
Showing 11 changed files with 142 additions and 114 deletions.
8 changes: 2 additions & 6 deletions lib/core/versions/latest/CoreIndexFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import Did from './Did';
import ErrorCode from './ErrorCode';
import InputValidator from './InputValidator';
import JsonAsync from './util/JsonAsync';
import Multihash from './Multihash';
import OperationReferenceModel from './models/OperationReferenceModel';
import ProtocolParameters from './ProtocolParameters';
import RecoverOperation from './RecoverOperation';
Expand Down Expand Up @@ -214,9 +213,7 @@ export default class CoreIndexFile {
}

const recoverReferences = recoverOperationArray.map(operation => {
// TODO:
const revealValue = Multihash.canonicalizeThenHashThenEncode(operation.signedData.recoveryKey);

const revealValue = operation.revealValue;
return { didSuffix: operation.didUniqueSuffix, revealValue };
});

Expand All @@ -226,8 +223,7 @@ export default class CoreIndexFile {
}

const deactivateReferences = deactivateOperationArray.map(operation => {
const revealValue = Multihash.canonicalizeThenHashThenEncode(operation.signedData.recoveryKey);

const revealValue = operation.revealValue;
return { didSuffix: operation.didUniqueSuffix, revealValue };
});

Expand Down
54 changes: 22 additions & 32 deletions lib/core/versions/latest/DeactivateOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import InputValidator from './InputValidator';
import JsonAsync from './util/JsonAsync';
import Jwk from './util/Jwk';
import Jws from './util/Jws';
import Multihash from './Multihash';
import OperationModel from './models/OperationModel';
import OperationType from '../../enums/OperationType';
import SidetreeError from '../../../common/SidetreeError';
Expand All @@ -13,37 +14,19 @@ import SignedDataModel from './models/DeactivateSignedDataModel';
* A class that represents a deactivate operation.
*/
export default class DeactivateOperation implements OperationModel {

/** The original request buffer sent by the requester. */
public readonly operationBuffer: Buffer;

/** The unique suffix of the DID. */
public readonly didUniqueSuffix: string;

/** The type of operation. */
public readonly type: OperationType;

/** Signed data. */
public readonly signedDataJws: Jws;

/** Decoded signed data payload. */
public readonly signedData: SignedDataModel;
public readonly type: OperationType = OperationType.Deactivate;

/**
* NOTE: should only be used by `parse()` and `parseObject()` else the constructed instance could be invalid.
*/
private constructor (
operationBuffer: Buffer,
didUniqueSuffix: string,
signedDataJws: Jws,
signedData: SignedDataModel
) {
this.operationBuffer = operationBuffer;
this.type = OperationType.Deactivate;
this.didUniqueSuffix = didUniqueSuffix;
this.signedDataJws = signedDataJws;
this.signedData = signedData;
}
public readonly operationBuffer: Buffer,
public readonly didUniqueSuffix: string,
public readonly revealValue: string,
public readonly signedDataJws: Jws,
public readonly signedData: SignedDataModel
) { }

/**
* Parses the given buffer as a `UpdateOperation`.
Expand All @@ -62,27 +45,34 @@ export default class DeactivateOperation implements OperationModel {
* JSON parsing is not required to be performed more than once when an operation buffer of an unknown operation type is given.
*/
public static async parseObject (operationObject: any, operationBuffer: Buffer): Promise<DeactivateOperation> {
InputValidator.validateObjectContainsOnlyAllowedProperties(operationObject, ['type', 'didSuffix', 'revealValue', 'signedData'], 'deactivate request');
const errorLoggingContext = 'deactivate request';
InputValidator.validateObjectContainsOnlyAllowedProperties(
operationObject, ['type', 'didSuffix', 'revealValue', 'signedData'], errorLoggingContext
);

if (operationObject.type !== OperationType.Deactivate) {
throw new SidetreeError(ErrorCode.DeactivateOperationTypeIncorrect);
}

if (typeof operationObject.didSuffix !== 'string') {
throw new SidetreeError(ErrorCode.DeactivateOperationMissingOrInvalidDidUniqueSuffix);
}

InputValidator.validateRevealValue(operationObject.revealValue, 'deactivate request');
InputValidator.validateRevealValue(operationObject.revealValue, errorLoggingContext);

const signedDataJws = Jws.parseCompactJws(operationObject.signedData);
const signedData = await DeactivateOperation.parseSignedDataPayload(
const signedDataModel = await DeactivateOperation.parseSignedDataPayload(
signedDataJws.payload, operationObject.didSuffix);

if (operationObject.type !== OperationType.Deactivate) {
throw new SidetreeError(ErrorCode.DeactivateOperationTypeIncorrect);
}
// Validate that the canonicalized recovery public key hash is the same as `revealValue`.
Multihash.validateCanonicalizeObjectHash(signedDataModel.recoveryKey, operationObject.revealValue, errorLoggingContext);

return new DeactivateOperation(
operationBuffer,
operationObject.didSuffix,
operationObject.revealValue,
signedDataJws,
signedData
signedDataModel
);
}

Expand Down
3 changes: 2 additions & 1 deletion lib/core/versions/latest/ErrorCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ export default {
AnchoredDataNumberOfOperationsGreaterThanMax: 'anchored_data_number_of_operations_greater_than_max',
AnchoredDataNumberOfOperationsNotPositiveInteger: 'anchored_data_number_of_operations_not_positive_integer',
BatchWriterAlreadyHasOperationForDid: 'batch_writer_already_has_operation_for_did',
CasFileUriNotValid: 'cas_file_hash_not_valid',
CanonicalizedObjectHashMismatch: 'canonicalized_object_hash_mismatch',
CasFileNotAFile: 'cas_file_not_a_file',
CasFileNotFound: 'cas_file_not_found',
CasFileTooLarge: 'cas_file_too_large',
CasFileUriNotValid: 'cas_file_uri_not_valid',
CasNotReachable: 'cas_not_reachable',
ChunkFileDeltaCountIncorrect: 'chunk_file_delta_count_incorrect',
ChunkFileDeltasNotArrayOfObjects: 'chunk_file_deltas_not_array_of_objects',
Expand Down
16 changes: 16 additions & 0 deletions lib/core/versions/latest/Multihash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,22 @@ export default class Multihash {
}
}

/**
* Canonicalizes the given content object, then validates the multihash of the canonicalized UTF8 object buffer against the expected multihash.
* @param inputContextForErrorLogging This string is used for error logging purposes only. e.g. 'document', or 'suffix data'.
*/
public static validateCanonicalizeObjectHash (content: object, expectedEncodedMultihash: string, inputContextForErrorLogging: string) {
const contentBuffer = JsonCanonicalizer.canonicalizeAsBuffer(content);
const validHash = Multihash.verifyEncodedMultihashForContent(contentBuffer, expectedEncodedMultihash);

if (!validHash) {
throw new SidetreeError(
ErrorCode.CanonicalizedObjectHashMismatch,
`Canonicalized ${inputContextForErrorLogging} object hash does not match expected hash '${expectedEncodedMultihash}'.`
);
}
}

/**
* Canonicalizes the given content object, then verifies the multihash as a "double hash"
* (ie. the given multihash is the hash of a hash) against the canonicalized string as a UTF8 buffer.
Expand Down
3 changes: 1 addition & 2 deletions lib/core/versions/latest/ProvisionalIndexFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,7 @@ export default class ProvisionalIndexFile {
chunkFileUri: string, provisionalProofFileUri: string | undefined, updateOperationArray: UpdateOperation[]
): Promise<Buffer> {
const updateReferences = updateOperationArray.map(operation => {
const revealValue = Multihash.canonicalizeThenHashThenEncode(operation.signedData.updateKey);

const revealValue = operation.revealValue;
return { didSuffix: operation.didUniqueSuffix, revealValue };
});

Expand Down
59 changes: 22 additions & 37 deletions lib/core/versions/latest/RecoverOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,42 +16,20 @@ import SignedDataModel from './models/RecoverSignedDataModel';
* A class that represents a recover operation.
*/
export default class RecoverOperation implements OperationModel {

/** The original request buffer sent by the requester. */
public readonly operationBuffer: Buffer;

/** The unique suffix of the DID. */
public readonly didUniqueSuffix: string;

/** The type of operation. */
public readonly type: OperationType;

/** Signed data. */
public readonly signedDataJws: Jws;

/** Decoded signed data payload. */
public readonly signedData: SignedDataModel;

/** Patch data. */
public readonly delta: DeltaModel | undefined;
public readonly type: OperationType = OperationType.Recover;

/**
* NOTE: should only be used by `parse()` and `parseObject()` else the constructed instance could be invalid.
*/
private constructor (
operationBuffer: Buffer,
didUniqueSuffix: string,
signedDataJws: Jws,
signedData: SignedDataModel,
delta: DeltaModel | undefined
) {
this.operationBuffer = operationBuffer;
this.type = OperationType.Recover;
this.didUniqueSuffix = didUniqueSuffix;
this.signedDataJws = signedDataJws;
this.signedData = signedData;
this.delta = delta;
}
public readonly operationBuffer: Buffer,
public readonly didUniqueSuffix: string,
public readonly revealValue: string,
public readonly signedDataJws: Jws,
public readonly signedData: SignedDataModel,
public readonly delta: DeltaModel | undefined
) { }

/**
* Parses the given buffer as a `RecoverOperation`.
Expand All @@ -70,20 +48,26 @@ export default class RecoverOperation implements OperationModel {
* JSON parsing is not required to be performed more than once when an operation buffer of an unknown operation type is given.
*/
public static async parseObject (operationObject: any, operationBuffer: Buffer): Promise<RecoverOperation> {
InputValidator.validateObjectContainsOnlyAllowedProperties(operationObject, ['type', 'didSuffix', 'revealValue', 'signedData', 'delta'], 'recover request');
const errorLoggingContext = 'recover request';
InputValidator.validateObjectContainsOnlyAllowedProperties(
operationObject, ['type', 'didSuffix', 'revealValue', 'signedData', 'delta'], errorLoggingContext
);

if (operationObject.type !== OperationType.Recover) {
throw new SidetreeError(ErrorCode.RecoverOperationTypeIncorrect);
}

if (typeof operationObject.didSuffix !== 'string') {
throw new SidetreeError(ErrorCode.RecoverOperationMissingOrInvalidDidUniqueSuffix);
}

InputValidator.validateRevealValue(operationObject.revealValue, 'recover request');
InputValidator.validateRevealValue(operationObject.revealValue, errorLoggingContext);

const signedDataJws = Jws.parseCompactJws(operationObject.signedData);
const signedData = await RecoverOperation.parseSignedDataPayload(signedDataJws.payload);
const signedDataModel = await RecoverOperation.parseSignedDataPayload(signedDataJws.payload);

if (operationObject.type !== OperationType.Recover) {
throw new SidetreeError(ErrorCode.RecoverOperationTypeIncorrect);
}
// Validate that the canonicalized recovery public key hash is the same as `revealValue`.
Multihash.validateCanonicalizeObjectHash(signedDataModel.recoveryKey, operationObject.revealValue, errorLoggingContext);

let delta;
try {
Expand All @@ -98,8 +82,9 @@ export default class RecoverOperation implements OperationModel {
return new RecoverOperation(
operationBuffer,
operationObject.didSuffix,
operationObject.revealValue,
signedDataJws,
signedData,
signedDataModel,
delta
);
}
Expand Down
55 changes: 20 additions & 35 deletions lib/core/versions/latest/UpdateOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,41 +16,20 @@ import SignedDataModel from './models/UpdateSignedDataModel';
* A class that represents an update operation.
*/
export default class UpdateOperation implements OperationModel {

/** The original request buffer sent by the requester. */
public readonly operationBuffer: Buffer;

/** The unique suffix of the DID. */
public readonly didUniqueSuffix: string;

/** The type of operation. */
public readonly type: OperationType;

/** Signed data for the operation. */
public readonly signedDataJws: Jws;

/** Decoded signed data payload. */
public readonly signedData: SignedDataModel;

/** Patch data. */
public readonly delta: DeltaModel | undefined;
public readonly type: OperationType = OperationType.Update;

/**
* NOTE: should only be used by `parse()` and `parseObject()` else the constructed instance could be invalid.
*/
private constructor (
operationBuffer: Buffer,
didUniqueSuffix: string,
signedDataJws: Jws,
signedData: SignedDataModel,
delta: DeltaModel | undefined) {
this.operationBuffer = operationBuffer;
this.type = OperationType.Update;
this.didUniqueSuffix = didUniqueSuffix;
this.signedDataJws = signedDataJws;
this.signedData = signedData;
this.delta = delta;
}
public readonly operationBuffer: Buffer,
public readonly didUniqueSuffix: string,
public readonly revealValue: string,
public readonly signedDataJws: Jws,
public readonly signedData: SignedDataModel,
public readonly delta: DeltaModel | undefined
) { }

/**
* Parses the given buffer as a `UpdateOperation`.
Expand All @@ -69,24 +48,30 @@ export default class UpdateOperation implements OperationModel {
* JSON parsing is not required to be performed more than once when an operation buffer of an unknown operation type is given.
*/
public static async parseObject (operationObject: any, operationBuffer: Buffer): Promise<UpdateOperation> {
InputValidator.validateObjectContainsOnlyAllowedProperties(operationObject, ['type', 'didSuffix', 'revealValue', 'signedData', 'delta'], 'update request');
const errorLoggingContext = 'update request';
InputValidator.validateObjectContainsOnlyAllowedProperties(
operationObject, ['type', 'didSuffix', 'revealValue', 'signedData', 'delta'], errorLoggingContext
);

if (operationObject.type !== OperationType.Update) {
throw new SidetreeError(ErrorCode.UpdateOperationTypeIncorrect);
}

if (typeof operationObject.didSuffix !== 'string') {
throw new SidetreeError(ErrorCode.UpdateOperationMissingDidUniqueSuffix);
}

InputValidator.validateRevealValue(operationObject.revealValue, 'update request');
InputValidator.validateRevealValue(operationObject.revealValue, errorLoggingContext);

const signedData = Jws.parseCompactJws(operationObject.signedData);
const signedDataModel = await UpdateOperation.parseSignedDataPayload(signedData.payload);

if (operationObject.type !== OperationType.Update) {
throw new SidetreeError(ErrorCode.UpdateOperationTypeIncorrect);
}
// Validate that the canonicalized update key hash is the same as `revealValue`.
Multihash.validateCanonicalizeObjectHash(signedDataModel.updateKey, operationObject.revealValue, errorLoggingContext);

Operation.validateDelta(operationObject.delta);

return new UpdateOperation(operationBuffer, operationObject.didSuffix, signedData, signedDataModel, operationObject.delta);
return new UpdateOperation(operationBuffer, operationObject.didSuffix, operationObject.revealValue, signedData, signedDataModel, operationObject.delta);
}

/**
Expand Down
17 changes: 17 additions & 0 deletions tests/core/DeactivateOperation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,23 @@ describe('DeactivateOperation', async () => {
});
});

describe('parseObject()', async () => {
it('should throw if hash of `recoveryKey` does not match the revealValue.', async () => {
const didSuffix = OperationGenerator.generateRandomHash();
const [, recoveryPrivateKey] = await Jwk.generateEs256kKeyPair();
const deactivateRequest = await OperationGenerator.createDeactivateOperationRequest(didSuffix, recoveryPrivateKey);

// Intentionally have a mismatching reveal value.
deactivateRequest.revealValue = OperationGenerator.generateRandomHash();

await JasmineSidetreeErrorValidator.expectSidetreeErrorToBeThrownAsync(
() => DeactivateOperation.parseObject(deactivateRequest, Buffer.from('unused')),
ErrorCode.CanonicalizedObjectHashMismatch,
'deactivate request'
);
});
});

describe('parseSignedDataPayload()', async () => {
it('should throw if signedData contains an additional unknown property.', async (done) => {
const didUniqueSuffix = 'anyUnusedDidUniqueSuffix';
Expand Down
11 changes: 10 additions & 1 deletion tests/core/OperationProcessor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,16 @@ describe('OperationProcessor', async () => {
);
const recoverOperation = await RecoverOperation.parse(Buffer.from(JSON.stringify(recoverOperationRequest)));
const anchoredRecoverOperationModel = OperationGenerator.createAnchoredOperationModelFromOperationModel(recoverOperation, 2, 2, 2);
verifyEncodedMultihashForContentSpy.and.returnValue(false);

verifyEncodedMultihashForContentSpy.and.callFake((_content, expectedHash) => {
if (expectedHash === recoverOperation.signedData.deltaHash) {
// Intentionally failing recovery delta operation hash check.
return false;
} else {
return true;
}
});

const newDidState = await operationProcessor.apply(anchoredRecoverOperationModel, didState);
expect(newDidState!.lastOperationTransactionNumber).toEqual(2);
expect(newDidState!.document).toEqual({ });
Expand Down
Loading

0 comments on commit 5025c70

Please sign in to comment.