You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							364 lines
						
					
					
						
							12 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							364 lines
						
					
					
						
							12 KiB
						
					
					
				
								"""
							 | 
						|
								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
							 | 
						|
								    pathlib: For path handling
							 | 
						|
								    dotenv: For environment variable loading
							 | 
						|
								    os: For environment variable access
							 | 
						|
								
							 | 
						|
								Usage:
							 | 
						|
								    python did_generator.py [options]
							 | 
						|
								
							 | 
						|
								Options:
							 | 
						|
								    --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
							 | 
						|
								import json
							 | 
						|
								import base64
							 | 
						|
								from eth_account.messages import encode_defunct
							 | 
						|
								from eth_keys import keys
							 | 
						|
								import time
							 | 
						|
								import requests
							 | 
						|
								import argparse
							 | 
						|
								import secrets
							 | 
						|
								import hashlib
							 | 
						|
								from pathlib import Path
							 | 
						|
								from dotenv import load_dotenv
							 | 
						|
								import os
							 | 
						|
								
							 | 
						|
								# Load environment variables
							 | 
						|
								load_dotenv()
							 | 
						|
								
							 | 
						|
								class DIDRegistration:
							 | 
						|
								    """
							 | 
						|
								    Handles the creation and registration of DIDs with admin authorization.
							 | 
						|
								    
							 | 
						|
								    This class manages the complete lifecycle of DID creation and registration:
							 | 
						|
								    1. Generating secure Ethereum keypairs with compressed public keys
							 | 
						|
								    2. Creating DIDs from public keys in did:ethr format
							 | 
						|
								    3. Signing registration claims with admin credentials
							 | 
						|
								    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:
							 | 
						|
								        api_url (str): Endpoint for DID registration
							 | 
						|
								        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_keypair: dict, api_url: str = None):
							 | 
						|
								        """
							 | 
						|
								        Initialize DID registration with admin credentials.
							 | 
						|
								        
							 | 
						|
								        Args:
							 | 
						|
								            admin_keypair (dict): Admin's DID and private key for signing
							 | 
						|
								            api_url (str, optional): Override default API URL
							 | 
						|
								        """
							 | 
						|
								        self.api_url = api_url or "https://test-api.endorser.ch/api/v2/claim"
							 | 
						|
								        self.admin_keypair = admin_keypair  # Store full admin keypair
							 | 
						|
								        Account.enable_unaudited_hdwallet_features()
							 | 
						|
								
							 | 
						|
								    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. Uses compressed public key format to match
							 | 
						|
								        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(private_key_bytes)
							 | 
						|
								
							 | 
						|
								        return {
							 | 
						|
								            'private_key': private_key,  # No 0x prefix
							 | 
						|
								            'public_key': public_key,    # With 0x prefix, compressed format
							 | 
						|
								            'address': account.address,
							 | 
						|
								            'did': f"did:ethr:{account.address}"
							 | 
						|
								        }
							 | 
						|
								
							 | 
						|
								    def sign_jwt(self, payload: dict, private_key: str, did: str) -> str:
							 | 
						|
								        """
							 | 
						|
								        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('=')
							 | 
						|
								        message = f"{header_b64}.{payload_b64}"
							 | 
						|
								
							 | 
						|
								        # Hash the message with sha256
							 | 
						|
								        message_hash = hashlib.sha256(message.encode()).digest()
							 | 
						|
								
							 | 
						|
								        # Sign using eth_keys directly
							 | 
						|
								        private_key_bytes = bytes.fromhex(private_key)
							 | 
						|
								        private_key_obj = keys.PrivateKey(private_key_bytes)
							 | 
						|
								        signature = private_key_obj.sign_msg_hash(message_hash)
							 | 
						|
								
							 | 
						|
								        # Get r and s from signature
							 | 
						|
								        r = signature.r.to_bytes(32, 'big')
							 | 
						|
								        s = signature.s.to_bytes(32, 'big')
							 | 
						|
								        signature_bytes = r + s
							 | 
						|
								
							 | 
						|
								        # Format signature
							 | 
						|
								        signature_b64 = base64.urlsafe_b64encode(signature_bytes).decode().rstrip('=')
							 | 
						|
								
							 | 
						|
								        return f"{message}.{signature_b64}"
							 | 
						|
								
							 | 
						|
								    def create_jwt(self, new_did: str) -> str:
							 | 
						|
								        """
							 | 
						|
								        Create a signed JWT for DID registration
							 | 
						|
								        
							 | 
						|
								        Args:
							 | 
						|
								            new_did (str): The DID being registered
							 | 
						|
								        """
							 | 
						|
								        now = int(time.time())
							 | 
						|
								
							 | 
						|
								        # Create registration claim with admin as agent
							 | 
						|
								        register_claim = {
							 | 
						|
								            "@context": "https://schema.org",
							 | 
						|
								            "@type": "RegisterAction",
							 | 
						|
								            "agent": { "did": self.admin_keypair['did'] },
							 | 
						|
								            "participant": { "did": new_did },
							 | 
						|
								            "object": "endorser.ch"
							 | 
						|
								        }
							 | 
						|
								
							 | 
						|
								        payload = {
							 | 
						|
								            "iat": now,
							 | 
						|
								            "exp": now + 300,
							 | 
						|
								            "sub": "RegisterAction",
							 | 
						|
								            "vc": {
							 | 
						|
								                "@context": ["https://www.w3.org/2018/credentials/v1"],
							 | 
						|
								                "type": ["VerifiableCredential"],
							 | 
						|
								                "credentialSubject": register_claim
							 | 
						|
								            }
							 | 
						|
								        }
							 | 
						|
								
							 | 
						|
								        print(f"\nDebug - JWT payload: {json.dumps(payload, indent=2)}")
							 | 
						|
								
							 | 
						|
								        # Sign with admin's private key
							 | 
						|
								        return self.sign_jwt(
							 | 
						|
								            payload, 
							 | 
						|
								            self.admin_keypair['private_key'], 
							 | 
						|
								            self.admin_keypair['did']
							 | 
						|
								        )
							 | 
						|
								
							 | 
						|
								    def register_did(self, jwt_token: str) -> dict:
							 | 
						|
								        """
							 | 
						|
								        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,
							 | 
						|
								                    'response': response.json()
							 | 
						|
								                }
							 | 
						|
								            else:
							 | 
						|
								                try:
							 | 
						|
								                    error_json = response.json()
							 | 
						|
								                    error_msg = error_json.get('error', {}).get(
							 | 
						|
								                        'message', 'Unknown error'
							 | 
						|
								                    )
							 | 
						|
								                    return {
							 | 
						|
								                        'success': False,
							 | 
						|
								                        '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}): "
							 | 
						|
								                                f"{response.text}",
							 | 
						|
								                        'response': response.text
							 | 
						|
								                    }
							 | 
						|
								        except (requests.RequestException, ConnectionError) as e:
							 | 
						|
								            return {
							 | 
						|
								                'success': False,
							 | 
						|
								                'error': f"Request failed: {str(e)}"
							 | 
						|
								            }
							 | 
						|
								
							 | 
						|
								def main():
							 | 
						|
								    """
							 | 
						|
								    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 [options]
							 | 
						|
								    """
							 | 
						|
								    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(
							 | 
						|
								        '--api-url',
							 | 
						|
								        help='Override API URL',
							 | 
						|
								        default=os.getenv('ENDORSER_API_URL', 'https://test-api.endorser.ch/api/v2/claim')
							 | 
						|
								    )
							 | 
						|
								    args = parser.parse_args()
							 | 
						|
								
							 | 
						|
								    # Get admin credentials from environment
							 | 
						|
								    admin_keypair = {
							 | 
						|
								        'did': os.getenv('ADMIN_DID', 'did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F'),
							 | 
						|
								        'private_key': os.getenv('ADMIN_PRIVATE_KEY', '2b6472c026ec2aa2c4235c994a63868fc9212d18b58f6cbfe861b52e71330f5b')
							 | 
						|
								    }
							 | 
						|
								
							 | 
						|
								    admin_did = args.admin_did
							 | 
						|
								    if not admin_did:
							 | 
						|
								        admin_did = admin_keypair['did']
							 | 
						|
								
							 | 
						|
								    print(f"Admin DID: {admin_did}")
							 | 
						|
								    print(f"API URL: {args.api_url}")
							 | 
						|
								
							 | 
						|
								    print('Starting DID Generation...\n')
							 | 
						|
								    registrar = DIDRegistration(admin_keypair, args.api_url)
							 | 
						|
								
							 | 
						|
								    print("Generating new keypair...")
							 | 
						|
								    new_keypair = registrar.create_keypair()
							 | 
						|
								
							 | 
						|
								    print("\nGenerated DID Details:")
							 | 
						|
								    print("----------------------")
							 | 
						|
								    print(f"DID: {new_keypair['did']}")
							 | 
						|
								    print(f"Admin DID: {admin_did}")
							 | 
						|
								    print(f"Address: {new_keypair['address']}")
							 | 
						|
								    print(f"Private Key: {new_keypair['private_key']}")
							 | 
						|
								    print(f"Public Key: {new_keypair['public_key']}\n")
							 | 
						|
								
							 | 
						|
								    print("Creating JWT...")
							 | 
						|
								    jwt_token = registrar.create_jwt(new_keypair['did'])
							 | 
						|
								
							 | 
						|
								    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("Registration successful!")
							 | 
						|
								        print("Response: {json.dumps(result['response'], indent=2)}")
							 | 
						|
								    else:
							 | 
						|
								        print("Registration failed!")
							 | 
						|
								        print(f"Error: {result['error']}")
							 | 
						|
								        if 'response' in result:
							 | 
						|
								            print(f"Full response: {json.dumps(result['response'], indent=2)}")
							 | 
						|
								
							 | 
						|
								if __name__ == "__main__":
							 | 
						|
								    main()
							 | 
						|
								
							 |