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 |
# 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