Skip to content

Commit

Permalink
add update, recover and deactivate to sdk (#14)
Browse files Browse the repository at this point in the history
* Added 1st iteration of an SDK for creating update requests.

* update and recovery done

* commit deactivate

* add model types

* refactor

* add test

* use enum for replace

* address pr comments

* address pr comment

Co-authored-by: Henry Tsai <[email protected]>
  • Loading branch information
isaacJChen and thehenrytsai authored Apr 22, 2021
1 parent d5cf6fa commit 06469e0
Show file tree
Hide file tree
Showing 14 changed files with 530 additions and 22 deletions.
3 changes: 3 additions & 0 deletions lib/ErrorCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ export default {
DidDocumentPublicKeyIdDuplicated: 'DidDocumentPublicKeyIdDuplicated',
DidDocumentPublicKeyMissingOrIncorrectType: 'DidDocumentPublicKeyMissingOrIncorrectType',
DidDocumentServiceIdDuplicated: 'DidDocumentServiceIdDuplicated',
DidSuffixIncorrectEncoding: 'DidSuffixIncorrectEncoding',
DidSuffixIncorrectLength: 'DidSuffixIncorrectLength',
IdNotUsingBase64UrlCharacterSet: 'IdNotUsingBase64UrlCharacterSet',
IdTooLong: 'IdTooLong',
JwkEs256kMissingOrInvalidCrv: 'JwkEs256kMissingOrInvalidCrv',
JwkEs256kMissingOrInvalidKty: 'JwkEs256kMissingOrInvalidKty',
JwkEs256kHasIncorrectLengthOfX: 'JwkEs256kHasIncorrectLengthOfX',
JwkEs256kHasIncorrectLengthOfY: 'JwkEs256kHasIncorrectLengthOfY',
JwkEs256kHasIncorrectLengthOfD: 'JwkEs256kHasIncorrectLengthOfD',
MultihashUnsupportedHashAlgorithm: 'MultihashUnsupportedHashAlgorithm',
PublicKeyJwkEs256kHasUnexpectedProperty: 'PublicKeyJwkEs256kHasUnexpectedProperty',
PublicKeyPurposeDuplicated: 'PublicKeyPurposeDuplicated',
Expand Down
291 changes: 269 additions & 22 deletions lib/IonRequest.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import * as URI from 'uri-js';
import Encoder from './Encoder';
import ErrorCode from './ErrorCode';
import InputValidator from './InputValidator';
import IonCreateRequestModel from './models/IonCreateRequestModel';
import IonDeactivateRequestModel from './models/IonDeactivateRequestModel';
import IonDocumentModel from './models/IonDocumentModel';
import IonError from './IonError';
import IonPublicKeyModel from './models/IonPublicKeyModel';
import IonRecoverRequestModel from './models/IonRecoverRequestModel';
import IonSdkConfig from './IonSdkConfig';
import IonServiceModel from './models/IonServiceModel';
import IonUpdateRequestModel from './models/IonUpdateRequestModel';
import JsonCanonicalizer from './JsonCanonicalizer';
import JwkEs256k from './models/JwkEs256k';
import Multihash from './Multihash';
import OperationKeyType from './enums/OperationKeyType';
import OperationType from './enums/OperationType';
import PatchAction from './enums/PatchAction';
const secp256k1 = require('@transmute/did-key-secp256k1');

/**
* Class containing operations related to ION requests.
Expand All @@ -31,28 +38,19 @@ export default class IonRequest {
const services = input.document.services;

// Validate recovery and update public keys.
IonRequest.validateEs256kOperationPublicKey(recoveryKey);
IonRequest.validateEs256kOperationPublicKey(updateKey);
IonRequest.validateEs256kOperationKey(recoveryKey, OperationKeyType.Public);
IonRequest.validateEs256kOperationKey(updateKey, OperationKeyType.Public);

// Validate all given DID Document keys.
IonRequest.validateDidDocumentKeys(didDocumentKeys);

// Validate all given service.
if (services !== undefined) {
const serviceIdSet: Set<string> = new Set();
for (const service of services) {
IonRequest.validateService(service);
if (serviceIdSet.has(service.id)) {
throw new IonError(ErrorCode.DidDocumentServiceIdDuplicated, 'Service id has to be unique');
}
serviceIdSet.add(service.id);
}
}
IonRequest.validateServices(services);

const hashAlgorithmInMultihashCode = IonSdkConfig.hashAlgorithmInMultihashCode;

const patches = [{
action: 'replace',
action: PatchAction.Replace,
document: input.document
}];

Expand All @@ -79,33 +77,260 @@ export default class IonRequest {
return operationRequest;
}

public static async createDeactivateRequest (input: {
didSuffix: string,
recoveryPrivateKey: JwkEs256k
}): Promise<IonDeactivateRequestModel> {
// Validate DID suffix
IonRequest.validateDidSuffix(input.didSuffix);

// Validates recovery private key
IonRequest.validateEs256kOperationKey(input.recoveryPrivateKey, OperationKeyType.Private);

const recoveryPublicKey = this.getPublicKeyFromPrivateKey(input.recoveryPrivateKey);
const hashAlgorithmInMultihashCode = IonSdkConfig.hashAlgorithmInMultihashCode;
const revealValue = Multihash.canonicalizeThenHashThenEncode(recoveryPublicKey, hashAlgorithmInMultihashCode);

const signedDataPayloadObject = {
didSuffix: input.didSuffix,
recoveryKey: recoveryPublicKey
};

const compactJws = await secp256k1.ES256K.sign(
signedDataPayloadObject,
input.recoveryPrivateKey,
{ alg: 'ES256K' }
);

return {
type: OperationType.Deactivate,
didSuffix: input.didSuffix,
revealValue: revealValue,
signedData: compactJws
};
}

public static async createRecoverRequest (input: {
didSuffix: string,
recoveryPrivateKey: JwkEs256k,
nextRecoveryPublicKey: JwkEs256k,
nextUpdatePublicKey: JwkEs256k,
document: IonDocumentModel
}): Promise<IonRecoverRequestModel> {
// Validate DID suffix
IonRequest.validateDidSuffix(input.didSuffix);

// Validate recovery private key
IonRequest.validateEs256kOperationKey(input.recoveryPrivateKey, OperationKeyType.Private);

// Validate next recovery public key
IonRequest.validateEs256kOperationKey(input.nextRecoveryPublicKey, OperationKeyType.Public);

// Validate next update public key
IonRequest.validateEs256kOperationKey(input.nextUpdatePublicKey, OperationKeyType.Public);

// Validate all given DID Document keys.
IonRequest.validateDidDocumentKeys(input.document.publicKeys);

// Validate all given service.
IonRequest.validateServices(input.document.services);

const recoveryPublicKey = this.getPublicKeyFromPrivateKey(input.recoveryPrivateKey);
const hashAlgorithmInMultihashCode = IonSdkConfig.hashAlgorithmInMultihashCode;
const revealValue = Multihash.canonicalizeThenHashThenEncode(recoveryPublicKey, hashAlgorithmInMultihashCode);

const patches = [{
action: PatchAction.Replace,
document: input.document
}];

const nextUpdateCommitmentHash = Multihash.canonicalizeThenDoubleHashThenEncode(input.nextUpdatePublicKey, hashAlgorithmInMultihashCode);
const delta = {
patches,
updateCommitment: nextUpdateCommitmentHash
};

const deltaHash = Multihash.canonicalizeThenHashThenEncode(delta, hashAlgorithmInMultihashCode);
const nextRecoveryCommitmentHash = Multihash.canonicalizeThenDoubleHashThenEncode(input.nextRecoveryPublicKey, hashAlgorithmInMultihashCode);

const signedDataPayloadObject = {
recoveryCommitment: nextRecoveryCommitmentHash,
recoveryKey: recoveryPublicKey,
deltaHash: deltaHash
};

const compactJws = await secp256k1.ES256K.sign(
signedDataPayloadObject,
input.recoveryPrivateKey,
{ alg: 'ES256K' }
);

return {
type: OperationType.Recover,
didSuffix: input.didSuffix,
revealValue: revealValue,
delta: delta,
signedData: compactJws
};
}

public static async createUpdateRequest (input: {
didSuffix: string;
updatePrivateKey: JwkEs256k;
nextUpdatePublicKey: JwkEs256k;
servicesToAdd?: IonServiceModel[];
idsOfServicesToRemove?: string[];
publicKeysToAdd?: IonPublicKeyModel[];
idsOfPublicKeysToRemove?: string[];
}): Promise<IonUpdateRequestModel> {
// Validate DID suffix
IonRequest.validateDidSuffix(input.didSuffix);

// Validate update private key
IonRequest.validateEs256kOperationKey(input.updatePrivateKey, OperationKeyType.Private);

// Validate next update public key
IonRequest.validateEs256kOperationKey(input.nextUpdatePublicKey, OperationKeyType.Public);

// Validate all given service.
IonRequest.validateServices(input.servicesToAdd);

// Validate all given DID Document keys.
IonRequest.validateDidDocumentKeys(input.publicKeysToAdd);

// Validate all given service id to remove.
if (input.idsOfServicesToRemove !== undefined) {
for (const id of input.idsOfServicesToRemove) {
InputValidator.validateId(id);
}
}

// Validate all given public key id to remove.
if (input.idsOfPublicKeysToRemove !== undefined) {
for (const id of input.idsOfPublicKeysToRemove) {
InputValidator.validateId(id);
}
}

const patches = [];
// Create patches for add services
const servicesToAdd = input.servicesToAdd;
if (servicesToAdd !== undefined && servicesToAdd.length > 0) {
const patch = {
action: PatchAction.AddServices,
services: servicesToAdd
};

patches.push(patch);
}

// Create patches for remove services
const idsOfServicesToRemove = input.idsOfServicesToRemove;
if (idsOfServicesToRemove !== undefined && idsOfServicesToRemove.length > 0) {
const patch = {
action: PatchAction.RemoveServices,
ids: idsOfServicesToRemove
};

patches.push(patch);
}

// Create patches for adding public keys
const publicKeysToAdd = input.publicKeysToAdd;
if (publicKeysToAdd !== undefined && publicKeysToAdd.length > 0) {
const patch = {
action: PatchAction.AddPublicKeys,
publicKeys: publicKeysToAdd
};

patches.push(patch);
}

// Create patch for removing public keys
const idsOfPublicKeysToRemove = input.idsOfPublicKeysToRemove;
if (idsOfPublicKeysToRemove !== undefined && idsOfPublicKeysToRemove.length > 0) {
const patch = {
action: PatchAction.RemovePublicKeys,
ids: idsOfPublicKeysToRemove
};

patches.push(patch);
}

const updatePublicKey = this.getPublicKeyFromPrivateKey(input.updatePrivateKey);
const hashAlgorithmInMultihashCode = IonSdkConfig.hashAlgorithmInMultihashCode;
const revealValue = Multihash.canonicalizeThenHashThenEncode(updatePublicKey, hashAlgorithmInMultihashCode);

const nextUpdateCommitmentHash = Multihash.canonicalizeThenDoubleHashThenEncode(input.nextUpdatePublicKey, hashAlgorithmInMultihashCode);
const delta = {
patches,
updateCommitment: nextUpdateCommitmentHash
};
const deltaHash = Multihash.canonicalizeThenHashThenEncode(delta, hashAlgorithmInMultihashCode);

const signedDataPayloadObject = {
updateKey: updatePublicKey,
deltaHash: deltaHash
};
const compactJws = await secp256k1.ES256K.sign(
signedDataPayloadObject,
input.updatePrivateKey,
{ alg: 'ES256K' }
);

return {
type: OperationType.Update,
didSuffix: input.didSuffix,
revealValue,
delta,
signedData: compactJws
};
}

/**
* Validates the schema of a ES256K JWK public key.
* Validates the schema of a ES256K JWK key.
*/
private static validateEs256kOperationPublicKey (publicKeyJwk: JwkEs256k) {
private static validateEs256kOperationKey (operationKeyJwk: JwkEs256k, operationKeyType: OperationKeyType) {
const allowedProperties = new Set(['kty', 'crv', 'x', 'y']);
for (const property in publicKeyJwk) {
if (operationKeyType === OperationKeyType.Private) {
allowedProperties.add('d');
}
for (const property in operationKeyJwk) {
if (!allowedProperties.has(property)) {
throw new IonError(ErrorCode.PublicKeyJwkEs256kHasUnexpectedProperty, `SECP256K1 JWK key has unexpected property '${property}'.`);
}
}

if (publicKeyJwk.crv !== 'secp256k1') {
throw new IonError(ErrorCode.JwkEs256kMissingOrInvalidCrv, `SECP256K1 JWK 'crv' property must be 'secp256k1' but got '${publicKeyJwk.crv}.'`);
if (operationKeyJwk.crv !== 'secp256k1') {
throw new IonError(ErrorCode.JwkEs256kMissingOrInvalidCrv, `SECP256K1 JWK 'crv' property must be 'secp256k1' but got '${operationKeyJwk.crv}.'`);
}

if (publicKeyJwk.kty !== 'EC') {
throw new IonError(ErrorCode.JwkEs256kMissingOrInvalidKty, `SECP256K1 JWK 'kty' property must be 'EC' but got '${publicKeyJwk.kty}.'`);
if (operationKeyJwk.kty !== 'EC') {
throw new IonError(ErrorCode.JwkEs256kMissingOrInvalidKty, `SECP256K1 JWK 'kty' property must be 'EC' but got '${operationKeyJwk.kty}.'`);
}

// `x` and `y` need 43 Base64URL encoded bytes to contain 256 bits.
if (publicKeyJwk.x.length !== 43) {
if (operationKeyJwk.x.length !== 43) {
throw new IonError(ErrorCode.JwkEs256kHasIncorrectLengthOfX, `SECP256K1 JWK 'x' property must be 43 bytes.`);
}

if (publicKeyJwk.y.length !== 43) {
if (operationKeyJwk.y.length !== 43) {
throw new IonError(ErrorCode.JwkEs256kHasIncorrectLengthOfY, `SECP256K1 JWK 'y' property must be 43 bytes.`);
}

if (operationKeyType === OperationKeyType.Private && (operationKeyJwk.d === undefined || operationKeyJwk.d.length !== 43)) {
throw new IonError(ErrorCode.JwkEs256kHasIncorrectLengthOfD, `SECP256K1 JWK 'd' property must be 43 bytes.`);
}
}

private static validateDidSuffix (didSuffix: string) {
if (didSuffix.length !== 46) {
throw new IonError(ErrorCode.DidSuffixIncorrectLength, 'DID suffix must be 46 bytes.');
}

if (!Encoder.isBase64UrlString(didSuffix)) {
throw new IonError(ErrorCode.DidSuffixIncorrectEncoding, 'DID suffix must be base64url string.');
}
}

private static validateDidDocumentKeys (publicKeys?: IonPublicKeyModel[]) {
Expand All @@ -132,6 +357,19 @@ export default class IonRequest {
}
}

private static validateServices (services?: IonServiceModel[]) {
if (services !== undefined && services.length !== 0) {
const serviceIdSet: Set<string> = new Set();
for (const service of services) {
IonRequest.validateService(service);
if (serviceIdSet.has(service.id)) {
throw new IonError(ErrorCode.DidDocumentServiceIdDuplicated, 'Service id has to be unique');
}
serviceIdSet.add(service.id);
}
}
}

private static validateService (service: IonServiceModel) {
InputValidator.validateId(service.id);

Expand Down Expand Up @@ -162,4 +400,13 @@ export default class IonRequest {
throw new IonError(ErrorCode.DeltaExceedsMaximumSize, errorMessage);
}
}

private static getPublicKeyFromPrivateKey (privateKey: JwkEs256k) {
return {
crv: privateKey.crv,
kty: privateKey.kty,
x: privateKey.x,
y: privateKey.y
};
}
}
9 changes: 9 additions & 0 deletions lib/enums/OperationKeyType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Operation key type, indicates if a key is a public or private key.
*/
enum OperationKeyType {
Public = 'public',
Private = 'private'
}

export default OperationKeyType;
12 changes: 12 additions & 0 deletions lib/enums/PatchAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Sidetree patch actions. These are the valid values in the action property of a patch.
*/
enum PatchAction {
Replace = 'replace',
AddPublicKeys = 'add-public-keys',
RemovePublicKeys = 'remove-public-keys',
AddServices = 'add-services',
RemoveServices = 'remove-services'
}

export default PatchAction;
Loading

0 comments on commit 06469e0

Please sign in to comment.