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
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
|
|
};
|