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.pull/127/head
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