Browse Source

feat: Improve DID registration with admin credentials

- Add admin keypair (DID + private key) for proper registration signing
- Remove SQLite database dependency for admin DID lookup
- Update both TypeScript and Python implementations to use admin credentials
- Enhance documentation with detailed usage, options, and security notes
- Fix JWT signing to use admin's key as issuer
- Standardize API URL handling with defaults
- Add command-line options for admin DID and API URL override

Both implementations now successfully register new DIDs using the admin's
credentials to sign the registration claims. The admin acts as both the
agent in the claim and the issuer of the JWT.
pull/127/head
Matthew Raymer 1 week ago
parent
commit
9e6f0ab468
  1. 2
      package.json
  2. 4
      test-scripts/.prettierrc
  3. 150
      test-scripts/did_generator.py
  4. 106
      test-scripts/did_generator.ts

2
package.json

@ -31,7 +31,7 @@
"pywebview:package-win": "vite build --mode pywebview --config vite.config.pywebview.mts && .venv/Scripts/python -m PyInstaller --name TimeSafari --add-data 'dist;www' src/pywebview/main.py", "pywebview:package-win": "vite build --mode pywebview --config vite.config.pywebview.mts && .venv/Scripts/python -m PyInstaller --name TimeSafari --add-data 'dist;www' src/pywebview/main.py",
"pywebview:package-mac": "vite build --mode pywebview --config vite.config.pywebview.mts && .venv/bin/python -m PyInstaller --name TimeSafari --add-data 'dist:www' src/pywebview/main.py", "pywebview:package-mac": "vite build --mode pywebview --config vite.config.pywebview.mts && .venv/bin/python -m PyInstaller --name TimeSafari --add-data 'dist:www' src/pywebview/main.py",
"build:did-generator": "tsc -p test-scripts/tsconfig.json", "build:did-generator": "tsc -p test-scripts/tsconfig.json",
"generate-did": "node test-scripts/dist/did_generator.js --db-path ../endorser-ch-test-local.sqlite3" "generate-did": "node test-scripts/dist/did_generator.js"
}, },
"dependencies": { "dependencies": {
"@capacitor/android": "^6.2.0", "@capacitor/android": "^6.2.0",

4
test-scripts/.prettierrc

@ -0,0 +1,4 @@
{
"tabWidth": 2,
"useTabs": false
}

150
test-scripts/did_generator.py

@ -21,14 +21,18 @@ Dependencies:
hashlib: For SHA-256 hashing hashlib: For SHA-256 hashing
base64: For JWT encoding base64: For JWT encoding
argparse: For command-line argument parsing argparse: For command-line argument parsing
sqlite3: For database operations
pathlib: For path handling pathlib: For path handling
Usage: Usage:
python did_generator.py <admin_did> python did_generator.py [options]
Example: Options:
python did_generator.py did:ethr:0x1234...5678 --admin-did <did> Override default admin DID
--api-url <url> Override default API endpoint
Environment:
Default Admin DID: did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F
Default API URL: https://test-api.endorser.ch/api/v2/claim
""" """
from eth_account import Account from eth_account import Account
@ -41,34 +45,39 @@ import requests
import argparse import argparse
import secrets import secrets
import hashlib import hashlib
import sqlite3
from pathlib import Path from pathlib import Path
class DIDRegistration: class DIDRegistration:
""" """
Handles the creation and registration of DIDs with admin authorization. Handles the creation and registration of DIDs with admin authorization.
This class manages the complete lifecycle of DID creation: This class manages the complete lifecycle of DID creation and registration:
1. Generating secure Ethereum keypairs 1. Generating secure Ethereum keypairs with compressed public keys
2. Creating DIDs from public keys 2. Creating DIDs from public keys in did:ethr format
3. Signing registration claims 3. Signing registration claims with admin credentials
4. Submitting registration to the endorser.ch API 4. Submitting registration to the endorser.ch API
The registration process uses two keypairs:
1. Admin keypair: Used to sign and authorize the registration
2. New DID keypair: The identity being registered
Attributes: Attributes:
api_url (str): Endpoint for DID registration api_url (str): Endpoint for DID registration
admin_did (str): Administrator DID for authorization admin_keypair (dict): Admin's credentials containing:
- did: Admin's DID in did:ethr format
- private_key: Admin's private key for signing
""" """
def __init__(self, admin_did: str): def __init__(self, admin_keypair: dict, api_url: str = None):
""" """
Initialize DID registration with admin credentials. Initialize DID registration with admin credentials.
Args: Args:
admin_did (str): Administrator DID for authorizing registrations admin_keypair (dict): Admin's DID and private key for signing
Format: did:ethr:0x... api_url (str, optional): Override default API URL
""" """
self.api_url = "https://api.endorser.ch/api/v2/claim" self.api_url = api_url or "https://test-api.endorser.ch/api/v2/claim"
self.admin_did = admin_did self.admin_keypair = admin_keypair # Store full admin keypair
Account.enable_unaudited_hdwallet_features() Account.enable_unaudited_hdwallet_features()
def create_keypair(self) -> dict: def create_keypair(self) -> dict:
@ -76,7 +85,7 @@ class DIDRegistration:
Generate a new Ethereum keypair and associated DID. Generate a new Ethereum keypair and associated DID.
Creates a secure random keypair and formats it for use with the Creates a secure random keypair and formats it for use with the
endorser.ch API, including compressed public key format matching endorser.ch API. Uses compressed public key format to match
ethers.js implementation. ethers.js implementation.
Returns: Returns:
@ -154,41 +163,30 @@ class DIDRegistration:
).decode().rstrip('=') ).decode().rstrip('=')
message = f"{header_b64}.{payload_b64}" message = f"{header_b64}.{payload_b64}"
# Hash the message with sha256 first (like did-jwt) # Hash the message with sha256
message_hash = hashlib.sha256(message.encode()).digest() message_hash = hashlib.sha256(message.encode()).digest()
# Sign the hash directly (not as an Ethereum message) # Sign using eth_keys directly
private_key_bytes = bytes.fromhex(private_key) private_key_bytes = bytes.fromhex(private_key)
account = Account.from_key(private_key_bytes) private_key_obj = keys.PrivateKey(private_key_bytes)
signed = account.sign_message(message_hash) signature = private_key_obj.sign_msg_hash(message_hash)
# Get r and s from signature # Get r and s from signature
r = signed.r.to_bytes(32, 'big') r = signature.r.to_bytes(32, 'big')
s = signed.s.to_bytes(32, 'big') s = signature.s.to_bytes(32, 'big')
signature_bytes = r + s signature_bytes = r + s
signature = base64.urlsafe_b64encode(signature_bytes).decode().rstrip('=') # Format signature
signature_b64 = base64.urlsafe_b64encode(signature_bytes).decode().rstrip('=')
return f"{message}.{signature}" return f"{message}.{signature_b64}"
def create_jwt(self, keypair: dict) -> str: def create_jwt(self, new_did: str) -> str:
""" """
Create a signed JWT for DID registration. 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: Args:
keypair (dict): Generated keypair information new_did (str): The DID being registered
Returns:
str: Signed JWT containing registration claim
Note:
Matches TypeScript implementation exactly for compatibility
""" """
now = int(time.time()) now = int(time.time())
@ -196,12 +194,11 @@ class DIDRegistration:
register_claim = { register_claim = {
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "RegisterAction", "@type": "RegisterAction",
"agent": { "did": self.admin_did }, "agent": { "did": self.admin_keypair['did'] },
"participant": { "did": keypair['did'] }, "participant": { "did": new_did },
"object": "endorser.ch" "object": "endorser.ch"
} }
# Match the TypeScript vcPayload exactly - no iss field
payload = { payload = {
"iat": now, "iat": now,
"exp": now + 300, "exp": now + 300,
@ -215,8 +212,12 @@ class DIDRegistration:
print(f"\nDebug - JWT payload: {json.dumps(payload, indent=2)}") print(f"\nDebug - JWT payload: {json.dumps(payload, indent=2)}")
# Sign with new DID's private key # Sign with admin's private key
return self.sign_jwt(payload, keypair['private_key'], keypair['did']) return self.sign_jwt(
payload,
self.admin_keypair['private_key'],
self.admin_keypair['did']
)
def register_did(self, jwt_token: str) -> dict: def register_did(self, jwt_token: str) -> dict:
""" """
@ -281,30 +282,6 @@ class DIDRegistration:
'error': f"Request failed: {str(e)}" '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(): def main():
""" """
Main entry point for DID generation script. Main entry point for DID generation script.
@ -315,7 +292,7 @@ def main():
3. Result output and error display 3. Result output and error display
Usage: Usage:
python did_generator.py <admin_did> python did_generator.py [options]
""" """
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Generate a DID with admin authorization' description='Generate a DID with admin authorization'
@ -326,38 +303,39 @@ def main():
required=False required=False
) )
parser.add_argument( parser.add_argument(
'--db-path', '--api-url',
help='Path to SQLite database containing root DID', help='Override API URL',
required=False default="https://test-api.endorser.ch/api/v2/claim"
) )
args = parser.parse_args() args = parser.parse_args()
admin_keypair = {
'did': 'did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F',
'private_key': '2b6472c026ec2aa2c4235c994a63868fc9212d18b58f6cbfe861b52e71330f5b'
}
admin_did = args.admin_did admin_did = args.admin_did
if not admin_did: if not admin_did:
try: admin_did = admin_keypair['did']
admin_did = get_root_did_from_db(args.db_path)
print(f"Found root DID in database: {admin_did}") print(f"Admin DID: {admin_did}")
except (FileNotFoundError, ValueError, sqlite3.Error) as e: print(f"API URL: {args.api_url}")
print(f"Error: {str(e)}")
print("Please provide --admin-did argument")
return
print('Starting DID Generation...\n') print('Starting DID Generation...\n')
registrar = DIDRegistration(admin_did) registrar = DIDRegistration(admin_keypair, args.api_url)
print("Generating new keypair...") print("Generating new keypair...")
keypair = registrar.create_keypair() new_keypair = registrar.create_keypair()
print("\nGenerated DID Details:") print("\nGenerated DID Details:")
print("----------------------") print("----------------------")
print(f"DID: {keypair['did']}") print(f"DID: {new_keypair['did']}")
print(f"Admin DID: {admin_did}") print(f"Admin DID: {admin_did}")
print(f"Address: {keypair['address']}") print(f"Address: {new_keypair['address']}")
print(f"Private Key: {keypair['private_key']}") print(f"Private Key: {new_keypair['private_key']}")
print(f"Public Key: {keypair['public_key']}\n") print(f"Public Key: {new_keypair['public_key']}\n")
print("Creating JWT...") print("Creating JWT...")
jwt_token = registrar.create_jwt(keypair) jwt_token = registrar.create_jwt(new_keypair['did'])
print('\nSuccessfully generated DID with admin authorization!') print('\nSuccessfully generated DID with admin authorization!')
print(f'Registration JWT: {jwt_token[:50]}...') print(f'Registration JWT: {jwt_token[:50]}...')

