diff --git a/lib/ErrorCode.ts b/lib/ErrorCode.ts index 0a59a0e..19551bd 100644 --- a/lib/ErrorCode.ts +++ b/lib/ErrorCode.ts @@ -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', diff --git a/lib/IonRequest.ts b/lib/IonRequest.ts index 6a18e92..95f0501 100644 --- a/lib/IonRequest.ts +++ b/lib/IonRequest.ts @@ -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. @@ -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 = 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 }]; @@ -79,33 +77,260 @@ export default class IonRequest { return operationRequest; } + public static async createDeactivateRequest (input: { + didSuffix: string, + recoveryPrivateKey: JwkEs256k + }): Promise { + // 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 { + // 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 { + // 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[]) { @@ -132,6 +357,19 @@ export default class IonRequest { } } + private static validateServices (services?: IonServiceModel[]) { + if (services !== undefined && services.length !== 0) { + const serviceIdSet: Set = 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); @@ -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 + }; + } } diff --git a/lib/enums/OperationKeyType.ts b/lib/enums/OperationKeyType.ts new file mode 100644 index 0000000..85883f4 --- /dev/null +++ b/lib/enums/OperationKeyType.ts @@ -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; diff --git a/lib/enums/PatchAction.ts b/lib/enums/PatchAction.ts new file mode 100644 index 0000000..7d4cb23 --- /dev/null +++ b/lib/enums/PatchAction.ts @@ -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; diff --git a/lib/models/IonAddPublicKeysActionModel.ts b/lib/models/IonAddPublicKeysActionModel.ts new file mode 100644 index 0000000..7baba50 --- /dev/null +++ b/lib/models/IonAddPublicKeysActionModel.ts @@ -0,0 +1,6 @@ +import IonPublicKeyModel from './IonPublicKeyModel'; + +export default interface IonAddPublicKeysActionModel { + action: string; + publicKeys: IonPublicKeyModel[]; +} diff --git a/lib/models/IonAddServicesActionModel.ts b/lib/models/IonAddServicesActionModel.ts new file mode 100644 index 0000000..9faf4c8 --- /dev/null +++ b/lib/models/IonAddServicesActionModel.ts @@ -0,0 +1,6 @@ +import IonServiceModel from './IonServiceModel'; + +export default interface IonAddServicesActionModel { + action: string; + services: IonServiceModel[]; +} diff --git a/lib/models/IonDeactivateRequestModel.ts b/lib/models/IonDeactivateRequestModel.ts new file mode 100644 index 0000000..ff21b5d --- /dev/null +++ b/lib/models/IonDeactivateRequestModel.ts @@ -0,0 +1,11 @@ +import OperationType from '../enums/OperationType'; + +/** + * Data model representing a public key in the DID Document. + */ +export default interface IonDeactivateRequestModel { + type: OperationType; + didSuffix: string; + revealValue: string; + signedData: string; +}; diff --git a/lib/models/IonRecoverRequestModel.ts b/lib/models/IonRecoverRequestModel.ts new file mode 100644 index 0000000..c885d2b --- /dev/null +++ b/lib/models/IonRecoverRequestModel.ts @@ -0,0 +1,19 @@ +import IonDocumentModel from './IonDocumentModel'; +import OperationType from '../enums/OperationType'; + +/** + * Data model representing a public key in the DID Document. + */ +export default interface IonRecoverRequestModel { + type: OperationType; + didSuffix: string; + revealValue: string; + delta: { + updateCommitment: string, + patches: { + action: string, + document: IonDocumentModel; + }[] + }, + signedData: string +} diff --git a/lib/models/IonRemovePublicKeysActionModel.ts b/lib/models/IonRemovePublicKeysActionModel.ts new file mode 100644 index 0000000..694c63a --- /dev/null +++ b/lib/models/IonRemovePublicKeysActionModel.ts @@ -0,0 +1,4 @@ +export default interface IonRemovePublicKeysActionModel { + action: string; + ids: string[]; +} diff --git a/lib/models/IonRemoveServicesActionModel.ts b/lib/models/IonRemoveServicesActionModel.ts new file mode 100644 index 0000000..b44504b --- /dev/null +++ b/lib/models/IonRemoveServicesActionModel.ts @@ -0,0 +1,4 @@ +export default interface IonRemoveServicesActionModel { + action: string; + ids: string[]; +} diff --git a/lib/models/IonUpdateRequestModel.ts b/lib/models/IonUpdateRequestModel.ts new file mode 100644 index 0000000..940efc7 --- /dev/null +++ b/lib/models/IonUpdateRequestModel.ts @@ -0,0 +1,19 @@ +import IonAddPublicKeysActionModel from './IonAddPublicKeysActionModel'; +import IonAddServicesActionModel from './IonAddServicesActionModel'; +import IonRemovePublicKeysActionModel from './IonRemovePublicKeysActionModel'; +import IonRemoveServicesActionModel from './IonRemoveServicesActionModel'; +import OperationType from '../enums/OperationType'; + +/** + * Data model representing a public key in the DID Document. + */ +export default interface IonUpdateRequestModel { + type: OperationType; + didSuffix: string; + revealValue: string; + delta: { + updateCommitment: string, + patches: (IonAddServicesActionModel | IonAddPublicKeysActionModel | IonRemoveServicesActionModel | IonRemovePublicKeysActionModel)[] + }, + signedData: string +} diff --git a/tests/IonRequest.spec.ts b/tests/IonRequest.spec.ts new file mode 100644 index 0000000..3f080d1 --- /dev/null +++ b/tests/IonRequest.spec.ts @@ -0,0 +1,155 @@ +import { IonDocumentModel } from '../lib'; +import IonRequest from '../lib/IonRequest'; +import OperationKeyType from '../lib/enums/OperationKeyType'; +import OperationType from '../lib/enums/OperationType'; + +describe('IonRequest', () => { + describe('createCreateRequest', () => { + it('should generate a create request with desired arguments', async () => { + const recoveryKey = require('./vectors/inputs/jwkEs256k1Public.json'); + const updateKey = require('./vectors/inputs/jwkEs256k2Public.json'); + const publicKey = require('./vectors/inputs/publicKeyModel1.json'); + const publicKeys = [publicKey]; + + const service = require('./vectors/inputs/service1.json'); + const services = [service]; + + const document : IonDocumentModel = { + publicKeys, + services + }; + const input = { recoveryKey, updateKey, document }; + const result = IonRequest.createCreateRequest(input); + expect(result.type).toEqual(OperationType.Create); + expect(result.delta.updateCommitment).toEqual('EiDKIkwqO69IPG3pOlHkdb86nYt0aNxSHZu2r-bhEznjdA'); + expect(result.delta.patches.length).toEqual(1); + expect(result.suffixData.recoveryCommitment).toEqual('EiBfOZdMtU6OBw8Pk879QtZ-2J-9FbbjSZyoaA_bqD4zhA'); + expect(result.suffixData.deltaHash).toEqual('EiCfDWRnYlcD9EGA3d_5Z1AHu-iYqMbJ9nfiqdz5S8VDbg'); + }); + }); + + describe('createUpdateRequest', () => { + it('should generate an update request with the given arguments', async () => { + const publicKey = require('./vectors/inputs/publicKeyModel1.json'); + const publicKeys = [publicKey]; + + const service = require('./vectors/inputs/service1.json'); + const services = [service]; + const input = { + didSuffix: 'EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg', + updatePrivateKey: require('./vectors/inputs/jwkEs256k1Private.json'), + nextUpdatePublicKey: require('./vectors/inputs/jwkEs256k2Public.json'), + servicesToAdd: services, + idsOfServicesToRemove: ['someId1'], + publicKeysToAdd: publicKeys, + idsOfPublicKeysToRemove: ['someId2'] + }; + + const result = await IonRequest.createUpdateRequest(input); + expect(result.didSuffix).toEqual('EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg'); + expect(result.type).toEqual(OperationType.Update); + expect(result.revealValue).toEqual('EiAJ-97Is59is6FKAProwDo870nmwCeP8n5nRRFwPpUZVQ'); + expect(result.signedData).toEqual('eyJhbGciOiJFUzI1NksifQ.eyJ1cGRhdGVLZXkiOnsiY3J2Ijoic2VjcDI1NmsxIiwia3R5IjoiRUMiLCJ4IjoibklxbFJDeDBleUJTWGNRbnFEcFJlU3Y0enVXaHdDUldzc29jOUxfbmo2QSIsInkiOiJpRzI5Vks2bDJVNXNLQlpVU0plUHZ5RnVzWGdTbEsyZERGbFdhQ004RjdrIn0sImRlbHRhSGFzaCI6IkVpQXZsbVVRYy1jaDg0Slp5bmdQdkJzUkc3eWh4aUFSenlYOE5lNFQ4LTlyTncifQ.mbXK3d_KruRQB5ZviM-ow3UaIdUY3m1o1o9TdHAW23Z11upHglVr7Yfb-cvmJL6iL0qZxWiT9R5hpoIOPOkWJQ'); + expect(result.delta.updateCommitment).toEqual('EiDKIkwqO69IPG3pOlHkdb86nYt0aNxSHZu2r-bhEznjdA'); + expect(result.delta.patches.length).toEqual(4); // add/remove service and add/remove key + }); + + it('should generate an update request with the no arguments', async () => { + const input = { + didSuffix: 'EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg', + updatePrivateKey: require('./vectors/inputs/jwkEs256k1Private.json'), + nextUpdatePublicKey: require('./vectors/inputs/jwkEs256k2Public.json') + }; + + const result = await IonRequest.createUpdateRequest(input); + expect(result.didSuffix).toEqual('EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg'); + }); + }); + + describe('createRecoverRequest', () => { + it('should generate a recover request with given arguments', async () => { + const publicKey = require('./vectors/inputs/publicKeyModel1.json'); + const publicKeys = [publicKey]; + + const service = require('./vectors/inputs/service1.json'); + const services = [service]; + + const document : IonDocumentModel = { + publicKeys, + services + }; + const result = await IonRequest.createRecoverRequest({ + didSuffix: 'EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg', + recoveryPrivateKey: require('./vectors/inputs/jwkEs256k1Private.json'), + nextRecoveryPublicKey: require('./vectors/inputs/jwkEs256k2Public.json'), + nextUpdatePublicKey: require('./vectors/inputs/jwkEs256k3Public.json'), + document + }); + + expect(result.didSuffix).toEqual('EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg'); + expect(result.revealValue).toEqual('EiAJ-97Is59is6FKAProwDo870nmwCeP8n5nRRFwPpUZVQ'); + expect(result.type).toEqual(OperationType.Recover); + expect(result.signedData).toEqual('eyJhbGciOiJFUzI1NksifQ.eyJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaURLSWt3cU82OUlQRzNwT2xIa2RiODZuWXQwYU54U0hadTJyLWJoRXpuamRBIiwicmVjb3ZlcnlLZXkiOnsiY3J2Ijoic2VjcDI1NmsxIiwia3R5IjoiRUMiLCJ4IjoibklxbFJDeDBleUJTWGNRbnFEcFJlU3Y0enVXaHdDUldzc29jOUxfbmo2QSIsInkiOiJpRzI5Vks2bDJVNXNLQlpVU0plUHZ5RnVzWGdTbEsyZERGbFdhQ004RjdrIn0sImRlbHRhSGFzaCI6IkVpQm9HNlFtamlTSm5ON2phaldnaV9vZDhjR3dYSm9Nc2RlWGlWWTc3NXZ2SkEifQ.ZL5ThTp1rLPtcsf6rUk8DwkkkmP8f70Mor-lk2Jru5VJlMBlPOKb3saCqlCxlopD8e-sGZsyx3xi4Pf4KeY_NQ'); + expect(result.delta.updateCommitment).toEqual('EiBJGXo0XUiqZQy0r-fQUHKS3RRVXw5nwUpqGVXEGuTs-g'); + expect(result.delta.patches.length).toEqual(1); // replace + }); + }); + + describe('createDeactivateRequest', () => { + it('shuold generate a deactivate request with the given arguments', async () => { + const result = await IonRequest.createDeactivateRequest({ + didSuffix: 'EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg', + recoveryPrivateKey: require('./vectors/inputs/jwkEs256k1Private.json') + }); + + expect(result.didSuffix).toEqual('EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg'); + expect(result.type).toEqual(OperationType.Deactivate); + expect(result.revealValue).toEqual('EiAJ-97Is59is6FKAProwDo870nmwCeP8n5nRRFwPpUZVQ'); + expect(result.signedData).toEqual('eyJhbGciOiJFUzI1NksifQ.eyJkaWRTdWZmaXgiOiJFaUR5T1FiYlpBYTNhaVJ6ZUNrVjdMT3gzU0VSampIOTNFWG9JTTNVb040b1dnIiwicmVjb3ZlcnlLZXkiOnsiY3J2Ijoic2VjcDI1NmsxIiwia3R5IjoiRUMiLCJ4IjoibklxbFJDeDBleUJTWGNRbnFEcFJlU3Y0enVXaHdDUldzc29jOUxfbmo2QSIsInkiOiJpRzI5Vks2bDJVNXNLQlpVU0plUHZ5RnVzWGdTbEsyZERGbFdhQ004RjdrIn19.9rSNNrh5vaT0cSsHt4lElKTm7rbxNhmIGGSA238O91dxs9-OKDM_ktfK5RmhBd7qfM6wJTJcdPCOnufTj5jbRA'); + }); + }); + + describe('validateEs256kOperationKey', () => { + it('should throw if given private key does not have d', () => { + const privKey = require('./vectors/inputs/jwkEs256k1Private.json'); + privKey.d = undefined; + try { + (IonRequest as any).validateEs256kOperationKey(privKey, OperationKeyType.Private); + fail(); + } catch (e) { + expect(e.message).toEqual(`JwkEs256kHasIncorrectLengthOfD: SECP256K1 JWK 'd' property must be 43 bytes.`); + } + }); + + it('should throw if given private key d value is not the correct length', () => { + const privKey = require('./vectors/inputs/jwkEs256k1Private.json'); + privKey.d = 'abc'; + try { + (IonRequest as any).validateEs256kOperationKey(privKey, OperationKeyType.Private); + fail(); + } catch (e) { + expect(e.message).toEqual(`JwkEs256kHasIncorrectLengthOfD: SECP256K1 JWK 'd' property must be 43 bytes.`); + } + }); + }); + + describe('validateDidSuffix', () => { + it('should throw if id is incorrect length', () => { + try { + (IonRequest as any).validateDidSuffix('someString'); + fail(); + } catch (e) { + expect(e.message).toEqual('DidSuffixIncorrectLength: DID suffix must be 46 bytes.'); + } + }); + + it('should throw if id is incorrect encoding', () => { + try { + (IonRequest as any).validateDidSuffix('123456789012345678901234567890123456789012345/'); + fail(); + } catch (e) { + expect(e.message).toEqual('DidSuffixIncorrectEncoding: DID suffix must be base64url string.'); + } + }); + }); +}); diff --git a/tests/vectors/inputs/jwkEs256k3Private.json b/tests/vectors/inputs/jwkEs256k3Private.json new file mode 100644 index 0000000..75485d3 --- /dev/null +++ b/tests/vectors/inputs/jwkEs256k3Private.json @@ -0,0 +1,7 @@ +{ + "kty": "EC", + "crv": "secp256k1", + "d": "S6MkezrCVFEbZL2TR-jHL4x_aDY_mzSqLdNnS_9ZR3c", + "x": "z2f-UmRilQ2jVzYo-lYtn2TeBWWC284MlUaGcjSdmJ4", + "y": "KAuEa77izdMyjdnR6r1CZL1g4bixlsMfbXifkjAlBg8" +} \ No newline at end of file diff --git a/tests/vectors/inputs/jwkEs256k3Public.json b/tests/vectors/inputs/jwkEs256k3Public.json new file mode 100644 index 0000000..499c44e --- /dev/null +++ b/tests/vectors/inputs/jwkEs256k3Public.json @@ -0,0 +1,6 @@ +{ + "kty": "EC", + "crv": "secp256k1", + "x": "z2f-UmRilQ2jVzYo-lYtn2TeBWWC284MlUaGcjSdmJ4", + "y": "KAuEa77izdMyjdnR6r1CZL1g4bixlsMfbXifkjAlBg8" +} \ No newline at end of file