Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add update, recover and deactivate to sdk #14

Merged
merged 9 commits into from
Apr 22, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/ErrorCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default {
DidDocumentPublicKeyIdDuplicated: 'DidDocumentPublicKeyIdDuplicated',
DidDocumentPublicKeyMissingOrIncorrectType: 'DidDocumentPublicKeyMissingOrIncorrectType',
DidDocumentServiceIdDuplicated: 'DidDocumentServiceIdDuplicated',
DidSuffixIncorrectLength: 'DidSuffixIncorrectLength',
IdNotUsingBase64UrlCharacterSet: 'IdNotUsingBase64UrlCharacterSet',
IdTooLong: 'IdTooLong',
JwkEs256kMissingOrInvalidCrv: 'JwkEs256kMissingOrInvalidCrv',
Expand Down
68 changes: 64 additions & 4 deletions lib/IonRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export default class IonRequest {
const services = input.document.services;

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

// Validate all given DID Document keys.
IonRequest.validateDidDocumentKeys(didDocumentKeys);
Expand Down Expand Up @@ -79,6 +79,12 @@ export default class IonRequest {
didSuffix: string,
recoveryPrivateKey: JwkEs256k
}): Promise<IonDeactivateRequestModel> {
// Validate DID suffix
IonRequest.validateDidSuffix(input.didSuffix);

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

const recoveryPublicKey = this.getPublicKeyFromPrivateKey(input.recoveryPrivateKey);
const hashAlgorithmInMultihashCode = IonSdkConfig.hashAlgorithmInMultihashCode;
const revealValue = Multihash.canonicalizeThenHashThenEncode(recoveryPublicKey, hashAlgorithmInMultihashCode);
Expand Down Expand Up @@ -109,6 +115,18 @@ export default class IonRequest {
nextUpdatePublicKey: JwkEs256k,
document: IonDocumentModel
}): Promise<IonRecoverRequestModel> {
// Validate DID suffix
IonRequest.validateDidSuffix(input.didSuffix);

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

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

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

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

Expand Down Expand Up @@ -163,6 +181,35 @@ export default class IonRequest {
publicKeysToAdd?: IonPublicKeyModel[];
idsOfPublicKeysToRemove?: string[];
}): Promise<IonUpdateRequestModel> {
// Validate DID suffix
IonRequest.validateDidSuffix(input.didSuffix);

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

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

// 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;
Expand Down Expand Up @@ -239,10 +286,13 @@ export default class IonRequest {
}

/**
* 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 (publicKeyJwk: JwkEs256k, isPrivateKey?: boolean) {
const allowedProperties = new Set(['kty', 'crv', 'x', 'y']);
if (isPrivateKey) {
allowedProperties.add('d');
}
for (const property in publicKeyJwk) {
if (!allowedProperties.has(property)) {
throw new IonError(ErrorCode.PublicKeyJwkEs256kHasUnexpectedProperty, `SECP256K1 JWK key has unexpected property '${property}'.`);
Expand All @@ -265,6 +315,16 @@ export default class IonRequest {
if (publicKeyJwk.y.length !== 43) {
throw new IonError(ErrorCode.JwkEs256kHasIncorrectLengthOfY, `SECP256K1 JWK 'y' property must be 43 bytes.`);
}

if (isPrivateKey && (publicKeyJwk.d === undefined || publicKeyJwk.d.length !== 43)) {
throw new IonError(ErrorCode.JwkEs256kHasIncorrectLengthOfY, `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.');
}
}

private static validateDidDocumentKeys (publicKeys?: IonPublicKeyModel[]) {
Expand Down
6 changes: 6 additions & 0 deletions lib/models/IonAddPublicKeysActionModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import IonPublicKeyModel from './IonPublicKeyModel';

export default interface IonAddPublicKeysActionModel {
action: string;
publicKeys: IonPublicKeyModel[];
}
6 changes: 6 additions & 0 deletions lib/models/IonAddServicesActionModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import IonServiceModel from './IonServiceModel';

export default interface IonAddServicesActionModel {
action: string;
services: IonServiceModel[];
}
4 changes: 4 additions & 0 deletions lib/models/IonRemovePublicKeysActionModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default interface IonRemovePublicKeysActionModel {
action: string;
ids: string[];
}
4 changes: 4 additions & 0 deletions lib/models/IonRemoveServicesActionModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default interface IonRemoveServicesActionModel {
action: string;
ids: string[];
}
13 changes: 5 additions & 8 deletions lib/models/IonUpdateRequestModel.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import IonPublicKeyModel from './IonPublicKeyModel';
import IonServiceModel from './IonServiceModel';
import IonAddPublicKeysActionModel from './IonAddPublicKeysActionModel';
import IonAddServicesActionModel from './IonAddServicesActionModel';
import IonRemovePublicKeysActionModel from './IonRemovePublicKeysActionModel';
import IonRemoveServicesActionModel from './IonRemoveServicesActionModel';
import OperationType from '../enums/OperationType';

/**
Expand All @@ -11,12 +13,7 @@ export default interface IonUpdateRequestModel {
revealValue: string;
delta: {
updateCommitment: string,
patches: {
action: string,
servicesToAdd?: IonServiceModel[],
publicKeys?: IonPublicKeyModel[],
ids?: string[]
}[]
patches: (IonAddServicesActionModel | IonAddPublicKeysActionModel | IonRemoveServicesActionModel | IonRemovePublicKeysActionModel)[]
},
signedData: string
}
84 changes: 59 additions & 25 deletions tests/IonRequest.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,7 @@ describe('IonRequest', () => {
const publicKey = require('./vectors/inputs/publicKeyModel1.json');
const publicKeys = [publicKey];

const service = {
id: 'service1',
type: 'website',
serviceEndpoint: 'https://www.some.web.site.com'
};
const service = require('./vectors/inputs/service1.json');
const services = [service];

const document : IonDocumentModel = {
Expand All @@ -27,7 +23,7 @@ describe('IonRequest', () => {
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('EiAdz754IZmW-nizq4Zpr_hoX5P5r7KbVfJWfDclCxKnHg');
expect(result.suffixData.deltaHash).toEqual('EiCfDWRnYlcD9EGA3d_5Z1AHu-iYqMbJ9nfiqdz5S8VDbg');
});
});

Expand All @@ -36,14 +32,10 @@ describe('IonRequest', () => {
const publicKey = require('./vectors/inputs/publicKeyModel1.json');
const publicKeys = [publicKey];

const service = {
id: 'service1',
type: 'website',
serviceEndpoint: 'https://www.some.web.site.com'
};
const service = require('./vectors/inputs/service1.json');
const services = [service];
const input = {
didSuffix: 'someString',
didSuffix: 'EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg',
updatePrivateKey: require('./vectors/inputs/jwkEs256k1Private.json'),
nextUpdatePublicKey: require('./vectors/inputs/jwkEs256k2Public.json'),
servicesToAdd: services,
Expand All @@ -53,43 +45,50 @@ describe('IonRequest', () => {
};

const result = await IonRequest.createUpdateRequest(input);
expect(result.didSuffix).toEqual('someString');
expect(result.didSuffix).toEqual('EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg');
expect(result.type).toEqual(OperationType.Update);
expect(result.revealValue).toEqual('EiAJ-97Is59is6FKAProwDo870nmwCeP8n5nRRFwPpUZVQ');
expect(result.signedData).toEqual('eyJhbGciOiJFUzI1NksifQ.eyJ1cGRhdGVLZXkiOnsiY3J2Ijoic2VjcDI1NmsxIiwia3R5IjoiRUMiLCJ4IjoibklxbFJDeDBleUJTWGNRbnFEcFJlU3Y0enVXaHdDUldzc29jOUxfbmo2QSIsInkiOiJpRzI5Vks2bDJVNXNLQlpVU0plUHZ5RnVzWGdTbEsyZERGbFdhQ004RjdrIn0sImRlbHRhSGFzaCI6IkVpRFUzREM3TTVERjRvRFBIYmk0ZV9lTTBmWm1aMHNhSWFabERlNV9qMWJEZncifQ.Lwd1I0mmM1_7jTPxklynXRaE0cFlUVBju0ZChZzZaNsQTqTEgX5_DNRcDCapeCYn6JpRhEhay-SYaFLwS29vWg');
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 = {
id: 'service1',
type: 'website',
serviceEndpoint: 'https://www.some.web.site.com'
};
const service = require('./vectors/inputs/service1.json');
const services = [service];

const document : IonDocumentModel = {
publicKeys,
services
};
const result = await IonRequest.createRecoverRequest({
didSuffix: 'someString',
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('someString');
expect(result.didSuffix).toEqual('EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg');
expect(result.revealValue).toEqual('EiAJ-97Is59is6FKAProwDo870nmwCeP8n5nRRFwPpUZVQ');
expect(result.type).toEqual(OperationType.Recover);
expect(result.signedData).toEqual('eyJhbGciOiJFUzI1NksifQ.eyJyZWNvdmVyeUNvbW1pdG1lbnQiOiJFaURLSWt3cU82OUlQRzNwT2xIa2RiODZuWXQwYU54U0hadTJyLWJoRXpuamRBIiwicmVjb3ZlcnlLZXkiOnsiY3J2Ijoic2VjcDI1NmsxIiwia3R5IjoiRUMiLCJ4IjoibklxbFJDeDBleUJTWGNRbnFEcFJlU3Y0enVXaHdDUldzc29jOUxfbmo2QSIsInkiOiJpRzI5Vks2bDJVNXNLQlpVU0plUHZ5RnVzWGdTbEsyZERGbFdhQ004RjdrIn0sImRlbHRhSGFzaCI6IkVpQXZJWUdXYzlaRi1CM3N5UU80bU9uOTZrajI2b21zTkFIclkxdVY5WWxRSXcifQ.WzbEhitn1pLeXEFqfupbpOqCVXA6V-VQccdEo6pH7rEZULfwuFfqns1APVwrBUNM7CX_MiaIajnZrMVXhrdS1g');
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
});
Expand All @@ -98,14 +97,49 @@ describe('IonRequest', () => {
describe('createDeactivateRequest', () => {
it('shuold generate a deactivate request with the given arguments', async () => {
const result = await IonRequest.createDeactivateRequest({
didSuffix: 'someString',
didSuffix: 'EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg',
recoveryPrivateKey: require('./vectors/inputs/jwkEs256k1Private.json')
});

expect(result.didSuffix).toEqual('someString');
expect(result.didSuffix).toEqual('EiDyOQbbZAa3aiRzeCkV7LOx3SERjjH93EXoIM3UoN4oWg');
expect(result.type).toEqual(OperationType.Deactivate);
expect(result.revealValue).toEqual('EiAJ-97Is59is6FKAProwDo870nmwCeP8n5nRRFwPpUZVQ');
expect(result.signedData).toEqual('eyJhbGciOiJFUzI1NksifQ.eyJkaWRTdWZmaXgiOiJzb21lU3RyaW5nIiwicmVjb3ZlcnlLZXkiOnsiY3J2Ijoic2VjcDI1NmsxIiwia3R5IjoiRUMiLCJ4IjoibklxbFJDeDBleUJTWGNRbnFEcFJlU3Y0enVXaHdDUldzc29jOUxfbmo2QSIsInkiOiJpRzI5Vks2bDJVNXNLQlpVU0plUHZ5RnVzWGdTbEsyZERGbFdhQ004RjdrIn19.egHlZRc3kSUCfVe0JPIzI6FnUtGVmM-DFsyejHvcXTF0gPnKKSzvVVVWJE2wb-ctKGLnqohmyLgn31OOuDXEzQ');
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.validateEs256kOperationKey(privKey, true);
fail();
} catch (e) {
expect(e.message).toEqual(`JwkEs256kHasIncorrectLengthOfY: 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.validateEs256kOperationKey(privKey, true);
fail();
} catch (e) {
expect(e.message).toEqual(`JwkEs256kHasIncorrectLengthOfY: SECP256K1 JWK 'd' property must be 43 bytes.`);
}
});
});

describe('validateDidSuffix', () => {
it('should throw if id is not valid', () => {
try {
IonRequest.validateDidSuffix('someString');
fail();
} catch (e) {
expect(e.message).toEqual('DidSuffixIncorrectLength: DID suffix must be 46 bytes.');
}
});
});
});