106
test-scripts/did_generator.ts

@ -17,6 +17,18 @@
* did-jwt: For JWT creation and signing * did-jwt: For JWT creation and signing
* ethers: For Ethereum account operations * ethers: For Ethereum account operations
* node-fetch: For API communication * node-fetch: For API communication
* commander: For CLI argument parsing
*
* Usage:
* npm run generate-did -- [options]
*
* Options:
* -a, --admin-did <did> Override default admin DID
* --api-url <url> Override default API endpoint
*
* Environment:
* Default Admin DID: did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F
* Default API URL: https://test-api.endorser.ch/api/v2/claim
*/ */
/// <reference types="node" /> /// <reference types="node" />
@ -24,12 +36,18 @@
import * as didJwt from 'did-jwt'; import * as didJwt from 'did-jwt';
import { ethers } from 'ethers'; import { ethers } from 'ethers';
import fetch from 'node-fetch'; 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'; import { program } from 'commander';
const admin_keypair = {
did: 'did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F',
privateKey: '2b6472c026ec2aa2c4235c994a63868fc9212d18b58f6cbfe861b52e71330f5b'
}
// Default admin DID
const DEFAULT_ADMIN_DID = admin_keypair.did;
// Add constant for default API URL
const DEFAULT_API_URL = 'https://test-api.endorser.ch/api/v2/claim';
/** /**
* Result interface for DID creation process * Result interface for DID creation process
*/ */
@ -54,23 +72,20 @@ interface RegistrationResult {
* Creates and validates a new DID with admin authorization * Creates and validates a new DID with admin authorization
* *
* Workflow: * Workflow:
* 1. Validates admin DID input * 1. Generates new Ethereum keypair using ethers.js
* 2. Generates Ethereum keypair * 2. Creates DID from public key in did:ethr format
* 3. Creates registration claim * 3. Constructs registration claim with:
* 4. Signs claim as JWT * - Admin DID as the agent (authorizer)
* - New DID as the participant (being registered)
* 4. Signs claim as JWT using admin's private key
* *
* @param adminDid - Administrator DID for authorization * @param adminDid - Administrator DID for authorization
* @returns Promise<DIDCreationResult> - Generated DID details and JWT * @returns Promise<DIDCreationResult> - Generated DID details and JWT
* @throws Error if admin DID is missing
*/ */
async function createAndValidateDID(adminDid: string): Promise<DIDCreationResult> { async function createAndValidateDID(adminDid: string): Promise<DIDCreationResult> {
if (!adminDid) {
throw new Error('Admin DID is required for registration');
}
console.log('Using admin DID:', adminDid); console.log('Using admin DID:', adminDid);
// 1. Generate keypair // Generate new DID keypair
console.log('Generating new keypair...'); console.log('Generating new keypair...');
const wallet = ethers.Wallet.createRandom(); const wallet = ethers.Wallet.createRandom();
const did = `did:ethr:${wallet.address}`; const did = `did:ethr:${wallet.address}`;
@ -115,11 +130,11 @@ async function createAndValidateDID(adminDid: string): Promise<DIDCreationResult
}); });
console.log('Payload:', vcPayload); console.log('Payload:', vcPayload);
// Create and sign JWT // Create and sign JWT with admin's key
console.log('\nCreating JWT...'); console.log('\nCreating JWT...');
const signer = didJwt.SimpleSigner(privateKey); const signer = didJwt.SimpleSigner(admin_keypair.privateKey); // Use admin's private key
const jwt = await didJwt.createJWT(vcPayload, { const jwt = await didJwt.createJWT(vcPayload, {
issuer: did, issuer: admin_keypair.did, // Admin DID as issuer
signer: signer signer: signer
}); });
@ -137,15 +152,18 @@ async function createAndValidateDID(adminDid: string): Promise<DIDCreationResult
* *
* Workflow: * Workflow:
* 1. Submits JWT to API endpoint * 1. Submits JWT to API endpoint
* 2. Processes response * 2. Processes response (success/error)
* 3. Handles success/error cases * 3. Handles error cases:
* - Network errors
* - API errors (400, 401, etc)
* - Malformed responses
* *
* @param jwt - Signed JWT for registration * @param jwt - Signed JWT for registration
* @returns Promise<RegistrationResult> - Registration result * @returns Promise<RegistrationResult> - Registration result with response/error
*/ */
async function registerDID(jwt: string): Promise<RegistrationResult> { async function registerDID(jwt: string): Promise<RegistrationResult> {
try { try {
const response = await fetch('https://api.endorser.ch/api/v2/claim', { const response = await fetch(DEFAULT_API_URL, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -187,52 +205,22 @@ async function registerDID(jwt: string): Promise<RegistrationResult> {
} }
} }
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);
}
);
});
}
// Command line handling // Command line handling
program program
.option('--admin-did <did>', 'Admin DID (e.g., did:ethr:0x0000...)') .name('did-generator')
.option('--db-path <path>', 'Path to SQLite database (e.g., ../endorser-ch-test-local.sqlite3)') .description('Generate and register a new DID')
.parse(process.argv); .option('-a, --admin-did <did>', 'Admin DID', DEFAULT_ADMIN_DID)
.option('--api-url <url>', 'Override API URL', DEFAULT_API_URL)
.parse();
const options = program.opts(); const options = program.opts();
const adminDid = options.adminDid; // Use the option instead of args
const apiUrl = options.apiUrl;
console.log('Starting DID Generation...\n'); console.log('Starting DID Generation...\n');
console.log('Using admin DID:', adminDid); // Log the admin DID being used
(async () => { (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);
}
}
try { try {
const result = await createAndValidateDID(adminDid); const result = await createAndValidateDID(adminDid);
console.log('\nSuccessfully generated DID with admin authorization!'); console.log('\nSuccessfully generated DID with admin authorization!');

Loading…
Cancel
Save