def withdrawal_credentials(self) -> bytes: if self.custom_withdrawal_credentials: return self.custom_withdrawal_credentials withdrawal_credentials = BLS_WITHDRAWAL_PREFIX if self.custom_withdrawal_pk: withdrawal_credentials += SHA256(self.custom_withdrawal_pk)[1:] else: withdrawal_credentials += SHA256(self.withdrawal_pk)[1:] return withdrawal_credentials
def _parent_SK_to_lamport_PK(*, parent_SK: int, index: int) -> bytes: """ Derives the `index`th child's lamport PK from the `parent_SK`. """ salt = index.to_bytes(4, byteorder='big') IKM = parent_SK.to_bytes(32, byteorder='big') lamport_0 = _IKM_to_lamport_SK(IKM=IKM, salt=salt) not_IKM = _flip_bits_256(parent_SK).to_bytes(32, byteorder='big') lamport_1 = _IKM_to_lamport_SK(IKM=not_IKM, salt=salt) lamport_SKs = lamport_0 + lamport_1 lamport_PKs = [SHA256(sk) for sk in lamport_SKs] compressed_PK = SHA256(b''.join(lamport_PKs)) return compressed_PK
def _parent_SK_to_lamport_PK(*, parent_SK: int, index: int) -> bytes: """ Derives the `index`th child's lamport PK from the `parent_SK`. Ref: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2333.md#parent_sk_to_lamport_pk """ salt = index.to_bytes(4, byteorder='big') IKM = parent_SK.to_bytes(32, byteorder='big') lamport_0 = _IKM_to_lamport_SK(IKM=IKM, salt=salt) not_IKM = _flip_bits_256(parent_SK).to_bytes(32, byteorder='big') lamport_1 = _IKM_to_lamport_SK(IKM=not_IKM, salt=salt) lamport_SKs = lamport_0 + lamport_1 lamport_PKs = [SHA256(sk) for sk in lamport_SKs] compressed_PK = SHA256(b''.join(lamport_PKs)) return compressed_PK
def encrypt( cls, *, secret: bytes, password: str, path: str = '', kdf_salt: bytes = randbits(256).to_bytes(32, 'big'), aes_iv: bytes = randbits(128).to_bytes(16, 'big') ) -> 'Keystore': """ Encrypt a secret (BLS SK) as an EIP 2335 Keystore. """ keystore = cls() keystore.uuid = str(uuid4()) keystore.crypto.kdf.params['salt'] = kdf_salt decryption_key = keystore.kdf(password=cls._process_password(password), **keystore.crypto.kdf.params) keystore.crypto.cipher.params['iv'] = aes_iv cipher = AES_128_CTR(key=decryption_key[:16], **keystore.crypto.cipher.params) keystore.crypto.cipher.message = cipher.encrypt(secret) keystore.crypto.checksum.message = SHA256( decryption_key[16:32] + keystore.crypto.cipher.message) keystore.pubkey = bls.SkToPk(int.from_bytes(secret, 'big')).hex() keystore.path = path return keystore
def _get_checksum(entropy: bytes) -> int: """ Determine the index of the checksum word given the entropy """ _validate_entropy_length(entropy) checksum_length = len(entropy) // 4 return int.from_bytes(SHA256(entropy), 'big') >> (256 - checksum_length)
def get_mnemonic(*, language: str, words_path: str, entropy: Optional[bytes] = None) -> str: """ Return a mnemonic string in a given `language` based on `entropy` via the calculated checksum. Ref: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic """ if entropy is None: entropy = randbits(256).to_bytes(32, 'big') entropy_length = len(entropy) * 8 if entropy_length not in range(128, 257, 32): raise IndexError( f"`entropy_length` should be in [128, 160, 192,224, 256]. Got {entropy_length}." ) checksum_length = (entropy_length // 32) checksum = int.from_bytes(SHA256(entropy), 'big') >> 256 - checksum_length entropy_bits = int.from_bytes(entropy, 'big') << checksum_length entropy_bits += checksum entropy_length += checksum_length mnemonic = [] word_list = _get_word_list(language, words_path) for i in range(entropy_length // 11 - 1, -1, -1): index = (entropy_bits >> i * 11) & 2**11 - 1 word = _get_word(word_list=word_list, index=index) mnemonic.append(word) return ' '.join(mnemonic)
def create_from_private_key( cls, *, secret: bytes, password: str, index: int = DEFAULT_INDEX, threshold: int = DEFAULT_THRESHOLD, shared_public_key: str = DEFAULT_SHARED_PUBLIC_KEY, shared_withdrawal_credentials: str = DEFAULT_SHARED_WITHDRAWAL_CREDS ) -> HorcruxPbkdf2Keystore: keystore = cls() keystore.uuid = str(uuid4()) keystore.crypto.kdf.params["salt"] = randbits(256).to_bytes(32, "big") decryption_key = keystore.kdf(password=cls._process_password(password), **keystore.crypto.kdf.params) keystore.crypto.cipher.params["iv"] = randbits(128).to_bytes(16, "big") cipher = AES_128_CTR(key=decryption_key[:16], **keystore.crypto.cipher.params) keystore.crypto.cipher.message = cipher.encrypt(secret) keystore.crypto.checksum.message = SHA256( decryption_key[16:32] + keystore.crypto.cipher.message) keystore.index = index keystore.threshold = threshold keystore.shared_public_key = shared_public_key keystore.shared_withdrawal_credentials = shared_withdrawal_credentials return keystore
def decrypt(self, password: str) -> bytes: decryption_key = self.kdf(password=password, **self.crypto.kdf.params) assert SHA256( decryption_key[16:32] + self.crypto.cipher.message) == self.crypto.checksum.message cipher = AES_128_CTR(key=decryption_key[:16], **self.crypto.cipher.params) return cipher.decrypt(self.crypto.cipher.message)
def withdrawal_credentials(self) -> bytes: if self.withdrawal_type == WithdrawalType.BLS_WITHDRAWAL: withdrawal_credentials = BLS_WITHDRAWAL_PREFIX withdrawal_credentials += SHA256(self.withdrawal_pk)[1:] elif (self.withdrawal_type == WithdrawalType.ETH1_ADDRESS_WITHDRAWAL and self.eth1_withdrawal_address is not None): withdrawal_credentials = ETH1_ADDRESS_WITHDRAWAL_PREFIX withdrawal_credentials += b'\x00' * 11 withdrawal_credentials += self.eth1_withdrawal_address else: raise ValueError(f"Invalid withdrawal_type {self.withdrawal_type}") return withdrawal_credentials
def decrypt(self, password: str) -> bytes: """ Retrieve the secret (BLS SK) from the self keystore by decrypting it with `password` """ decryption_key = self.kdf(password=self._process_password(password), **self.crypto.kdf.params) if SHA256(decryption_key[16:32] + self.crypto.cipher.message) != self.crypto.checksum.message: raise ValueError("Checksum message error") cipher = AES_128_CTR(key=decryption_key[:16], **self.crypto.cipher.params) return cipher.decrypt(self.crypto.cipher.message)
def encrypt(cls, *, secret: bytes, password: str, path: str='', kdf_salt: bytes=randbits(256).to_bytes(32, 'big'), aes_iv: bytes=randbits(128).to_bytes(16, 'big')): keystore = cls() keystore.crypto.kdf.params['salt'] = kdf_salt decryption_key = keystore.kdf(password=password, **keystore.crypto.kdf.params) keystore.crypto.cipher.params['iv'] = aes_iv cipher = AES_128_CTR(key=decryption_key[:16], **keystore.crypto.cipher.params) keystore.crypto.cipher.message = cipher.encrypt(secret) keystore.crypto.checksum.message = SHA256(decryption_key[16:32] + keystore.crypto.cipher.message) keystore.pubkey = bls.PrivToPub(int.from_bytes(secret, 'big')).hex() keystore.path = path return keystore
def _HKDF_mod_r(*, IKM: bytes, key_info: bytes = b'') -> int: """ Hashes the IKM using HKDF and returns the answer as an int modulo r, the BLS field order. Ref: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2333.md#hkdf_mod_r """ L = 48 # `ceil((3 * ceil(log2(r))) / 16)`, where `r` is the order of the BLS 12-381 curve salt = b'BLS-SIG-KEYGEN-SALT-' SK = 0 while SK == 0: salt = SHA256(salt) okm = HKDF( salt=salt, IKM=IKM + b'\x00', # add postfix `I2OSP(0, 1)` L=L, info=key_info + L.to_bytes(2, 'big'), ) SK = int.from_bytes(okm, byteorder='big') % bls_curve_order return SK
def get_mnemonic(*, language: str, words_path: str, entropy: Optional[bytes]=None) -> str: """ Return a mnemonic string in a given `language` based on `entropy`. """ if entropy is None: entropy = randbits(256).to_bytes(32, 'big') entropy_length = len(entropy) * 8 assert entropy_length in range(128, 257, 32) checksum_length = (entropy_length // 32) checksum = int.from_bytes(SHA256(entropy), 'big') >> 256 - checksum_length entropy_bits = int.from_bytes(entropy, 'big') << checksum_length entropy_bits += checksum entropy_length += checksum_length mnemonic = [] word_list = _get_word_list(language, words_path) for i in range(entropy_length // 11 - 1, -1, -1): index = (entropy_bits >> i * 11) & 2**11 - 1 word = _get_word(word_list=word_list, index=index) mnemonic.append(word) return ' '.join(mnemonic)
def export_deposit_data_json(*, credentials: List[ValidatorCredentials], folder: str): deposit_data: List[dict] = [] for credential in credentials: deposit_datum = DepositMessage( pubkey=credential.signing_pk, withdrawal_credentials=SHA256(credential.withdrawal_pk), amount=credential.amount, ) signed_deposit_datum = sign_deposit_data(deposit_datum, credential.signing_sk) datum_dict = signed_deposit_datum.as_dict() datum_dict.update({'deposit_data_root': deposit_datum.hash_tree_root}) datum_dict.update( {'signed_deposit_data_root': signed_deposit_datum.hash_tree_root}) deposit_data.append(datum_dict) filefolder = os.path.join(folder, 'deposit_data-%i.json' % time.time()) with open(filefolder, 'w') as f: json.dump(deposit_data, f, default=lambda x: x.hex()) return filefolder
def validate_deposit(deposit_data_dict: Dict[str, Any], credential: Credential) -> bool: ''' Checks whether a deposit is valid based on the eth2 rules. https://github.com/ethereum/eth2.0-specs/blob/dev/specs/phase0/beacon-chain.md#deposits ''' pubkey = BLSPubkey(bytes.fromhex(deposit_data_dict['pubkey'])) withdrawal_credentials = bytes.fromhex( deposit_data_dict['withdrawal_credentials']) amount = deposit_data_dict['amount'] signature = BLSSignature(bytes.fromhex(deposit_data_dict['signature'])) deposit_message_root = bytes.fromhex( deposit_data_dict['deposit_data_root']) fork_version = bytes.fromhex(deposit_data_dict['fork_version']) # Verify pubkey if len(pubkey) != 48: return False if pubkey != credential.signing_pk: return False # Verify withdrawal credential if len(withdrawal_credentials) != 32: return False if withdrawal_credentials[: 1] == BLS_WITHDRAWAL_PREFIX == credential.withdrawal_prefix: if withdrawal_credentials[1:] != SHA256(credential.withdrawal_pk)[1:]: return False elif withdrawal_credentials[: 1] == ETH1_ADDRESS_WITHDRAWAL_PREFIX == credential.withdrawal_prefix: if withdrawal_credentials[1:12] != b'\x00' * 11: return False if credential.eth1_withdrawal_address is None: return False if withdrawal_credentials[12:] != credential.eth1_withdrawal_address: return False else: return False # Verify deposit amount if not MIN_DEPOSIT_AMOUNT < amount <= MAX_DEPOSIT_AMOUNT: return False # Verify deposit signature && pubkey deposit_message = DepositMessage( pubkey=pubkey, withdrawal_credentials=withdrawal_credentials, amount=amount) domain = compute_deposit_domain(fork_version) signing_root = compute_signing_root(deposit_message, domain) if not bls.Verify(pubkey, signing_root, signature): return False # Verify Deposit Root signed_deposit = DepositData( pubkey=pubkey, withdrawal_credentials=withdrawal_credentials, amount=amount, signature=signature, ) return signed_deposit.hash_tree_root == deposit_message_root
def process_dispatcher_output( dispatcher_output: List[Dict[str, Any]], my_bls_public_key: BLSPubkey, my_bls_public_key_shares: List[BLSPubkey], my_bls_private_key_shares: List[int], my_rsa_key: RsaKey, my_index: int, ) -> Tuple[BLSPubkey, bytes, int]: """ Processes output from the dispatcher to generate final horcrux BLS private key and shared BLS public key. """ final_public_key_shares = [my_bls_public_key] horcrux_private_key_shares = [my_bls_private_key_shares[my_index]] horcrux_public_key_shares = [my_bls_public_key_shares[my_index]] for encrypted_data in dispatcher_output: # verify the RSA signature ciphertext = bytes.fromhex(encrypted_data["ciphertext"]) signature = bytes.fromhex(encrypted_data["signature"]) if not rsa_verify( RSA.import_key(encrypted_data["sender_rsa_public_key"]), ciphertext, signature, ): raise ValueError("Failed to verify the RSA signature.") data = json.loads( rsa_decrypt( private_key=my_rsa_key, enc_session_key=bytes.fromhex( encrypted_data["enc_session_key"]), nonce=bytes.fromhex(encrypted_data["nonce"]), tag=bytes.fromhex(encrypted_data["tag"]), ciphertext=ciphertext, )) recipient_bls_public_keys = [ bytes.fromhex(pub_key) for pub_key in data["public_key_shares"] ] horcrux_private_key_share = int(data["private_key_share"]) if (bls_pop.SkToPk(horcrux_private_key_share) != recipient_bls_public_keys[my_index]): raise ValueError("Received invalid BLS private key share.") final_public_key_shares.append( BLSPubkey(bytes.fromhex(data["public_key"]))) horcrux_public_key_shares.append( BLSPubkey(recipient_bls_public_keys[my_index])) horcrux_private_key_shares.append(horcrux_private_key_share) final_public_key = bls_pop._AggregatePKs(final_public_key_shares) click.echo("Shared BLS Public Key: " f"{click.style(f'0x{final_public_key.hex()}', fg='green')}") withdrawal_credentials = BLS_WITHDRAWAL_PREFIX withdrawal_credentials += SHA256(final_public_key)[1:] click.echo( "Withdrawal Credentials: " f"{click.style(f'0x{withdrawal_credentials.hex()}', fg='green')}") horcrux_private_key = 0 for private_key_share in horcrux_private_key_shares: horcrux_private_key += private_key_share horcrux_private_key %= PRIME if bls_pop.SkToPk(horcrux_private_key) != bls_pop._AggregatePKs( horcrux_public_key_shares): raise ValueError("Invalid calculated horcrux private key") return final_public_key, withdrawal_credentials, horcrux_private_key
def withdrawal_credentials(self) -> bytes: withdrawal_credentials = BLS_WITHDRAWAL_PREFIX withdrawal_credentials += SHA256(self.withdrawal_pk)[1:] return withdrawal_credentials