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.
 
 
 
 

502 lines
14 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';
import { mkdir, writeFile } from 'fs/promises';
import { join } from 'path';
// 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[];
}
// Create .generated directory if it doesn't exist
const GENERATED_DIR = '.generated';
// Initialize directory creation
async function initializeDirectories() {
try {
await mkdir(GENERATED_DIR, { recursive: true });
} catch (err) {
console.error('Error creating .generated directory:', err);
throw err;
}
}
/**
* 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)}...`);
// Save derivation data
const derivationData = {
mnemonic,
derivationPath,
address,
privateKey: privateHex,
publicKey: publicHex
};
try {
await mkdir(GENERATED_DIR, { recursive: true });
await writeFile(
join(GENERATED_DIR, 'key_derivation.json'),
JSON.stringify(derivationData, null, 2)
);
} catch (err) {
console.error('Error saving derivation data:', err);
}
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();
// Save account data
await writeFile(
join(GENERATED_DIR, 'account_init.json'),
JSON.stringify(accountData, null, 2)
);
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
const registrationData = {
activeDid,
jwtToken,
response: response.data.success
};
await writeFile(
join(GENERATED_DIR, 'registration.json'),
JSON.stringify(registrationData, null, 2)
);
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) {
const registrationData = {
activeDid,
jwtToken,
response: response.data.success
};
await writeFile(
join(GENERATED_DIR, 'registration.json'),
JSON.stringify(registrationData, null, 2)
);
return {
success: true,
...response.data.success
};
} else {
const registrationData = {
activeDid,
jwtToken,
response: response.data
};
await writeFile(
join(GENERATED_DIR, 'registration.json'),
JSON.stringify(registrationData, null, 2)
);
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();
const registrationData = {
activeDid,
jwtToken,
response: { error: `Error submitting claim: ${error.message}` }
};
await writeFile(
join(GENERATED_DIR, 'registration.json'),
JSON.stringify(registrationData, null, 2)
);
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'
}
}
);
const claimData = response.data;
// Save claim data
try {
await writeFile(
join(GENERATED_DIR, 'claim_details.json'),
JSON.stringify({
claim_id: claimId,
active_did: activeDid,
response: claimData
}, null, 2)
);
} catch (err) {
console.error('Error saving claim data:', err);
}
return claimData;
} 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);
}
// Save error state
try {
await writeFile(
join(GENERATED_DIR, 'claim_details.json'),
JSON.stringify({
claim_id: claimId,
active_did: activeDid,
error: error.message || 'Unknown error',
response: error.response?.data
}, null, 2)
);
} catch (writeErr) {
console.error('Error saving claim error:', writeErr);
}
throw error;
}
}
/**
* Generate test environment data for deeplink testing
*/
async function generateTestEnv(
identity: IIdentifier,
jwtToken: string,
apiServer: string = API_SERVER
): Promise<void> {
// Create test data structure
const testEnvData = {
CONTACT1_DID: identity.did,
CONTACT1_KEY: identity.keys[0].privateKeyHex,
CONTACT2_DID: `did:ethr:${Wallet.createRandom().address}`, // Generate random DID for contact 2
CONTACT2_KEY: Wallet.createRandom().privateKey.substring(2), // Remove '0x'
ISSUER_DID: ENDORSER_DID,
ISSUER_KEY: ENDORSER_PRIVATE_KEY,
TEST_JWT: jwtToken,
API_SERVER: apiServer
};
// Write test environment variables
const envContent = Object.entries(testEnvData)
.map(([key, value]) => `export ${key}="${value}"`)
.join('\n');
await writeFile(
join(GENERATED_DIR, 'test-env.sh'),
envContent + '\n',
'utf-8'
);
// Write test contacts data
const contactsData = {
contacts: [
{ did: testEnvData.CONTACT1_DID, name: 'Test Contact 1' },
{ did: testEnvData.CONTACT2_DID, name: 'Test Contact 2' }
]
};
await writeFile(
join(GENERATED_DIR, 'contacts.json'),
JSON.stringify(contactsData, null, 2),
'utf-8'
);
}
/**
* Main execution flow
*/
async function main() {
// Initialize directories first
await initializeDirectories();
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));
// Generate test environment data
await generateTestEnv(identity, result.claimId);
}
} 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,
generateTestEnv
};