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 | 
				
			|||
eth-keys>=0.4.0 | 
				
			|||
PyJWT>=2.8.0 | 
				
			|||
requests>=2.31.0 | 
				
			|||
cryptography>=41.0.0 | 
				
			|||
python-dotenv==1.0.0 | 
				
			|||
# DID Generator Python Requirements | 
				
			|||
mnemonic>=0.20        # For seed phrase generation | 
				
			|||
eth-account>=0.5.9    # For Ethereum account operations | 
				
			|||
eth-keys>=0.4.0       # For key manipulation | 
				
			|||
requests>=2.28.2      # For API communication | 
				
			|||
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