This commit is contained in:
Ronny Bjones 2021-01-19 14:46:30 +00:00
Родитель 7b541c3fe5
Коммит 520f22bc5b
5 изменённых файлов: 139 добавлений и 130 удалений

Просмотреть файл

@ -1,151 +1,76 @@
import { Crypto, KeyReference, JsonWebKey } from './index';
import base64url from 'base64url';
import OperationType from '@decentralized-identity/sidetree/dist/lib/core/enums/OperationType';
import CreateOperation from '@decentralized-identity/sidetree/dist/lib/core/versions/latest/CreateOperation';
import Multihash from '@decentralized-identity/sidetree/dist/lib/core/versions/latest/Multihash';
import { KeyStoreOptions } from 'verifiablecredentials-crypto-sdk-typescript-keystore';
const canonicalize = require('canonicalize');
import { IonDid } from '@decentralized-identity/ion-sdk';
const clone = require('clone');
/**
* Helper class to work with long form DID's
*/
export default class LongFormDid {
constructor(private crypto: Crypto) { }
constructor(private crypto: Crypto, private services: any = []) {
}
/**
* Create key and return longform
* @param recoveryKeyReference Reference to the recovery key
* Create longform DID based on keys in crypto object
*/
public async serialize(): Promise<string> {
// See https://github.com/diafygi/webcrypto-examples for examples how to use the W3C web Crypto stamdard
if (!this.crypto.builder.signingKeyReference) {
throw new Error(`No signing key reference. Use CryptoBuilder.useSigningKeyReference.`)
if (this.crypto.builder.signingAlgorithm !== 'ES256K') {
return Promise.reject(new Error(`Longform DIDs only support ES256K. Signing algorithm: ${this.crypto.builder.signingAlgorithm}`));
}
if (!this.crypto.builder.recoveryKeyReference) {
throw new Error(`No recovery key reference. Use CryptoBuilder.useRecoveryKeyReference.`)
if (this.crypto.builder.recoveryAlgorithm !== 'ES256K') {
return Promise.reject(new Error(`Longform DIDs only support ES256K. Recovery algorithm: ${this.crypto.builder.recoveryAlgorithm}`));
}
let signingPublic = await (await this.crypto.builder.keyStore.get(this.crypto.builder.signingKeyReference, new KeyStoreOptions({publicKeyOnly: true}))).getKey<JsonWebKey>();
signingPublic = this.normalizeJwk(signingPublic);
let recoveryPublic = await (await this.crypto.builder.keyStore.get(this.crypto.builder.recoveryKeyReference, new KeyStoreOptions({publicKeyOnly: true}))).getKey<JsonWebKey>();
recoveryPublic = this.normalizeJwk(recoveryPublic);
if (this.crypto.builder.updateAlgorithm !== 'ES256K') {
return Promise.reject(new Error(`Longform DIDs only support ES256K. Update algorithm: ${this.crypto.builder.updateAlgorithm}`));
}
let signingPublic: any;
let recoveryKey: any;
let updateKey: any;
try {
signingPublic = await (await this.crypto.builder.keyStore.get(this.crypto.builder.signingKeyReference, new KeyStoreOptions({ publicKeyOnly: true }))).getKey<JsonWebKey>();
signingPublic = this.normalizeJwk(signingPublic);
recoveryKey = await (await this.crypto.builder.keyStore.get(this.crypto.builder.recoveryKeyReference, new KeyStoreOptions({ publicKeyOnly: true }))).getKey<JsonWebKey>();
recoveryKey = this.normalizeJwk(recoveryKey);
updateKey = await (await this.crypto.builder.keyStore.get(this.crypto.builder.updateKeyReference, new KeyStoreOptions({ publicKeyOnly: true }))).getKey<JsonWebKey>();
updateKey = this.normalizeJwk(updateKey);
} catch (exception) {
return Promise.reject(exception);
}
// Create long-form did
const createOperationData = await this.generateCreateOperation(recoveryPublic, signingPublic, this.crypto.builder.signingKeyReference);
const didMethodName = 'ion';
const didUniqueSuffix = createOperationData.createOperation.didUniqueSuffix;
const shortFormDid = `did:${didMethodName}:${didUniqueSuffix}`;
const encodedSuffixData = createOperationData.createOperation.encodedSuffixData;
const encodedDelta = createOperationData.createOperation.encodedDelta;
const longFormDid = `${shortFormDid}?-ion-initial-state=${encodedSuffixData}.${encodedDelta}`;
const didDocumentKeys: any = {
id: this.crypto.builder.recoveryKeyReference.keyReference,
type: "EcdsaSecp256k1VerificationKey2019",
"publicKeyJwk": signingPublic,
"purposes": [
"authentication", "keyAgreement"
]
};
// const did = await Did.create(longFormDid, didMethodName);
const document = {
publicKeys: [didDocumentKeys],
services: this.services
};
const longFormDid = IonDid.createLongFormDid({ recoveryKey, updateKey, document });
return longFormDid;
};
/**
* Generates an create operation.
*/
public async generateCreateOperation(recoveryPublicKey: any, signingPublicKey: any, keyReference: KeyReference) {
const operationRequest = this.generateCreateOperationRequest(
recoveryPublicKey,
signingPublicKey,
keyReference
);
const operationBuffer = Buffer.from(JSON.stringify(operationRequest));
const createOperation = await CreateOperation.parse(operationBuffer);
return {
createOperation,
operationRequest,
recoveryPublicKey,
signingPublicKey
};
}
/**
* Canonicalize the given content, then multihashes the result using the lastest supported hash algorithm, then encodes the multihash.
* Mainly used for testing purposes.
*/
public static canonicalizeThenHashThenEncode (content: object) {
const contentBuffer = LongFormDid.canonicalizeAsBuffer(content);
const contentHash = Multihash.hash(contentBuffer, 18);
const contentHashEncodedString = base64url.encode(contentHash);
return contentHashEncodedString;
}
/**
* Canonicalizes the given content as a UTF8 buffer.
*/
public static canonicalizeAsBuffer (content: object): Buffer {
const canonicalizedString: string = canonicalize(content);
const contentBuffer = Buffer.from(canonicalizedString);
return contentBuffer;
}
/**
* Generates a create operation request.
* @param nextRecoveryCommitment The encoded commitment hash for the next recovery.
* @param nextUpdateCommitment The encoded commitment hash for the next update.
*/
public generateCreateOperationRequest(
recoveryPublicKey: any,
signingPublicKey: any,
keyReference: KeyReference) {
const publicKey = {
id: keyReference.keyReference,
type: "EcdsaSecp256k1VerificationKey2019",
jwk: signingPublicKey,
purpose: [
"auth",
"general"
]
}
const document = {
public_keys : [publicKey]
};
const patches = [{
action: 'replace',
document
}];
const updateCommitment = LongFormDid.canonicalizeThenHashThenEncode(signingPublicKey);
const delta = {
update_commitment: updateCommitment,
patches
};
const deltaBuffer = Buffer.from(JSON.stringify(delta));
const deltaHash = base64url.encode(Multihash.hash(deltaBuffer));
const recoveryCommitment = LongFormDid.canonicalizeThenHashThenEncode(recoveryPublicKey);
const suffixData = {
delta_hash: deltaHash,
recovery_commitment: recoveryCommitment
};
const suffixDataEncodedString = base64url.encode(JSON.stringify(suffixData));
const deltaEncodedString = base64url.encode(deltaBuffer);
const operation = {
type: OperationType.Create,
suffix_data: suffixDataEncodedString,
delta: deltaEncodedString
};
return operation;
}
private normalizeJwk(key: any) {
delete key.key_ops;
delete key.ext;
key.crv = 'secp256k1';
return key;
private normalizeJwk(key: any): any {
const jwk = clone(key);
delete jwk.key_ops;
delete jwk.ext;
delete jwk.kid;
delete jwk.use;
delete jwk.alg;
jwk.crv = 'secp256k1';
return jwk;
}
}

Просмотреть файл

@ -45,7 +45,6 @@
"@azure/keyvault-keys": "4.0.2",
"@azure/keyvault-secrets": "4.0.2",
"@decentralized-identity/ion-sdk": "0.4.1",
"@decentralized-identity/sidetree": "0.11.0",
"base64url": "3.0.1",
"bs58": "4.0.1",
"canonicalize": "1.0.1",

Просмотреть файл

@ -14,7 +14,7 @@ describe('SuiteJcsEd25519Signature2020', () => {
.build();
crypto = await crypto.generateKey(KeyUse.Signature);
crypto = await crypto.generateKey(KeyUse.Signature, 'recovery');
const did = await new LongFormDid(crypto).serialize();
const did = 'did:test:12345';
crypto = crypto.builder.useDid(did).build();
let jsonLdProofs = new JoseBuilder(crypto)

Просмотреть файл

@ -23,7 +23,8 @@ describe('JSONLD proofs', () => {
.build();
crypto = await crypto.generateKey(KeyUse.Signature);
crypto = await crypto.generateKey(KeyUse.Signature, 'recovery');
crypto.builder.useDid(await new LongFormDid(crypto).serialize());
const did = 'did:test:12345';
crypto.builder.useDid(did);
let jsonLdProofBuilder = new JoseBuilder(crypto)
.useJsonLdProofsProtocol('JcsEd25519Signature2020')
let jsonLdProof: IPayloadProtectionSigning = jsonLdProofBuilder.build();

Просмотреть файл

@ -6,10 +6,11 @@
import { CryptoBuilder, CryptoFactory, KeyStoreFactory, Subtle, KeyUse, KeyStoreOptions, KeyReference, LongFormDid } from '../lib/index';
describe('LongFormDid', () => {
it('should generate a longform DID', async () =>{
it('should generate a longform DID', async () => {
let crypto = new CryptoBuilder()
.useSigningKeyReference(new KeyReference('mars'))
.useRecoveryKeyReference(new KeyReference('recovery'))
.useUpdateKeyReference(new KeyReference('update'))
.build();
crypto = await crypto.generateKey(KeyUse.Signature);
crypto = await crypto.generateKey(KeyUse.Signature, 'recovery');
@ -21,5 +22,88 @@ describe('LongFormDid', () => {
let did = await new LongFormDid(crypto).serialize();
expect(did.startsWith('did:ion')).toBeTruthy();
console.log(did);
});
// negative cases
// missing keys
crypto = new CryptoBuilder()
.useSigningKeyReference(new KeyReference('mars'))
.useRecoveryKeyReference(new KeyReference('recovery'))
.useUpdateKeyReference(new KeyReference('update'))
.build();
try {
await new LongFormDid(crypto).serialize();
fail('missing signing key should fail');
} catch (exception) {
expect(exception.message).toEqual('mars not found');
}
crypto = await crypto.generateKey(KeyUse.Signature);
try {
await new LongFormDid(crypto).serialize();
fail('missing recovery key should fail');
} catch (exception) {
expect(exception.message).toEqual('recovery not found');
}
crypto = await crypto.generateKey(KeyUse.Signature, 'recovery');
try {
await new LongFormDid(crypto).serialize();
fail('missing update key should fail');
} catch (exception) {
expect(exception.message).toEqual('update not found');
}
// wrong key algos
crypto = new CryptoBuilder()
.useUpdateAlgorithm('EdDSA')
.build();
let longform = new LongFormDid(crypto);
try {
await longform.serialize();
fail('wrong update key algorithm should fail');
} catch (exception) {
expect(exception.message).toEqual('Longform DIDs only support ES256K. Update algorithm: EdDSA');
}
crypto = new CryptoBuilder()
.useRecoveryAlgorithm('EdDSA')
.build();
longform = new LongFormDid(crypto);
try {
await longform.serialize();
fail('wrong recovery key algorithm should fail');
} catch (exception) {
expect(exception.message).toEqual('Longform DIDs only support ES256K. Recovery algorithm: EdDSA');
}
crypto = new CryptoBuilder()
.useSigningAlgorithm('EdDSA')
.build();
longform = new LongFormDid(crypto);
try {
await longform.serialize();
fail('wrong signing key algorithm should fail');
} catch (exception) {
expect(exception.message).toEqual('Longform DIDs only support ES256K. Signing algorithm: EdDSA');
}
});
it('should add services to the longform DID', async () => {
let crypto = new CryptoBuilder()
.useSigningKeyReference(new KeyReference('mars'))
.useRecoveryKeyReference(new KeyReference('recovery'))
.useUpdateKeyReference(new KeyReference('update'))
.build();
crypto = await crypto.generateKey(KeyUse.Signature);
crypto = await crypto.generateKey(KeyUse.Signature, 'recovery');
crypto = await crypto.generateKey(KeyUse.Signature, 'update');
let did1 = await new LongFormDid(crypto).serialize();
let did2 = await new LongFormDid(crypto).serialize();
expect(did1).toEqual(did2);
const services = {
id: "service1Id",
type: "service1Type",
serviceEndpoint: "http://www.service1.com"
}
did2 = await new LongFormDid(crypto, [services]).serialize();
expect(did1).not.toEqual(did2);
});
});