@dwn-protocol/id-sdk
Version:
SDK for accessing the features and capabilities
459 lines • 21.9 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { Convert, universalTypeOf } from '../common/index.js';
import IonProofOfWork from '@decentralized-identity/ion-pow-sdk';
import { EcdsaAlgorithm, EdDsaAlgorithm, Jose } from '../crypto/index.js';
import { IonDid, IonPublicKeyPurpose, IonRequest } from '@decentralized-identity/ion-sdk';
import { getServices, isDwnServiceEndpoint, parseDid } from './utils.js';
var OperationType;
(function (OperationType) {
OperationType["Create"] = "create";
OperationType["Update"] = "update";
OperationType["Deactivate"] = "deactivate";
OperationType["Recover"] = "recover";
})(OperationType || (OperationType = {}));
const SupportedCryptoAlgorithms = [
'Ed25519',
'secp256k1'
];
const VerificationRelationshipToIonPublicKeyPurpose = {
assertionMethod: IonPublicKeyPurpose.AssertionMethod,
authentication: IonPublicKeyPurpose.Authentication,
capabilityDelegation: IonPublicKeyPurpose.CapabilityDelegation,
capabilityInvocation: IonPublicKeyPurpose.CapabilityInvocation,
keyAgreement: IonPublicKeyPurpose.KeyAgreement
};
export class DidIonMethod {
static anchor(options) {
return __awaiter(this, void 0, void 0, function* () {
const { challengeEnabled = true, challengeEndpoint = 'https://beta.ion.msidentity.com/api/v1.0/proof-of-work-challenge', keySet, services, operationsEndpoint = 'https://beta.ion.msidentity.com/api/v1.0/operations' } = options;
// Create ION Document.
const ionDocument = yield DidIonMethod.createIonDocument({
keySet: keySet,
services
});
const createRequest = yield DidIonMethod.getIonCreateRequest({
ionDocument,
recoveryPublicKeyJwk: keySet.recoveryKey.publicKeyJwk,
updatePublicKeyJwk: keySet.updateKey.publicKeyJwk
});
let resolutionResult;
if (challengeEnabled) {
const response = yield IonProofOfWork.submitIonRequest(challengeEndpoint, operationsEndpoint, JSON.stringify(createRequest));
if (response !== undefined && universalTypeOf(response) === 'String') {
resolutionResult = JSON.parse(response);
}
}
else {
const response = yield fetch(operationsEndpoint, {
method: 'POST',
mode: 'cors',
body: JSON.stringify(createRequest),
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
resolutionResult = yield response.json();
}
}
return resolutionResult;
});
}
static create(options) {
return __awaiter(this, void 0, void 0, function* () {
let { anchor, keyAlgorithm, keySet, services } = options !== null && options !== void 0 ? options : {};
// Begin constructing a PortableDid.
const did = {};
// If any member of the key set is missing, generate the keys.
did.keySet = yield DidIonMethod.generateKeySet({ keyAlgorithm, keySet });
// Generate Long Form DID URI.
did.did = yield DidIonMethod.getLongFormDid({
keySet: did.keySet,
services
});
// Get short form DID.
did.canonicalId = yield DidIonMethod.getShortFormDid({ didUrl: did.did });
let didResolutionResult;
if (anchor) {
// Attempt to anchor the DID.
didResolutionResult = yield DidIonMethod.anchor({
keySet: did.keySet,
services
});
}
else {
// If anchoring was not requested, then resolve the long form DID.
didResolutionResult = yield DidIonMethod.resolve({ didUrl: did.did });
}
// Store the DID Document.
did.document = didResolutionResult.didDocument;
return did;
});
}
static decodeLongFormDid(options) {
return __awaiter(this, void 0, void 0, function* () {
const { didUrl } = options;
const parsedDid = parseDid({ didUrl });
if (!parsedDid) {
throw new Error(`DidIonMethod: Unable to parse DID: ${didUrl}`);
}
const decodedLongFormDid = Convert.base64Url(parsedDid.id.split(':').pop()).toObject();
const createRequest = Object.assign(Object.assign({}, decodedLongFormDid), { type: OperationType.Create });
return createRequest;
});
}
/**
* Generates two key pairs used for authorization and encryption purposes
* when interfacing with DWNs. The IDs of these keys are referenced in the
* service object that includes the dwnUrls provided.
*/
static generateDwnOptions(options) {
return __awaiter(this, void 0, void 0, function* () {
const { signingKeyAlgorithm = 'Ed25519', // Generate Ed25519 key pairs, by default.
serviceId = '#dwn', // Use default ID value, unless overridden.
signingKeyId = '#dwn-sig', // Use default key ID value, unless overridden.
encryptionKeyId = '#dwn-enc', // Use default key ID value, unless overridden.
serviceEndpointNodes } = options;
const signingKeyPair = yield DidIonMethod.generateJwkKeyPair({
keyAlgorithm: signingKeyAlgorithm,
keyId: signingKeyId
});
/** Currently, `id` has only implemented support for record
* encryption using the `ECIES-ES256K` crypto algorithm. Until the
* DWN SDK supports ECIES with EdDSA, the encryption key pair must
* use secp256k1. */
const encryptionKeyPair = yield DidIonMethod.generateJwkKeyPair({
keyAlgorithm: 'secp256k1',
keyId: encryptionKeyId
});
const keySet = {
verificationMethodKeys: [
Object.assign(Object.assign({}, signingKeyPair), { relationships: ['authentication'] }),
Object.assign(Object.assign({}, encryptionKeyPair), { relationships: ['keyAgreement'] })
]
};
const serviceEndpoint = {
encryptionKeys: [encryptionKeyId],
nodes: serviceEndpointNodes,
signingKeys: [signingKeyId]
};
const services = [{
id: serviceId,
serviceEndpoint,
type: 'DecentralizedWebNode',
}];
return { keySet, services };
});
}
static generateJwkKeyPair(options) {
return __awaiter(this, void 0, void 0, function* () {
const { keyAlgorithm, keyId } = options;
let cryptoKeyPair;
switch (keyAlgorithm) {
case 'Ed25519': {
cryptoKeyPair = yield new EdDsaAlgorithm().generateKey({
algorithm: { name: 'EdDSA', namedCurve: 'Ed25519' },
extractable: true,
keyUsages: ['sign', 'verify']
});
break;
}
case 'secp256k1': {
cryptoKeyPair = yield new EcdsaAlgorithm().generateKey({
algorithm: { name: 'ECDSA', namedCurve: 'secp256k1' },
extractable: true,
keyUsages: ['sign', 'verify']
});
break;
}
default: {
throw new Error(`Unsupported crypto algorithm: '${keyAlgorithm}'`);
}
}
// Convert the CryptoKeyPair to JwkKeyPair.
const jwkKeyPair = yield Jose.cryptoKeyToJwkPair({ keyPair: cryptoKeyPair });
// Set kid values.
if (keyId) {
jwkKeyPair.privateKeyJwk.kid = keyId;
jwkKeyPair.publicKeyJwk.kid = keyId;
}
else {
// If a key ID is not specified, generate RFC 7638 JWK thumbprint.
const jwkThumbprint = yield Jose.jwkThumbprint({ key: jwkKeyPair.publicKeyJwk });
jwkKeyPair.privateKeyJwk.kid = jwkThumbprint;
jwkKeyPair.publicKeyJwk.kid = jwkThumbprint;
}
return jwkKeyPair;
});
}
static generateKeySet(options) {
var _a, _b;
var _c, _d;
return __awaiter(this, void 0, void 0, function* () {
// Generate Ed25519 authentication key pair, by default.
let { keyAlgorithm = 'Ed25519', keySet = {} } = options !== null && options !== void 0 ? options : {};
// If keySet lacks verification method keys, generate one.
if (keySet.verificationMethodKeys === undefined) {
const authenticationkeyPair = yield DidIonMethod.generateJwkKeyPair({
keyAlgorithm,
keyId: 'dwn-sig'
});
keySet.verificationMethodKeys = [Object.assign(Object.assign({}, authenticationkeyPair), { relationships: ['authentication', 'assertionMethod'] })];
}
// If keySet lacks recovery key, generate one.
if (keySet.recoveryKey === undefined) {
// Note: ION/Sidetree only supports secp256k1 recovery keys.
keySet.recoveryKey = yield DidIonMethod.generateJwkKeyPair({
keyAlgorithm: 'secp256k1',
keyId: 'ion-recovery-1'
});
}
// If keySet lacks update key, generate one.
if (keySet.updateKey === undefined) {
// Note: ION/Sidetree only supports secp256k1 update keys.
keySet.updateKey = yield DidIonMethod.generateJwkKeyPair({
keyAlgorithm: 'secp256k1',
keyId: 'ion-update-1'
});
}
// Generate RFC 7638 JWK thumbprints if `kid` is missing from any key.
for (const key of [...keySet.verificationMethodKeys, keySet.recoveryKey, keySet.updateKey]) {
if ('publicKeyJwk' in key)
(_a = (_c = key.publicKeyJwk).kid) !== null && _a !== void 0 ? _a : (_c.kid = yield Jose.jwkThumbprint({ key: key.publicKeyJwk }));
if ('privateKeyJwk' in key)
(_b = (_d = key.privateKeyJwk).kid) !== null && _b !== void 0 ? _b : (_d.kid = yield Jose.jwkThumbprint({ key: key.privateKeyJwk }));
}
return keySet;
});
}
/**
* Given the W3C DID Document of a `did:ion` DID, return the identifier of
* the verification method key that will be used for signing messages and
* credentials, by default.
*
* @param document = DID Document to get the default signing key from.
* @returns Verification method identifier for the default signing key.
*/
static getDefaultSigningKey(options) {
return __awaiter(this, void 0, void 0, function* () {
const { didDocument } = options;
if (!didDocument.id) {
throw new Error(`DidIonMethod: DID document is missing 'id' property`);
}
/** If the DID document contains a DWN service endpoint in the expected
* format, return the first entry in the `signingKeys` array. */
const [dwnService] = getServices({ didDocument, type: 'DecentralizedWebNode' });
if (isDwnServiceEndpoint(dwnService === null || dwnService === void 0 ? void 0 : dwnService.serviceEndpoint)) {
const [verificationMethodId] = dwnService.serviceEndpoint.signingKeys;
const did = didDocument.id;
const signingKeyId = `${did}${verificationMethodId}`;
return signingKeyId;
}
/** Otherwise, fallback to a naive approach of returning the first key ID
* in the `authentication` verification relationships array. */
if (didDocument.authentication
&& Array.isArray(didDocument.authentication)
&& didDocument.authentication.length > 0
&& typeof didDocument.authentication[0] === 'string') {
const [verificationMethodId] = didDocument.authentication;
const did = didDocument.id;
const signingKeyId = `${did}${verificationMethodId}`;
return signingKeyId;
}
});
}
static getLongFormDid(options) {
return __awaiter(this, void 0, void 0, function* () {
const { services = [], keySet } = options;
// Create ION Document.
const ionDocument = yield DidIonMethod.createIonDocument({
keySet: keySet,
services
});
// Filter JWK to only those properties expected by ION/Sidetree.
const recoveryKey = DidIonMethod.jwkToIonJwk({ key: keySet.recoveryKey.publicKeyJwk });
const updateKey = DidIonMethod.jwkToIonJwk({ key: keySet.updateKey.publicKeyJwk });
// Create an ION DID create request operation.
const did = yield IonDid.createLongFormDid({
document: ionDocument,
recoveryKey,
updateKey
});
return did;
});
}
static getShortFormDid(options) {
return __awaiter(this, void 0, void 0, function* () {
const { didUrl } = options;
const parsedDid = parseDid({ didUrl });
if (!parsedDid) {
throw new Error(`DidIonMethod: Unable to parse DID: ${didUrl}`);
}
const shortFormDid = parsedDid.did.split(':', 3).join(':');
return shortFormDid;
});
}
static resolve(options) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
// TODO: add resolutionOptions as defined in https://www.w3.org/TR/did-core/#did-resolution
const { didUrl, resolutionOptions = {} } = options;
const parsedDid = parseDid({ didUrl });
if (!parsedDid) {
return {
'@context': 'https://w3id.org/did-resolution/v1',
didDocument: undefined,
didDocumentMetadata: {},
didResolutionMetadata: {
contentType: 'application/did+json',
error: 'invalidDid',
errorMessage: `Cannot parse DID: ${didUrl}`
}
};
}
if (parsedDid.method !== 'ion') {
return {
'@context': 'https://w3id.org/did-resolution/v1',
didDocument: undefined,
didDocumentMetadata: {},
didResolutionMetadata: {
contentType: 'application/did+json',
error: 'methodNotSupported',
errorMessage: `Method not supported: ${parsedDid.method}`
}
};
}
const { resolutionEndpoint = 'https://discover.did.msidentity.com/1.0/identifiers/' } = resolutionOptions;
const normalizeUrl = (url) => url.endsWith('/') ? url : url + '/';
const resolutionUrl = `${normalizeUrl(resolutionEndpoint)}${parsedDid.did}`;
const response = yield fetch(resolutionUrl);
let resolutionResult;
try {
resolutionResult = yield response.json();
}
catch (error) {
resolutionResult = {};
}
if (response.ok) {
return resolutionResult;
}
// Response was not "OK" (HTTP 4xx-5xx status code)
// Return result if it contains DID resolution metadata.
if ('didResolutionMetadata' in resolutionResult) {
return resolutionResult;
}
// Set default error code and message.
let error = 'internalError';
let errorMessage = `DID resolver responded with HTTP status code: ${response.status}`;
/** The Microsoft resolution endpoint does not return a valid DidResolutionResult
* when an ION DID is "not found" so normalization is needed. */
if ('error' in resolutionResult &&
typeof resolutionResult.error === 'object' &&
'code' in resolutionResult.error &&
typeof resolutionResult.error.code === 'string' &&
'message' in resolutionResult.error &&
typeof resolutionResult.error.message === 'string') {
error = resolutionResult.error.code.includes('not_found') ? 'notFound' : error;
errorMessage = (_a = resolutionResult.error.message) !== null && _a !== void 0 ? _a : errorMessage;
}
return {
'@context': 'https://w3id.org/did-resolution/v1',
didDocument: undefined,
didDocumentMetadata: {},
didResolutionMetadata: {
contentType: 'application/did+json',
error,
errorMessage
}
};
});
}
static createIonDocument(options) {
return __awaiter(this, void 0, void 0, function* () {
const { services = [], keySet } = options;
/**
* STEP 1: Convert key set verification method keys to ION SDK format.
*/
const ionPublicKeys = [];
for (const key of keySet.verificationMethodKeys) {
// Map W3C DID verification relationship names to ION public key purposes.
const ionPurposes = [];
for (const relationship of key.relationships) {
ionPurposes.push(VerificationRelationshipToIonPublicKeyPurpose[relationship]);
}
/** During certain ION operations, JWK validation will throw an error
* if key IDs provided as input are prefixed with `#`. ION operation
* outputs and DID document resolution always include the `#` prefix
* for key IDs resulting in a confusing mismatch between inputs and
* outputs. To improve the developer experience, this inconsistency
* is addressed by normalizing input key IDs before being passed
* to ION SDK methods. */
const publicKeyId = (key.publicKeyJwk.kid.startsWith('#'))
? key.publicKeyJwk.kid.substring(1)
: key.publicKeyJwk.kid;
// Convert public key JWK to ION format.
const publicKey = {
id: publicKeyId,
publicKeyJwk: DidIonMethod.jwkToIonJwk({ key: key.publicKeyJwk }),
purposes: ionPurposes,
type: 'JsonWebKey2020'
};
ionPublicKeys.push(publicKey);
}
/**
* STEP 2: Convert service entries, if any, to ION SDK format.
*/
const ionServices = services.map(service => (Object.assign(Object.assign({}, service), { id: service.id.startsWith('#') ? service.id.substring(1) : service.id })));
/**
* STEP 3: Format as ION document.
*/
const ionDocumentModel = {
publicKeys: ionPublicKeys,
services: ionServices
};
return ionDocumentModel;
});
}
static getIonCreateRequest(options) {
return __awaiter(this, void 0, void 0, function* () {
const { ionDocument, recoveryPublicKeyJwk, updatePublicKeyJwk } = options;
// Create an ION DID create request operation.
const createRequest = yield IonRequest.createCreateRequest({
document: ionDocument,
recoveryKey: DidIonMethod.jwkToIonJwk({ key: recoveryPublicKeyJwk }),
updateKey: DidIonMethod.jwkToIonJwk({ key: updatePublicKeyJwk })
});
return createRequest;
});
}
static jwkToIonJwk({ key }) {
let ionJwk = {};
if ('crv' in key) {
ionJwk.crv = key.crv;
ionJwk.kty = key.kty;
ionJwk.x = key.x;
if ('d' in key)
ionJwk.d = key.d;
if ('y' in key && key.y) {
// secp256k1 JWK.
return Object.assign(Object.assign({}, ionJwk), { y: key.y });
}
// Ed25519 JWK.
return Object.assign({}, ionJwk);
}
throw new Error(`jwkToIonJwk: Unsupported key algorithm.`);
}
}
/**
* Name of the DID method
*/
DidIonMethod.methodName = 'ion';
//# sourceMappingURL=did-ion.js.map