forked from jsnbuchanan/crowd-funder-for-time-pwa
feat: implement DID registration with JWT signing
- Add full DID registration flow matching TypeScript version - Implement ES256K JWT signing with PEM key format - Add async/await support for JWT operations - Improve error handling and debug output - Add rich documentation and type hints Technical Changes: - Convert private key to PEM format for JWT signing - Match TypeScript's JWT payload structure - Add proper JWT header with ES256K algorithm - Implement async functions for JWT creation - Add detailed debug output for JWT parts Documentation: - Add module-level docstring with flow description - Add function-level docstrings with examples - Document security considerations - Add technical details and error handling info Dependencies: - Add cryptography for key format conversion - Add jwcrypto for JWT operations - Update requirements.txt with versions and comments This commit implements the complete DID registration flow, matching the TypeScript implementation's behavior and adding comprehensive documentation and error handling.
This commit is contained in:
335
test-scripts/new_flow.ts
Normal file
335
test-scripts/new_flow.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
function deriveAddress(
|
||||
mnemonic: string,
|
||||
derivationPath: string = DEFAULT_ROOT_DERIVATION_PATH
|
||||
): [string, string, string, string] {
|
||||
mnemonic = mnemonic.trim().toLowerCase();
|
||||
const hdnode: HDNode = HDNode.fromMnemonic(mnemonic);
|
||||
const rootNode: HDNode = hdnode.derivePath(derivationPath);
|
||||
|
||||
const privateHex = rootNode.privateKey.substring(2); // remove '0x'
|
||||
const publicHex = rootNode.publicKey.substring(2); // remove '0x'
|
||||
const address = rootNode.address;
|
||||
|
||||
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] = 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
|
||||
};
|
||||
Reference in New Issue
Block a user