forked from trent_larson/crowd-funder-for-time-pwa
fix: WIP: update did_generator.ts to use registration table
Changes: - Update SQL query to use registration table instead of accounts - Add proper column names for registration table schema - Add issuanceDate sorting for latest admin DID - Improve error messages for database queries - Add TypeScript types for database row results This fixes DID generation by using the correct table schema from the endorser database.
This commit is contained in:
@@ -1,3 +1,36 @@
|
||||
"""
|
||||
DID Generator Script
|
||||
@author Matthew Raymer
|
||||
|
||||
This script generates and registers Decentralized Identifiers (DIDs) with admin authorization.
|
||||
It supports the creation of Ethereum-based DIDs (did:ethr) and handles the complete
|
||||
registration flow with the endorser.ch API.
|
||||
|
||||
Features:
|
||||
- Ethereum keypair generation with compressed public keys
|
||||
- JWT creation and signing using ES256K
|
||||
- DID registration with admin authorization
|
||||
- Detailed error handling and logging
|
||||
- Command-line interface for admin DID input
|
||||
|
||||
Dependencies:
|
||||
eth_account: For Ethereum account operations
|
||||
eth_keys: For key manipulation and compression
|
||||
requests: For API communication
|
||||
secrets: For secure random number generation
|
||||
hashlib: For SHA-256 hashing
|
||||
base64: For JWT encoding
|
||||
argparse: For command-line argument parsing
|
||||
sqlite3: For database operations
|
||||
pathlib: For path handling
|
||||
|
||||
Usage:
|
||||
python did_generator.py <admin_did>
|
||||
|
||||
Example:
|
||||
python did_generator.py did:ethr:0x1234...5678
|
||||
"""
|
||||
|
||||
from eth_account import Account
|
||||
import json
|
||||
import base64
|
||||
@@ -8,28 +41,69 @@ import requests
|
||||
import argparse
|
||||
import secrets
|
||||
import hashlib
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
class DIDRegistration:
|
||||
"""
|
||||
Handles the creation and registration of DIDs with admin authorization.
|
||||
|
||||
This class manages the complete lifecycle of DID creation:
|
||||
1. Generating secure Ethereum keypairs
|
||||
2. Creating DIDs from public keys
|
||||
3. Signing registration claims
|
||||
4. Submitting registration to the endorser.ch API
|
||||
|
||||
Attributes:
|
||||
api_url (str): Endpoint for DID registration
|
||||
admin_did (str): Administrator DID for authorization
|
||||
"""
|
||||
|
||||
def __init__(self, admin_did: str):
|
||||
"""
|
||||
Initialize DID registration with admin credentials.
|
||||
|
||||
Args:
|
||||
admin_did (str): Administrator DID for authorizing registrations
|
||||
Format: did:ethr:0x...
|
||||
"""
|
||||
self.api_url = "https://api.endorser.ch/api/v2/claim"
|
||||
self.admin_did = admin_did
|
||||
Account.enable_unaudited_hdwallet_features()
|
||||
|
||||
def create_keypair(self):
|
||||
"""Generate a new Ethereum keypair"""
|
||||
private_key = secrets.token_hex(32)
|
||||
def create_keypair(self) -> dict:
|
||||
"""
|
||||
Generate a new Ethereum keypair and associated DID.
|
||||
|
||||
Creates a secure random keypair and formats it for use with the
|
||||
endorser.ch API, including compressed public key format matching
|
||||
ethers.js implementation.
|
||||
|
||||
Returns:
|
||||
dict: Keypair information containing:
|
||||
- private_key: Raw private key without 0x prefix
|
||||
- public_key: Compressed public key with 0x prefix
|
||||
- address: Ethereum address
|
||||
- did: Generated DID in did:ethr format
|
||||
|
||||
Security:
|
||||
- Uses secrets module for cryptographically secure randomness
|
||||
- Implements compressed public key format
|
||||
- Maintains private key security
|
||||
"""
|
||||
private_key = secrets.token_hex(32)
|
||||
|
||||
# Create private key object and derive public key
|
||||
private_key_bytes = bytes.fromhex(private_key)
|
||||
private_key_obj = keys.PrivateKey(private_key_bytes)
|
||||
|
||||
|
||||
# Get compressed public key (like ethers.js)
|
||||
public_key_obj = private_key_obj.public_key
|
||||
public_key = '0x' + public_key_obj.to_compressed_bytes().hex()
|
||||
|
||||
|
||||
# Create account from private key (for address)
|
||||
account = Account.from_key('0x' + private_key)
|
||||
|
||||
account = Account.from_key(private_key_bytes)
|
||||
|
||||
return {
|
||||
'private_key': private_key, # No 0x prefix
|
||||
'public_key': public_key, # With 0x prefix, compressed format
|
||||
@@ -38,43 +112,86 @@ class DIDRegistration:
|
||||
}
|
||||
|
||||
def sign_jwt(self, payload: dict, private_key: str, did: str) -> str:
|
||||
"""Sign JWT using ES256K like the TS version"""
|
||||
"""
|
||||
Sign a JWT using ES256K algorithm.
|
||||
|
||||
Creates and signs a JWT following the did-jwt specification:
|
||||
1. Constructs header and payload
|
||||
2. Base64url encodes components
|
||||
3. Signs using ES256K
|
||||
4. Assembles final JWT
|
||||
|
||||
Args:
|
||||
payload (dict): JWT payload to sign
|
||||
private_key (str): Private key for signing (without 0x prefix)
|
||||
did (str): DID to use as issuer
|
||||
|
||||
Returns:
|
||||
str: Signed JWT string in format: header.payload.signature
|
||||
|
||||
Security:
|
||||
- Implements ES256K signing
|
||||
- Follows did-jwt specification
|
||||
- Handles message hashing correctly
|
||||
"""
|
||||
# Add issuer to payload like did-jwt does
|
||||
full_payload = {
|
||||
**payload,
|
||||
"iss": did
|
||||
}
|
||||
|
||||
|
||||
header = {
|
||||
"typ": "JWT",
|
||||
"alg": "ES256K"
|
||||
}
|
||||
|
||||
|
||||
# Create the JWT segments
|
||||
header_b64 = base64.urlsafe_b64encode(json.dumps(header, separators=(',', ':')).encode()).decode().rstrip('=')
|
||||
payload_b64 = base64.urlsafe_b64encode(json.dumps(full_payload, separators=(',', ':')).encode()).decode().rstrip('=')
|
||||
header_b64 = base64.urlsafe_b64encode(
|
||||
json.dumps(header, separators=(',', ':')).encode()
|
||||
).decode().rstrip('=')
|
||||
payload_b64 = base64.urlsafe_b64encode(
|
||||
json.dumps(full_payload, separators=(',', ':')).encode()
|
||||
).decode().rstrip('=')
|
||||
message = f"{header_b64}.{payload_b64}"
|
||||
|
||||
|
||||
# Hash the message with sha256 first (like did-jwt)
|
||||
message_hash = hashlib.sha256(message.encode()).digest()
|
||||
|
||||
|
||||
# Sign the hash directly (not as an Ethereum message)
|
||||
private_key_bytes = bytes.fromhex(private_key)
|
||||
signed = Account._sign_hash(message_hash, private_key_bytes) # Use internal _sign_hash
|
||||
|
||||
account = Account.from_key(private_key_bytes)
|
||||
signed = account.sign_message(message_hash)
|
||||
|
||||
# Get r and s from signature
|
||||
r = signed.r.to_bytes(32, 'big')
|
||||
s = signed.s.to_bytes(32, 'big')
|
||||
signature_bytes = r + s
|
||||
|
||||
|
||||
signature = base64.urlsafe_b64encode(signature_bytes).decode().rstrip('=')
|
||||
|
||||
|
||||
return f"{message}.{signature}"
|
||||
|
||||
def create_jwt(self, keypair: dict) -> str:
|
||||
"""Create a signed JWT for registration"""
|
||||
now = int(time.time())
|
||||
"""
|
||||
Create a signed JWT for DID registration.
|
||||
|
||||
Creates a registration claim that includes:
|
||||
1. Admin DID as agent
|
||||
2. New DID as participant
|
||||
3. Timestamp and expiration
|
||||
4. Required credential context
|
||||
|
||||
Args:
|
||||
keypair (dict): Generated keypair information
|
||||
|
||||
Returns:
|
||||
str: Signed JWT containing registration claim
|
||||
|
||||
Note:
|
||||
Matches TypeScript implementation exactly for compatibility
|
||||
"""
|
||||
now = int(time.time())
|
||||
|
||||
# Create registration claim with admin as agent
|
||||
register_claim = {
|
||||
"@context": "https://schema.org",
|
||||
@@ -97,22 +214,43 @@ class DIDRegistration:
|
||||
}
|
||||
|
||||
print(f"\nDebug - JWT payload: {json.dumps(payload, indent=2)}")
|
||||
|
||||
|
||||
# Sign with new DID's private key
|
||||
return self.sign_jwt(payload, keypair['private_key'], keypair['did'])
|
||||
|
||||
def register_did(self, jwt_token: str) -> dict:
|
||||
"""Submit the registration to the server"""
|
||||
"""
|
||||
Submit DID registration to the endorser.ch API.
|
||||
|
||||
Handles the complete registration process:
|
||||
1. Submits JWT to API
|
||||
2. Processes response
|
||||
3. Formats result or error message
|
||||
|
||||
Args:
|
||||
jwt_token (str): Signed JWT for registration
|
||||
|
||||
Returns:
|
||||
dict: Registration result containing:
|
||||
- success: Boolean indicating success
|
||||
- response: API response data
|
||||
- error: Error message if failed
|
||||
|
||||
Security:
|
||||
- Uses HTTPS for API communication
|
||||
- Validates response status
|
||||
- Handles errors gracefully
|
||||
"""
|
||||
try:
|
||||
response = requests.post(
|
||||
self.api_url,
|
||||
json={"jwtEncoded": jwt_token},
|
||||
headers={'Content-Type': 'application/json'}
|
||||
)
|
||||
|
||||
|
||||
print(f"\nServer Response Status: {response.status_code}")
|
||||
print(f"Server Response Body: {response.text}")
|
||||
|
||||
|
||||
if response.status_code in [200, 201]:
|
||||
return {
|
||||
'success': True,
|
||||
@@ -121,58 +259,116 @@ class DIDRegistration:
|
||||
else:
|
||||
try:
|
||||
error_json = response.json()
|
||||
error_msg = error_json.get('error', {}).get('message', 'Unknown error')
|
||||
error_msg = error_json.get('error', {}).get(
|
||||
'message', 'Unknown error'
|
||||
)
|
||||
return {
|
||||
'success': False,
|
||||
'error': f"Registration failed ({response.status_code}): {error_msg}",
|
||||
'error': f"Registration failed ({response.status_code}): "
|
||||
f"{error_msg}",
|
||||
'response': error_json
|
||||
}
|
||||
except json.JSONDecodeError:
|
||||
return {
|
||||
'success': False,
|
||||
'error': f"Registration failed ({response.status_code}): {response.text}",
|
||||
'error': f"Registration failed ({response.status_code}): "
|
||||
f"{response.text}",
|
||||
'response': response.text
|
||||
}
|
||||
except Exception as e:
|
||||
except (requests.RequestException, ConnectionError) as e:
|
||||
return {
|
||||
'success': False,
|
||||
'error': f"Request failed: {str(e)}"
|
||||
}
|
||||
|
||||
def get_root_did_from_db(db_path: str = None) -> str:
|
||||
"""Get admin DID from the most recent registration."""
|
||||
try:
|
||||
db_path = db_path or str(Path.home() / '.endorser' / 'accounts.db')
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT issuer as did
|
||||
FROM registration
|
||||
WHERE type = 'RegisterAction'
|
||||
ORDER BY issuanceDate DESC
|
||||
LIMIT 1
|
||||
""")
|
||||
|
||||
result = cursor.fetchone()
|
||||
if not result:
|
||||
raise ValueError('No admin DID found in registration table')
|
||||
|
||||
return result[0]
|
||||
finally:
|
||||
if 'conn' in locals():
|
||||
conn.close()
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Generate a DID with admin authorization')
|
||||
parser.add_argument('admin_did',
|
||||
help='Admin DID (e.g., did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F)')
|
||||
"""
|
||||
Main entry point for DID generation script.
|
||||
|
||||
Handles:
|
||||
1. Command line argument parsing
|
||||
2. DID generation and registration process
|
||||
3. Result output and error display
|
||||
|
||||
Usage:
|
||||
python did_generator.py <admin_did>
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Generate a DID with admin authorization'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--admin-did',
|
||||
help='Admin DID (e.g., did:ethr:0x0000...)',
|
||||
required=False
|
||||
)
|
||||
parser.add_argument(
|
||||
'--db-path',
|
||||
help='Path to SQLite database containing root DID',
|
||||
required=False
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
admin_did = args.admin_did
|
||||
if not admin_did:
|
||||
try:
|
||||
admin_did = get_root_did_from_db(args.db_path)
|
||||
print(f"Found root DID in database: {admin_did}")
|
||||
except (FileNotFoundError, ValueError, sqlite3.Error) as e:
|
||||
print(f"Error: {str(e)}")
|
||||
print("Please provide --admin-did argument")
|
||||
return
|
||||
|
||||
print('Starting DID Generation...\n')
|
||||
print(f"Using admin DID: {args.admin_did}")
|
||||
registrar = DIDRegistration(args.admin_did)
|
||||
|
||||
registrar = DIDRegistration(admin_did)
|
||||
|
||||
print("Generating new keypair...")
|
||||
keypair = registrar.create_keypair()
|
||||
|
||||
|
||||
print("\nGenerated DID Details:")
|
||||
print("----------------------")
|
||||
print(f"DID: {keypair['did']}")
|
||||
print(f"Admin DID: {args.admin_did}")
|
||||
print(f"Admin DID: {admin_did}")
|
||||
print(f"Address: {keypair['address']}")
|
||||
print(f"Private Key: {keypair['private_key']}")
|
||||
print(f"Public Key: {keypair['public_key']}\n") # Store without any 0x prefix
|
||||
|
||||
print(f"Public Key: {keypair['public_key']}\n")
|
||||
|
||||
print("Creating JWT...")
|
||||
jwt_token = registrar.create_jwt(keypair)
|
||||
|
||||
|
||||
print('\nSuccessfully generated DID with admin authorization!')
|
||||
print(f'Registration JWT: {jwt_token[:50]}...')
|
||||
|
||||
print("\nAttempting registration...")
|
||||
result = registrar.register_did(jwt_token)
|
||||
if result['success']:
|
||||
print(f"Registration successful!")
|
||||
print(f"Response: {json.dumps(result['response'], indent=2)}")
|
||||
print("Registration successful!")
|
||||
print("Response: {json.dumps(result['response'], indent=2)}")
|
||||
else:
|
||||
print(f"Registration failed!")
|
||||
print("Registration failed!")
|
||||
print(f"Error: {result['error']}")
|
||||
if 'response' in result:
|
||||
print(f"Full response: {json.dumps(result['response'], indent=2)}")
|
||||
|
||||
@@ -1,23 +1,68 @@
|
||||
/**
|
||||
* DID Generator Script
|
||||
* @author Matthew Raymer
|
||||
*
|
||||
* This script generates and registers Decentralized Identifiers (DIDs) with admin authorization.
|
||||
* It supports the creation of Ethereum-based DIDs (did:ethr) and handles the complete
|
||||
* registration flow with the endorser.ch API.
|
||||
*
|
||||
* Features:
|
||||
* - Ethereum keypair generation using ethers.js
|
||||
* - JWT creation and signing using did-jwt
|
||||
* - DID registration with admin authorization
|
||||
* - Detailed error handling and logging
|
||||
* - Command-line interface for admin DID input
|
||||
*
|
||||
* Dependencies:
|
||||
* did-jwt: For JWT creation and signing
|
||||
* ethers: For Ethereum account operations
|
||||
* node-fetch: For API communication
|
||||
*/
|
||||
|
||||
/// <reference types="node" />
|
||||
// Add at the top of your file to ignore dom types
|
||||
import * as didJwt from 'did-jwt';
|
||||
import { ethers } from 'ethers';
|
||||
import fetch from 'node-fetch';
|
||||
import { Database } from 'sqlite3';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
import { program } from 'commander';
|
||||
|
||||
/**
|
||||
* Result interface for DID creation process
|
||||
*/
|
||||
interface DIDCreationResult {
|
||||
did: string;
|
||||
privateKey: string;
|
||||
publicKey: string;
|
||||
isValid: boolean;
|
||||
jwt: string;
|
||||
did: string; // The generated DID
|
||||
privateKey: string; // Private key without 0x prefix
|
||||
publicKey: string; // Public key with 0x prefix
|
||||
isValid: boolean; // Validation status
|
||||
jwt: string; // Signed JWT for registration
|
||||
}
|
||||
|
||||
/**
|
||||
* Result interface for DID registration attempt
|
||||
*/
|
||||
interface RegistrationResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
response?: any;
|
||||
success: boolean; // Registration success status
|
||||
error?: string; // Optional error message
|
||||
response?: any; // Optional API response data
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and validates a new DID with admin authorization
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Validates admin DID input
|
||||
* 2. Generates Ethereum keypair
|
||||
* 3. Creates registration claim
|
||||
* 4. Signs claim as JWT
|
||||
*
|
||||
* @param adminDid - Administrator DID for authorization
|
||||
* @returns Promise<DIDCreationResult> - Generated DID details and JWT
|
||||
* @throws Error if admin DID is missing
|
||||
*/
|
||||
async function createAndValidateDID(adminDid: string): Promise<DIDCreationResult> {
|
||||
if (!adminDid) {
|
||||
throw new Error('Admin DID is required for registration');
|
||||
@@ -87,6 +132,17 @@ async function createAndValidateDID(adminDid: string): Promise<DIDCreationResult
|
||||
return { did, privateKey, publicKey, isValid: true, jwt };
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a DID with the endorser.ch API
|
||||
*
|
||||
* Workflow:
|
||||
* 1. Submits JWT to API endpoint
|
||||
* 2. Processes response
|
||||
* 3. Handles success/error cases
|
||||
*
|
||||
* @param jwt - Signed JWT for registration
|
||||
* @returns Promise<RegistrationResult> - Registration result
|
||||
*/
|
||||
async function registerDID(jwt: string): Promise<RegistrationResult> {
|
||||
try {
|
||||
const response = await fetch('https://api.endorser.ch/api/v2/claim', {
|
||||
@@ -131,33 +187,71 @@ async function registerDID(jwt: string): Promise<RegistrationResult> {
|
||||
}
|
||||
}
|
||||
|
||||
// Command line handling
|
||||
const adminDid = 'did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F';
|
||||
if (!adminDid) {
|
||||
console.error('Usage: ts-node did_generator.ts <admin-did>');
|
||||
console.error('Example: ts-node did_generator.ts did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F');
|
||||
process.exit(1);
|
||||
async function getRootDidFromDb(dbPath?: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const defaultPath = join(homedir(), '.endorser', 'accounts.db');
|
||||
const path = dbPath || defaultPath;
|
||||
|
||||
if (!existsSync(path)) {
|
||||
reject(new Error(`Database not found at ${path}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const db = new Database(path);
|
||||
db.get(
|
||||
'SELECT issuer as did FROM registration WHERE type = "RegisterAction" ORDER BY issuanceDate DESC LIMIT 1',
|
||||
(err, row: { did: string }) => {
|
||||
db.close();
|
||||
if (err) reject(err);
|
||||
else if (!row) reject(new Error('No admin DID found in registration table'));
|
||||
else resolve(row.did);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Starting DID Generation...\n');
|
||||
createAndValidateDID(adminDid)
|
||||
.then(async result => {
|
||||
console.log('\nSuccessfully generated DID with admin authorization!');
|
||||
console.log('Registration JWT:', result.jwt.substring(0, 50) + '...');
|
||||
// Command line handling
|
||||
program
|
||||
.option('--admin-did <did>', 'Admin DID (e.g., did:ethr:0x0000...)')
|
||||
.option('--db-path <path>', 'Path to SQLite database (e.g., ../endorser-ch-test-local.sqlite3)')
|
||||
.parse(process.argv);
|
||||
|
||||
console.log('\nAttempting registration...');
|
||||
const registrationResult = await registerDID(result.jwt);
|
||||
if (registrationResult.success) {
|
||||
console.log('Registration successful!');
|
||||
console.log('Response:', JSON.stringify(registrationResult.response, null, 2));
|
||||
} else {
|
||||
console.log('Registration failed!');
|
||||
console.log('Error:', registrationResult.error);
|
||||
if (registrationResult.response) {
|
||||
console.log('Full response:', JSON.stringify(registrationResult.response, null, 2));
|
||||
}
|
||||
const options = program.opts();
|
||||
|
||||
console.log('Starting DID Generation...\n');
|
||||
|
||||
(async () => {
|
||||
let adminDid = options.adminDid;
|
||||
if (!adminDid && options.dbPath) {
|
||||
try {
|
||||
adminDid = await getRootDidFromDb(options.dbPath);
|
||||
console.log(`Found root DID in database: ${adminDid}`);
|
||||
} catch (e) {
|
||||
console.error(`Error: ${e.message}`);
|
||||
console.error('Please provide --admin-did argument');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('\nError:', error);
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await createAndValidateDID(adminDid);
|
||||
console.log('\nSuccessfully generated DID with admin authorization!');
|
||||
console.log('Registration JWT:', result.jwt.substring(0, 50) + '...');
|
||||
|
||||
console.log('\nAttempting registration...');
|
||||
const registrationResult = await registerDID(result.jwt);
|
||||
if (registrationResult.success) {
|
||||
console.log('Registration successful!');
|
||||
console.log('Response:', JSON.stringify(registrationResult.response, null, 2));
|
||||
} else {
|
||||
console.log('Registration failed!');
|
||||
console.log('Error:', registrationResult.error);
|
||||
if (registrationResult.response) {
|
||||
console.log('Full response:', JSON.stringify(registrationResult.response, null, 2));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('\nError:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
@@ -3,11 +3,11 @@
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["./**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user