Skip to content

Commit

Permalink
feat(ref-imp): #781 - make long form use jcs
Browse files Browse the repository at this point in the history
  • Loading branch information
isaacJChen committed Sep 21, 2020
1 parent 8c6cb55 commit 8eba485
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 28 deletions.
74 changes: 67 additions & 7 deletions lib/core/versions/latest/CreateOperation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Operation from './Operation';
import OperationModel from './models/OperationModel';
import OperationType from '../../enums/OperationType';
import SidetreeError from '../../../common/SidetreeError';
import JsonCanonicalizer from './util/JsonCanonicalizer';

interface SuffixDataModel {
deltaHash: string;
Expand Down Expand Up @@ -69,6 +70,17 @@ export default class CreateOperation implements OperationModel {
return encodedMultihash;
}

/**
* Computes the DID unique suffix given the encoded suffix data object.
* @param suffixData the suffix data object to calculate unique suffix from
*/
private static computeJcsDidUniqueSuffix (suffixData: object): string {
const suffixDataBuffer = JsonCanonicalizer.canonicalizeAsBuffer(suffixData);
const multihash = Multihash.hash(suffixDataBuffer);
const encodedMultihash = Encoder.encode(multihash);
return encodedMultihash;
}

/**
* Parses the given input as a create operation entry in the anchor file.
*/
Expand All @@ -85,10 +97,51 @@ export default class CreateOperation implements OperationModel {
public static async parse (operationBuffer: Buffer): Promise<CreateOperation> {
const operationJsonString = operationBuffer.toString();
const operationObject = await JsonAsync.parse(operationJsonString);
const createOperation = await CreateOperation.parseObject(operationObject, operationBuffer, false);
let createOperation;
if (typeof operationObject.suffix_data === 'string') {
createOperation = await CreateOperation.parseObject(operationObject, operationBuffer, false);
} else {
createOperation = CreateOperation.parseJcsObject(operationObject, operationBuffer);
}
return createOperation;
}

/**
* Parse the given operation object as a CreateOperation
* @param operationObject The operationObject is a json object with no encoding
* @param operationBuffer The buffer format of the operationObject
*/
public static parseJcsObject (operationObject: any, operationBuffer: Buffer): CreateOperation {
let expectedPropertyCount = 3;
const properties = Object.keys(operationObject);
if (properties.length !== expectedPropertyCount) {
throw new SidetreeError(ErrorCode.CreateOperationMissingOrUnknownProperty);
}

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

CreateOperation.validateSuffixData(operationObject.suffix_data);

// For compatibility with data pruning, we have to assume that `delta` may be unavailable,
// thus an operation with invalid `delta` needs to be processed as an operation with unavailable `delta`,
// so here we let `delta` be `undefined`.
let delta;
try {
Operation.validateDelta(operationObject.delta);
delta = operationObject.delta;
} catch {
delta = undefined;
}

const didUniqueSuffix = CreateOperation.computeJcsDidUniqueSuffix(operationObject.suffix_data);

const encodedSuffixData = Encoder.encode(JsonCanonicalizer.canonicalizeAsBuffer(operationObject.suffix_data));
const encodedDelta = Encoder.encode(JsonCanonicalizer.canonicalizeAsBuffer(operationObject.delta));
return new CreateOperation(operationBuffer, didUniqueSuffix, encodedSuffixData, operationObject.suffix_data, encodedDelta, delta);
}

/**
* Parses the given operation object as a `CreateOperation`.
* The `operationBuffer` given is assumed to be valid and is assigned to the `operationBuffer` directly.
Expand Down Expand Up @@ -132,14 +185,11 @@ export default class CreateOperation implements OperationModel {
return new CreateOperation(operationBuffer, didUniqueSuffix, encodedSuffixData, suffixData, encodedDelta, delta);
}

private static async parseSuffixData (suffixDataEncodedString: any): Promise<SuffixDataModel> {
if (typeof suffixDataEncodedString !== 'string') {
throw new SidetreeError(ErrorCode.CreateOperationSuffixDataMissingOrNotString);
private static validateSuffixData (suffixData: any): void {
if (typeof suffixData !== 'object') {
throw new SidetreeError(ErrorCode.CreateOperationSuffixDataIsNotObject)
}

const suffixDataJsonString = Encoder.decodeAsString(suffixDataEncodedString);
const suffixData = await JsonAsync.parse(suffixDataJsonString);

const properties = Object.keys(suffixData);
// will have 3 if has type
if (properties.length !== 3 && properties.length !== 2) {
Expand All @@ -166,6 +216,16 @@ export default class CreateOperation implements OperationModel {
throw new SidetreeError(ErrorCode.CreateOperationSuffixDataTypeInvalidCharacter);
}
}
}

private static async parseSuffixData (suffixDataEncodedString: any): Promise<SuffixDataModel> {
if (typeof suffixDataEncodedString !== 'string') {
throw new SidetreeError(ErrorCode.CreateOperationSuffixDataMissingOrNotString);
}

const suffixDataJsonString = Encoder.decodeAsString(suffixDataEncodedString);
const suffixData = await JsonAsync.parse(suffixDataJsonString);
CreateOperation.validateSuffixData(suffixData);

return {
deltaHash: suffixData.delta_hash,
Expand Down
44 changes: 33 additions & 11 deletions lib/core/versions/latest/Did.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Multihash from './Multihash';
import OperationType from '../../enums/OperationType';
import SidetreeError from '../../../common/SidetreeError';
import { URL } from 'url';
import Encoder from './Encoder';

/**
* Class containing reusable Sidetree DID related operations.
Expand Down Expand Up @@ -40,9 +41,10 @@ export default class Did {
throw new SidetreeError(ErrorCode.DidIncorrectPrefix);
}

const indexOfDotChar = did.indexOf('.');
// If there is no 'dot', then DID can only be in short-form.
if (indexOfDotChar < 0) {
// split by : and ?, if there are 3 elements, then it's short form. Long form has 4 elements
// when the ? format is deprecated, `:` will be the only seperator.
const didSplitLength = did.split(/:|\?/).length;
if (didSplitLength === 3) {
this.isShortForm = true;
} else {
this.isShortForm = false;
Expand All @@ -53,7 +55,7 @@ export default class Did {
} else {
// Long-form can be in the form of:
// 'did:<methodName>:<unique-portion>?-<methodName>-initial-state=<create-operation-suffix-data>.<create-operation-delta>' or
// 'did:<methodName>:<unique-portion>:<create-operation-suffix-data>.<create-operation-delta>'
// 'did:<methodName>:<unique-portion>:Base64url(JCS({suffix-data, delta}))'

const indexOfQuestionMarkChar = did.indexOf('?');
if (indexOfQuestionMarkChar > 0) {
Expand Down Expand Up @@ -83,18 +85,18 @@ export default class Did {
if (!did.isShortForm) {
// Long-form can be in the form of:
// 'did:<methodName>:<unique-portion>?-<methodName>-initial-state=<create-operation-suffix-data>.<create-operation-delta>' or
// 'did:<methodName>:<unique-portion>:<create-operation-suffix-data>.<create-operation-delta>'
// 'did:<methodName>:<unique-portion>:Base64url(JCS({suffix-data, delta}))'

const indexOfQuestionMarkChar = didString.indexOf('?');
let initialState;
let createOperation;
if (indexOfQuestionMarkChar > 0) {
initialState = Did.getInitialStateFromDidStringWithQueryParameter(didString, didMethodName);
const initialState = Did.getInitialStateFromDidStringWithQueryParameter(didString, didMethodName);
createOperation = await Did.constructCreateOperationFromInitialState(initialState);
} else {
initialState = Did.getInitialStateFromDidStringWithExtraColon(didString);
const initialStateEncodedJcs = Did.getInitialStateFromDidStringWithExtraColon(didString);
createOperation = Did.constructCreateOperationFromEncodedJCS(initialStateEncodedJcs);
}

const createOperation = await Did.constructCreateOperationFromInitialState(initialState);

// NOTE: we cannot use the unique suffix directly from `createOperation.didUniqueSuffix` for comparison,
// because a given long-form DID may have been created long ago,
// thus this version of `CreateOperation.parse()` maybe using a different hashing algorithm than that of the unique DID suffix (short-form).
Expand Down Expand Up @@ -152,7 +154,7 @@ export default class Did {
}

private static getInitialStateFromDidStringWithExtraColon (didString: string): string {
// DID example: 'did:<methodName>:<unique-portion>:<create-operation-suffix-data>.<create-operation-delta>'
// DID example: 'did:<methodName>:<unique-portion>:Base64url(JCS({suffix-data, delta}))'

const lastColonIndex = didString.lastIndexOf(':');

Expand All @@ -161,6 +163,26 @@ export default class Did {
return initialStateValue;
}

private static constructCreateOperationFromEncodedJCS (initialStateEncodedJcs: string): CreateOperation {
// Initial state should be in the format base64url(JCS(initialState))
const initialStateDecodedJcs = Encoder.decodeAsString(initialStateEncodedJcs);
let initialStateObject;
try {
initialStateObject = JSON.parse(initialStateDecodedJcs)
} catch {
throw new SidetreeError(ErrorCode.DidInitialStateJcsIsNotJosn, 'long form initial state should be encoded jcs');
}

const createOperationRequest = {
type: OperationType.Create,
suffix_data: initialStateObject.suffix_data,
delta: initialStateObject.delta
};
const createOperationBuffer = Buffer.from(JSON.stringify(createOperationRequest));
const createOperation = CreateOperation.parseJcsObject(createOperationRequest, createOperationBuffer);
return createOperation;
}

private static async constructCreateOperationFromInitialState (initialState: string): Promise<CreateOperation> {
// Initial state should be in the format: <suffix-data>.<delta>
const firstIndexOfDot = initialState.indexOf('.');
Expand Down
3 changes: 3 additions & 0 deletions lib/core/versions/latest/ErrorCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default {
ChunkFileUnexpectedProperty: 'chunk_file_unexpected_property',
CompressorMaxAllowedDecompressedDataSizeExceeded: 'compressor_max_allowed_decompressed_data_size_exceeded',
CreateOperationMissingOrUnknownProperty: 'create_operation_missing_or_unknown_property',
CreateOperationSuffixDataIsNotObject: 'create_operation_suffix_data_is_not_object',
CreateOperationSuffixDataMissingOrNotString: 'create_operation_suffix_data_missing_or_not_string',
CreateOperationSuffixDataMissingOrUnknownProperty: 'create_operation_suffix_data_missing_or_unknown_property',
CreateOperationSuffixDataTypeInvalidCharacter: 'create_operation_suffix_data_type_invalid_character',
Expand All @@ -42,10 +43,12 @@ export default {
DeactivateOperationSignedDataMissingOrUnknownProperty: 'deactivate_operation_signed_data_missing_or_unknown_property',
DeactivateOperationSignedDidUniqueSuffixMismatch: 'deactivate_operation_signed_did_unique_suffix_mismatch',
DeactivateOperationTypeIncorrect: 'deactivate_operation_type_incorrect',
DeltaIsNotObject: 'delta_is_not_object',
DeltaMissingOrNotString: 'delta_missing_or_not_string',
DeltaExceedsMaximumSize: 'delta_exceeds_maximum_size',
DeltaMissingOrUnknownProperty: 'delta_missing_or_unknown_property',
DidIncorrectPrefix: 'did_incorrect_prefix',
DidInitialStateJcsIsNotJosn: 'did_initial_state_jcs_is_not_json',
DidInitialStateValueContainsMoreThanOneDot: 'did_initial_state_value_contains_more_than_one_dot',
DidInitialStateValueContainsNoDot: 'did_initial_state_value_contains_no_dot',
DidInitialStateValueDoesNotContainTwoParts: 'did_initial_state_value_does_not_contain_two_parts',
Expand Down
26 changes: 17 additions & 9 deletions lib/core/versions/latest/Operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,11 @@ export default class Operation {
}
}

/**
* Parses the given encoded delta string into an internal `DeltaModel`.
*/
public static async parseDelta (deltaEncodedString: any): Promise<DeltaModel> {
if (typeof deltaEncodedString !== 'string') {
throw new SidetreeError(ErrorCode.DeltaMissingOrNotString);
public static validateDelta (delta: any): void {
if (typeof delta !== 'object') {
throw new SidetreeError(ErrorCode.DeltaIsNotObject);
}

const deltaJsonString = Encoder.decodeAsString(deltaEncodedString);
const delta = await JsonAsync.parse(deltaJsonString);

const properties = Object.keys(delta);
if (properties.length !== 2) {
throw new SidetreeError(ErrorCode.DeltaMissingOrUnknownProperty);
Expand All @@ -66,6 +60,20 @@ export default class Operation {
DocumentComposer.validateDocumentPatches(delta.patches);
const nextUpdateCommitment = Encoder.decodeAsBuffer(delta.update_commitment);
Multihash.verifyHashComputedUsingLatestSupportedAlgorithm(nextUpdateCommitment);
}

/**
* Parses the given encoded delta string into an internal `DeltaModel`.
*/
public static async parseDelta (deltaEncodedString: any): Promise<DeltaModel> {
if (typeof deltaEncodedString !== 'string') {
throw new SidetreeError(ErrorCode.DeltaMissingOrNotString);
}

const deltaJsonString = Encoder.decodeAsString(deltaEncodedString);
const delta = await JsonAsync.parse(deltaJsonString);

Operation.validateDelta(delta);

return {
patches: delta.patches,
Expand Down
2 changes: 1 addition & 1 deletion lib/core/versions/latest/RequestHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export default class RequestHandler implements IRequestHandler {
* @param shortOrLongFormDid Can either be:
* 1. A short-form DID. e.g. 'did:<methodName>:abc' or
* 2. A long-form DID. e.g. 'did:<methodName>:<unique-portion>?-<methodName>-initial-state=<create-operation-suffix-data>.<create-operation-delta>' or
* 'did:<methodName>:<unique-portion>:<create-operation-suffix-data>.<create-operation-delta>'
* 'did:<methodName>:<unique-portion>:Base64url(JCS({suffix-data, delta}))'
*/
public async handleResolveRequest (shortOrLongFormDid: string): Promise<ResponseModel> {
try {
Expand Down

0 comments on commit 8eba485

Please sign in to comment.