You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

343 lines
9.6 KiB

/**
* DID Creation and Registration Flow
* @author Matthew Raymer
*
* This module implements the creation and registration of Decentralized Identifiers (DIDs)
* with the endorser.ch service. It matches the Python implementation in new_flow.py.
*/
import { HDNode } from '@ethersproject/hdnode';
import { Wallet } from '@ethersproject/wallet';
import { createJWT, ES256KSigner, SimpleSigner } from 'did-jwt';
import axios from 'axios';
// Constants
const DEFAULT_ROOT_DERIVATION_PATH = "m/84737769'/0'/0'/0'";
const API_SERVER = "https://test-api.endorser.ch";
const ENDORSER_DID = "did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F";
const ENDORSER_PRIVATE_KEY = "2b6472c026ec2aa2c4235c994a63868fc9212d18b58f6cbfe861b52e71330f5b";
interface IKey {
id: string;
type: string;
controller: string;
ethereumAddress: string;
publicKeyHex: string;
privateKeyHex: string;
}
interface IIdentifier {
did: string;
keys: IKey[];
services: any[];
}
/**
* Generate a new mnemonic seed phrase
*/
function generateMnemonic(): string {
return Wallet.createRandom().mnemonic.phrase;
}
/**
* Derive Ethereum address and keys from a mnemonic phrase
*/
async function deriveAddress(
mnemonic: string,
derivationPath: string = DEFAULT_ROOT_DERIVATION_PATH
): Promise<[string, string, string, string]> {
mnemonic = mnemonic.trim().toLowerCase();
const hdnode: HDNode = HDNode.fromMnemonic(mnemonic);
const rootNode: HDNode = hdnode.derivePath(derivationPath);
console.log("\nKey Derivation:");
console.log(` Mnemonic (first 4 words): ${mnemonic.split(' ').slice(0,4).join(' ')}...`);
console.log(` Derivation Path: ${derivationPath}`);
const privateHex = rootNode.privateKey.substring(2); // remove '0x'
const publicHex = rootNode.publicKey.substring(2); // remove '0x'
const address = rootNode.address;
console.log(` Address: ${address}`);
console.log(` Private Key: ${privateHex.substring(0,8)}...`);
console.log(` Public Key: ${publicHex.substring(0,8)}...`);
return [address, privateHex, publicHex, derivationPath];
}
/**
* Create a new DID identifier with associated keys
*/
function newIdentifier(
address: string,
publicKeyHex: string,
privateKeyHex: string,
derivationPath: string
): IIdentifier {
const did = `did:ethr:${address}`;
const keyId = `${did}#keys-1`;
const identifier: IIdentifier = {
did,
keys: [{
id: keyId,
type: "Secp256k1VerificationKey2018",
controller: did,
ethereumAddress: address,
publicKeyHex,
privateKeyHex
}],
services: []
};
return identifier;
}
/**
* Initialize a new DID account or load existing one
*/
async function initializeAccount(): Promise<IIdentifier> {
const mnemonic = generateMnemonic();
const [address, privateHex, publicHex, derivationPath] = await deriveAddress(mnemonic);
const identity = newIdentifier(address, publicHex, privateHex, derivationPath);
// Format and display account data
const accountData = {
did: identity.did,
identity,
mnemonic,
derivation_path: derivationPath
};
console.log("\nAccount initialized:");
console.log(JSON.stringify(accountData, null, 2));
console.log();
return identity;
}
/**
* Create a signed JWT for DID registration
*/
async function createEndorserJwt(
did: string,
privateKeyHex: string,
payload: any,
subDid?: string,
expiresIn: number = 3600
): Promise<string> {
const signer = await SimpleSigner(privateKeyHex);
const jwt = await createJWT(
payload,
{
issuer: did,
signer,
expiresIn,
...(subDid && { subject: subDid })
}
);
return jwt;
}
/**
* Register a DID with the endorser service
*/
async function register(
activeDid: string,
privateKeyHex: string,
apiServer: string = API_SERVER
): Promise<{
success?: boolean;
handleId?: string;
registrationId?: number;
claimId?: string;
error?: string;
details?: any;
}> {
console.log("\nEndorser DID:", ENDORSER_DID);
console.log("Active DID:", activeDid);
const vcPayload = {
vc: {
"@context": ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiableCredential"],
credentialSubject: {
"@context": "https://schema.org",
"@type": "RegisterAction",
"agent": {
"identifier": ENDORSER_DID
},
"participant": {
"identifier": activeDid
},
"object": "endorser.ch",
"endTime": Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
}
}
};
// Debug output
console.log("\nRegistration Claim:");
console.log(JSON.stringify(vcPayload, null, 2));
console.log();
// Sign with endorser's DID and private key
const jwtToken = await createEndorserJwt(
ENDORSER_DID,
ENDORSER_PRIVATE_KEY,
vcPayload,
activeDid
);
// Debug output
console.log("Generated JWT:");
console.log(jwtToken);
console.log();
// Debug JWT parts
const [header, payload, signature] = jwtToken.split('.');
console.log("\nJWT Header:", JSON.parse(Buffer.from(header, 'base64url').toString()));
console.log("JWT Payload:", JSON.parse(Buffer.from(payload, 'base64url').toString()));
console.log("JWT Signature length:", Buffer.from(signature, 'base64url').length, "bytes");
console.log();
try {
const response = await axios.post(
`${apiServer}/api/v2/claim`,
{ jwtEncoded: jwtToken },
{ headers: { 'Content-Type': 'application/json' } }
);
if (response.status === 200 && response.data.success?.handleId) {
// Return all success details
return {
success: true,
handleId: response.data.success.handleId,
registrationId: response.data.success.registrationId,
claimId: response.data.success.claimId
};
}
// Log full error response
console.error("\nAPI Error Response:");
console.error(JSON.stringify(response.data, null, 2));
console.error();
// If we have success data but no handleId, it might be a partial success
if (response.data.success) {
return {
success: true,
...response.data.success
};
} else {
return { error: "Registration failed", details: response.data };
}
} catch (error: any) {
// Log detailed error information
console.error("\nAPI Request Error:");
if (error.response) {
console.error("Status:", error.response.status);
console.error("Headers:", error.response.headers);
console.error("Data:", JSON.stringify(error.response.data, null, 2));
} else if (error.request) {
console.error("No response received");
console.error(error.request);
} else {
console.error("Error setting up request:", error.message);
}
console.error();
return { error: `Error submitting claim: ${error.message}` };
}
}
/**
* Fetch claim details from the endorser service
*/
async function fetchClaim(
claimId: string,
activeDid: string,
privateKeyHex: string,
apiServer: string = API_SERVER
): Promise<any> {
// Create authentication JWT
const authPayload = {
intent: 'claim.read',
claimId: claimId
};
// Sign with the active DID (not endorser DID)
const authToken = await createEndorserJwt(
activeDid,
privateKeyHex,
authPayload
);
try {
const response = await axios.get(
`${apiServer}/api/claim/byHandle/${claimId}`,
{
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
}
);
return response.data;
} catch (error: any) {
console.error("\nError fetching claim:");
if (error.response) {
console.error("Status:", error.response.status);
console.error("Data:", JSON.stringify(error.response.data, null, 2));
} else {
console.error(error.message);
}
throw error;
}
}
/**
* Main execution flow
*/
async function main() {
try {
// Create a new DID
const identity = await initializeAccount();
const activeDid = identity.did;
const privateKeyHex = identity.keys[0].privateKeyHex;
// Register the DID
const result = await register(activeDid, privateKeyHex);
console.log("Registration result:", result);
// If registration was successful, fetch the claim details
if (result.success && result.claimId) {
console.log("\nFetching claim details...");
const claimDetails = await fetchClaim(
result.claimId,
activeDid,
privateKeyHex
);
console.log("\nClaim Details:");
console.log(JSON.stringify(claimDetails, null, 2));
}
} catch (error: any) {
console.error("Error:", error.message);
}
}
// Run if called directly
if (require.main === module) {
main().catch(console.error);
}
export {
generateMnemonic,
deriveAddress,
newIdentifier,
initializeAccount,
createEndorserJwt,
register,
fetchClaim
};