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.

257 lines
7.9 KiB

/**
* DID Generator Script
* @author Matthew Raymer
*
* This script generates and registers Decentralized Identifiers (DIDs) with admin authorization.
* It supports the creation of Ethereum-based DIDs (did:ethr) and handles the complete
* registration flow with the endorser.ch API.
*
* Features:
* - Ethereum keypair generation using ethers.js
* - JWT creation and signing using did-jwt
* - DID registration with admin authorization
* - Detailed error handling and logging
* - Command-line interface for admin DID input
*
* Dependencies:
* did-jwt: For JWT creation and signing
* ethers: For Ethereum account operations
* node-fetch: For API communication
*/
/// <reference types="node" />
// Add at the top of your file to ignore dom types
import * as didJwt from 'did-jwt';
import { ethers } from 'ethers';
import fetch from 'node-fetch';
import { Database } from 'sqlite3';
import { homedir } from 'os';
import { join } from 'path';
import { existsSync } from 'fs';
import { program } from 'commander';
/**
* Result interface for DID creation process
*/
interface DIDCreationResult {
did: string; // The generated DID
privateKey: string; // Private key without 0x prefix
publicKey: string; // Public key with 0x prefix
isValid: boolean; // Validation status
jwt: string; // Signed JWT for registration
}
/**
* Result interface for DID registration attempt
*/
interface RegistrationResult {
success: boolean; // Registration success status
error?: string; // Optional error message
response?: any; // Optional API response data
}
/**
* Creates and validates a new DID with admin authorization
*
* Workflow:
* 1. Validates admin DID input
* 2. Generates Ethereum keypair
* 3. Creates registration claim
* 4. Signs claim as JWT
*
* @param adminDid - Administrator DID for authorization
* @returns Promise<DIDCreationResult> - Generated DID details and JWT
* @throws Error if admin DID is missing
*/
async function createAndValidateDID(adminDid: string): Promise<DIDCreationResult> {
if (!adminDid) {
throw new Error('Admin DID is required for registration');
}
console.log('Using admin DID:', adminDid);
// 1. Generate keypair
console.log('Generating new keypair...');
const wallet = ethers.Wallet.createRandom();
const did = `did:ethr:${wallet.address}`;
const privateKey = wallet.privateKey.slice(2);
const publicKey = wallet.publicKey;
// Create registration claim with admin as agent
const registerClaim = {
"@context": "https://schema.org",
"@type": "RegisterAction",
agent: { did: adminDid },
participant: { did: did },
object: "endorser.ch"
};
const vcPayload = {
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 300,
sub: "RegisterAction",
vc: {
"@context": ["https://www.w3.org/2018/credentials/v1"],
type: ["VerifiableCredential"],
credentialSubject: registerClaim
}
};
console.log('\nGenerated DID Details:');
console.log('----------------------');
console.log('DID:', did);
console.log('Admin DID:', adminDid);
console.log('Address:', wallet.address);
console.log('Private Key:', wallet.privateKey);
console.log('Public Key:', wallet.publicKey);
console.log('\nDebug Details:');
console.log('-------------');
console.log('Private Key (hex):', privateKey); // Should be without 0x
console.log('Public Key (hex):', publicKey); // Should be with 0x
console.log('Header:', {
typ: "JWT",
alg: "ES256K"
});
console.log('Payload:', vcPayload);
// Create and sign JWT
console.log('\nCreating JWT...');
const signer = didJwt.SimpleSigner(privateKey);
const jwt = await didJwt.createJWT(vcPayload, {
issuer: did,
signer: signer
});
console.log('\nJWT Parts:');
const [header, payload, signature] = jwt.split('.');
console.log('Header (base64):', header);
console.log('Payload (base64):', payload);
console.log('Signature (base64):', signature);
return { did, privateKey, publicKey, isValid: true, jwt };
}
/**
* Registers a DID with the endorser.ch API
*
* Workflow:
* 1. Submits JWT to API endpoint
* 2. Processes response
* 3. Handles success/error cases
*
* @param jwt - Signed JWT for registration
* @returns Promise<RegistrationResult> - Registration result
*/
async function registerDID(jwt: string): Promise<RegistrationResult> {
try {
const response = await fetch('https://api.endorser.ch/api/v2/claim', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ jwtEncoded: jwt }),
});
console.log(`\nServer Response Status: ${response.status}`);
const responseText = await response.text();
console.log(`Server Response Body: ${responseText}`);
if (response.ok) {
return {
success: true,
response: JSON.parse(responseText)
};
} else {
try {
const errorJson = JSON.parse(responseText);
const errorMsg = errorJson.error?.message || 'Unknown error';
return {
success: false,
error: `Registration failed (${response.status}): ${errorMsg}`,
response: errorJson
};
} catch {
return {
success: false,
error: `Registration failed (${response.status}): ${responseText}`,
response: responseText
};
}
}
} catch (e) {
return {
success: false,
error: `Request failed: ${e}`
};
}
}
async function getRootDidFromDb(dbPath?: string): Promise<string> {
return new Promise((resolve, reject) => {
const defaultPath = join(homedir(), '.endorser', 'accounts.db');
const path = dbPath || defaultPath;
if (!existsSync(path)) {
reject(new Error(`Database not found at ${path}`));
return;
}
const db = new Database(path);
db.get(
'SELECT issuer as did FROM registration WHERE type = "RegisterAction" ORDER BY issuanceDate DESC LIMIT 1',
(err, row: { did: string }) => {
db.close();
if (err) reject(err);
else if (!row) reject(new Error('No admin DID found in registration table'));
else resolve(row.did);
}
);
});
}
// Command line handling
program
.option('--admin-did <did>', 'Admin DID (e.g., did:ethr:0x0000...)')
.option('--db-path <path>', 'Path to SQLite database (e.g., ../endorser-ch-test-local.sqlite3)')
.parse(process.argv);
const options = program.opts();
console.log('Starting DID Generation...\n');
(async () => {
let adminDid = options.adminDid;
if (!adminDid && options.dbPath) {
try {
adminDid = await getRootDidFromDb(options.dbPath);
console.log(`Found root DID in database: ${adminDid}`);
} catch (e) {
console.error(`Error: ${e.message}`);
console.error('Please provide --admin-did argument');
process.exit(1);
}
}
try {
const result = await createAndValidateDID(adminDid);
console.log('\nSuccessfully generated DID with admin authorization!');
console.log('Registration JWT:', result.jwt.substring(0, 50) + '...');
console.log('\nAttempting registration...');
const registrationResult = await registerDID(result.jwt);
if (registrationResult.success) {
console.log('Registration successful!');
console.log('Response:', JSON.stringify(registrationResult.response, null, 2));
} else {
console.log('Registration failed!');
console.log('Error:', registrationResult.error);
if (registrationResult.response) {
console.log('Full response:', JSON.stringify(registrationResult.response, null, 2));
}
}
} catch (error) {
console.error('\nError:', error);
process.exit(1);
}
})();