Browse Source
			
			
			
			
				
		- 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.
				 6 changed files with 2081 additions and 65 deletions
			
			
		@ -0,0 +1 @@ | 
				
			|||||
 | 
					usage gas they pyramid walnut mammal absorb major crystal nurse element congress assist panic bomb entire area slogan film educate decrease buddy describe finish | 
				
			||||
@ -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 | 
				
			||||
 | 
					};  | 
				
			||||
								
									
										File diff suppressed because it is too large
									
								
							
						
					@ -0,0 +1,23 @@ | 
				
			|||||
 | 
					{ | 
				
			||||
 | 
					  "name": "did-registration", | 
				
			||||
 | 
					  "version": "1.0.0", | 
				
			||||
 | 
					  "description": "DID creation and registration flow", | 
				
			||||
 | 
					  "main": "new_flow.js", | 
				
			||||
 | 
					  "scripts": { | 
				
			||||
 | 
					    "start": "ts-node new_flow.ts", | 
				
			||||
 | 
					    "build": "tsc" | 
				
			||||
 | 
					  }, | 
				
			||||
 | 
					  "author": "Matthew Raymer", | 
				
			||||
 | 
					  "license": "MIT", | 
				
			||||
 | 
					  "dependencies": { | 
				
			||||
 | 
					    "@ethersproject/hdnode": "^5.7.0", | 
				
			||||
 | 
					    "@ethersproject/wallet": "^5.7.0", | 
				
			||||
 | 
					    "axios": "^1.6.2", | 
				
			||||
 | 
					    "did-jwt": "^6.11.6" | 
				
			||||
 | 
					  }, | 
				
			||||
 | 
					  "devDependencies": { | 
				
			||||
 | 
					    "@types/node": "^20.10.0", | 
				
			||||
 | 
					    "ts-node": "^10.9.1", | 
				
			||||
 | 
					    "typescript": "^5.3.2" | 
				
			||||
 | 
					  } | 
				
			||||
 | 
					}  | 
				
			||||
@ -1,6 +1,11 @@ | 
				
			|||||
eth-account>=0.8.0 | 
					# DID Generator Python Requirements | 
				
			||||
eth-keys>=0.4.0 | 
					mnemonic>=0.20        # For seed phrase generation | 
				
			||||
PyJWT>=2.8.0 | 
					eth-account>=0.5.9    # For Ethereum account operations | 
				
			||||
requests>=2.31.0 | 
					eth-keys>=0.4.0       # For key manipulation | 
				
			||||
cryptography>=41.0.0 | 
					requests>=2.28.2      # For API communication | 
				
			||||
python-dotenv==1.0.0 | 
					typing-extensions>=4.7.1  # For type hints | 
				
			||||
 | 
					web3>=6.0.0          # For Ethereum interaction | 
				
			||||
 | 
					eth-utils>=2.1.0     # For Ethereum utilities | 
				
			||||
 | 
					pyjwt>=2.8.0        # For JWT operations | 
				
			||||
 | 
					cryptography>=42.0.0  # For key format conversion | 
				
			||||
 | 
					jwcrypto | 
				
			||||
					Loading…
					
					
				
		Reference in new issue