@sphereon/ssi-sdk.ebsi-support
Version:
458 lines • 23.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
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());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ebsiCreateDidOnLedger = exports.randomRpcId = exports.setDefaultPurposes = exports.assertedPurposes = exports.toMinimalImportableKey = exports.ebsiGenerateOrUseKeyPair = exports.ebsiSignAndSendTransaction = exports.determineWellknownEndpoint = exports.ebsiGetRegistryAPIUrls = exports.ebsiGetAuthorisationServer = exports.ebsiGetIssuerMock = exports.formatEbsiPublicKey = void 0;
exports.generateEbsiMethodSpecificId = generateEbsiMethodSpecificId;
exports.generateOrUseProvidedEbsiPrivateKeyHex = generateOrUseProvidedEbsiPrivateKeyHex;
const random_1 = require("@ethersproject/random");
const oid4vci_common_1 = require("@sphereon/oid4vci-common");
const ssi_sdk_ext_did_utils_1 = require("@sphereon/ssi-sdk-ext.did-utils");
const ssi_sdk_ext_key_utils_1 = require("@sphereon/ssi-sdk-ext.key-utils");
const ethers_1 = require("ethers");
const base58_1 = require("multiformats/bases/base58");
const u8a = __importStar(require("uint8arrays"));
const functions_1 = require("../functions");
const index_1 = require("../index");
const EbsiRestService_1 = require("./services/EbsiRestService");
const EbsiRPCService_1 = require("./services/EbsiRPCService");
const types_1 = require("./types");
function generateEbsiMethodSpecificId(specInfo) {
var _a;
const spec = specInfo !== null && specInfo !== void 0 ? specInfo : types_1.EBSI_DID_SPEC_INFOS.V1;
const length = (_a = spec.didLength) !== null && _a !== void 0 ? _a : 16;
const result = new Uint8Array(length + (spec.version ? 1 : 0));
if (spec.version) {
result.set([spec.version]);
}
result.set((0, random_1.randomBytes)(length), spec.version ? 1 : 0);
return base58_1.base58btc.encode(result);
}
function generateOrUseProvidedEbsiPrivateKeyHex(specInfo, privateKeyBytes) {
const spec = specInfo !== null && specInfo !== void 0 ? specInfo : types_1.EBSI_DID_SPEC_INFOS.V1;
const length = spec.didLength ? 2 * spec.didLength : 32;
if (privateKeyBytes) {
if (privateKeyBytes.length !== length) {
throw Error(`Invalid private key length supplied (${privateKeyBytes.length}. Expected ${length} for ${spec.type}`);
}
return u8a.toString(privateKeyBytes, 'base16');
}
return u8a.toString((0, random_1.randomBytes)(length), 'base16');
}
/**
* Returns the public key in the correct format to be used with the did registry v5
* - in case of Secp256k1 - returns the uncompressed public key as hex string prefixed with 0x04
* - in case of Secp256r1 - returns the jwk public key as hex string
* @param {{ key: IKey, type: EbsiKeyType }} args
* - key is the cryptographic key containing the public key
* - type is the type of the key which can be Secp256k1 or Secp256r1
* @returns {string} The properly formatted public key
* @throws {Error} If the key type is invalid
*/
const formatEbsiPublicKey = (args) => {
const { key, type } = args;
switch (type) {
case 'Secp256k1': {
const bytes = (0, ethers_1.getBytes)('0x' + key.publicKeyHex, 'key');
return ethers_1.SigningKey.computePublicKey(bytes, false);
}
case 'Secp256r1': {
/*
Public key as hex string. For an ES256K key, it must be in uncompressed format prefixed with "0x04".
For other algorithms, it must be the JWK transformed to string and then to hex format.
*/
const jwk = (0, ssi_sdk_ext_key_utils_1.toJwk)(key.publicKeyHex, type, { use: ssi_sdk_ext_key_utils_1.JwkKeyUse.Signature, key });
/*
Converting JWK to string and then hex is odd and may lead to errors. Implementing
it like that because it's how EBSI does it. However, it may be a point of pain
in the future.
*/
const jwkString = JSON.stringify(jwk, null, 2);
return `0x${u8a.toString(u8a.fromString(jwkString), 'base16')}`;
}
default:
throw new Error(`Unsupported EBSI key type: ${type}`);
}
};
exports.formatEbsiPublicKey = formatEbsiPublicKey;
const ebsiGetIssuerMock = (args) => {
const { environment = 'conformance', version = 'v3' } = args;
if (environment === 'pilot') {
throw Error(`EBSI Pilot network does not have a issuer mock server`);
}
return `${(0, functions_1.getEbsiApiBaseUrl)({ environment, version, system: environment })}/issuer-mock`;
};
exports.ebsiGetIssuerMock = ebsiGetIssuerMock;
const ebsiGetAuthorisationServer = (args) => {
const { environment = 'pilot', version = 'v4' } = args;
return `${(0, functions_1.getEbsiApiBaseUrl)({ environment, version, system: 'authorisation' })}`;
};
exports.ebsiGetAuthorisationServer = ebsiGetAuthorisationServer;
const ebsiGetRegistryAPIUrls = (args) => {
const { environment = 'pilot', version = 'v5' } = args;
const baseUrl = `${(0, functions_1.getEbsiApiBaseUrl)({ environment, version, system: 'did-registry' })}`;
return {
mutate: `${baseUrl}/jsonrpc`,
query: `${baseUrl}/identifiers`,
};
};
exports.ebsiGetRegistryAPIUrls = ebsiGetRegistryAPIUrls;
const determineWellknownEndpoint = ({ environment, version, type, system = environment, mock }) => {
const url = `${(0, functions_1.getEbsiApiBaseUrl)({ environment, version, system })}${mock ? `/${mock}` : ''}/.well-known/${type}`;
index_1.logger.debug(`wellknown url: ${url}`);
return url;
};
exports.determineWellknownEndpoint = determineWellknownEndpoint;
const ebsiSignAndSendTransaction = (args, context) => __awaiter(void 0, void 0, void 0, function* () {
var _a;
const { rpcRequest, accessToken, kid, apiOpts, previousTxResponse } = args;
const unsignedTxResponse = yield (0, EbsiRPCService_1.callRpcMethod)(rpcRequest);
const nonce = 'result' in unsignedTxResponse ? unsignedTxResponse.result.nonce : undefined;
// We should get a new nonce once the actual previous transaction has been anchored. Thus we retry if the nonce remains the same
if (previousTxResponse &&
'result' in unsignedTxResponse &&
'nonce' in previousTxResponse &&
'nonce' in unsignedTxResponse.result &&
typeof unsignedTxResponse.result === 'object' &&
previousTxResponse.nonce === unsignedTxResponse.result.nonce) {
yield (0, functions_1.wait)(1000);
return yield (0, exports.ebsiSignAndSendTransaction)(Object.assign(Object.assign({}, args), { previousTxResponse }), context);
}
if ('error' in unsignedTxResponse && !!unsignedTxResponse.error) {
index_1.logger.error(JSON.stringify(unsignedTxResponse));
throw new Error((_a = unsignedTxResponse.error.message) !== null && _a !== void 0 ? _a : 'Unknown error occurred');
}
const unsignedTx = unsignedTxResponse.result;
const agentUnsignedTx = JSON.parse(JSON.stringify(unsignedTx));
if (unsignedTx && 'chainId' in unsignedTx && typeof unsignedTx.chainId === 'string' && unsignedTx.chainId.toLowerCase().startsWith('0x')) {
// We expect the chain id to be a regular number and not a hex string
agentUnsignedTx.chainId = Number.parseInt(unsignedTx.chainId, 16);
}
const signedRawTx = yield context.agent.keyManagerSignEthTX({
kid,
transaction: agentUnsignedTx,
});
const sig = ethers_1.Transaction.from(signedRawTx).signature;
const { r, s, v } = sig;
const sTResponse = yield (0, EbsiRPCService_1.callRpcMethod)({
params: [
{
protocol: 'eth',
unsignedTransaction: unsignedTx,
r,
s,
v: `0x${v.toString(16)}`,
signedRawTransaction: signedRawTx,
},
],
rpcMethod: types_1.EbsiRpcMethod.SEND_SIGNED_TRANSACTION,
rpcId: unsignedTxResponse.id,
apiOpts,
accessToken: accessToken,
});
if ('status' in sTResponse) {
throw new Error(JSON.stringify(sTResponse, null, 2));
}
return Object.assign(Object.assign({}, sTResponse), { nonce });
});
exports.ebsiSignAndSendTransaction = ebsiSignAndSendTransaction;
const ebsiGenerateOrUseKeyPair = (args, context) => __awaiter(void 0, void 0, void 0, function* () {
const { keyOpts, keyType, kms, controllerKey = false } = args;
let privateKeyHex = generateOrUseProvidedEbsiPrivateKeyHex(types_1.EBSI_DID_SPEC_INFOS.V1, (keyOpts === null || keyOpts === void 0 ? void 0 : keyOpts.privateKeyHex) ? u8a.fromString(keyOpts.privateKeyHex, 'base16') : undefined);
if (privateKeyHex.startsWith('0x')) {
privateKeyHex = privateKeyHex.substring(2);
}
if (!privateKeyHex || privateKeyHex.length !== 64) {
throw new Error('Private key should be 32 bytes / 64 chars hex');
}
const importableKey = yield (0, exports.toMinimalImportableKey)({ key: Object.assign(Object.assign({}, keyOpts), { privateKeyHex }), type: keyType, kms });
if (keyType === 'Secp256k1') {
importableKey.meta = Object.assign(Object.assign({}, importableKey.meta), { ebsi: {
anchored: false,
controllerKey,
} });
}
return importableKey;
});
exports.ebsiGenerateOrUseKeyPair = ebsiGenerateOrUseKeyPair;
const toMinimalImportableKey = (args) => __awaiter(void 0, void 0, void 0, function* () {
var _a, _b, _c;
const { key, kms } = args;
const minimalImportableKey = Object.assign({}, key);
const type = (_b = (_a = args.key) === null || _a === void 0 ? void 0 : _a.type) !== null && _b !== void 0 ? _b : args.type;
minimalImportableKey.kms = kms;
minimalImportableKey.type = type;
if (!minimalImportableKey.privateKeyHex) {
throw Error(`Minimal importable key needs a private key`);
}
minimalImportableKey.meta = {
purposes: (_c = (0, exports.assertedPurposes)({ key })) !== null && _c !== void 0 ? _c : (0, exports.setDefaultPurposes)({ key, type }),
jwkThumbprint: (0, ssi_sdk_ext_key_utils_1.calculateJwkThumbprintForKey)({
key: minimalImportableKey,
digestAlgorithm: 'sha256',
}),
};
return minimalImportableKey;
});
exports.toMinimalImportableKey = toMinimalImportableKey;
const assertedPurposes = (args) => {
var _a;
const { key } = args;
if ((key === null || key === void 0 ? void 0 : key.purposes) && key.purposes.length > 0) {
switch (key.type) {
case 'Secp256k1': {
if ((key === null || key === void 0 ? void 0 : key.purposes) && key.purposes.length > 0 && ((_a = key.purposes) === null || _a === void 0 ? void 0 : _a.includes(types_1.EbsiPublicKeyPurpose.CapabilityInvocation))) {
return key.purposes;
}
throw new Error(`Secp256k1/ES256K key requires ${types_1.EbsiPublicKeyPurpose.CapabilityInvocation} purpose`);
}
case 'Secp256r1': {
if ((key === null || key === void 0 ? void 0 : key.purposes) &&
key.purposes.length > 0 &&
key.purposes.includes(types_1.EbsiPublicKeyPurpose.AssertionMethod) &&
key.purposes.includes(types_1.EbsiPublicKeyPurpose.Authentication)) {
return key.purposes;
}
throw new Error(`Secp256r1/ES256 key requires ${[types_1.EbsiPublicKeyPurpose.AssertionMethod, types_1.EbsiPublicKeyPurpose.Authentication].join(', ')} purposes`);
}
default:
throw new Error(`Unsupported key type: ${key.type}`);
}
}
return key === null || key === void 0 ? void 0 : key.purposes;
};
exports.assertedPurposes = assertedPurposes;
const setDefaultPurposes = (args) => {
const { key, type } = args;
if (!(key === null || key === void 0 ? void 0 : key.purposes) || key.purposes.length === 0) {
switch (type) {
case 'Secp256k1':
return [types_1.EbsiPublicKeyPurpose.CapabilityInvocation];
case 'Secp256r1':
return [types_1.EbsiPublicKeyPurpose.AssertionMethod, types_1.EbsiPublicKeyPurpose.Authentication];
default:
throw new Error(`Unsupported key type: ${key === null || key === void 0 ? void 0 : key.type}`);
}
}
return key.purposes;
};
exports.setDefaultPurposes = setDefaultPurposes;
const randomRpcId = () => {
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
};
exports.randomRpcId = randomRpcId;
const ebsiCreateDidOnLedger = (args, context) => __awaiter(void 0, void 0, void 0, function* () {
var _a, _b, _c;
const { accessTokenOpts, notBefore = Math.floor(Date.now() / 1000 - 60), notAfter = Math.floor(Date.now() / 1000 + 10 * 365 * 24 * 60 * 60), baseDocument, identifier, } = args;
const { clientId, redirectUri, environment, credentialIssuer } = accessTokenOpts;
const controllerKey = (0, ssi_sdk_ext_did_utils_1.getControllerKey)({ identifier });
const secp256r1 = (_a = (0, ssi_sdk_ext_did_utils_1.getKeys)({ identifier, keyType: 'Secp256r1' })) === null || _a === void 0 ? void 0 : _a[0];
let { attestationToOnboard, attestationToOnboardCredentialRole } = accessTokenOpts;
if (!controllerKey || !secp256r1) {
return Promise.reject(`No secp256k1 controller key and/or secp2561r key found for identifier ${identifier}`);
}
const from = (0, ssi_sdk_ext_did_utils_1.getEthereumAddressFromKey)({ key: controllerKey });
if (!from) {
return Promise.reject(Error(`EBSI 'from' address expected for key ${controllerKey.publicKeyHex}`));
}
const did = identifier.did;
const kid = controllerKey.kid;
const idOpts = { identifier, kid };
let rpcId = (_b = args.rpcId) !== null && _b !== void 0 ? _b : (0, exports.randomRpcId)();
const apiOpts = {
environment,
version: 'v5',
};
const jwksUri = (_c = args.accessTokenOpts.jwksUri) !== null && _c !== void 0 ? _c : `${clientId}/.well-known/jwks/dids/${encodeURIComponent(identifier.did)}.json`;
if (!attestationToOnboard) {
console.log(`No attestation to onboard present. Will get one`);
const authReqResult = yield context.agent.ebsiCreateAttestationAuthRequestURL({
credentialIssuer,
idOpts: idOpts,
formats: ['jwt_vc'],
clientId,
redirectUri,
credentialType: 'VerifiableAuthorisationToOnboard',
requestObjectOpts: { iss: clientId, requestObjectMode: oid4vci_common_1.CreateRequestObjectMode.REQUEST_OBJECT, jwksUri },
});
const attestationResult = yield context.agent.ebsiGetAttestation({
clientId,
authReqResult,
opts: { timeout: 120000 },
});
attestationToOnboard = attestationResult.credentials[0].rawVerifiableCredential;
console.log(`Attestation to onboard received`, attestationToOnboard);
}
const insertDidAccessTokenResponse = yield context.agent.ebsiAccessTokenGet({
credentialRole: attestationToOnboardCredentialRole,
attestationCredential: attestationToOnboard,
jwksUri,
scope: 'didr_invite',
idOpts: idOpts,
redirectUri,
credentialIssuer,
clientId,
environment,
skipDidResolution: true,
});
const insertDidDocRequest = {
params: [
{
from,
did,
baseDocument: baseDocument !== null && baseDocument !== void 0 ? baseDocument : types_1.BASE_CONTEXT_DOC,
vMethodId: (0, ssi_sdk_ext_key_utils_1.calculateJwkThumbprint)({ jwk: (0, ssi_sdk_ext_key_utils_1.toJwk)(controllerKey.publicKeyHex, 'Secp256k1') }),
isSecp256k1: true,
publicKey: (0, exports.formatEbsiPublicKey)({ key: controllerKey, type: 'Secp256k1' }),
notBefore,
notAfter,
},
],
rpcMethod: types_1.EbsiRpcMethod.INSERT_DID_DOCUMENT,
rpcId,
apiOpts,
accessToken: insertDidAccessTokenResponse.accessTokenResponse.access_token,
};
const insertDidDocResponse = yield (0, exports.ebsiSignAndSendTransaction)({
rpcRequest: insertDidDocRequest,
kid,
accessToken: insertDidAccessTokenResponse.accessTokenResponse.access_token,
apiOpts,
}, context);
let anchorTime = yield (0, EbsiRestService_1.ebsiWaitTillDocumentAnchored)(Object.assign(Object.assign({ did }, apiOpts), { maxWaitTime: 30000, startIntervalMS: 2000, minIntervalMS: 500, decreaseIntervalMSPerStep: 750 }));
if (!anchorTime.didDocument) {
throw Error(`did ${did} was not registered on EBSI network ${apiOpts.environment} in 45 seconds`);
}
index_1.logger.debug(`Anchoring did ${did} on network ${apiOpts.environment} took ${anchorTime.totalWaitTime / 1000} seconds in ${anchorTime.count} tries`);
// Update to the controller key for the remainder
idOpts.kid = (0, ssi_sdk_ext_key_utils_1.calculateJwkThumbprintForKey)({ key: controllerKey });
const addVMAccessTokenResponse = yield context.agent.ebsiAccessTokenGet({
credentialRole: attestationToOnboardCredentialRole,
// attestationCredential: attestationToOnboard,
jwksUri,
scope: 'didr_write',
idOpts: idOpts,
redirectUri,
credentialIssuer: undefined,
clientId,
environment,
skipDidResolution: true,
});
const vMethodId = (0, ssi_sdk_ext_key_utils_1.calculateJwkThumbprint)({ jwk: (0, ssi_sdk_ext_key_utils_1.toJwk)(secp256r1.publicKeyHex, 'Secp256r1') });
const publicKey = (0, exports.formatEbsiPublicKey)({ key: secp256r1, type: 'Secp256r1' });
const addVerificationMethodRequest = {
params: [
{
from,
did,
isSecp256k1: false,
vMethodId,
publicKey,
},
],
rpcMethod: types_1.EbsiRpcMethod.ADD_VERIFICATION_METHOD,
rpcId,
apiOpts,
accessToken: addVMAccessTokenResponse.accessTokenResponse.access_token,
};
const addVerificationMethodResponse = yield (0, exports.ebsiSignAndSendTransaction)({
rpcRequest: addVerificationMethodRequest,
previousTxResponse: insertDidDocResponse,
kid,
accessToken: addVMAccessTokenResponse.accessTokenResponse.access_token,
apiOpts,
}, context);
// We need to wait, even after the anchor. The methods below also retry in case the nonce does not get updated.
// But we simply know that at this point we need to introduce some delay
yield (0, functions_1.wait)(2000);
const addAssertionMethodRelationshipRequest = {
params: [
{
from,
did,
vMethodId,
name: 'assertionMethod',
notAfter,
notBefore,
},
],
rpcMethod: types_1.EbsiRpcMethod.ADD_VERIFICATION_RELATIONSHIP,
rpcId,
apiOpts,
accessToken: addVMAccessTokenResponse.accessTokenResponse.access_token,
};
const addAssertionMethodRelationshipResponse = yield (0, exports.ebsiSignAndSendTransaction)({
rpcRequest: addAssertionMethodRelationshipRequest,
previousTxResponse: addVerificationMethodResponse,
kid,
accessToken: addVMAccessTokenResponse.accessTokenResponse.access_token,
apiOpts,
}, context);
anchorTime = yield (0, EbsiRestService_1.ebsiWaitTillDocumentAnchored)(Object.assign(Object.assign({ did }, apiOpts), { maxWaitTime: 20000, minIntervalMS: 500, decreaseIntervalMSPerStep: 500, searchForObject: { assertionMethod: [`${did}#${vMethodId}`] } }));
if (!anchorTime.didDocument) {
throw Error(`did ${did} assertionMethod id ${vMethodId} was not registered on EBSI network ${apiOpts.environment} in 20 seconds`);
}
index_1.logger.debug(`Anchoring assertionMethod ${vMethodId} for DID ${did} on network ${apiOpts.environment} took ${anchorTime.totalWaitTime / 1000} seconds in ${anchorTime.count} tries`);
const addAuthenticationRelationshipRequest = {
params: [
{
from,
did,
vMethodId,
name: 'authentication',
notAfter,
notBefore,
},
],
rpcMethod: types_1.EbsiRpcMethod.ADD_VERIFICATION_RELATIONSHIP,
rpcId,
apiOpts,
accessToken: addVMAccessTokenResponse.accessTokenResponse.access_token,
};
const addAuthenticationRelationshipResponse = yield (0, exports.ebsiSignAndSendTransaction)({
rpcRequest: addAuthenticationRelationshipRequest,
previousTxResponse: addAssertionMethodRelationshipResponse,
kid,
accessToken: addVMAccessTokenResponse.accessTokenResponse.access_token,
apiOpts,
}, context);
return {
identifier,
insertDidDoc: insertDidDocResponse,
addVerificationMethod: addVerificationMethodResponse,
addAuthenticationRelationship: addAuthenticationRelationshipResponse,
addAssertionMethodRelationship: addAssertionMethodRelationshipResponse,
};
});
exports.ebsiCreateDidOnLedger = ebsiCreateDidOnLedger;
//# sourceMappingURL=functions.js.map