Merge pull request #2701 from KPrasch/keyring-v2

Deterministic Keystore
pull/2744/head
KPrasch 2021-07-03 16:46:40 -07:00 committed by GitHub
commit 527d4142c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
97 changed files with 1513 additions and 1901 deletions

View File

@ -67,7 +67,7 @@
become_flags: "-H -S"
shell: "{{ nucypher_exec }} felix init --geth --network {{ network }}"
environment:
NUCYPHER_KEYRING_PASSWORD: "{{ lookup('env', 'NUCYPHER_FELIX_KEYRING_PASSWORD') }}"
NUCYPHER_KEYSTORE_PASSWORD: "{{ lookup('env', 'NUCYPHER_FELIX_KEYSTORE_PASSWORD') }}"
LC_ALL: en_US.UTF-8
LANG: en_US.UTF-8
vars:
@ -87,7 +87,7 @@
become_flags: "-H -S"
shell: "{{ nucypher_exec }} felix createdb --geth --network {{ network }}"
environment:
NUCYPHER_KEYRING_PASSWORD: "{{ lookup('env', 'NUCYPHER_FELIX_KEYRING_PASSWORD') }}"
NUCYPHER_KEYSTORE_PASSWORD: "{{ lookup('env', 'NUCYPHER_FELIX_KEYSTORE_PASSWORD') }}"
NUCYPHER_FELIX_DB_SECRET: "{{ lookup('env', 'NUCYPHER_FELIX_DB_SECRET') }}"
LC_ALL: en_US.UTF-8
LANG: en_US.UTF-8
@ -111,7 +111,7 @@
dest: /etc/systemd/system/felix_faucet.service
mode: 0755
vars:
keyring_password: "{{ lookup('env', 'NUCYPHER_FELIX_KEYRING_PASSWORD') }}"
keystore_password: "{{ lookup('env', 'NUCYPHER_FELIX_KEYSTORE_PASSWORD') }}"
db_secret: "{{ lookup('env', 'NUCYPHER_FELIX_DB_SECRET') }}"
virtualenv_path: '/home/ubuntu/venv'
nucypher_network_domain: "{{ lookup('env', 'NUCYPHER_NETWORK_NAME') }}"

View File

@ -27,7 +27,7 @@
dest: /etc/systemd/system/felix_faucet.service
mode: 0755
vars:
keyring_password: "{{ lookup('env', 'NUCYPHER_FELIX_KEYRING_PASSWORD') }}"
keystore_password: "{{ lookup('env', 'NUCYPHER_FELIX_KEYSTORE_PASSWORD') }}"
db_secret: "{{ lookup('env', 'NUCYPHER_FELIX_DB_SECRET') }}"
virtualenv_path: '/home/ubuntu/venv'
nucypher_network_domain: "{{ lookup('env', 'NUCYPHER_NETWORK_NAME') }}"

View File

@ -57,7 +57,7 @@
args:
chdir: ./code
environment:
NUCYPHER_KEYRING_PASSWORD: "{{ ursula_password.stdout }}"
NUCYPHER_KEYSTORE_PASSWORD: "{{ ursula_password.stdout }}"
LC_ALL: en_US.UTF-8
LANG: en_US.UTF-8
ignore_errors: yes

View File

@ -9,17 +9,11 @@
paths: "{{geth_dir}}keystore"
register: keystore_files
- name: find Ursula private keyring files
- name: find Ursula keystore
become: yes
find:
paths: /home/nucypher/nucypher/keyring/private
register: private_keyrings
- name: find Ursula public keyring files
become: yes
find:
paths: /home/nucypher/nucypher/keyring/public
register: public_keyrings
paths: /home/nucypher/nucypher/keystore/
register: keystore
- name: find Ursula database files
find:
@ -44,21 +38,13 @@
- "/home/nucypher/nucypher/ursula.json"
- "{{geth_dir}}account.txt"
- name: "Backup Public Keyrings locally to: {{deployer_config_path}}/remote_worker_backups/"
- name: "Backup NuCypher Keystores locally to: {{deployer_config_path}}/remote_worker_backups/"
become: yes
# become_user: nucypher
fetch:
src: "{{item.path}}"
dest: "{{deployer_config_path}}/remote_worker_backups/"
with_items: "{{public_keyrings.files}}"
- name: "Backup Private Keyrings locally to: {{deployer_config_path}}/remote_worker_backups/"
become: yes
# become_user: nucypher
fetch:
src: "{{item.path}}"
dest: "{{deployer_config_path}}/remote_worker_backups/"
with_items: "{{private_keyrings.files}}"
with_items: "{{keystore.files}}"
- name: "Backup ursula.db to: {{deployer_config_path}}/remote_worker_backups/"
become: yes

View File

@ -71,6 +71,6 @@
become: yes
become_user: nucypher
when: ursula_check.stat.exists == False
command: "docker run -v /home/nucypher:/root/.local/share/ -e NUCYPHER_KEYRING_PASSWORD -it {{ nucypher_image | default('nucypher/nucypher:latest') }} nucypher ursula init --provider {{ blockchain_provider }} --worker-address {{active_account.stdout}} --rest-host {{ip_response.content}} --network {{network_name}} {{nucypher_ursula_init_options | default('')}} {{signer_options}}"
command: "docker run -v /home/nucypher:/root/.local/share/ -e NUCYPHER_KEYSTORE_PASSWORD -it {{ nucypher_image | default('nucypher/nucypher:latest') }} nucypher ursula init --provider {{ blockchain_provider }} --worker-address {{active_account.stdout}} --rest-host {{ip_response.content}} --network {{network_name}} {{nucypher_ursula_init_options | default('')}} {{signer_options}}"
environment:
NUCYPHER_KEYRING_PASSWORD: "{{runtime_envvars['NUCYPHER_KEYRING_PASSWORD']}}"
NUCYPHER_KEYSTORE_PASSWORD: "{{runtime_envvars['NUCYPHER_KEYSTORE_PASSWORD']}}"

View File

@ -51,7 +51,7 @@
- name: "update Ursula worker config"
become: yes
become_user: nucypher
command: "docker run -v /home/nucypher:/root/.local/share/ -e NUCYPHER_KEYRING_PASSWORD -it {{ nucypher_image | default('nucypher/nucypher:latest') }} nucypher ursula config --provider {{ blockchain_provider }} --worker-address {{active_account.stdout}} --rest-host {{ip_response.content}} --network {{network_name}} {{nucypher_ursula_init_options | default('')}} {{signer_options}} --config-file /root/.local/share/nucypher/ursula.json"
command: "docker run -v /home/nucypher:/root/.local/share/ -e NUCYPHER_KEYSTORE_PASSWORD -it {{ nucypher_image | default('nucypher/nucypher:latest') }} nucypher ursula config --provider {{ blockchain_provider }} --worker-address {{active_account.stdout}} --rest-host {{ip_response.content}} --network {{network_name}} {{nucypher_ursula_init_options | default('')}} {{signer_options}} --config-file /root/.local/share/nucypher/ursula.json"
environment: "{{runtime_envvars}}"
- name: "Backup Worker Nucypher Keystore locally to: {{deployer_config_path}}/remote_worker_backups/"
@ -129,7 +129,7 @@
msg:
"{{ursula_logs['stdout']}}"
- name: "Wait until we see that Ursula has decrypted her keyring and gotten started"
- name: "Wait until we see that Ursula has decrypted her keystore and gotten started"
become: yes
ignore_errors: yes
wait_for:

View File

@ -59,7 +59,7 @@ all:
ansible_python_interpreter: /usr/bin/python3
# these can be overridden at the instance level if desired
NUCYPHER_KEYRING_PASSWORD: xxxxxxxxxxxxxxxxxxxxxxxpanda
NUCYPHER_KEYSTORE_PASSWORD: xxxxxxxxxxxxxxxxxxxxxxxpanda
NUCYPHER_WORKER_ETH_PASSWORD: yyyyyyyyyyyyyyyyyyyystainpants
#nucypher_ursula_run_options: "--debug"
#nucypher_ursula_init_options: "--debug"

View File

@ -16,7 +16,7 @@
with_items:
- "{{geth_dir}}keystore"
- /home/nucypher/nucypher/ursula.db
- /home/nucypher/nucypher/keyring/
- /home/nucypher/nucypher/keystore/
- "{{geth_dir}}account.txt"
- home/nucypher/nucypher/ursula.json
@ -28,8 +28,7 @@
with_items:
- "{{geth_dir}}keystore"
- /home/nucypher/nucypher/ursula.db
- /home/nucypher/nucypher/keyring/private
- /home/nucypher/nucypher/keyring/public
- /home/nucypher/nucypher/keystore
- name: Restore Geth Keystore
become: yes
@ -41,25 +40,15 @@
with_fileglob:
- "{{restore_path}}{{geth_dir}}keystore/*"
- name: Restore private keyring
- name: Restore keystore
become: yes
copy:
src: "{{ item }}"
dest: /home/nucypher/nucypher/keyring/private/
dest: /home/nucypher/nucypher/keystore
owner: "nucypher"
mode: 0600
with_fileglob:
- "{{restore_path}}/home/nucypher/nucypher/keyring/private/*"
- name: Restore public keyring
become: yes
copy:
src: "{{ item }}"
dest: /home/nucypher/nucypher/keyring/public/
owner: "nucypher"
mode: 0600
with_fileglob:
- "{{restore_path}}/home/nucypher/nucypher/keyring/public/*"
- "{{restore_path}}/home/nucypher/nucypher/keystore/*"
- name: Restore Ursula database files
become: yes

View File

@ -4,7 +4,7 @@ Description="Run 'Felix', A NuCypher Test-ERC20 Faucet."
[Service]
User=root
Type=simple
Environment="NUCYPHER_KEYRING_PASSWORD={{ keyring_password }}"
Environment="NUCYPHER_KEYSTORE_PASSWORD={{ keystore_password }}"
Environment="NUCYPHER_FELIX_DB_SECRET={{ db_secret }}"
ExecStart={{ virtualenv_path }}/bin/nucypher felix run --debug --network {{ nucypher_network_domain }} --geth

View File

@ -4,7 +4,7 @@ Description="Run 'Lonely Ursula' - The Original Network Node."
[Service]
User=ubuntu
Type=simple
Environment="NUCYPHER_KEYRING_PASSWORD={{ ursula_password.stdout }}"
Environment="NUCYPHER_KEYSTORE_PASSWORD={{ ursula_password.stdout }}"
ExecStart={{ virtualenv_path }}/bin/nucypher ursula run --debug --lonely --network {{ nucypher_network_domain }}
[Install]

View File

@ -4,7 +4,7 @@ Description="Run 'Ursula', A NuCypher Staking Node."
[Service]
User=ubuntu
Type=simple
Environment="NUCYPHER_KEYRING_PASSWORD={{ursula_password.stdout}}"
Environment="NUCYPHER_KEYSTORE_PASSWORD={{ursula_password.stdout}}"
ExecStart={{ virtualenv_path }}/bin/nucypher ursula run --debug --network {{ nucypher_network_domain }} --federated-only --teacher {{ seed_node_metadata.checksum_address }}@https://{{ seed_node_metadata.rest_host }}:{{seed_node_metadata.rest_port}}
[Install]

View File

@ -101,7 +101,7 @@ Setup Alice Keys
^^^^^^^^^^^^^^^^
Alice uses an ethereum wallet to create publish access control policies to the ethereum blockchain,
and a set of related keys called a *"nucypher keyring"*.
and a set of related keys derived from a *"nucypher keystore"*.
First, instantiate a ``Signer`` to use for signing transactions. This is an API for Alice's ethereum
wallet, which can be an keystore file, trezor, ethereum node, or clef. The signer type and address
@ -136,44 +136,38 @@ If you are using a software wallet, be sure to unlock it:
>>> software_wallet.unlock_account(account='0x287A817426DD1AE78ea23e9918e2273b6733a43D', password=<ETH_PASSWORD>)
Next, create a NuCypher Keyring. This step will generate a new set of related private keys used for nucypher cryptography operations,
Next, create a NuCypher Keystore. This step will generate a new set of related private keys used for nucypher cryptography operations,
which can be integrated into your application's user on-boarding or setup logic. These keys will be stored on the disk,
encrypted-at-rest using the supplied password. Use the same account as the signer; Keyrings are labeled and associated
with ethereum accounts, so be sure to specify an account you control with a ``Signer``.
encrypted-at-rest using the supplied password. Use the same account as the signer; Keystores are timestamped and named by public key,
so be sure to specify an account you control with a ``Signer``.
.. code-block:: python
from nucypher.config.keyring import NucypherKeyring
from nucypher.crypto.keystore import Keystore
keyring = NucypherKeyring.generate(
checksum_address='0x287A817426DD1AE78ea23e9918e2273b6733a43D',
password=NEW_PASSWORD # used to encrypt nucypher private keys
)
keystore = Keystore.generate(password=NEW_PASSWORD) # used to encrypt nucypher private keys
# The keyring identifier
>>> keyring.checksum_address
0x287A817426DD1AE78ea23e9918e2273b6733a43D
# Be sure to use an address controlled by your signer!
>>> keyring.checksum_address in signer.accounts
True
# Public Key
>>> keystore.id
e76f101f35846f18d80bfda5c61e9ec2
# The root directory containing the private keys
>>> keyring.keyring_root
'/home/user/.local/share/nucypher/keyring'
>>> keystore.keystore_dir
'/home/user/.local/share/nucypher/keystore'
After generating a keyring, any future usage can decrypt the keys from the disk:
After generating a keystore, any future usage can decrypt the keys from the disk:
.. code-block:: python
from nucypher.config.keyring import NucypherKeyring
from nucypher.crypto.keystore import Keystore
# Restore an existing Alice keyring
keyring = NucypherKeyring(account='0x287A817426DD1AE78ea23e9918e2273b6733a43D')
# Restore an existing Alice keystore
path = '/home/user/.local/share/nucypher/keystore/1621399628-e76f101f35846f18d80bfda5c61e9ec2.priv'
keystore = Keystore(path)
# Unlock Alice's keyring
keyring.unlock(password=NUCYPHER_PASSWORD)
# Unlock Alice's keystore
keystore.unlock(password=NUCYPHER_PASSWORD)
.. code-block:: python
@ -185,7 +179,7 @@ After generating a keyring, any future usage can decrypt the keys from the disk:
# Instantiate Alice
alice = Alice(
keyring=keyring, # NuCypher Keyring
keystore=keystore, # NuCypher Keystore
known_nodes=[ursula], # Peers (Optional)
signer=signer, # Alice Wallet
provider_uri=<RPC ENDPOINT>, # Ethereum RPC endpoint
@ -247,12 +241,12 @@ Alice can grant access to Bob using his public keys:
Putting it all together, here's an example starter script for granting access using a
software wallet and an existing keyring:
software wallet and an existing keystore:
.. code-block:: python
from nucypher.blockchain.eth.signers import Signer
from nucypher.config.keyring import NucypherKeyring
from nucypher.crypto.keystore import Keystore
from nucypher.characters.lawful import Alice, Bob
from umbral.keys import UmbralPublicKey
from datetime import timedelta
@ -260,9 +254,9 @@ software wallet and an existing keyring:
import maya
# Restore Existing NuCypher Keyring
keyring = NucypherKeyring(account='0x287A817426DD1AE78ea23e9918e2273b6733a43D')
keyring.unlock('KEYRING PASSWORD')
# Restore Existing NuCypher Keystore
keystore = Keystore(keystore_path=path)
keystore.unlock('YOUR KEYSTORE PASSWORD')
# Ethereum Software Wallet
wallet = Signer.from_signer_uri("keystore:///home/user/.ethereum/goerli/keystore/UTC--2021...0278ad02...')
@ -272,7 +266,7 @@ software wallet and an existing keyring:
alice = Alice(
domain='lynx', # testnet
provider_uri='GOERLI RPC ENDPOINT',
keyring=keyring,
keystore=keystore,
signer=wallet,
)
@ -347,13 +341,13 @@ Bob's setup is similar to Alice's above.
alice = Alice.from_public_keys(verifying_key=alice_verifying_key)
enrico = Enrico(policy_encrypting_key=policy_encrypting_key)
# Restore Existing Bob keyring
keyring = NucypherKeyring(account='0xC080708026a3A280894365Efd51Bb64521c45147')
# Restore Existing Bob keystore
keystore = Keystore(keystore_path=path)
# Unlock keyring and make Bob
keyring.unlock(PASSWORD)
# Unlock keystore and make Bob
keystore.unlock(PASSWORD)
bob = Bob(
keyring=keyring,
keystore=keystore,
known_nodes=[ursula],
domain='lynx'
)

View File

@ -15,8 +15,8 @@ Where applicable, values are evaluated in the following order of precedence:
General
-------
* `NUCYPHER_KEYRING_PASSWORD`
Password for the `nucypher` Keyring.
* `NUCYPHER_KEYSTORE_PASSWORD`
Password for the `nucypher` Keystore.
* `NUCYPHER_PROVIDER_URI`
Default Web3 node provider URI.

View File

@ -13,12 +13,13 @@ three core areas of responsibility (in order of importance):
1. Keystore Diligence
---------------------
Requires that private keys used by the worker are backup or can be restored.
Requires that private keys used by the worker are backed up and can be restored.
Keystore diligence an be exercised by:
- Backing up the worker's private keys (both ethereum and nucypher).
- Using a password manager to generate a strong password when one is required.
- Keeping an offline record of the mnemonic recovery phrase.
- Backing up the worker's keystores (both ethereum and nucypher).
- Using a password manager to generate and store a strong password when one is required.
.. note::
@ -29,20 +30,14 @@ Keystore diligence an be exercised by:
$ nucypher --config-path
Encrypted worker keys can be found in the ``keyring`` directory:
Encrypted worker keys can be found in the ``keystore`` directory:
.. code-block:: bash
/home/user/.local/share/nucypher
├── ursula.json
├── keyring
│ ├── private
│ │ ├── delegating-0x12304EF1Dc04587225FEe3420CA6C786cdd58893.priv
│ │ ├── root-0x12304EF1Dc04587225FEe3420CA6C786cdd58893.priv
│ │ └── signing-0x12304EF1Dc04587225FEe3420CA6C786cdd58893.priv
│ └── public
│ ├── root-0x12304EF1Dc04587225FEe3420CA6C786cdd58893.pub
│ └── signing-0x12304EF1Dc04587225FEe3420CA6C786cdd58893.pub
├── keystore
│ ├── 1621399628-e76f101f35846f18d80bfda5c61e9ec2.priv
└── ...
2. Datastore Diligence

View File

@ -127,7 +127,7 @@ Export Worker Environment Variables
.. code:: bash
# Passwords used for both creation and unlocking
export NUCYPHER_KEYRING_PASSWORD=<YOUR KEYRING_PASSWORD>
export NUCYPHER_KEYSTORE_PASSWORD=<YOUR KEYSTORE_PASSWORD>
export NUCYPHER_WORKER_ETH_PASSWORD=<YOUR WORKER ETH ACCOUNT PASSWORD>
Initialize a new Worker
@ -140,7 +140,7 @@ Initialize a new Worker
-v ~/.local/share/nucypher:/root/.local/share/nucypher \
-v ~/.ethereum/:/root/.ethereum \
-p 9151:9151 \
-e NUCYPHER_KEYRING_PASSWORD \
-e NUCYPHER_KEYSTORE_PASSWORD \
nucypher/nucypher:latest \
nucypher ursula init \
--signer keystore:///root/.ethereum/keystore \
@ -166,7 +166,7 @@ Launch the worker
-v ~/.local/share/nucypher:/root/.local/share/nucypher \
-v ~/.ethereum/:/root/.ethereum \
-p 9151:9151 \
-e NUCYPHER_KEYRING_PASSWORD \
-e NUCYPHER_KEYSTORE_PASSWORD \
-e NUCYPHER_WORKER_ETH_PASSWORD \
nucypher/nucypher:latest \
nucypher ursula run \
@ -253,7 +253,7 @@ The configuration settings will be stored in an ursula configuration file.
User=<YOUR USER>
Type=simple
Environment="NUCYPHER_WORKER_ETH_PASSWORD=<YOUR WORKER ADDRESS PASSWORD>"
Environment="NUCYPHER_KEYRING_PASSWORD=<YOUR PASSWORD>"
Environment="NUCYPHER_KEYSTORE_PASSWORD=<YOUR PASSWORD>"
ExecStart=<VIRTUALENV PATH>/bin/nucypher ursula run
[Install]
@ -264,7 +264,7 @@ Replace the following values with your own:
* ``<YOUR USER>`` - The host system's username to run the process with (best practice is to use a dedicated user)
* ``<YOUR WORKER ADDRESS PASSWORD>`` - Worker's ETH account password
* ``<YOUR PASSWORD>`` - Ursula's keyring password
* ``<YOUR PASSWORD>`` - Ursula's keystore password
* ``<VIRTUALENV PATH>`` - The absolute path to the python virtual environment containing the ``nucypher`` executable

View File

@ -82,7 +82,7 @@ alice_config = AliceConfiguration(
alice_config.initialize(password=passphrase)
alice_config.keyring.unlock(password=passphrase)
alice_config.keystore.unlock(password=passphrase)
alicia = alice_config.produce()
# We will save Alicia's config to a file for later use

View File

@ -0,0 +1 @@
Characters use mnemonic seed words to derive deterministic keystore, taking the place of the "keyring".

View File

@ -0,0 +1 @@
Renames enviorment variable `NUCYPHER_KEYRING_PASSWORD` to `NUCYPHER_KEYSTORE_PASSWORD`

View File

@ -27,7 +27,7 @@ import maya
from bytestring_splitter import BytestringSplitter
from eth_typing import ChecksumAddress
from nucypher.crypto.api import keccak_digest
from ..crypto.utils import keccak_digest
from nucypher.utilities.logging import Logger
from .nicknames import Nickname

View File

@ -54,7 +54,7 @@ from nucypher.blockchain.eth.decorators import contract_api
from nucypher.blockchain.eth.events import ContractEvents
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
from nucypher.blockchain.eth.registry import BaseContractRegistry
from nucypher.crypto.api import sha256_digest
from nucypher.crypto.utils import sha256_digest
from nucypher.crypto.powers import TransactingPower
from nucypher.types import (
Agent,

View File

@ -513,7 +513,7 @@ class EthereumTesterClient(EthereumClient):
is_local = True
def unlock_account(self, account, password, duration: int = None) -> bool:
"""Returns True if the testing backend keyring has control of the given address."""
"""Returns True if the testing backend keystore has control of the given address."""
account = to_checksum_address(account)
keystore_accounts = self.w3.provider.ethereum_tester.get_accounts()
if account in keystore_accounts:
@ -524,7 +524,7 @@ class EthereumTesterClient(EthereumClient):
unlock_seconds=duration)
def lock_account(self, account) -> bool:
"""Returns True if the testing backend keyring has control of the given address."""
"""Returns True if the testing backend keystore has control of the given address."""
account = to_canonical_address(account)
keystore_accounts = self.w3.provider.ethereum_tester.backend.get_accounts()
if account in keystore_accounts:

View File

@ -17,6 +17,9 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import contextlib
from contextlib import suppress
from typing import ClassVar, Dict, List, Optional, Union
from constant_sorrow import default_constant_splitter
from constant_sorrow.constants import (
DO_NOT_SIGN,
@ -29,19 +32,15 @@ from constant_sorrow.constants import (
SIGNATURE_TO_FOLLOW,
STRANGER
)
from contextlib import suppress
from cryptography.exceptions import InvalidSignature
from eth_keys import KeyAPI as EthKeyAPI
from eth_utils import to_canonical_address, to_checksum_address
from typing import ClassVar, Dict, List, Optional, Union
from nucypher.acumen.nicknames import Nickname
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
from nucypher.blockchain.eth.registry import BaseContractRegistry, InMemoryContractRegistry
from nucypher.blockchain.eth.signers.base import Signer
from nucypher.characters.control.controllers import CLIController, JSONRPCController
from nucypher.config.keyring import NucypherKeyring
from nucypher.crypto.api import encrypt_and_sign
from nucypher.crypto.keystore import Keystore
from nucypher.crypto.kits import UmbralMessageKit
from nucypher.crypto.powers import (
CryptoPower,
@ -57,6 +56,7 @@ from nucypher.crypto.signing import (
)
from nucypher.crypto.splitters import signature_splitter
from nucypher.crypto.umbral_adapter import PublicKey, Signature
from nucypher.crypto.utils import encrypt_and_sign
from nucypher.network.middleware import RestMiddleware
from nucypher.network.nodes import Learner
@ -76,7 +76,7 @@ class Character(Learner):
federated_only: bool = False,
checksum_address: str = None,
network_middleware: RestMiddleware = None,
keyring: NucypherKeyring = None,
keystore: Keystore = None,
crypto_power: CryptoPower = None,
crypto_power_ups: List[CryptoPowerUp] = None,
provider_uri: str = None,
@ -138,18 +138,12 @@ class Character(Learner):
# Keys & Powers
#
if keyring:
keyring_root, keyring_checksum_address = keyring.keyring_root, keyring.checksum_address
if checksum_address and (keyring_checksum_address != checksum_address):
raise ValueError(f"Provided checksum address {checksum_address} "
f"does not match character's keyring checksum address {keyring_checksum_address}")
checksum_address = keyring_checksum_address
if keystore:
crypto_power_ups = list()
for power_up in self._default_crypto_powerups:
power = keyring.derive_crypto_power(power_class=power_up)
power = keystore.derive_crypto_power(power_class=power_up)
crypto_power_ups.append(power)
self.keyring = keyring
self.keystore = keystore
if crypto_power and crypto_power_ups:
raise ValueError("Pass crypto_power or crypto_power_ups (or neither), but not both.")
@ -203,6 +197,7 @@ class Character(Learner):
try:
derived_federated_address = self.derive_federated_address()
except NoSigningPower:
# TODO: Why allow such a character (without signing power) to be created at all?
derived_federated_address = NO_SIGNING_POWER.bool_value(False)
if checksum_address and (checksum_address != derived_federated_address):
@ -225,7 +220,7 @@ class Character(Learner):
verifying_key = self.public_keys(SigningPower)
self._stamp = StrangerStamp(verifying_key)
self.keyring_root = STRANGER
self.keystore_dir = STRANGER
self.network_middleware = STRANGER
self.checksum_address = checksum_address

View File

@ -24,7 +24,6 @@ from nucypher.characters.control.specifications.exceptions import InvalidInputDa
from nucypher.characters.control.specifications.fields.base import BaseField
from nucypher.crypto.constants import HRAC_LENGTH
from nucypher.crypto.kits import UmbralMessageKit
from nucypher.crypto.signing import Signature
from nucypher.crypto.splitters import signature_splitter

View File

@ -16,14 +16,20 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import json
from collections import OrderedDict, defaultdict
import contextlib
import maya
import json
import random
import time
from base64 import b64decode, b64encode
from collections import OrderedDict, defaultdict
from datetime import datetime
from functools import partial
from json.decoder import JSONDecodeError
from queue import Queue
from random import shuffle
from typing import Dict, Iterable, List, NamedTuple, Tuple, Union, Optional, Sequence, Set, Any
import maya
import time
from bytestring_splitter import (
BytestringKwargifier,
BytestringSplitter,
@ -43,19 +49,13 @@ from constant_sorrow.constants import (
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import Encoding
from cryptography.x509 import Certificate, NameOID, load_pem_x509_certificate
from datetime import datetime
from eth_typing.evm import ChecksumAddress
from eth_utils import to_checksum_address
from flask import Response, request
from functools import partial
from json.decoder import JSONDecodeError
from queue import Queue
from random import shuffle
from twisted.internet import reactor, stdio, threads
from twisted.internet.defer import Deferred
from twisted.internet.task import LoopingCall
from twisted.logger import Logger
from typing import Dict, Iterable, List, NamedTuple, Tuple, Union, Optional, Sequence, Set, Any
import nucypher
from nucypher.acumen.nicknames import Nickname
@ -74,7 +74,6 @@ from nucypher.characters.control.interfaces import AliceInterface, BobInterface,
from nucypher.cli.processes import UrsulaCommandProtocol
from nucypher.config.constants import END_OF_POLICIES_PROBATIONARY_PERIOD
from nucypher.config.storages import ForgetfulNodeStorage, NodeStorage
from nucypher.crypto.api import encrypt_and_sign, keccak_digest
from nucypher.crypto.constants import HRAC_LENGTH
from nucypher.crypto.keypairs import HostingKeypair
from nucypher.crypto.kits import UmbralMessageKit
@ -88,6 +87,7 @@ from nucypher.crypto.powers import (
from nucypher.crypto.signing import InvalidSignature
from nucypher.crypto.splitters import key_splitter, signature_splitter
from nucypher.crypto.umbral_adapter import Capsule, PublicKey, VerifiedKeyFrag, Signature, VerificationError, reencrypt
from nucypher.crypto.utils import keccak_digest, encrypt_and_sign
from nucypher.datastore.datastore import DatastoreTransactionError, RecordNotFound
from nucypher.datastore.queries import find_expired_policies, find_expired_treasure_maps
from nucypher.network.exceptions import NodeSeemsToBeDown
@ -1186,14 +1186,12 @@ class Ursula(Teacher, Character, Worker):
# Pre-existing or injected power
tls_hosting_power = self._crypto_power.power_ups(TLSHostingPower)
except TLSHostingPower.not_found_error:
if self.keyring:
# Restore from TLS private key on-disk
tls_hosting_power = self.keyring.derive_crypto_power(TLSHostingPower, host=host)
if self.keystore:
# Derive TLS private key from seed
tls_hosting_power = self.keystore.derive_crypto_power(TLSHostingPower, host=host)
else:
# Generate ephemeral private key ("Dev Mode")
tls_hosting_keypair = HostingKeypair(host=host,
checksum_address=self.checksum_address,
generate_certificate=True)
tls_hosting_keypair = HostingKeypair(host=host, generate_certificate=True)
tls_hosting_power = TLSHostingPower(keypair=tls_hosting_keypair, host=host)
self._crypto_power.consume_power_up(tls_hosting_power) # Consume!
return tls_hosting_power
@ -1514,11 +1512,6 @@ class Ursula(Teacher, Character, Worker):
if network_middleware is None:
network_middleware = RestMiddleware(registry=registry)
#
# WARNING: xxx Poison xxx
# Let's learn what we can about the ... "seednode".
#
# Parse node URI
host, port, staker_address = parse_node_uri(seed_uri)
@ -1533,7 +1526,7 @@ class Ursula(Teacher, Character, Worker):
# Create a temporary certificate storage area
temp_node_storage = ForgetfulNodeStorage(federated_only=federated_only)
temp_certificate_filepath = temp_node_storage.store_node_certificate(certificate=certificate)
temp_certificate_filepath = temp_node_storage.store_node_certificate(certificate=certificate, port=port)
# Load the host as a potential seed node
potential_seed_node = cls.from_rest_url(

View File

@ -25,7 +25,7 @@ from eth_tester.exceptions import ValidationError
from nucypher.blockchain.eth.signers.software import Web3Signer
from nucypher.characters.lawful import Alice, Ursula
from nucypher.config.constants import TEMPORARY_DOMAIN
from nucypher.crypto.api import encrypt_and_sign
from nucypher.crypto.utils import encrypt_and_sign
from nucypher.crypto.powers import CryptoPower, SigningPower, DecryptingPower, TransactingPower
from nucypher.exceptions import DevelopmentInstallationRequired
from nucypher.policy.collections import SignedTreasureMap
@ -101,7 +101,7 @@ class Vladimir(Ursula):
password = 'iamverybadass'
blockchain.w3.provider.ethereum_tester.add_account(cls.fraud_key, password=password)
except (ValidationError,):
# check if Vlad's key is already on the keyring...
# check if Vlad's key is already on the keystore...
if cls.fraud_address in blockchain.client.accounts:
return True
else:

View File

@ -16,10 +16,10 @@
"""
import click
import os
import click
from constant_sorrow.constants import NO_PASSWORD
from nacl.exceptions import CryptoError
from nucypher.blockchain.eth.decorators import validate_checksum_address
from nucypher.blockchain.eth.signers.software import ClefSigner
@ -27,13 +27,13 @@ from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.cli.literature import (
COLLECT_ETH_PASSWORD,
COLLECT_NUCYPHER_PASSWORD,
DECRYPTING_CHARACTER_KEYRING,
DECRYPTING_CHARACTER_KEYSTORE,
GENERIC_PASSWORD_PROMPT,
PASSWORD_COLLECTION_NOTICE
)
from nucypher.config.base import CharacterConfiguration
from nucypher.config.constants import NUCYPHER_ENVVAR_KEYRING_PASSWORD
from nucypher.config.keyring import NucypherKeyring
from nucypher.config.constants import NUCYPHER_ENVVAR_KEYSTORE_PASSWORD
from nucypher.crypto.keystore import Keystore, _WORD_COUNT
def get_password_from_prompt(prompt: str = GENERIC_PASSWORD_PROMPT, envvar: str = None, confirm: bool = False) -> str:
@ -78,30 +78,38 @@ def unlock_signer_account(config: CharacterConfiguration, json_ipc: bool) -> Non
config.signer.unlock_account(account=config.checksum_address, password=__password)
def get_nucypher_password(emitter, confirm: bool = False, envvar=NUCYPHER_ENVVAR_KEYRING_PASSWORD) -> str:
def get_nucypher_password(emitter, confirm: bool = False, envvar=NUCYPHER_ENVVAR_KEYSTORE_PASSWORD) -> str:
"""Interactively collect a nucypher password"""
prompt = COLLECT_NUCYPHER_PASSWORD
if confirm:
from nucypher.config.keyring import NucypherKeyring
emitter.message(PASSWORD_COLLECTION_NOTICE)
prompt += f" ({NucypherKeyring.MINIMUM_PASSWORD_LENGTH} character minimum)"
keyring_password = get_password_from_prompt(prompt=prompt, confirm=confirm, envvar=envvar)
return keyring_password
prompt += f" ({Keystore._MINIMUM_PASSWORD_LENGTH} character minimum)"
keystore_password = get_password_from_prompt(prompt=prompt, confirm=confirm, envvar=envvar)
return keystore_password
def unlock_nucypher_keyring(emitter: StdoutEmitter, password: str, character_configuration: CharacterConfiguration) -> bool:
"""Unlocks a nucypher keyring and attaches it to the supplied configuration if successful."""
emitter.message(DECRYPTING_CHARACTER_KEYRING.format(name=character_configuration.NAME.capitalize()), color='yellow')
def unlock_nucypher_keystore(emitter: StdoutEmitter, password: str, character_configuration: CharacterConfiguration) -> bool:
"""Unlocks a nucypher keystore and attaches it to the supplied configuration if successful."""
emitter.message(DECRYPTING_CHARACTER_KEYSTORE.format(name=character_configuration.NAME.capitalize()), color='yellow')
# precondition
if character_configuration.dev_mode:
return True # Dev accounts are always unlocked
# unlock
try:
character_configuration.attach_keyring()
character_configuration.keyring.unlock(password=password) # Takes ~3 seconds, ~1GB Ram
except CryptoError:
raise NucypherKeyring.AuthenticationFailed
else:
return True
character_configuration.keystore.unlock(password=password) # Takes ~3 seconds, ~1GB Ram
return True
def recover_keystore(emitter) -> None:
emitter.message('This procedure will recover your nucypher keystore from mnemonic seed words. '
'You will need to provide the entire mnemonic (space seperated) in the correct '
'order and choose a new password.', color='cyan')
click.confirm('Do you want to continue', abort=True)
__words = click.prompt("Enter nucypher keystore seed words")
word_count = len(__words.split())
if word_count != _WORD_COUNT:
emitter.message(f'Invalid mnemonic - Number of words must be {str(_WORD_COUNT)}, but only got {word_count}')
__password = get_nucypher_password(emitter=emitter, confirm=True)
keystore = Keystore.restore(words=__words, password=__password)
emitter.message(f'Recovered nucypher keystore {keystore.id} to \n {keystore.keystore_path}', color='green')

View File

@ -115,7 +115,7 @@ def confirm_destroy_configuration(config: CharacterConfiguration) -> bool:
database = "No database found"
confirmation = CHARACTER_DESTRUCTION.format(name=config.NAME,
root=config.config_root,
keystore=config.keyring_root,
keystore=config.keystore_dir,
nodestore=config.node_storage.source,
config=config.filepath,
database=database)

View File

@ -65,7 +65,7 @@ from nucypher.config.characters import AliceConfiguration
from nucypher.config.constants import (
TEMPORARY_DOMAIN,
)
from nucypher.config.keyring import NucypherKeyring
from nucypher.crypto.keystore import Keystore
from nucypher.network.middleware import RestMiddleware
from nucypher.policy.identity import Card
@ -264,7 +264,7 @@ class AliceCharacterOptions:
try:
ALICE = make_cli_character(character_config=config,
emitter=emitter,
unlock_keyring=not config.dev_mode,
unlock_keystore=not config.dev_mode,
unlock_signer=not config.federated_only,
teacher_uri=self.teacher_uri,
min_stake=self.min_stake,
@ -272,7 +272,7 @@ class AliceCharacterOptions:
lonely=self.config_options.lonely,
json_ipc=json_ipc)
return ALICE
except NucypherKeyring.AuthenticationFailed as e:
except Keystore.AuthenticationFailed as e:
emitter.echo(str(e), color='red', bold=True)
click.get_current_context().exit(1)

View File

@ -200,7 +200,7 @@ class BobCharacterOptions:
config = self.config_options.create_config(emitter, config_file)
BOB = make_cli_character(character_config=config,
emitter=emitter,
unlock_keyring=not self.config_options.dev,
unlock_keystore=not self.config_options.dev,
unlock_signer=not config.federated_only and config.signer_uri,
teacher_uri=self.teacher_uri,
min_stake=self.min_stake,

View File

@ -23,7 +23,7 @@ from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.cli.actions.auth import (
get_client_password,
get_nucypher_password,
unlock_nucypher_keyring
unlock_nucypher_keystore
)
from nucypher.cli.actions.configure import destroy_configuration, handle_missing_configuration_file
from nucypher.cli.actions.select import select_config_file
@ -161,7 +161,7 @@ class FelixCharacterOptions:
try:
# Authenticate
unlock_nucypher_keyring(emitter,
unlock_nucypher_keystore(emitter,
character_configuration=felix_config,
password=get_nucypher_password(emitter=emitter, confirm=False))

View File

@ -19,7 +19,7 @@
import click
from nucypher.blockchain.eth.signers.software import ClefSigner
from nucypher.cli.actions.auth import get_client_password, get_nucypher_password
from nucypher.cli.actions.auth import get_client_password, get_nucypher_password, recover_keystore
from nucypher.cli.actions.configure import (
destroy_configuration,
handle_missing_configuration_file,
@ -63,7 +63,7 @@ from nucypher.config.constants import (
NUCYPHER_ENVVAR_WORKER_ETH_PASSWORD,
TEMPORARY_DOMAIN
)
from nucypher.config.keyring import NucypherKeyring
from nucypher.crypto.keystore import Keystore
class UrsulaConfigOptions:
@ -156,7 +156,7 @@ class UrsulaConfigOptions:
)
except FileNotFoundError:
return handle_missing_configuration_file(character_config_class=UrsulaConfiguration, config_file=config_file)
except NucypherKeyring.AuthenticationFailed as e:
except Keystore.AuthenticationFailed as e:
emitter.echo(str(e), color='red', bold=True)
# TODO: Exit codes (not only for this, but for other exceptions)
return click.get_current_context().exit(1)
@ -202,7 +202,7 @@ class UrsulaConfigOptions:
db_filepath=self.db_filepath,
domain=self.domain,
federated_only=self.federated_only,
checksum_address=self.worker_address,
worker_address=self.worker_address,
registry_filepath=self.registry_filepath,
provider_uri=self.provider_uri,
signer_uri=self.signer_uri,
@ -263,7 +263,7 @@ class UrsulaCharacterOptions:
emitter=emitter,
min_stake=self.min_stake,
teacher_uri=self.teacher_uri,
unlock_keyring=not self.config_options.dev,
unlock_keystore=not self.config_options.dev,
client_password=__password,
unlock_signer=False, # Ursula's unlock is managed separately using client_password.
lonely=self.config_options.lonely,
@ -271,7 +271,7 @@ class UrsulaCharacterOptions:
json_ipc=json_ipc)
return ursula_config, URSULA
except NucypherKeyring.AuthenticationFailed as e:
except Keystore.AuthenticationFailed as e:
emitter.echo(str(e), color='red', bold=True)
# TODO: Exit codes (not only for this, but for other exceptions)
return click.get_current_context().exit(1)
@ -310,6 +310,16 @@ def init(general_config, config_options, force, config_root):
paint_new_installation_help(emitter, new_configuration=ursula_config, filepath=filepath)
@ursula.command()
@group_config_options
@group_general_config
def recover(general_config, config_options):
# TODO: Combine with work in PR #2682
# TODO: Integrate regeneration of configuration files
emitter = setup_emitter(general_config, config_options.worker_address)
recover_keystore(emitter=emitter)
@ursula.command()
@group_config_options
@option_config_file

View File

@ -426,11 +426,11 @@ Do not forget this password, and ideally store it using a password manager.
COLLECT_ETH_PASSWORD = "Enter ethereum account password ({checksum_address})"
COLLECT_NUCYPHER_PASSWORD = 'Enter nucypher keyring password'
COLLECT_NUCYPHER_PASSWORD = 'Enter nucypher keystore password'
GENERIC_PASSWORD_PROMPT = "Enter password"
DECRYPTING_CHARACTER_KEYRING = 'Authenticating {name}'
DECRYPTING_CHARACTER_KEYSTORE = 'Authenticating {name}'
#

View File

@ -17,11 +17,15 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import click
import maya
from constant_sorrow.constants import NO_KEYSTORE_ATTACHED
from nucypher.blockchain.eth.sol.__conf__ import SOLIDITY_COMPILER_VERSION
from nucypher.characters.banners import NUCYPHER_BANNER
from nucypher.config.constants import DEFAULT_CONFIG_ROOT, USER_LOG_DIR, END_OF_POLICIES_PROBATIONARY_PERIOD
from constant_sorrow.constants import NO_KEYRING_ATTACHED
from nucypher.config.constants import (
DEFAULT_CONFIG_ROOT,
USER_LOG_DIR,
END_OF_POLICIES_PROBATIONARY_PERIOD
)
def echo_version(ctx, param, value):
@ -41,35 +45,33 @@ def echo_solidity_version(ctx, param, value):
def echo_config_root_path(ctx, param, value):
if not value or ctx.resilient_parsing:
return
click.secho(DEFAULT_CONFIG_ROOT)
click.secho(str(DEFAULT_CONFIG_ROOT.resolve()))
ctx.exit()
def echo_logging_root_path(ctx, param, value):
if not value or ctx.resilient_parsing:
return
click.secho(USER_LOG_DIR)
click.secho(str(USER_LOG_DIR.resolve()))
ctx.exit()
def paint_new_installation_help(emitter, new_configuration, filepath):
character_config_class = new_configuration.__class__
character_name = character_config_class.NAME.lower()
if new_configuration.keyring != NO_KEYRING_ATTACHED:
maybe_public_key = bytes(new_configuration.keyring.signing_public_key).hex()
if new_configuration.keystore != NO_KEYSTORE_ATTACHED:
maybe_public_key = new_configuration.keystore.id
else:
maybe_public_key = "(no keyring attached)"
emitter.message(f"Generated keyring", color='green')
maybe_public_key = "(no keystore attached)"
emitter.message(f"Generated keystore", color='green')
emitter.message(f"""
Public key (stamp): {maybe_public_key}
Path to keyring: {new_configuration.keyring_root}
Public Key: {maybe_public_key}
Path to Keystore: {new_configuration.keystore_dir}
- You can share your public key with anyone. Others need it to interact with you.
- Never share secret keys with anyone!
- Backup your keyring! Character keys are required to interact with the protocol!
- Backup your keystore! Character keys are required to interact with the protocol!
- Remember your password! Without the password, it's impossible to decrypt the key!
""")

View File

@ -42,7 +42,7 @@ from nucypher.characters.base import Character
from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.cli.actions.auth import (
get_nucypher_password,
unlock_nucypher_keyring,
unlock_nucypher_keystore,
unlock_signer_account
)
from nucypher.cli.literature import (
@ -68,7 +68,7 @@ def setup_emitter(general_config, banner: str = None) -> StdoutEmitter:
def make_cli_character(character_config,
emitter,
unlock_keyring: bool = True,
unlock_keystore: bool = True,
unlock_signer: bool = True,
teacher_uri: str = None,
min_stake: int = 0,
@ -80,9 +80,9 @@ def make_cli_character(character_config,
# Pre-Init
#
# Handle Keyring
if unlock_keyring:
unlock_nucypher_keyring(emitter,
# Handle KEYSTORE
if unlock_keystore:
unlock_nucypher_keystore(emitter,
character_configuration=character_config,
password=get_nucypher_password(emitter=emitter, confirm=False))

View File

@ -16,6 +16,6 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from constant_sorrow.constants import NO_KEYRING_ATTACHED
from constant_sorrow.constants import NO_KEYSTORE_ATTACHED
NO_KEYRING_ATTACHED.bool_value(False)
NO_KEYSTORE_ATTACHED.bool_value(False)

View File

@ -21,15 +21,15 @@ import os
import re
from abc import ABC, abstractmethod
from decimal import Decimal
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Union, Callable, Optional, List
from constant_sorrow.constants import (
UNKNOWN_VERSION,
UNINITIALIZED_CONFIGURATION,
NO_KEYRING_ATTACHED,
NO_KEYSTORE_ATTACHED,
NO_BLOCKCHAIN_CONNECTION,
FEDERATED_ADDRESS,
DEVELOPMENT_CONFIGURATION,
LIVE_CONFIGURATION
)
@ -45,12 +45,12 @@ from nucypher.blockchain.eth.registry import (
from nucypher.blockchain.eth.signers import Signer
from nucypher.characters.lawful import Ursula
from nucypher.config import constants
from nucypher.config.keyring import NucypherKeyring
from nucypher.config.storages import (
ForgetfulNodeStorage,
LocalFileBasedNodeStorage,
NodeStorage
)
from nucypher.crypto.keystore import Keystore
from nucypher.crypto.powers import CryptoPower, CryptoPowerUp
from nucypher.crypto.umbral_adapter import Signature
from nucypher.network.middleware import RestMiddleware
@ -312,16 +312,18 @@ class CharacterConfiguration(BaseConfiguration):
'Sideways Engagement' of Character classes; a reflection of input parameters.
"""
VERSION = 2 # bump when static payload scheme changes
VERSION = 3 # bump when static payload scheme changes
CHARACTER_CLASS = NotImplemented
DEFAULT_CONTROLLER_PORT = NotImplemented
MNEMONIC_KEYSTORE = False
DEFAULT_DOMAIN = NetworksInventory.DEFAULT
DEFAULT_NETWORK_MIDDLEWARE = RestMiddleware
TEMP_CONFIGURATION_DIR_PREFIX = 'tmp-nucypher'
SIGNER_ENVVAR = None
# When we begin to support other threshold schemes, this will be one of the concepts that makes us want a factory. #571
# When we begin to support other threshold schemes,
# this will be one of the concepts that makes us want a factory. #571
known_node_class = Ursula
# Gas
@ -336,7 +338,7 @@ class CharacterConfiguration(BaseConfiguration):
'gas_strategy',
'max_gas_price', # gwei
'signer_uri',
'keyring_root'
'keystore_path'
)
def __init__(self,
@ -354,9 +356,9 @@ class CharacterConfiguration(BaseConfiguration):
checksum_address: str = None,
crypto_power: CryptoPower = None,
# Keyring
keyring: NucypherKeyring = None,
keyring_root: str = None,
# Keystore
keystore: Keystore = None,
keystore_path: Path = None,
# Learner
learn_on_same_thread: bool = False,
@ -402,10 +404,12 @@ class CharacterConfiguration(BaseConfiguration):
self.is_me = True
self.checksum_address = checksum_address
# Keyring
# Keystore
self.crypto_power = crypto_power
self.keyring = keyring or NO_KEYRING_ATTACHED
self.keyring_root = keyring_root or UNINITIALIZED_CONFIGURATION
if keystore_path and not keystore:
keystore = Keystore(keystore_path=keystore_path)
self.__keystore = self.__keystore = keystore or NO_KEYSTORE_ATTACHED.bool_value(False)
self.keystore_dir = Path(keystore.keystore_path).parent if keystore else UNINITIALIZED_CONFIGURATION
# Contract Registry
if registry and registry_filepath:
@ -521,11 +525,18 @@ class CharacterConfiguration(BaseConfiguration):
def __call__(self, **character_kwargs):
return self.produce(**character_kwargs)
@property
def keystore(self) -> Keystore:
return self.__keystore
def attach_keystore(self, keystore: Keystore) -> None:
self.__keystore = keystore
@classmethod
def checksum_address_from_filepath(cls, filepath: str) -> str:
pattern = re.compile(r'''
(^\w+)-
(0x{1} # Then, 0x the start of the string, exactly once
(0x{1} # Then, 0x the start of the string, exactly once
[0-9a-fA-F]{40}) # Followed by exactly 40 hex chars
''',
re.VERBOSE)
@ -587,8 +598,6 @@ class CharacterConfiguration(BaseConfiguration):
def destroy(self) -> None:
"""Parse a node configuration and remove all associated files from the filesystem"""
self.attach_keyring()
self.keyring.destroy()
os.remove(self.config_file_location)
def generate_parameters(self, **overrides) -> dict:
@ -655,13 +664,13 @@ class CharacterConfiguration(BaseConfiguration):
def static_payload(self) -> dict:
"""Exported static configuration values for initializing Ursula"""
keystore_path = str(self.keystore.keystore_path) if self.keystore else None
payload = dict(
# Identity
federated_only=self.federated_only,
checksum_address=self.checksum_address,
keyring_root=self.keyring_root,
keystore_path=keystore_path,
# Behavior
domain=self.domain,
@ -695,9 +704,12 @@ class CharacterConfiguration(BaseConfiguration):
return payload
@property # TODO: Graduate to a method and "derive" dynamic from static payload.
@property
def dynamic_payload(self) -> dict:
"""Exported dynamic configuration values for initializing Ursula"""
"""
Exported dynamic configuration values for initializing Ursula.
These values are used to init a character instance but are not saved to the JSON configuration.
"""
payload = dict()
if not self.federated_only:
payload.update(dict(registry=self.registry, signer=self.signer))
@ -705,7 +717,7 @@ class CharacterConfiguration(BaseConfiguration):
payload.update(dict(network_middleware=self.network_middleware or self.DEFAULT_NETWORK_MIDDLEWARE(),
known_nodes=self.known_nodes,
node_storage=self.node_storage,
keyring=self.keyring,
keystore=self.keystore,
crypto_power_ups=self.derive_node_power_ups()))
return payload
@ -718,7 +730,7 @@ class CharacterConfiguration(BaseConfiguration):
@property
def runtime_filepaths(self) -> dict:
filepaths = dict(config_root=self.config_root,
keyring_root=self.keyring_root,
keystore_dir=self.keystore_dir,
registry_filepath=self.registry_filepath)
return filepaths
@ -727,7 +739,7 @@ class CharacterConfiguration(BaseConfiguration):
"""Dynamically generate paths based on configuration root directory"""
filepaths = dict(config_root=config_root,
config_file_location=os.path.join(config_root, cls.generate_filename()),
keyring_root=os.path.join(config_root, 'keyring'))
keystore_dir=os.path.join(config_root, 'keystore'))
return filepaths
def _cache_runtime_filepaths(self) -> None:
@ -737,21 +749,11 @@ class CharacterConfiguration(BaseConfiguration):
if getattr(self, field) is UNINITIALIZED_CONFIGURATION:
setattr(self, field, filepath)
def attach_keyring(self, checksum_address: str = None, *args, **kwargs) -> None:
account = checksum_address or self.checksum_address
if not account:
raise self.ConfigurationError("No account specified to unlock keyring")
if self.keyring is not NO_KEYRING_ATTACHED:
if self.keyring.checksum_address != account:
raise self.ConfigurationError("There is already a keyring attached to this configuration.")
return
self.keyring = NucypherKeyring(keyring_root=self.keyring_root, account=account, *args, **kwargs)
def derive_node_power_ups(self) -> List[CryptoPowerUp]:
power_ups = list()
if self.is_me and not self.dev_mode:
for power_class in self.CHARACTER_CLASS._default_crypto_powerups:
power_up = self.keyring.derive_crypto_power(power_class)
power_up = self.keystore.derive_crypto_power(power_class)
power_ups.append(power_up)
return power_ups
@ -766,7 +768,7 @@ class CharacterConfiguration(BaseConfiguration):
# Persistent
else:
self._ensure_config_root_exists()
self.write_keyring(password=password)
self.write_keystore(password=password, interactive=self.MNEMONIC_KEYSTORE)
self._cache_runtime_filepaths()
self.node_storage.initialize()
@ -780,27 +782,9 @@ class CharacterConfiguration(BaseConfiguration):
self.log.debug(message)
return self.config_root
def write_keyring(self, password: str, checksum_address: str = None, **generation_kwargs) -> NucypherKeyring:
# Configure checksum address
checksum_address = checksum_address or self.checksum_address
if self.federated_only:
checksum_address = FEDERATED_ADDRESS
elif not checksum_address:
raise self.ConfigurationError(f'No checksum address provided for decentralized configuration.')
# Generate new keys
self.keyring = NucypherKeyring.generate(password=password,
keyring_root=self.keyring_root,
checksum_address=checksum_address,
**generation_kwargs)
# In the case of a federated keyring generation,
# the generated federated address must be set here.
if self.federated_only:
self.checksum_address = self.keyring.checksum_address
return self.keyring
def write_keystore(self, password: str, interactive: bool = True) -> Keystore:
self.__keystore = Keystore.generate(password=password, keystore_dir=self.keystore_dir, interactive=interactive)
return self.keystore
@classmethod
def load_node_storage(cls, storage_payload: dict, federated_only: bool):

View File

@ -33,7 +33,6 @@ from nucypher.config.constants import (
NUCYPHER_ENVVAR_ALICE_ETH_PASSWORD,
NUCYPHER_ENVVAR_BOB_ETH_PASSWORD
)
from nucypher.config.keyring import NucypherKeyring
from nucypher.utilities.networking import LOOPBACK_ADDRESS
@ -50,6 +49,7 @@ class UrsulaConfiguration(CharacterConfiguration):
DEFAULT_AVAILABILITY_CHECKS = False
LOCAL_SIGNERS_ALLOWED = True
SIGNER_ENVVAR = NUCYPHER_ENVVAR_WORKER_ETH_PASSWORD
MNEMONIC_KEYSTORE = True
def __init__(self,
rest_host: str = None,
@ -84,7 +84,6 @@ class UrsulaConfiguration(CharacterConfiguration):
"""
Extracts worker address by "peeking" inside the ursula configuration file.
"""
checksum_address = cls.peek(filepath=filepath, field='checksum_address')
federated = bool(cls.peek(filepath=filepath, field='federated_only'))
if not federated:
@ -101,7 +100,7 @@ class UrsulaConfiguration(CharacterConfiguration):
return base_filepaths
def generate_filepath(self, modifier: str = None, *args, **kwargs) -> str:
filepath = super().generate_filepath(modifier=modifier or bytes(self.keyring.signing_public_key).hex()[:8], *args, **kwargs)
filepath = super().generate_filepath(modifier=modifier or self.keystore.id[:8], *args, **kwargs)
return filepath
def static_payload(self) -> dict:
@ -138,22 +137,6 @@ class UrsulaConfiguration(CharacterConfiguration):
return ursula
def attach_keyring(self, checksum_address: str = None, *args, **kwargs) -> None:
if self.federated_only:
account = checksum_address or self.checksum_address
else:
account = checksum_address or self.worker_address
return super().attach_keyring(checksum_address=account)
def write_keyring(self, password: str, **generation_kwargs) -> NucypherKeyring:
keyring = super().write_keyring(password=password,
encrypting=True,
rest=True,
host=self.rest_host,
checksum_address=self.worker_address,
**generation_kwargs)
return keyring
def destroy(self) -> None:
if os.path.isfile(self.db_filepath):
os.remove(self.db_filepath)
@ -217,12 +200,6 @@ class AliceConfiguration(CharacterConfiguration):
payload['payment_periods'] = self.payment_periods
return {**super().static_payload(), **payload}
def write_keyring(self, password: str, **generation_kwargs) -> NucypherKeyring:
return super().write_keyring(password=password,
encrypting=True,
rest=False,
**generation_kwargs)
class BobConfiguration(CharacterConfiguration):
from nucypher.characters.lawful import Bob
@ -230,7 +207,7 @@ class BobConfiguration(CharacterConfiguration):
CHARACTER_CLASS = Bob
NAME = CHARACTER_CLASS.__name__.lower()
DEFAULT_CONTROLLER_PORT = 7151
DEFFAULT_STORE_POLICIES = True
DEFAULT_STORE_POLICIES = True
DEFAULT_STORE_CARDS = True
SIGNER_ENVVAR = NUCYPHER_ENVVAR_BOB_ETH_PASSWORD
@ -241,19 +218,13 @@ class BobConfiguration(CharacterConfiguration):
)
def __init__(self,
store_policies: bool = DEFFAULT_STORE_POLICIES,
store_policies: bool = DEFAULT_STORE_POLICIES,
store_cards: bool = DEFAULT_STORE_CARDS,
*args, **kwargs):
super().__init__(*args, **kwargs)
self.store_policies = store_policies
self.store_cards = store_cards
def write_keyring(self, password: str, **generation_kwargs) -> NucypherKeyring:
return super().write_keyring(password=password,
encrypting=True,
rest=False,
**generation_kwargs)
def static_payload(self) -> dict:
payload = dict(
store_policies=self.store_policies,
@ -302,14 +273,6 @@ class FelixConfiguration(CharacterConfiguration):
)
return {**super().static_payload(), **payload}
def write_keyring(self, password: str, **generation_kwargs) -> NucypherKeyring:
return super().write_keyring(password=password,
encrypting=True, # TODO: #668
rest=True,
host=self.rest_host,
curve=self.tls_curve,
**generation_kwargs)
class StakeHolderConfiguration(CharacterConfiguration):

View File

@ -27,7 +27,7 @@ from maya import MayaDT
import nucypher
# Environment variables
NUCYPHER_ENVVAR_KEYRING_PASSWORD = "NUCYPHER_KEYRING_PASSWORD"
NUCYPHER_ENVVAR_KEYSTORE_PASSWORD = "NUCYPHER_KEYSTORE_PASSWORD"
NUCYPHER_ENVVAR_WORKER_ADDRESS = "NUCYPHER_WORKER_ADDRESS"
NUCYPHER_ENVVAR_WORKER_ETH_PASSWORD = "NUCYPHER_WORKER_ETH_PASSWORD"
NUCYPHER_ENVVAR_ALICE_ETH_PASSWORD = "NUCYPHER_ALICE_ETH_PASSWORD"
@ -42,8 +42,8 @@ NUCYPHER_TEST_DIR = BASE_DIR / 'tests'
# User Application Filepaths
APP_DIR = AppDirs(nucypher.__title__, nucypher.__author__)
DEFAULT_CONFIG_ROOT = os.getenv('NUCYPHER_CONFIG_ROOT', default=APP_DIR.user_data_dir)
USER_LOG_DIR = os.getenv('NUCYPHER_USER_LOG_DIR', default=APP_DIR.user_log_dir)
DEFAULT_CONFIG_ROOT = Path(os.getenv('NUCYPHER_CONFIG_ROOT', default=APP_DIR.user_data_dir))
USER_LOG_DIR = Path(os.getenv('NUCYPHER_USER_LOG_DIR', default=APP_DIR.user_log_dir))
DEFAULT_LOG_FILENAME = "nucypher.log"
DEFAULT_JSON_LOG_FILENAME = "nucypher.json"
@ -72,5 +72,6 @@ TEMPORARY_DOMAIN = ":temporary-domain:" # for use with `--dev` node runtimes
# Event Blocks Throttling
NUCYPHER_EVENTS_THROTTLE_MAX_BLOCKS = 'NUCYPHER_EVENTS_THROTTLE_MAX_BLOCKS'
# Probationary period (see #2353, #2584)
END_OF_POLICIES_PROBATIONARY_PERIOD = MayaDT.from_iso8601('2021-08-31T23:59:59.0Z')

View File

@ -1,693 +0,0 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import base64
import contextlib
import json
import os
import stat
from functools import partial
from json import JSONDecodeError
from os.path import abspath
from typing import Callable, ClassVar, Dict, List, Tuple, Union, Optional
from constant_sorrow.constants import FEDERATED_ADDRESS, KEYRING_LOCKED
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.backends.openssl.ec import _EllipticCurvePrivateKey
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve
from cryptography.hazmat.primitives.serialization import Encoding, load_pem_private_key
from cryptography.x509 import Certificate
from eth_account import Account
from eth_keys import KeyAPI as EthKeyAPI
from eth_utils import to_checksum_address
from nacl.exceptions import CryptoError
from nucypher.config.constants import DEFAULT_CONFIG_ROOT
from nucypher.crypto.api import generate_teacher_certificate, _TLS_CURVE
from nucypher.crypto.keypairs import HostingKeypair
from nucypher.crypto.passwords import (
secret_box_encrypt,
secret_box_decrypt,
SecretBoxAuthenticationError,
derive_key_material_from_password,
)
from nucypher.crypto.powers import (DecryptingPower, DerivedKeyBasedPower, KeyPairBasedPower, SigningPower)
from nucypher.crypto.umbral_adapter import SecretKey, PublicKey, SecretKeyFactory
from nucypher.network.server import TLSHostingPower
from nucypher.utilities.logging import Logger
FILE_ENCODING = 'utf-8'
KEY_ENCODER = base64.urlsafe_b64encode
KEY_DECODER = base64.urlsafe_b64decode
TLS_CERTIFICATE_ENCODING = Encoding.PEM
__PRIVATE_FLAGS = os.O_WRONLY | os.O_CREAT | os.O_EXCL # Write, Create, Non-Existing
__PRIVATE_MODE = stat.S_IRUSR | stat.S_IWUSR # 0o600
__PUBLIC_FLAGS = os.O_WRONLY | os.O_CREAT | os.O_EXCL # Write, Create, Non-Existing
__PUBLIC_MODE = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH # 0o644
PrivateKeyData = Union[
Dict[str, bytes],
bytes,
_EllipticCurvePrivateKey
]
class PrivateKeyExistsError(RuntimeError):
pass
class ExistingKeyringError(RuntimeError):
pass
def unlock_required(func):
"""Method decorator"""
def wrapped(keyring=None, *args, **kwargs):
if not keyring.is_unlocked:
raise NucypherKeyring.KeyringLocked("{} is locked. Unlock with .unlock".format(keyring.account))
return func(keyring, *args, **kwargs)
return wrapped
def _assemble_key_data(key_data: bytes,
master_salt: bytes,
wrap_salt: bytes) -> Dict[str, bytes]:
encoded_key_data = {
'key': key_data,
'master_salt': master_salt,
'wrap_salt': wrap_salt,
}
return encoded_key_data
def _read_keyfile(keypath: str,
deserializer: Union[Callable[[bytes], Union[PrivateKeyData, bytes, str]], None]
) -> Union[PrivateKeyData, bytes, str]:
"""
Parses a keyfile and return decoded, deserialized key data.
"""
with open(keypath, 'rb') as keyfile:
key_data = keyfile.read()
if deserializer:
key_data = deserializer(key_data)
return key_data
def _write_private_keyfile(keypath: str,
key_data: PrivateKeyData,
serializer: Union[Callable[[PrivateKeyData], bytes], None],
) -> str:
"""
Creates a permissioned keyfile and save it to the local filesystem.
The file must be created in this call, and will fail if the path exists.
Returns the filepath string used to write the keyfile.
Note: getting and setting the umask is not thread-safe!
See linux open docs: http://man7.org/linux/man-pages/man2/open.2.html
---------------------------------------------------------------------
O_CREAT - If pathname does not exist, create it as a regular file.
O_EXCL - Ensure that this call creates the file: if this flag is
specified in conjunction with O_CREAT, and pathname already
exists, then open() fails with the error EEXIST.
---------------------------------------------------------------------
"""
if os.path.exists(keypath):
raise PrivateKeyExistsError(f"Private keyfile {keypath} already exists.")
try:
keyfile_descriptor = os.open(keypath, flags=__PRIVATE_FLAGS, mode=__PRIVATE_MODE)
finally:
os.umask(0) # Set the umask to 0 after opening
if serializer:
key_data = serializer(key_data)
with os.fdopen(keyfile_descriptor, 'wb') as keyfile:
keyfile.write(key_data)
return keypath
def _write_public_keyfile(keypath: str,
key_data: bytes) -> str:
"""
Creates a permissioned keyfile and save it to the local filesystem.
The file must be created in this call, and will fail if the path exists.
Returns the filepath string used to write the keyfile.
Note: getting and setting the umask is not thread-safe!
See Linux open docs: http://man7.org/linux/man-pages/man2/open.2.html
---------------------------------------------------------------------
O_CREAT - If pathname does not exist, create it as a regular file.
O_EXCL - Ensure that this call creates the file: if this flag is
specified in conjunction with O_CREAT, and pathname already
exists, then open() fails with the error EEXIST.
---------------------------------------------------------------------
"""
try:
keyfile_descriptor = os.open(keypath, flags=__PUBLIC_FLAGS, mode=__PUBLIC_MODE)
finally:
os.umask(0) # Set the umask to 0 after opening
with os.fdopen(keyfile_descriptor, 'wb') as keyfile:
keyfile.write(key_data)
return keypath
def _write_tls_certificate(certificate: Certificate,
full_filepath: str,
force: bool = False,
) -> str:
cert_already_exists = os.path.isfile(full_filepath)
if force is False and cert_already_exists:
raise FileExistsError('A TLS certificate already exists at {}.'.format(full_filepath))
with open(full_filepath, 'wb') as certificate_file:
public_pem_bytes = certificate.public_bytes(TLS_CERTIFICATE_ENCODING)
certificate_file.write(public_pem_bytes)
return full_filepath
def _read_tls_public_certificate(filepath: str) -> Certificate:
"""Deserialize an X509 certificate from a filepath"""
try:
with open(filepath, 'rb') as certificate_file:
cert = x509.load_pem_x509_certificate(certificate_file.read(), backend=default_backend())
return cert
except FileNotFoundError:
raise FileNotFoundError("No SSL certificate found at {}".format(filepath))
#
# Keypair Generation
#
def _generate_encryption_keys() -> Tuple[SecretKey, PublicKey]:
"""Use pyUmbral keys to generate a new encrypting key pair"""
privkey = SecretKey.random()
pubkey = privkey.public_key()
return privkey, pubkey
def _generate_signing_keys() -> Tuple[SecretKey, PublicKey]:
"""
"""
privkey = SecretKey.random()
pubkey = privkey.public_key()
return privkey, pubkey
def _generate_wallet(password: str) -> Tuple[str, dict]:
"""Create a new wallet address and private "transacting" key encrypted with the password"""
account = Account.create(extra_entropy=os.urandom(32)) # max out entropy for keccak256
encrypted_wallet_data = Account.encrypt(private_key=account.privateKey, password=password)
return account.address, encrypted_wallet_data
def _generate_tls_keys(host: str, checksum_address: str, curve: EllipticCurve) -> Tuple[_EllipticCurvePrivateKey, Certificate]:
cert, private_key = generate_teacher_certificate(host=host, curve=curve, checksum_address=checksum_address)
return private_key, cert
def _serialize_private_key_to_pem(key_data: PrivateKeyData, password: bytes) -> bytes:
# TODO: Can we skip this check - below function will fail anyway, this is more informative though
if not isinstance(key_data, _EllipticCurvePrivateKey):
raise TypeError("Only _EllipticCurvePrivateKey is a valid type for serialization. Got {}".format(type(key_data)))
return key_data.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.BestAvailableEncryption(password=password)
)
def _deserialize_private_key_from_pem(key_data: bytes, password: bytes) -> PrivateKeyData:
private_key = load_pem_private_key(data=key_data, password=password)
return private_key
def _serialize_private_key(key_data: PrivateKeyData) -> bytes:
# TODO: Can we skip this check - below function will fail anyway, this is more informative though
if not isinstance(key_data, dict):
raise TypeError("Only dict is a valid type for serialization. Got {}".format(type(key_data)))
metadata = dict()
for field, value in key_data.items():
metadata[field] = KEY_ENCODER(bytes(value)).decode()
try:
metadata = json.dumps(metadata, indent=4)
except JSONDecodeError:
raise NucypherKeyring.KeyringError("Invalid or corrupted key data")
except TypeError:
raise
return bytes(metadata, encoding=FILE_ENCODING)
def _deserialize_private_key(key_data: bytes) -> PrivateKeyData:
key_metadata = key_data.decode(encoding=FILE_ENCODING)
try:
key_metadata = json.loads(key_metadata)
except JSONDecodeError:
raise NucypherKeyring.KeyringError("Invalid or corrupted key data")
key_metadata = {field: KEY_DECODER(value.encode())
for field, value in key_metadata.items()}
return key_metadata
class NucypherKeyring:
"""
Handles keys for a single identity, recognized by account.
Warning: This class handles private keys!
- keyring
- .private
- key.priv
- key.priv.pem
- public
- key.pub
- cert.pem
"""
MINIMUM_PASSWORD_LENGTH = 16
_default_keyring_root = os.path.join(DEFAULT_CONFIG_ROOT, 'keyring')
__DEFAULT_TLS_CURVE = ec.SECP384R1
log = Logger("keys")
class KeyringError(Exception):
pass
class KeyringLocked(KeyringError):
pass
class AuthenticationFailed(KeyringError):
pass
def __init__(self,
account: str,
keyring_root: str = None,
root_key_path: str = None,
pub_root_key_path: str = None,
signing_key_path: str = None,
pub_signing_key_path: str = None,
delegating_key_path: str = None,
tls_key_path: str = None,
tls_certificate_path: str = None,
) -> None:
"""
Generates a NuCypherKeyring instance with the provided key paths falling back to default keyring paths.
"""
# Identity
self.__account = account
self.__keyring_root = keyring_root or self._default_keyring_root
# Generate base filepaths
__default_base_filepaths = self._generate_base_filepaths(keyring_root=self.__keyring_root)
self.__public_key_dir = __default_base_filepaths['public_key_dir']
self.__private_key_dir = __default_base_filepaths['private_key_dir']
# Check for overrides
__default_key_filepaths = self._generate_key_filepaths(account=self.__account,
public_key_dir=self.__public_key_dir,
private_key_dir=self.__private_key_dir)
# Private
self.__root_keypath = root_key_path or __default_key_filepaths['root']
self.__signing_keypath = signing_key_path or __default_key_filepaths['signing']
self.__delegating_keypath = delegating_key_path or __default_key_filepaths['delegating']
self.__tls_keypath = tls_key_path or __default_key_filepaths['tls']
# Public
self.__root_pub_keypath = pub_root_key_path or __default_key_filepaths['root_pub']
self.__signing_pub_keypath = pub_signing_key_path or __default_key_filepaths['signing_pub']
self.__tls_certificate_path = tls_certificate_path or __default_key_filepaths['tls_certificate']
# Set Initial State
self.__derived_key_material = KEYRING_LOCKED
def __del__(self) -> None:
self.lock()
#
# Public Keys
#
@property
def checksum_address(self) -> str:
return to_checksum_address(self.__account)
@property
def signing_public_key(self):
signature_pubkey_bytes = _read_keyfile(keypath=self.__signing_pub_keypath, deserializer=None)
signature_pubkey = PublicKey.from_bytes(signature_pubkey_bytes)
return signature_pubkey
@property
def encrypting_public_key(self):
encrypting_pubkey_bytes = _read_keyfile(keypath=self.__root_pub_keypath, deserializer=None)
encrypting_pubkey = PublicKey.from_bytes(encrypting_pubkey_bytes)
return encrypting_pubkey
@property
def certificate_filepath(self) -> str:
return self.__tls_certificate_path
@property
def keyring_root(self) -> str:
return self.__keyring_root
#
# Utils
#
@staticmethod
def _generate_base_filepaths(keyring_root: str) -> Dict[str, str]:
base_paths = dict(public_key_dir=os.path.join(keyring_root, 'public'),
private_key_dir=os.path.join(keyring_root, 'private'))
return base_paths
@staticmethod
def _generate_key_filepaths(public_key_dir: str,
private_key_dir: str,
account: str) -> dict:
__key_filepaths = {
'root': os.path.join(private_key_dir, 'root-{}.priv'.format(account)),
'root_pub': os.path.join(public_key_dir, 'root-{}.pub'.format(account)),
'signing': os.path.join(private_key_dir, 'signing-{}.priv'.format(account)),
'delegating': os.path.join(private_key_dir, 'delegating-{}.priv'.format(account)),
'signing_pub': os.path.join(public_key_dir, 'signing-{}.pub'.format(account)),
'tls': os.path.join(private_key_dir, '{}.priv.pem'.format(account)),
'tls_certificate': os.path.join(public_key_dir, '{}.pem'.format(account))
}
return __key_filepaths
@unlock_required
def __decrypt_keyfile(self, key_path: str) -> SecretKey:
"""Returns plaintext version of decrypting key."""
key_data = _read_keyfile(key_path, deserializer=_deserialize_private_key)
try:
key_bytes = secret_box_decrypt(key_material=self.__derived_key_material,
salt=key_data['wrap_salt'],
ciphertext=key_data['key'])
except SecretBoxAuthenticationError as e:
raise self.AuthenticationFailed('Invalid or incorrect nucypher keyring password.') from e
plain_umbral_key = SecretKey.from_bytes(key_bytes)
return plain_umbral_key
#
# Public API
#
@property
def account(self) -> str:
return self.__account
@property
def is_unlocked(self) -> bool:
return self.__derived_key_material is not KEYRING_LOCKED
def lock(self) -> bool:
"""Make efforts to remove references to the cached key data"""
self.__derived_key_material = KEYRING_LOCKED
return self.is_unlocked
def unlock(self, password: str) -> bool:
if self.is_unlocked:
return self.is_unlocked
key_data = _read_keyfile(keypath=self.__root_keypath, deserializer=_deserialize_private_key)
self.log.info("Unlocking keyring.")
derived_key = derive_key_material_from_password(password=password.encode(), salt=key_data['master_salt'])
self.__derived_key_material = derived_key
self.log.info("Finished unlocking.")
return self.is_unlocked
@unlock_required
def derive_crypto_power(self, power_class: ClassVar, host: Optional[str] = None) -> Union[KeyPairBasedPower, DerivedKeyBasedPower]:
"""
Takes either a SigningPower or a DecryptingPower and returns
either a SigningPower or DecryptingPower with the coinciding
private key.
"""
# Keypair-Based
if issubclass(power_class, KeyPairBasedPower):
codex = {SigningPower: self.__signing_keypath,
DecryptingPower: self.__root_keypath,
TLSHostingPower: self.__tls_keypath}
try:
path = codex[power_class]
except KeyError:
failure_message = "{} is an invalid type for deriving a CryptoPower".format(power_class.__name__)
raise TypeError(failure_message)
if power_class is TLSHostingPower: # TODO: something more elegant
if not host:
raise ValueError('Host is required to derive a TLSHostingPower')
tls_key_deserializer = partial(_deserialize_private_key_from_pem, password=self.__derived_key_material)
private_key = _read_keyfile(keypath=path, deserializer=tls_key_deserializer)
keypair = HostingKeypair(host=host,
private_key=private_key,
checksum_address=self.checksum_address,
generate_certificate=False,
certificate_filepath=self.__tls_certificate_path)
new_cryptopower = TLSHostingPower(keypair=keypair, host=host)
else:
privkey = self.__decrypt_keyfile(key_path=path)
keypair = power_class._keypair_class(privkey)
new_cryptopower = power_class(keypair=keypair)
# Derived
elif issubclass(power_class, DerivedKeyBasedPower):
key_data = _read_keyfile(self.__delegating_keypath, deserializer=_deserialize_private_key)
try:
keying_material = secret_box_decrypt(key_material=self.__derived_key_material,
salt=key_data['wrap_salt'],
ciphertext=key_data['key'])
except SecretBoxAuthenticationError as e:
raise self.AuthenticationFailed('Invalid or incorrect nucypher keyring password.') from e
new_cryptopower = power_class(keying_material=keying_material)
else:
failure_message = "{} is an invalid type for deriving a CryptoPower.".format(power_class.__name__)
raise ValueError(failure_message)
return new_cryptopower
#
# Create
#
@classmethod
def generate(cls,
checksum_address: str,
password: str,
encrypting: bool = True,
rest: bool = False,
host: str = None,
curve: EllipticCurve = None,
keyring_root: str = None,
force: bool = False,
) -> 'NucypherKeyring':
"""
Generates new encrypting, signing, and wallet keys encrypted with the password,
respectively saving keyfiles on the local filesystem from *default* paths,
returning the corresponding Keyring instance.
"""
keyring_root = keyring_root or cls._default_keyring_root
failures = cls.validate_password(password)
if failures:
raise cls.AuthenticationFailed(", ".join(failures)) # TODO: Ensure this scope is seperable from the scope containing the password
if not any((encrypting, rest)):
raise ValueError('Either "encrypting", "wallet", or "tls" must be True '
'to generate new keys, or set "no_keys" to True to skip generation.')
if curve is None:
curve = _TLS_CURVE
_base_filepaths = cls._generate_base_filepaths(keyring_root=keyring_root)
_public_key_dir = _base_filepaths['public_key_dir']
_private_key_dir = _base_filepaths['private_key_dir']
#
# Generate New Keypairs
#
keyring_args = dict()
if checksum_address is not FEDERATED_ADDRESS:
# Addresses read from some node keyrings (clients) are *not* returned in checksum format.
checksum_address = to_checksum_address(checksum_address)
if encrypting is True:
signing_private_key, signing_public_key = _generate_signing_keys()
if checksum_address is FEDERATED_ADDRESS:
verifying_key_as_eth_key = EthKeyAPI.PublicKey.from_compressed_bytes(bytes(signing_public_key))
checksum_address = verifying_key_as_eth_key.to_checksum_address()
else:
# TODO: Consider a "Repair" mode here
# signing_private_key, signing_public_key = ...
pass
if not checksum_address:
raise ValueError("Checksum address must be provided for non-federated keyring generation")
__key_filepaths = cls._generate_key_filepaths(account=checksum_address,
private_key_dir=_private_key_dir,
public_key_dir=_public_key_dir)
if encrypting is True:
encrypting_private_key, encrypting_public_key = _generate_encryption_keys()
delegating_keying_material = bytes(SecretKeyFactory.random())
# Derive Wrapping Keys
password_salt, encrypting_salt, signing_salt, delegating_salt = (os.urandom(32) for _ in range(4))
cls.log.info("About to derive key from password.")
derived_key_material = derive_key_material_from_password(salt=password_salt, password=password.encode())
# Encapsulate Private Keys
encrypting_key_data = secret_box_encrypt(key_material=derived_key_material,
salt=encrypting_salt,
plaintext=bytes(encrypting_private_key))
signing_key_data = secret_box_encrypt(key_material=derived_key_material,
salt=signing_salt,
plaintext=bytes(signing_private_key))
delegating_key_data = secret_box_encrypt(key_material=derived_key_material,
salt=delegating_salt,
plaintext=delegating_keying_material)
# Assemble Private Keys
encrypting_key_metadata = _assemble_key_data(key_data=encrypting_key_data,
master_salt=password_salt,
wrap_salt=encrypting_salt)
signing_key_metadata = _assemble_key_data(key_data=signing_key_data,
master_salt=password_salt,
wrap_salt=signing_salt)
delegating_key_metadata = _assemble_key_data(key_data=delegating_key_data,
master_salt=password_salt,
wrap_salt=delegating_salt)
#
# Write Keys
#
# Create base paths if the do not exist.
os.makedirs(abspath(keyring_root), exist_ok=True, mode=0o700)
if not os.path.isdir(_public_key_dir):
os.mkdir(_public_key_dir, mode=0o744) # public dir
if not os.path.isdir(_private_key_dir):
os.mkdir(_private_key_dir, mode=0o700) # private dir
try:
rootkey_path = _write_private_keyfile(keypath=__key_filepaths['root'],
key_data=encrypting_key_metadata,
serializer=_serialize_private_key)
sigkey_path = _write_private_keyfile(keypath=__key_filepaths['signing'],
key_data=signing_key_metadata,
serializer=_serialize_private_key)
delegating_key_path = _write_private_keyfile(keypath=__key_filepaths['delegating'],
key_data=delegating_key_metadata,
serializer=_serialize_private_key)
# Write Public Keys
root_keypath = _write_public_keyfile(__key_filepaths['root_pub'], bytes(encrypting_public_key))
signing_keypath = _write_public_keyfile(__key_filepaths['signing_pub'], bytes(signing_public_key))
except (PrivateKeyExistsError, FileExistsError):
if not force:
raise ExistingKeyringError(f"There is an existing keyring for address '{checksum_address}'")
else:
# Commit
keyring_args.update(
keyring_root=keyring_root,
root_key_path=rootkey_path,
pub_root_key_path=root_keypath,
signing_key_path=sigkey_path,
pub_signing_key_path=signing_keypath,
delegating_key_path=delegating_key_path,
)
if rest is True:
if not all((host, curve, checksum_address)): # TODO: Do we want to allow showing up with an old wallet and generating a new cert? Probably.
raise ValueError("host, checksum_address and curve are required to make a new keyring TLS certificate. Got {}, {}".format(host, curve))
private_key, cert = _generate_tls_keys(host=host, checksum_address=checksum_address, curve=curve)
tls_key_serializer = partial(_serialize_private_key_to_pem, password=derived_key_material)
tls_key_path = _write_private_keyfile(keypath=__key_filepaths['tls'],
key_data=private_key,
serializer=tls_key_serializer)
certificate_filepath = _write_tls_certificate(full_filepath=__key_filepaths['tls_certificate'],
certificate=cert)
keyring_args.update(tls_certificate_path=certificate_filepath, tls_key_path=tls_key_path)
keyring_instance = cls(account=checksum_address, **keyring_args)
return keyring_instance
@classmethod
def validate_password(cls, password: str) -> List:
"""
Validate a password and return True or raise an error with a failure reason.
NOTICE: Do not raise inside this function.
"""
rules = (
(bool(password), 'Password must not be blank.'),
(len(password) >= cls.MINIMUM_PASSWORD_LENGTH,
f'Password must be at least {cls.MINIMUM_PASSWORD_LENGTH} characters long.'),
)
failures = list()
for rule, failure_message in rules:
if not rule:
failures.append(failure_message)
return failures
def destroy(self):
base_filepaths = self._generate_base_filepaths(keyring_root=self.__keyring_root)
public_key_dir = base_filepaths['public_key_dir']
private_key_dir = base_filepaths['private_key_dir']
keypaths = self._generate_key_filepaths(account=self.checksum_address,
public_key_dir=public_key_dir,
private_key_dir=private_key_dir)
# Remove the parsed paths from the disk, whether they exist or not.
for filepath in keypaths.values():
with contextlib.suppress(FileNotFoundError):
os.remove(filepath)

View File

@ -15,27 +15,24 @@ You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from pathlib import Path
import OpenSSL
import binascii
import os
import tempfile
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Any, Set, Union
import OpenSSL
import binascii
from bytestring_splitter import BytestringSplittingError
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import Encoding
from cryptography.x509 import Certificate, NameOID
from eth_utils import is_checksum_address
from typing import Any, Callable, Set, Tuple, Union
from cryptography.x509 import Certificate
from nucypher.acumen.nicknames import Nickname
from nucypher.blockchain.eth.decorators import validate_checksum_address
from nucypher.blockchain.eth.registry import BaseContractRegistry
from nucypher.config.constants import DEFAULT_CONFIG_ROOT
from nucypher.crypto.api import read_certificate_pseudonym, InvalidNodeCertificate
from nucypher.crypto.signing import SignatureStamp
from nucypher.utilities.logging import Logger
@ -71,9 +68,6 @@ class NodeStorage(ABC):
def __setitem__(self, key, value):
return self.store_node_metadata(node=value)
def __delitem__(self, key):
self.remove(checksum_address=key)
def __iter__(self):
return self.all(federated_only=self.federated_only)
@ -97,8 +91,8 @@ class NodeStorage(ABC):
return common_name_from_cert
def _write_tls_certificate(self,
port: int, # used to avoid duplicate certs with the same IP
certificate: Certificate,
host: str = None,
force: bool = True) -> str:
# Read
@ -106,26 +100,9 @@ class NodeStorage(ABC):
subject_components = x509.get_subject().get_components()
common_name_as_bytes = subject_components[0][1]
common_name_on_certificate = common_name_as_bytes.decode()
if not host:
host = common_name_on_certificate
host = common_name_on_certificate
try:
pseudonym = certificate.subject.get_attributes_for_oid(NameOID.PSEUDONYM)[0]
except IndexError:
raise InvalidNodeCertificate(f"Missing checksum address on certificate for host '{host}'. "
f"Does this certificate belong to an Ursula?")
else:
checksum_address = pseudonym.value
if not is_checksum_address(checksum_address):
raise InvalidNodeCertificate("Invalid certificate wallet address encountered: {}".format(checksum_address))
# Validate
# TODO: It's better for us to have checked this a while ago so that this situation is impossible. #443
if host and (host != common_name_on_certificate):
raise ValueError(f"You passed a hostname ('{host}') that does not match the certificate's common name.")
certificate_filepath = self.generate_certificate_filepath(checksum_address=checksum_address)
certificate_filepath = self.generate_certificate_filepath(host=host, port=port)
certificate_already_exists = os.path.isfile(certificate_filepath)
if force is False and certificate_already_exists:
raise FileExistsError('A TLS certificate already exists at {}.'.format(certificate_filepath))
@ -136,13 +113,11 @@ class NodeStorage(ABC):
public_pem_bytes = certificate.public_bytes(self.TLS_CERTIFICATE_ENCODING)
certificate_file.write(public_pem_bytes)
nickname = Nickname.from_seed(checksum_address)
self.log.debug(f"Saved TLS certificate for {nickname} {checksum_address}: {certificate_filepath}")
self.log.debug(f"Saved TLS certificate for {host} to {certificate_filepath}")
return certificate_filepath
@abstractmethod
def store_node_certificate(self, certificate: Certificate) -> str:
def store_node_certificate(self, certificate: Certificate, port: int) -> str:
raise NotImplementedError
@abstractmethod
@ -151,7 +126,7 @@ class NodeStorage(ABC):
raise NotImplementedError
@abstractmethod
def generate_certificate_filepath(self, checksum_address: str) -> str:
def generate_certificate_filepath(self, host: str, port: int) -> str:
raise NotImplementedError
@abstractmethod
@ -179,11 +154,6 @@ class NodeStorage(ABC):
"""Retrieve a single stored node"""
raise NotImplementedError
@abstractmethod
def remove(self, checksum_address: str) -> bool:
"""Remove a single stored node"""
raise NotImplementedError
@abstractmethod
def clear(self) -> bool:
"""Remove all stored nodes"""
@ -215,21 +185,21 @@ class ForgetfulNodeStorage(NodeStorage):
def get(self,
federated_only: bool,
host: str = None,
checksum_address: str = None,
stamp: SignatureStamp = None,
certificate_only: bool = False):
if not bool(checksum_address) ^ bool(host):
if not bool(stamp) ^ bool(host):
message = "Either pass checksum_address or host; Not both. Got ({} {})".format(checksum_address, host)
raise ValueError(message)
if certificate_only is True:
try:
return self.__certificates[checksum_address or host]
return self.__certificates[stamp or host]
except KeyError:
raise self.UnknownNode
else:
try:
return self.__metadata[checksum_address or host]
return self.__metadata[stamp or host]
except KeyError:
raise self.UnknownNode
@ -238,35 +208,19 @@ class ForgetfulNodeStorage(NodeStorage):
os.remove(temp_certificate)
return len(self.__temporary_certificates) == 0
def store_node_certificate(self, certificate: Certificate):
checksum_address = read_certificate_pseudonym(certificate=certificate)
self.__certificates[checksum_address] = certificate
filepath = self._write_tls_certificate(certificate=certificate)
def store_node_certificate(self, certificate: Certificate, port: int) -> str:
filepath = self._write_tls_certificate(certificate=certificate, port=port)
return filepath
def store_node_metadata(self, node, filepath: str = None):
self.__metadata[node.checksum_address] = node
return self.__metadata[node.checksum_address]
def store_node_metadata(self, node, filepath: str = None) -> bytes:
self.__metadata[node.stamp] = node
return self.__metadata[node.stamp]
@validate_checksum_address
def generate_certificate_filepath(self, checksum_address: str) -> str:
filename = '{}.pem'.format(checksum_address)
def generate_certificate_filepath(self, host: str, port: int) -> str:
filename = f'{host}:{port}.pem'
filepath = os.path.join(self._temp_certificates_dir, filename)
return filepath
@validate_checksum_address
def remove(self,
checksum_address: str,
metadata: bool = True,
certificate: bool = True
) -> Tuple[bool, str]:
if metadata is True:
del self.__metadata[checksum_address]
if certificate is True:
del self.__certificates[checksum_address]
return True, checksum_address
def clear(self, metadata: bool = True, certificates: bool = True) -> None:
"""Forget all stored nodes and certificates"""
if metadata is True:
@ -357,34 +311,26 @@ class LocalFileBasedNodeStorage(NodeStorage):
#
@validate_checksum_address
def __get_certificate_filename(self, checksum_address: str):
return '{}.{}'.format(checksum_address, Encoding.PEM.name.lower())
def __get_certificate_filename(self, host: str, port: int) -> str:
return f'{host}:{port}.{Encoding.PEM.name.lower()}'
def __get_certificate_filepath(self, certificate_filename: str) -> str:
return os.path.join(self.certificates_dir, certificate_filename)
@validate_checksum_address
def generate_certificate_filepath(self, checksum_address: str) -> str:
certificate_filename = self.__get_certificate_filename(checksum_address)
def generate_certificate_filepath(self, host: str, port: int) -> str:
certificate_filename = self.__get_certificate_filename(host=host, port=port)
certificate_filepath = self.__get_certificate_filepath(certificate_filename=certificate_filename)
return certificate_filepath
@validate_checksum_address
def __read_node_tls_certificate(self, filepath: str = None, checksum_address: str = None) -> Certificate:
def __read_node_tls_certificate(self, filepath: str) -> Certificate:
"""Deserialize an X509 certificate from a filepath"""
if not bool(filepath) ^ bool(checksum_address):
raise ValueError("Either pass filepath or checksum_address; Not both.")
if not filepath and checksum_address is not None:
filepath = self.generate_certificate_filepath(checksum_address)
try:
with open(filepath, 'rb') as certificate_file:
certificate = x509.load_pem_x509_certificate(certificate_file.read(), backend=default_backend())
# Sanity check:
# Validate the checksum address inside the cert as a consistency check against
# nodes that may have been altered on the disk somehow.
read_certificate_pseudonym(certificate=certificate)
return certificate
except FileNotFoundError:
raise FileNotFoundError("No SSL certificate found at {}".format(filepath))
@ -393,10 +339,11 @@ class LocalFileBasedNodeStorage(NodeStorage):
# Metadata
#
@validate_checksum_address
def __generate_metadata_filepath(self, checksum_address: str, metadata_dir: str = None) -> str:
def __generate_metadata_filepath(self, stamp: Union[SignatureStamp, str], metadata_dir: str = None) -> str:
if isinstance(stamp, SignatureStamp):
stamp = bytes(stamp).hex()
metadata_path = os.path.join(metadata_dir or self.metadata_dir,
self.__METADATA_FILENAME_TEMPLATE.format(checksum_address))
self.__METADATA_FILENAME_TEMPLATE.format(stamp))
return metadata_path
def __read_metadata(self, filepath: str):
@ -453,39 +400,23 @@ class LocalFileBasedNodeStorage(NodeStorage):
return known_nodes
@validate_checksum_address
def get(self, checksum_address: str, federated_only: bool, certificate_only: bool = False):
def get(self, stamp: str, federated_only: bool, certificate_only: bool = False):
if certificate_only is True:
certificate = self.__read_node_tls_certificate(checksum_address=checksum_address)
certificate = self.__read_node_tls_certificate(stamp=stamp)
return certificate
metadata_path = self.__generate_metadata_filepath(checksum_address=checksum_address)
metadata_path = self.__generate_metadata_filepath(stamp=stamp)
node = self.__read_metadata(filepath=metadata_path)
return node
def store_node_certificate(self, certificate: Certificate, force: bool = True):
certificate_filepath = self._write_tls_certificate(certificate=certificate, force=force)
def store_node_certificate(self, certificate: Certificate, port: int, force: bool = True):
certificate_filepath = self._write_tls_certificate(certificate=certificate, port=port, force=force)
return certificate_filepath
def store_node_metadata(self, node, filepath: str = None) -> str:
address = node.checksum_address
filepath = self.__generate_metadata_filepath(checksum_address=address, metadata_dir=filepath)
filepath = self.__generate_metadata_filepath(stamp=node.stamp, metadata_dir=filepath)
self.__write_metadata(filepath=filepath, node=node)
return filepath
@validate_checksum_address
def remove(self, checksum_address: str, metadata: bool = True, certificate: bool = True) -> None:
if metadata is True:
metadata_filepath = self.__generate_metadata_filepath(checksum_address=checksum_address)
os.remove(metadata_filepath)
self.log.debug("Deleted {} from the filesystem".format(checksum_address))
if certificate is True:
certificate_filepath = self.generate_certificate_filepath(checksum_address=checksum_address)
os.remove(certificate_filepath)
self.log.debug("Deleted {} from the filesystem".format(checksum_address))
return
def clear(self, metadata: bool = True, certificates: bool = True) -> None:
"""Forget all stored nodes and certificates"""

View File

@ -1,219 +0,0 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import datetime
import sha3
from constant_sorrow import constants
from cryptography import x509
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.backends.openssl import backend
from cryptography.hazmat.backends.openssl.ec import _EllipticCurvePrivateKey
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve
from cryptography.x509 import Certificate
from cryptography.x509.oid import NameOID
from eth_account import Account
from eth_account.messages import encode_defunct
from eth_utils import is_checksum_address, to_checksum_address
from ipaddress import IPv4Address
from random import SystemRandom
from typing import Tuple
from nucypher.crypto.constants import SHA256
from nucypher.crypto.kits import UmbralMessageKit
import nucypher.crypto.umbral_adapter as umbral
from nucypher.crypto.umbral_adapter import SecretKey, PublicKey, Signature
SYSTEM_RAND = SystemRandom()
_TLS_CURVE = ec.SECP384R1
class InvalidNodeCertificate(RuntimeError):
"""Raised when an Ursula's certificate is not valid because it is missing the checksum address."""
def secure_random(num_bytes: int) -> bytes:
"""
Returns an amount `num_bytes` of data from the OS's random device.
If a randomness source isn't found, returns a `NotImplementedError`.
In this case, a secure random source most likely doesn't exist and
randomness will have to found elsewhere.
:param num_bytes: Number of bytes to return.
:return: bytes
"""
# TODO: Should we just use os.urandom or avoid the import w/ this?
return SYSTEM_RAND.getrandbits(num_bytes * 8).to_bytes(num_bytes, byteorder='big')
def secure_random_range(min: int, max: int) -> int:
"""
Returns a number from a secure random source betwee the range of
`min` and `max` - 1.
:param min: Minimum number in the range
:param max: Maximum number in the range
:return: int
"""
return SYSTEM_RAND.randrange(min, max)
def keccak_digest(*messages: bytes) -> bytes:
"""
Accepts an iterable containing bytes and digests it returning a
Keccak digest of 32 bytes (keccak_256).
Although we use SHA256 in many cases, we keep keccak handy in order
to provide compatibility with the Ethereum blockchain.
:param bytes: Data to hash
:rtype: bytes
:return: bytestring of digested data
"""
_hash = sha3.keccak_256()
for message in messages:
_hash.update(bytes(message))
digest = _hash.digest()
return digest
def sha256_digest(*messages: bytes) -> bytes:
"""
Accepts an iterable containing bytes and digests it returning a
SHA256 digest of 32 bytes
:param bytes: Data to hash
:rtype: bytes
:return: bytestring of digested data
"""
_hash_ctx = hashes.Hash(hashes.SHA256(), backend=backend)
for message in messages:
_hash_ctx.update(bytes(message))
digest = _hash_ctx.finalize()
return digest
def recover_address_eip_191(message: bytes, signature: bytes) -> str:
"""
Recover checksum address from EIP-191 signature
"""
signable_message = encode_defunct(primitive=message)
recovery = Account.recover_message(signable_message=signable_message, signature=signature)
recovered_address = to_checksum_address(recovery)
return recovered_address
def verify_eip_191(address: str, message: bytes, signature: bytes) -> bool:
"""
EIP-191 Compatible signature verification for usage with w3.eth.sign.
"""
recovered_address = recover_address_eip_191(message=message, signature=signature)
signature_is_valid = recovered_address == to_checksum_address(address)
return signature_is_valid
def __generate_self_signed_certificate(host: str,
curve: EllipticCurve = _TLS_CURVE,
private_key: _EllipticCurvePrivateKey = None,
days_valid: int = 365, # TODO: Until end of stake / when to renew?
checksum_address: str = None
) -> Tuple[Certificate, _EllipticCurvePrivateKey]:
if not private_key:
private_key = ec.generate_private_key(curve, default_backend())
public_key = private_key.public_key()
now = datetime.datetime.utcnow()
fields = [
x509.NameAttribute(NameOID.COMMON_NAME, host),
]
if checksum_address:
# Teacher Certificate
pseudonym = x509.NameAttribute(NameOID.PSEUDONYM, checksum_address)
fields.append(pseudonym)
subject = issuer = x509.Name(fields)
cert = x509.CertificateBuilder().subject_name(subject)
cert = cert.issuer_name(issuer)
cert = cert.public_key(public_key)
cert = cert.serial_number(x509.random_serial_number())
cert = cert.not_valid_before(now)
cert = cert.not_valid_after(now + datetime.timedelta(days=days_valid))
cert = cert.add_extension(x509.SubjectAlternativeName([x509.IPAddress(IPv4Address(host))]), critical=False)
cert = cert.sign(private_key, hashes.SHA512(), default_backend())
return cert, private_key
def generate_teacher_certificate(checksum_address: str, *args, **kwargs):
cert = __generate_self_signed_certificate(checksum_address=checksum_address, *args, **kwargs)
return cert
def generate_self_signed_certificate(*args, **kwargs):
if 'checksum_address' in kwargs:
raise ValueError("checksum address cannot be used to generate standard self-signed certificates.")
cert = __generate_self_signed_certificate(checksum_address=None, *args, **kwargs)
return cert
def read_certificate_pseudonym(certificate: Certificate):
"""Return the checksum address written into a TLS certificates pseudonym field or raise an error."""
try:
pseudonym = certificate.subject.get_attributes_for_oid(NameOID.PSEUDONYM)[0]
except IndexError:
raise InvalidNodeCertificate("Invalid teacher certificate encountered: No checksum address present as pseudonym.")
checksum_address = pseudonym.value
if not is_checksum_address(checksum_address):
raise InvalidNodeCertificate("Invalid certificate checksum address encountered")
return checksum_address
def encrypt_and_sign(recipient_pubkey_enc: PublicKey,
plaintext: bytes,
signer: 'SignatureStamp',
sign_plaintext: bool = True
) -> Tuple[UmbralMessageKit, Signature]:
if signer is not constants.DO_NOT_SIGN:
# The caller didn't expressly tell us not to sign; we'll sign.
if sign_plaintext:
# Sign first, encrypt second.
sig_header = constants.SIGNATURE_TO_FOLLOW
signature = signer(plaintext)
capsule, ciphertext = umbral.encrypt(recipient_pubkey_enc, sig_header + bytes(signature) + plaintext)
else:
# Encrypt first, sign second.
sig_header = constants.SIGNATURE_IS_ON_CIPHERTEXT
capsule, ciphertext = umbral.encrypt(recipient_pubkey_enc, sig_header + plaintext)
signature = signer(ciphertext)
message_kit = UmbralMessageKit(ciphertext=ciphertext, capsule=capsule,
sender_verifying_key=signer.as_umbral_pubkey(),
signature=signature)
else:
# Don't sign.
signature = sig_header = constants.NOT_SIGNED
capsule, ciphertext = umbral.encrypt(recipient_pubkey_enc, sig_header + plaintext)
message_kit = UmbralMessageKit(ciphertext=ciphertext, capsule=capsule)
return message_kit, signature

View File

@ -14,7 +14,8 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import base64
from typing import Union
import sha3
@ -26,11 +27,17 @@ from hendrix.deploy.tls import HendrixDeployTLS
from hendrix.facilities.services import ExistingKeyTLSContextFactory
from nucypher.config.constants import MAX_UPLOAD_CONTENT_LENGTH
from nucypher.crypto import api as API
from nucypher.crypto.api import generate_teacher_certificate, _TLS_CURVE
from nucypher.crypto.kits import MessageKit
from nucypher.crypto.signing import SignatureStamp, StrangerStamp
from nucypher.crypto.umbral_adapter import SecretKey, PublicKey, Signature, Signer, decrypt_original, decrypt_reencrypted
from nucypher.crypto.tls import _read_tls_certificate, _TLS_CURVE, generate_self_signed_certificate
from nucypher.crypto.umbral_adapter import (
SecretKey,
PublicKey,
decrypt_original,
decrypt_reencrypted,
Signature,
Signer
)
from nucypher.network.resources import get_static_resources
@ -145,18 +152,15 @@ class HostingKeypair(Keypair):
) -> None:
if private_key:
if not certificate_filepath:
raise ValueError('public certificate required to load a hosting keypair.')
from nucypher.config.keyring import _read_tls_public_certificate
certificate = _read_tls_public_certificate(filepath=certificate_filepath)
if certificate_filepath:
certificate = _read_tls_certificate(filepath=certificate_filepath)
super().__init__(private_key=private_key)
elif certificate:
super().__init__(public_key=certificate.public_key())
elif certificate_filepath:
from nucypher.config.keyring import _read_tls_public_certificate
certificate = _read_tls_public_certificate(filepath=certificate_filepath)
certificate = _read_tls_certificate(filepath=certificate_filepath)
super().__init__(public_key=certificate.public_key())
elif generate_certificate:
@ -164,11 +168,9 @@ class HostingKeypair(Keypair):
message = "If you don't supply a TLS certificate, one will be generated for you." \
"But for that, you need to pass a host and checksum address."
raise TypeError(message)
certificate, private_key = generate_teacher_certificate(host=host,
checksum_address=checksum_address,
private_key=private_key)
certificate, private_key = generate_self_signed_certificate(host=host, private_key=private_key)
super().__init__(private_key=private_key)
else:
raise TypeError("You didn't provide a cert, but also told us not to generate keys. Not sure what to do.")

412
nucypher/crypto/keystore.py Normal file
View File

@ -0,0 +1,412 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import json
import os
import stat
import string
import time
from json import JSONDecodeError
from os.path import abspath
from pathlib import Path
from secrets import token_bytes
from typing import Callable, ClassVar, Dict, List, Union, Optional, Tuple
import click
from constant_sorrow.constants import KEYSTORE_LOCKED
from mnemonic.mnemonic import Mnemonic
from nucypher.characters.control.emitters import StdoutEmitter
from nucypher.config.constants import DEFAULT_CONFIG_ROOT
from nucypher.crypto.keypairs import HostingKeypair
from nucypher.crypto.passwords import (
secret_box_decrypt,
secret_box_encrypt,
derive_key_material_from_password,
SecretBoxAuthenticationError
)
from nucypher.crypto.powers import (
DecryptingPower,
DerivedKeyBasedPower,
KeyPairBasedPower,
SigningPower,
CryptoPowerUp,
DelegatingPower
)
from nucypher.crypto.tls import generate_self_signed_certificate
from nucypher.crypto.umbral_adapter import (
SecretKey,
secret_key_factory_from_seed,
secret_key_factory_from_secret_key_factory
)
from nucypher.network.server import TLSHostingPower
# HKDF
__INFO_BASE = b'NuCypher/'
_SIGNING_INFO = __INFO_BASE + b'signing'
_DECRYPTING_INFO = __INFO_BASE + b'decrypting'
_DELEGATING_INFO = __INFO_BASE + b'delegating'
_TLS_INFO = __INFO_BASE + b'tls'
# Wrapping key
_SALT_SIZE = 32
# Mnemonic
_ENTROPY_BITS = 256
_WORD_COUNT = 24
_MNEMONIC_LANGUAGE = "english"
# Keystore File
FILE_ENCODING = 'utf-8'
_KEYSTORE_VERSION = '2.0'
__PRIVATE_FLAGS = os.O_WRONLY | os.O_CREAT | os.O_EXCL # Write, Create, Non-Existing
__PRIVATE_MODE = stat.S_IRUSR | stat.S_IWUSR # 0o600
class InvalidPassword(ValueError):
pass
def _assemble_keystore(encrypted_secret: bytes, password_salt: bytes, wrapper_salt: bytes) -> Dict[str, Union[str, bytes]]:
encoded_key_data = {
'version': _KEYSTORE_VERSION,
'created': str(time.time()),
'key': encrypted_secret,
'password_salt': password_salt,
'wrapper_salt': wrapper_salt,
}
return encoded_key_data
def _read_keystore(path: Path, deserializer: Callable) -> Dict[str, Union[str, bytes]]:
"""Parses a keyfile and return decoded, deserialized key data."""
with open(path, 'rb') as keyfile:
key_data = keyfile.read()
if deserializer:
key_data = deserializer(key_data)
return key_data
def _write_keystore(path: Path, payload: Dict[str, bytes], serializer: Callable) -> Path:
"""
Creates a permissioned keyfile and save it to the local filesystem.
The file must be created in this call, and will fail if the path exists.
Returns the filepath string used to write the keyfile.
Note: getting and setting the umask is not thread-safe!
See linux open docs: http://man7.org/linux/man-pages/man2/open.2.html
---------------------------------------------------------------------
O_CREAT - If pathname does not exist, create it as a regular file.
O_EXCL - Ensure that this call creates the file: if this flag is
specified in conjunction with O_CREAT, and pathname already
exists, then open() fails with the error EEXIST.
---------------------------------------------------------------------
"""
if path.exists():
raise Keystore.Exists(f"Private keyfile {path} already exists.")
try:
keyfile_descriptor = os.open(path, flags=__PRIVATE_FLAGS, mode=__PRIVATE_MODE)
finally:
os.umask(0) # Set the umask to 0 after opening
if serializer:
payload = serializer(payload)
with os.fdopen(keyfile_descriptor, 'wb') as keyfile:
keyfile.write(payload)
return path
def _serialize_keystore(payload: Dict) -> bytes:
for field in ('key', 'password_salt', 'wrapper_salt'):
payload[field] = bytes(payload[field]).hex()
try:
metadata = json.dumps(payload, indent=4)
except JSONDecodeError:
raise Keystore.Invalid("Invalid or corrupted key data")
return bytes(metadata, encoding=FILE_ENCODING)
def _deserialize_keystore(payload: bytes):
payload = payload.decode(encoding=FILE_ENCODING)
try:
payload = json.loads(payload)
except JSONDecodeError:
raise Keystore.Invalid("Invalid or corrupted key data")
# TODO: Handle Keystore versioning.
# version = payload['version']
for field in ('key', 'password_salt', 'wrapper_salt'):
payload[field] = bytes.fromhex(payload[field])
return payload
def generate_keystore_filepath(parent: Path, id: str) -> Path:
utc_nowish = int(time.time()) # epoch
path = Path(parent) / f'{utc_nowish}-{id}.priv'
return path
def validate_keystore_password(password: str) -> List:
"""
NOTICE: Do not raise inside this function.
"""
rules = (
(bool(password), 'Password must not be blank.'),
(len(password) >= Keystore._MINIMUM_PASSWORD_LENGTH,
f'Password must be at least {Keystore._MINIMUM_PASSWORD_LENGTH} characters long.'),
)
failures = list()
for rule, failure_message in rules:
if not rule:
failures.append(failure_message)
return failures
def validate_keystore_filename(path: Path) -> None:
base_name = path.name.rstrip('.' + Keystore._SUFFIX)
parts = base_name.split(Keystore._DELIMITER)
try:
created, keystore_id = parts
except ValueError:
raise Keystore.Invalid(f'{path} is not a valid keystore filename')
validators = (
bool(len(keystore_id) == Keystore._ID_SIZE),
all(char in string.hexdigits for char in keystore_id)
)
valid_path = all(validators)
if not valid_path:
raise Keystore.Invalid(f'{path} is not a valid keystore filename')
def _parse_path(path: Path) -> Tuple[int, str]:
# validate keystore file
path = Path(path)
if not path.exists():
raise Keystore.NotFound(f"Keystore '{path}' does not exist.")
if not path.is_file():
raise ValueError('Keystore path must be a file.')
if not path.match(f'*{Keystore._DELIMITER}*.{Keystore._SUFFIX}'):
Keystore.Invalid(f'{path} is not a valid keystore filename')
# dissect keystore filename
validate_keystore_filename(path)
base_name = path.name.rstrip('.'+Keystore._SUFFIX)
parts = base_name.split(Keystore._DELIMITER)
created, keystore_id = parts
return created, keystore_id
def _derive_hosting_power(host: str, private_key: SecretKey) -> TLSHostingPower:
certificate, private_key = generate_self_signed_certificate(host=host, private_key=private_key)
keypair = HostingKeypair(host=host, private_key=private_key, certificate=certificate, generate_certificate=False)
power = TLSHostingPower(keypair=keypair, host=host)
return power
class Keystore:
# Wrapping Key
_MINIMUM_PASSWORD_LENGTH = 8
_ID_SIZE = 32
# Filepath
_DEFAULT_DIR: Path = DEFAULT_CONFIG_ROOT / 'keystore'
_DELIMITER = '-'
_SUFFIX = 'priv'
# Powers derivation
__HKDF_INFO = {SigningPower: _SIGNING_INFO,
DecryptingPower: _DECRYPTING_INFO,
DelegatingPower: _DELEGATING_INFO,
TLSHostingPower: _TLS_INFO}
class Exists(FileExistsError):
pass
class Invalid(Exception):
pass
class NotFound(FileNotFoundError):
pass
class Locked(RuntimeError):
pass
class AuthenticationFailed(RuntimeError):
pass
def __init__(self, keystore_path: Path):
self.keystore_path = keystore_path
self.__created, self.__id = _parse_path(keystore_path)
self.__secret = KEYSTORE_LOCKED
def __decrypt_keystore(self, path: Path, password: str) -> bool:
payload = _read_keystore(path, deserializer=_deserialize_keystore)
__password_material = derive_key_material_from_password(password=password.encode(),
salt=payload['password_salt'])
try:
self.__secret = secret_box_decrypt(key_material=__password_material,
ciphertext=payload['key'],
salt=payload['wrapper_salt'])
return True
except SecretBoxAuthenticationError:
self.__secret = KEYSTORE_LOCKED
raise self.AuthenticationFailed
@staticmethod
def __save(secret: bytes, password: str, keystore_dir: Optional[Path] = None) -> Path:
failures = validate_keystore_password(password)
if failures:
# TODO: Ensure this scope is separable from the scope containing the password
# to help avoid unintentional logging of the password.
raise InvalidPassword(''.join(failures))
# Derive verifying key (for use as ID)
verifying_key = secret_key_factory_from_seed(secret).secret_key_by_label(_SIGNING_INFO)
keystore_id = bytes(verifying_key).hex()[:Keystore._ID_SIZE]
# Generate paths
keystore_dir = keystore_dir or Keystore._DEFAULT_DIR
os.makedirs(abspath(keystore_dir), exist_ok=True, mode=0o700)
keystore_path = generate_keystore_filepath(parent=keystore_dir, id=keystore_id)
# Encrypt secret
__password_salt = token_bytes(_SALT_SIZE)
__password_material = derive_key_material_from_password(password=password.encode(),
salt=__password_salt)
__wrapper_salt = token_bytes(_SALT_SIZE)
encrypted_secret = secret_box_encrypt(plaintext=secret,
key_material=__password_material,
salt=__wrapper_salt)
# Create keystore file
keystore_payload = _assemble_keystore(encrypted_secret=encrypted_secret,
password_salt=__password_salt,
wrapper_salt=__wrapper_salt)
_write_keystore(path=keystore_path, payload=keystore_payload, serializer=_serialize_keystore)
return keystore_path
#
# Public API
#
@classmethod
def load(cls, id: str, keystore_dir: Path = _DEFAULT_DIR) -> 'Keystore':
filepath = generate_keystore_filepath(parent=keystore_dir, id=id)
instance = cls(keystore_path=filepath)
return instance
@classmethod
def restore(cls, words: str, password: str, keystore_dir: Optional[Path] = None) -> 'Keystore':
"""Restore a keystore from seed words"""
__mnemonic = Mnemonic(_MNEMONIC_LANGUAGE)
__secret = bytes(__mnemonic.to_entropy(words))
path = Keystore.__save(secret=__secret, password=password, keystore_dir=keystore_dir)
keystore = cls(keystore_path=path)
return keystore
@classmethod
def generate(cls, password: str, keystore_dir: Optional[Path] = None, interactive: bool = True) -> 'Keystore':
"""Generate a new nucypher keystore for use with characters"""
mnemonic = Mnemonic(_MNEMONIC_LANGUAGE)
__words = mnemonic.generate(strength=_ENTROPY_BITS)
if interactive:
cls._confirm_generate(__words)
__secret = bytes(mnemonic.to_entropy(__words))
path = Keystore.__save(secret=__secret, password=password, keystore_dir=keystore_dir)
keystore = cls(keystore_path=path)
return keystore
@staticmethod
def _confirm_generate(__words: str) -> None:
"""
Inform the caller of new keystore seed words generation the console
and optionally perform interactive confirmation.
"""
# notification
emitter = StdoutEmitter()
emitter.message(f'Backup your seed words, you will not be able to view them again.\n')
emitter.message(f'{__words}\n', color='cyan')
if not click.confirm("Have you backed up your seed phrase?"):
emitter.message('Keystore generation aborted.', color='red')
raise click.Abort()
click.clear()
# confirmation
__response = click.prompt("Confirm seed words")
if __response != __words:
raise ValueError('Incorrect seed word confirmation. No keystore has been created, try again.')
click.clear()
@property
def id(self) -> str:
return self.__id
@property
def is_unlocked(self) -> bool:
return self.__secret is not KEYSTORE_LOCKED
def lock(self) -> None:
self.__secret = KEYSTORE_LOCKED
def unlock(self, password: str) -> None:
self.__decrypt_keystore(path=self.keystore_path, password=password)
def derive_crypto_power(self,
power_class: ClassVar[CryptoPowerUp],
*power_args, **power_kwargs
) -> Union[KeyPairBasedPower, DerivedKeyBasedPower]:
if not self.is_unlocked:
raise Keystore.Locked(f"{self.id} is locked and must be unlocked before use.")
try:
info = self.__HKDF_INFO[power_class]
except KeyError:
failure_message = f"{power_class.__name__} is an invalid type for deriving a CryptoPower"
raise TypeError(failure_message)
else:
__private_key = secret_key_factory_from_seed(self.__secret).secret_key_by_label(info)
if power_class is TLSHostingPower: # TODO: something more elegant?
power = _derive_hosting_power(private_key=__private_key, *power_args, **power_kwargs)
elif issubclass(power_class, KeyPairBasedPower):
keypair = power_class._keypair_class(__private_key)
power = power_class(keypair=keypair, *power_args, **power_kwargs)
elif issubclass(power_class, DerivedKeyBasedPower):
parent_skf = secret_key_factory_from_seed(self.__secret)
child_skf = secret_key_factory_from_secret_key_factory(parent_skf, label=_DELEGATING_INFO)
power = power_class(secret_key_factory=child_skf, *power_args, **power_kwargs)
else:
failure_message = f"{power_class.__name__} is an invalid type for deriving a CryptoPower."
raise ValueError(failure_message)
return power

View File

@ -15,20 +15,17 @@ You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from typing import Optional
from cryptography.exceptions import InternalError
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from nacl.secret import SecretBox
from nacl.utils import random as nacl_random
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
from nacl.exceptions import CryptoError
from nacl.secret import SecretBox
from nucypher.crypto.constants import BLAKE2B
__MASTER_KEY_LENGTH = 32 # This will be passed to HKDF, but it is not picky about the length
__MASTER_KEY_LENGTH = 32 # This will be passed to HKDF, but it is not picky about the length
__WRAPPING_KEY_LENGTH = SecretBox.KEY_SIZE
__WRAPPING_KEY_INFO = b'NuCypher-KeyWrap'
__HKDF_HASH_ALGORITHM = BLAKE2B

View File

@ -17,9 +17,10 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import inspect
from typing import List, Optional, Tuple
from eth_typing.evm import ChecksumAddress
from hexbytes import HexBytes
from typing import List, Optional, Tuple
from nucypher.blockchain.eth.decorators import validate_checksum_address
from nucypher.blockchain.eth.signers.base import Signer
@ -245,14 +246,13 @@ class DerivedKeyBasedPower(CryptoPowerUp):
class DelegatingPower(DerivedKeyBasedPower):
def __init__(self, keying_material: Optional[bytes] = None):
if keying_material is None:
self.__umbral_keying_material = SecretKeyFactory.random()
else:
self.__umbral_keying_material = SecretKeyFactory.from_bytes(keying_material)
def __init__(self, secret_key_factory: Optional[SecretKeyFactory] = None):
if not secret_key_factory:
secret_key_factory = SecretKeyFactory.random()
self.__secret_key_factory = secret_key_factory
def _get_privkey_from_label(self, label):
return self.__umbral_keying_material.secret_key_by_label(label)
return self.__secret_key_factory.secret_key_by_label(label)
def get_pubkey_from_label(self, label):
return self._get_privkey_from_label(label).public_key()

View File

@ -15,11 +15,7 @@ You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from bytestring_splitter import BytestringSplitter
from nucypher.crypto.api import keccak_digest
from nucypher.crypto.umbral_adapter import Signature, Signer
from nucypher.crypto.umbral_adapter import Signer
class SignatureStamp(object):
@ -69,6 +65,7 @@ class SignatureStamp(object):
:return: Hexdigest fingerprint of key (keccak-256) in bytes
"""
from nucypher.crypto.utils import keccak_digest
return keccak_digest(bytes(self)).hex().encode()

90
nucypher/crypto/tls.py Normal file
View File

@ -0,0 +1,90 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import datetime
import os
from ipaddress import IPv4Address
from typing import Tuple, ClassVar
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.backends.openssl.ec import _EllipticCurvePrivateKey
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve
from cryptography.hazmat.primitives.serialization import Encoding
from cryptography.x509 import Certificate
from cryptography.x509.oid import NameOID
from nucypher.crypto.umbral_adapter import SecretKey
_TLS_CERTIFICATE_ENCODING = Encoding.PEM
_TLS_CURVE = ec.SECP384R1
def _write_tls_certificate(certificate: Certificate,
full_filepath: str,
force: bool = False,
) -> str:
cert_already_exists = os.path.isfile(full_filepath)
if force is False and cert_already_exists:
raise FileExistsError('A TLS certificate already exists at {}.'.format(full_filepath))
with open(full_filepath, 'wb') as certificate_file:
public_pem_bytes = certificate.public_bytes(_TLS_CERTIFICATE_ENCODING)
certificate_file.write(public_pem_bytes)
return full_filepath
def _read_tls_certificate(filepath: str) -> Certificate:
"""Deserialize an X509 certificate from a filepath"""
try:
with open(filepath, 'rb') as certificate_file:
cert = x509.load_pem_x509_certificate(certificate_file.read(), backend=default_backend())
return cert
except FileNotFoundError:
raise FileNotFoundError("No SSL certificate found at {}".format(filepath))
def generate_self_signed_certificate(host: str,
private_key: SecretKey = None,
days_valid: int = 365,
curve: ClassVar[EllipticCurve] = _TLS_CURVE,
) -> Tuple[Certificate, _EllipticCurvePrivateKey]:
if private_key:
private_bn = int.from_bytes(bytes(private_key), 'big')
private_key = ec.derive_private_key(private_value=private_bn, curve=curve())
else:
private_key = ec.generate_private_key(curve(), default_backend())
public_key = private_key.public_key()
now = datetime.datetime.utcnow()
fields = [x509.NameAttribute(NameOID.COMMON_NAME, host)]
subject = issuer = x509.Name(fields)
cert = x509.CertificateBuilder().subject_name(subject)
cert = cert.issuer_name(issuer)
cert = cert.public_key(public_key)
cert = cert.serial_number(x509.random_serial_number())
cert = cert.not_valid_before(now)
cert = cert.not_valid_after(now + datetime.timedelta(days=days_valid))
cert = cert.add_extension(x509.SubjectAlternativeName([x509.IPAddress(IPv4Address(host))]), critical=False)
cert = cert.sign(private_key, hashes.SHA512(), default_backend())
return cert, private_key

View File

@ -18,4 +18,37 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
# This module is used to have a single point where the Umbral implementation is chosen.
# Do not import Umbral directly, use re-exports from this module.
from umbral import *
from umbral import (
SecretKey,
PublicKey,
SecretKeyFactory,
Signature,
Signer,
Capsule,
KeyFrag,
VerifiedKeyFrag,
CapsuleFrag,
VerifiedCapsuleFrag,
VerificationError,
encrypt,
decrypt_original,
generate_kfrags,
reencrypt,
decrypt_reencrypted,
)
def secret_key_factory_from_seed(entropy: bytes) -> SecretKeyFactory:
"""TODO: Issue #57 in nucypher/rust-umbral"""
if len(entropy) < 32:
raise ValueError('Entropy must be at least 32 bytes.')
material = entropy.zfill(SecretKeyFactory.serialized_size())
instance = SecretKeyFactory.from_bytes(material)
return instance
def secret_key_factory_from_secret_key_factory(skf: SecretKeyFactory, label: bytes) -> SecretKeyFactory:
"""TODO: Issue #59 in nucypher/rust-umbral"""
secret_key = bytes(skf.secret_key_by_label(label)).zfill(SecretKeyFactory.serialized_size())
return SecretKeyFactory.from_bytes(secret_key)

View File

@ -15,21 +15,24 @@ You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from coincurve import PublicKey
from secrets import SystemRandom
from typing import Union, Tuple
import sha3
from constant_sorrow import constants
from cryptography.hazmat.backends.openssl.backend import backend
from cryptography.hazmat.primitives import hashes
from eth_account.account import Account
from eth_account.messages import encode_defunct
from eth_keys import KeyAPI as EthKeyAPI
from typing import Any, Union
from eth_utils.address import to_checksum_address
from nucypher.crypto.api import keccak_digest
import nucypher.crypto.umbral_adapter as umbral
from nucypher.crypto.kits import UmbralMessageKit
from nucypher.crypto.signing import SignatureStamp
from nucypher.crypto.umbral_adapter import PublicKey
from nucypher.crypto.umbral_adapter import Signature, PublicKey
def fingerprint_from_key(public_key: Any):
"""
Hashes a key using keccak-256 and returns the hexdigest in bytes.
:return: Hexdigest fingerprint of key (keccak-256) in bytes
"""
return keccak_digest(bytes(public_key)).hex().encode()
SYSTEM_RAND = SystemRandom()
def construct_policy_id(label: bytes, stamp: bytes) -> bytes:
@ -47,3 +50,116 @@ def canonical_address_from_umbral_key(public_key: Union[PublicKey, SignatureStam
eth_pubkey = EthKeyAPI.PublicKey.from_compressed_bytes(pubkey_compressed_bytes)
canonical_address = eth_pubkey.to_canonical_address()
return canonical_address
def secure_random(num_bytes: int) -> bytes:
"""
Returns an amount `num_bytes` of data from the OS's random device.
If a randomness source isn't found, returns a `NotImplementedError`.
In this case, a secure random source most likely doesn't exist and
randomness will have to found elsewhere.
:param num_bytes: Number of bytes to return.
:return: bytes
"""
# TODO: Should we just use os.urandom or avoid the import w/ this?
return SYSTEM_RAND.getrandbits(num_bytes * 8).to_bytes(num_bytes, byteorder='big')
def secure_random_range(min: int, max: int) -> int:
"""
Returns a number from a secure random source betwee the range of
`min` and `max` - 1.
:param min: Minimum number in the range
:param max: Maximum number in the range
:return: int
"""
return SYSTEM_RAND.randrange(min, max)
def keccak_digest(*messages: bytes) -> bytes:
"""
Accepts an iterable containing bytes and digests it returning a
Keccak digest of 32 bytes (keccak_256).
Although we use SHA256 in many cases, we keep keccak handy in order
to provide compatibility with the Ethereum blockchain.
:param bytes: Data to hash
:rtype: bytes
:return: bytestring of digested data
"""
_hash = sha3.keccak_256()
for message in messages:
_hash.update(bytes(message))
digest = _hash.digest()
return digest
def sha256_digest(*messages: bytes) -> bytes:
"""
Accepts an iterable containing bytes and digests it returning a
SHA256 digest of 32 bytes
:param bytes: Data to hash
:rtype: bytes
:return: bytestring of digested data
"""
_hash_ctx = hashes.Hash(hashes.SHA256(), backend=backend)
for message in messages:
_hash_ctx.update(bytes(message))
digest = _hash_ctx.finalize()
return digest
def recover_address_eip_191(message: bytes, signature: bytes) -> str:
"""
Recover checksum address from EIP-191 signature
"""
signable_message = encode_defunct(primitive=message)
recovery = Account.recover_message(signable_message=signable_message, signature=signature)
recovered_address = to_checksum_address(recovery)
return recovered_address
def verify_eip_191(address: str, message: bytes, signature: bytes) -> bool:
"""
EIP-191 Compatible signature verification for usage with w3.eth.sign.
"""
recovered_address = recover_address_eip_191(message=message, signature=signature)
signature_is_valid = recovered_address == to_checksum_address(address)
return signature_is_valid
def encrypt_and_sign(recipient_pubkey_enc: PublicKey,
plaintext: bytes,
signer: 'SignatureStamp',
sign_plaintext: bool = True
) -> Tuple[UmbralMessageKit, Signature]:
if signer is not constants.DO_NOT_SIGN:
# The caller didn't expressly tell us not to sign; we'll sign.
if sign_plaintext:
# Sign first, encrypt second.
sig_header = constants.SIGNATURE_TO_FOLLOW
signature = signer(plaintext)
capsule, ciphertext = umbral.encrypt(recipient_pubkey_enc, sig_header + bytes(signature) + plaintext)
else:
# Encrypt first, sign second.
sig_header = constants.SIGNATURE_IS_ON_CIPHERTEXT
capsule, ciphertext = umbral.encrypt(recipient_pubkey_enc, sig_header + plaintext)
signature = signer(ciphertext)
message_kit = UmbralMessageKit(ciphertext=ciphertext, capsule=capsule,
sender_verifying_key=signer.as_umbral_pubkey(),
signature=signature)
else:
# Don't sign.
signature = sig_header = constants.NOT_SIGNED
capsule, ciphertext = umbral.encrypt(recipient_pubkey_enc, sig_header + plaintext)
message_kit = UmbralMessageKit(ciphertext=ciphertext, capsule=capsule)
return message_kit, signature

View File

@ -14,10 +14,11 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from maya import MayaDT
from nucypher.crypto.signing import Signature
from nucypher.crypto.umbral_adapter import PublicKey, VerifiedKeyFrag
from nucypher.crypto.umbral_adapter import PublicKey, VerifiedKeyFrag, Signature
from nucypher.datastore.base import DatastoreRecord, RecordField

View File

@ -16,18 +16,19 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import contextlib
import time
from collections import defaultdict, deque
from contextlib import suppress
from queue import Queue
from typing import Iterable, List, Set, Tuple, Union
from typing import Iterable, List, Set, Tuple, Union, Callable
import maya
import requests
import time
from bytestring_splitter import (
BytestringSplitter,
PartiallyKwargifiedBytes,
VariableLengthBytestring
VariableLengthBytestring,
BytestringSplittingError
)
from constant_sorrow import constant_or_bytes
from constant_sorrow.constants import (
@ -54,16 +55,16 @@ from nucypher.blockchain.eth.networks import NetworksInventory
from nucypher.blockchain.eth.registry import BaseContractRegistry
from nucypher.config.constants import SeednodeMetadata
from nucypher.config.storages import ForgetfulNodeStorage
from nucypher.crypto.api import InvalidNodeCertificate, recover_address_eip_191, verify_eip_191
from nucypher.crypto.kits import UmbralMessageKit
from nucypher.crypto.powers import DecryptingPower, NoSigningPower, SigningPower
from nucypher.crypto.splitters import signature_splitter
from nucypher.crypto.umbral_adapter import Signature
from nucypher.crypto.utils import recover_address_eip_191, verify_eip_191
from nucypher.network import LEARNING_LOOP_VERSION
from nucypher.network.exceptions import NodeSeemsToBeDown
from nucypher.network.middleware import RestMiddleware
from nucypher.network.protocols import SuspiciousActivity
from nucypher.utilities.logging import Logger
from nucypher.crypto.umbral_adapter import Signature
TEACHER_NODES = {
NetworksInventory.MAINNET: (
@ -75,6 +76,7 @@ TEACHER_NODES = {
NetworksInventory.IBEX: ('https://ibex.nucypher.network:9151',),
}
class NodeSprout(PartiallyKwargifiedBytes):
"""
An abridged node class designed for optimization of instantiation of > 100 nodes simultaneously.
@ -152,7 +154,7 @@ class NodeSprout(PartiallyKwargifiedBytes):
self.__dict__ = mature_node.__dict__
# As long as we're doing egregious workarounds, here's another one. # TODO: 1481
filepath = mature_node._cert_store_function(certificate=mature_node.certificate)
filepath = mature_node._cert_store_function(certificate=mature_node.certificate, port=mature_node.rest_interface.port)
mature_node.certificate_filepath = filepath
_finishing_mutex.put(self)
@ -424,10 +426,7 @@ class Learner:
stranger_certificate = node.certificate
# Store node's certificate - It has been seen.
try:
certificate_filepath = self.node_storage.store_node_certificate(certificate=stranger_certificate)
except InvalidNodeCertificate:
return False # that was easy
certificate_filepath = self.node_storage.store_node_certificate(certificate=stranger_certificate, port=node.rest_interface.port)
# In some cases (seed nodes or other temp stored certs),
# this will update the filepath from the temp location to this one.
@ -1021,7 +1020,7 @@ class Teacher:
"We're version {}."
@classmethod
def set_cert_storage_function(cls, node_storage_function):
def set_cert_storage_function(cls, node_storage_function: Callable):
cls._cert_store_function = node_storage_function
def mature(self, *args, **kwargs):
@ -1205,7 +1204,7 @@ class Teacher:
# The node's metadata is valid; let's be sure the interface is in order.
if not certificate_filepath:
if self.certificate_filepath is CERTIFICATE_NOT_SAVED:
self.certificate_filepath = self._cert_store_function(self.certificate)
self.certificate_filepath = self._cert_store_function(self.certificate, port=self.rest_interface.port)
certificate_filepath = self.certificate_filepath
response_data = network_middleware_client.node_information(host=self.rest_interface.host,

View File

@ -16,28 +16,26 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import binascii
import os
import uuid
import weakref
from datetime import datetime, timedelta
from typing import Tuple
import binascii
from bytestring_splitter import BytestringSplitter
from constant_sorrow import constants
from constant_sorrow.constants import (
FLEET_STATES_MATCH,
NO_BLOCKCHAIN_CONNECTION,
NO_KNOWN_NODES,
RELAX
)
from datetime import datetime, timedelta
from flask import Flask, Response, jsonify, request
from mako import exceptions as mako_exceptions
from mako.template import Template
from maya import MayaDT
from typing import Tuple
from web3.exceptions import TimeExhausted
import nucypher
from nucypher.crypto.api import InvalidNodeCertificate
from nucypher.config.constants import MAX_UPLOAD_CONTENT_LENGTH
from nucypher.crypto.keypairs import HostingKeypair
from nucypher.crypto.kits import UmbralMessageKit
@ -163,9 +161,6 @@ def _make_rest_app(datastore: Datastore, this_node, domain: str, log: Logger) ->
except NodeSeemsToBeDown:
return Response({'error': 'Unreachable node'}, status=400) # ... toasted
except InvalidNodeCertificate:
return Response({'error': 'Invalid TLS certificate - missing checksum address'}, status=400) # ... invalid
# Compare the results of the outer POST with the inner GET... yum
if requesting_ursula_bytes == request.data:
return Response(status=200)

View File

@ -22,7 +22,6 @@ from twisted.internet import reactor
from twisted.internet.task import LoopingCall
from typing import Union
from nucypher.crypto.api import InvalidNodeCertificate
from nucypher.network.exceptions import NodeSeemsToBeDown
from nucypher.network.middleware import RestMiddleware
from nucypher.network.nodes import NodeSprout
@ -218,7 +217,6 @@ class AvailabilityTracker:
# TODO: Relocate?
Unreachable = (*NodeSeemsToBeDown,
self._ursula.NotStaking,
InvalidNodeCertificate,
self._ursula.network_middleware.UnexpectedResponse)
if not ursulas:

View File

@ -16,6 +16,7 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from collections import OrderedDict
from typing import Optional
import maya
from bytestring_splitter import BytestringKwargifier
@ -26,22 +27,20 @@ from bytestring_splitter import (
)
from constant_sorrow.constants import CFRAG_NOT_RETAINED, NO_DECRYPTION_PERFORMED
from constant_sorrow.constants import NOT_SIGNED
from cryptography.hazmat.backends.openssl import backend
from cryptography.hazmat.primitives import hashes
from eth_utils import to_canonical_address, to_checksum_address
from typing import Optional, Tuple
from nucypher.blockchain.eth.constants import ETH_ADDRESS_BYTE_LENGTH, ETH_HASH_BYTE_LENGTH
from nucypher.characters.lawful import Bob, Character
from nucypher.crypto.api import encrypt_and_sign, keccak_digest
from nucypher.crypto.api import verify_eip_191
from nucypher.crypto.constants import HRAC_LENGTH
from nucypher.crypto.kits import UmbralMessageKit
from nucypher.crypto.signing import InvalidSignature, Signature, SignatureStamp
from nucypher.crypto.signing import InvalidSignature, SignatureStamp
from nucypher.crypto.splitters import capsule_splitter, cfrag_splitter, key_splitter, signature_splitter
from nucypher.crypto.umbral_adapter import PublicKey, Capsule
from nucypher.crypto.umbral_adapter import PublicKey, Capsule, Signature
from nucypher.crypto.utils import (
canonical_address_from_umbral_key,
keccak_digest,
verify_eip_191,
encrypt_and_sign
)
from nucypher.network.middleware import RestMiddleware
@ -271,6 +270,7 @@ class SignedTreasureMap(TreasureMap):
"Can't cast a DecentralizedTreasureMap to bytes until it has a blockchain signature (otherwise, is it really a 'DecentralizedTreasureMap'?")
return self._blockchain_signature + super().__bytes__()
class WorkOrder:
class PRETask:

View File

@ -16,36 +16,24 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import datetime
from collections import OrderedDict
from queue import Queue, Empty
from typing import Callable, Tuple, Sequence, Set, Optional, Iterable, List, Dict, Type
import math
import maya
import random
import time
from abc import ABC, abstractmethod
from typing import Tuple, Sequence, Optional, Iterable, List, Dict, Type
import maya
from bytestring_splitter import BytestringSplitter, VariableLengthBytestring
from constant_sorrow.constants import NOT_SIGNED
from eth_typing.evm import ChecksumAddress
from hexbytes import HexBytes
from twisted._threads import AlreadyQuit
from twisted.internet import reactor
from twisted.internet.defer import ensureDeferred, Deferred
from twisted.python.threadpool import ThreadPool
from nucypher.blockchain.eth.actors import BlockchainPolicyAuthor
from nucypher.blockchain.eth.agents import PolicyManagerAgent, StakersReservoir, StakingEscrowAgent
from nucypher.blockchain.eth.agents import StakersReservoir, StakingEscrowAgent
from nucypher.characters.lawful import Alice, Ursula
from nucypher.crypto.api import keccak_digest, secure_random
from nucypher.crypto.constants import HRAC_LENGTH
from nucypher.crypto.kits import RevocationKit
from nucypher.crypto.powers import DecryptingPower, SigningPower, TransactingPower
from nucypher.crypto.splitters import key_splitter
from nucypher.crypto.umbral_adapter import PublicKey, KeyFrag
from nucypher.crypto.utils import construct_policy_id
from nucypher.network.exceptions import NodeSeemsToBeDown
from nucypher.crypto.utils import construct_policy_id, secure_random, keccak_digest
from nucypher.network.middleware import RestMiddleware
from nucypher.utilities.concurrency import WorkerPool, AllAtOnceFactory
from nucypher.utilities.logging import Logger
@ -464,6 +452,11 @@ class Policy(ABC):
Attempts to enact the policy, returns an `EnactedPolicy` object on success.
"""
# TODO: Why/is this needed here?
# Workaround for `RuntimeError: Learning loop is not running. Start it with start_learning().`
if not self.alice._learning_task.running:
self.alice.start_learning_loop()
arrangements = self._make_arrangements(network_middleware=network_middleware,
handpicked_ursulas=handpicked_ursulas)
@ -508,7 +501,6 @@ class Policy(ABC):
class FederatedPolicy(Policy):
from nucypher.policy.collections import TreasureMap as _treasure_map_class # TODO: Circular Import
def _not_enough_ursulas_exception(self):
@ -531,7 +523,7 @@ class BlockchainPolicy(Policy):
A collection of n Arrangements representing a single Policy
"""
from nucypher.policy.collections import SignedTreasureMap as _treasure_map_class # TODO: Circular Import
from nucypher.policy.collections import SignedTreasureMap as _treasure_map_class
class InvalidPolicyValue(ValueError):
pass

View File

@ -36,7 +36,7 @@ from pathlib import Path
from ansible import context as ansible_context
from nucypher.blockchain.eth.clients import PUBLIC_CHAINS
from nucypher.blockchain.eth.networks import NetworksInventory
from nucypher.config.constants import DEFAULT_CONFIG_ROOT, DEPLOY_DIR, NUCYPHER_ENVVAR_KEYRING_PASSWORD, \
from nucypher.config.constants import DEFAULT_CONFIG_ROOT, DEPLOY_DIR, NUCYPHER_ENVVAR_KEYSTORE_PASSWORD, \
NUCYPHER_ENVVAR_WORKER_ETH_PASSWORD
NODE_CONFIG_STORAGE_KEY = 'worker-configs'
@ -230,7 +230,7 @@ class BaseCloudNodeConfigurator:
self.config = {
"namespace": self.namespace_network,
"keyringpassword": b64encode(os.urandom(64)).decode('utf-8'),
"keystorepassword": b64encode(os.urandom(64)).decode('utf-8'),
"ethpassword": b64encode(os.urandom(64)).decode('utf-8'),
}
# configure provider specific attributes
@ -311,7 +311,7 @@ class BaseCloudNodeConfigurator:
defaults = {
'envvars':
[
(NUCYPHER_ENVVAR_KEYRING_PASSWORD, self.config['keyringpassword']),
(NUCYPHER_ENVVAR_KEYSTORE_PASSWORD, self.config['keystorepassword']),
(NUCYPHER_ENVVAR_WORKER_ETH_PASSWORD, self.config['ethpassword']),
],
'cliargs': [

View File

@ -22,7 +22,7 @@ from eth_utils import to_checksum_address
from nucypher.blockchain.eth.signers.software import Web3Signer
from nucypher.blockchain.eth.agents import NucypherTokenAgent
from nucypher.crypto.api import verify_eip_191
from nucypher.crypto.utils import verify_eip_191
from nucypher.crypto.powers import TransactingPower
from tests.conftest import LOCK_FUNCTION
from tests.constants import INSECURE_DEVELOPMENT_PASSWORD

View File

@ -19,7 +19,7 @@ import datetime
import maya
import pytest
from nucypher.crypto.api import keccak_digest
from nucypher.crypto.utils import keccak_digest
from nucypher.datastore.models import PolicyArrangement
from nucypher.datastore.models import TreasureMap as DatastoreTreasureMap
from nucypher.policy.collections import SignedTreasureMap as DecentralizedTreasureMap

View File

@ -20,7 +20,7 @@ from eth_utils import to_checksum_address
from nucypher.blockchain.eth.signers.software import Web3Signer
from nucypher.characters.lawful import Character
from nucypher.crypto.api import verify_eip_191
from nucypher.crypto.utils import verify_eip_191
from nucypher.crypto.powers import (TransactingPower)
from tests.constants import INSECURE_DEVELOPMENT_PASSWORD, MOCK_PROVIDER_URI

View File

@ -21,7 +21,7 @@ from eth_account._utils.signing import to_standard_signature_bytes
from nucypher.characters.lawful import Enrico
from nucypher.characters.unlawful import Vladimir
from nucypher.crypto.api import verify_eip_191
from nucypher.crypto.utils import verify_eip_191
from nucypher.crypto.powers import SigningPower
from nucypher.policy.policies import BlockchainPolicy
from tests.constants import INSECURE_DEVELOPMENT_PASSWORD

View File

@ -30,7 +30,7 @@ from web3 import Web3
from nucypher.cli.main import nucypher_cli
from nucypher.config.characters import AliceConfiguration, BobConfiguration
from nucypher.config.constants import NUCYPHER_ENVVAR_KEYRING_PASSWORD, TEMPORARY_DOMAIN, \
from nucypher.config.constants import NUCYPHER_ENVVAR_KEYSTORE_PASSWORD, TEMPORARY_DOMAIN, \
NUCYPHER_ENVVAR_ALICE_ETH_PASSWORD, NUCYPHER_ENVVAR_BOB_ETH_PASSWORD
from nucypher.crypto.kits import UmbralMessageKit
from nucypher.utilities.logging import GlobalLoggerSettings
@ -106,7 +106,7 @@ def run_entire_cli_lifecycle(click_runner,
# Boring Setup Stuff
alice_config_root = str(custom_filepath)
bob_config_root = str(custom_filepath_2)
envvars = {NUCYPHER_ENVVAR_KEYRING_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD,
envvars = {NUCYPHER_ENVVAR_KEYSTORE_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD,
NUCYPHER_ENVVAR_ALICE_ETH_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD,
NUCYPHER_ENVVAR_BOB_ETH_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD}

View File

@ -17,13 +17,16 @@
import os
from unittest import mock
from unittest.mock import PropertyMock
from nucypher.cli.commands.alice import AliceConfigOptions
from nucypher.cli.literature import SUCCESSFUL_DESTRUCTION, COLLECT_NUCYPHER_PASSWORD
from nucypher.cli.main import nucypher_cli
from nucypher.config.base import CharacterConfiguration
from nucypher.config.characters import AliceConfiguration
from nucypher.config.constants import NUCYPHER_ENVVAR_KEYRING_PASSWORD, TEMPORARY_DOMAIN
from nucypher.config.constants import NUCYPHER_ENVVAR_KEYSTORE_PASSWORD, TEMPORARY_DOMAIN
from nucypher.config.storages import LocalFileBasedNodeStorage
from nucypher.crypto.keystore import Keystore
from nucypher.policy.identity import Card
from tests.constants import (
FAKE_PASSWORD_CONFIRMED,
@ -33,22 +36,26 @@ from tests.constants import (
@mock.patch('nucypher.config.characters.AliceConfiguration.default_filepath', return_value='/non/existent/file')
def test_missing_configuration_file(default_filepath_mock, click_runner):
def test_missing_configuration_file(default_filepath_mock, click_runner, test_registry_source_manager):
cmd_args = ('alice', 'run', '--network', TEMPORARY_DOMAIN)
result = click_runner.invoke(nucypher_cli, cmd_args, catch_exceptions=False)
env = {NUCYPHER_ENVVAR_KEYSTORE_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD}
result = click_runner.invoke(nucypher_cli, cmd_args, catch_exceptions=False, env=env)
assert result.exit_code != 0
assert default_filepath_mock.called
assert "nucypher alice init" in result.output
def test_initialize_alice_defaults(click_runner, mocker, custom_filepath, monkeypatch, blockchain_ursulas):
monkeypatch.delenv(NUCYPHER_ENVVAR_KEYRING_PASSWORD, raising=False)
def test_initialize_alice_defaults(click_runner, mocker, custom_filepath, monkeypatch, blockchain_ursulas, tmpdir):
monkeypatch.delenv(NUCYPHER_ENVVAR_KEYSTORE_PASSWORD, raising=False)
# Mock out filesystem writes
mocker.patch.object(AliceConfiguration, 'initialize', autospec=True)
mocker.patch.object(AliceConfiguration, 'to_configuration_file', autospec=True)
mocker.patch.object(LocalFileBasedNodeStorage, 'all', return_value=blockchain_ursulas)
# Mock Keystore init
keystore = Keystore.generate(keystore_dir=tmpdir, password=INSECURE_DEVELOPMENT_PASSWORD)
mocker.patch.object(CharacterConfiguration, 'keystore', return_value=keystore, new_callable=PropertyMock)
# Use default alice init args
init_args = ('alice', 'init',
@ -66,13 +73,13 @@ def test_initialize_alice_defaults(click_runner, mocker, custom_filepath, monkey
assert 'Repeat for confirmation:' in result.output, 'User was not prompted to confirm password'
def test_alice_control_starts_with_mocked_keyring(click_runner, mocker, monkeypatch, custom_filepath):
monkeypatch.delenv(NUCYPHER_ENVVAR_KEYRING_PASSWORD, raising=False)
def test_alice_control_starts_with_mocked_keystore(click_runner, mocker, monkeypatch, custom_filepath):
monkeypatch.delenv(NUCYPHER_ENVVAR_KEYSTORE_PASSWORD, raising=False)
class MockKeyring:
class MockKeystore:
is_unlocked = False
keyring_root = custom_filepath / 'keyring'
checksum_address = None
keystore_dir = custom_filepath / 'keystore'
keystore_path = custom_filepath / 'keystore' / 'path.json'
def derive_crypto_power(self, power_class, *args, **kwargs):
return power_class()
@ -82,8 +89,7 @@ def test_alice_control_starts_with_mocked_keyring(click_runner, mocker, monkeypa
assert password == INSECURE_DEVELOPMENT_PASSWORD
cls.is_unlocked = True
mocker.patch.object(AliceConfiguration, "attach_keyring", return_value=None)
good_enough_config = AliceConfiguration(dev_mode=True, federated_only=True, keyring=MockKeyring())
good_enough_config = AliceConfiguration(dev_mode=True, federated_only=True, keystore=MockKeystore())
mocker.patch.object(AliceConfigOptions, "create_config", return_value=good_enough_config)
init_args = ('alice', 'run', '-x', '--lonely', '--network', TEMPORARY_DOMAIN)
result = click_runner.invoke(nucypher_cli, init_args, input=FAKE_PASSWORD_CONFIRMED)
@ -91,7 +97,7 @@ def test_alice_control_starts_with_mocked_keyring(click_runner, mocker, monkeypa
def test_initialize_alice_with_custom_configuration_root(custom_filepath, click_runner, monkeypatch):
monkeypatch.delenv(NUCYPHER_ENVVAR_KEYRING_PASSWORD, raising=False)
monkeypatch.delenv(NUCYPHER_ENVVAR_KEYSTORE_PASSWORD, raising=False)
# Use a custom local filepath for configuration
init_args = ('alice', 'init',
@ -109,7 +115,7 @@ def test_initialize_alice_with_custom_configuration_root(custom_filepath, click_
# Files and Directories
assert os.path.isdir(custom_filepath), 'Configuration file does not exist'
assert os.path.isdir(os.path.join(custom_filepath, 'keyring')), 'Keyring does not exist'
assert os.path.isdir(os.path.join(custom_filepath, 'keystore')), 'KEYSTORE does not exist'
assert os.path.isdir(os.path.join(custom_filepath, 'known_nodes')), 'known_nodes directory does not exist'
custom_config_filepath = os.path.join(custom_filepath, AliceConfiguration.generate_filename())

View File

@ -67,7 +67,7 @@ def test_initialize_bob_with_custom_configuration_root(custom_filepath, click_ru
# Files and Directories
assert os.path.isdir(custom_filepath), 'Configuration file does not exist'
assert os.path.isdir(os.path.join(custom_filepath, 'keyring')), 'Keyring does not exist'
assert os.path.isdir(os.path.join(custom_filepath, 'keystore')), 'KEYSTORE does not exist'
assert os.path.isdir(os.path.join(custom_filepath, 'known_nodes')), 'known_nodes directory does not exist'
custom_config_filepath = os.path.join(custom_filepath, BobConfiguration.generate_filename())
@ -143,7 +143,7 @@ def test_bob_retrieves_twice_via_cli(click_runner,
'--config-root', bob_config_root,
'--federated-only')
envvars = {'NUCYPHER_KEYRING_PASSWORD': INSECURE_DEVELOPMENT_PASSWORD}
envvars = {'NUCYPHER_KEYSTORE_PASSWORD': INSECURE_DEVELOPMENT_PASSWORD}
log.info("Init'ing a normal Bob; we'll substitute the Policy Bob in shortly.")
bob_init_response = click_runner.invoke(nucypher_cli, bob_init_args, catch_exceptions=False, env=envvars)

View File

@ -23,7 +23,7 @@ import pytest
from nucypher.blockchain.eth.registry import InMemoryContractRegistry
from nucypher.cli.main import nucypher_cli
from nucypher.config.characters import AliceConfiguration, BobConfiguration, UrsulaConfiguration
from nucypher.config.constants import NUCYPHER_ENVVAR_KEYRING_PASSWORD, TEMPORARY_DOMAIN
from nucypher.config.constants import NUCYPHER_ENVVAR_KEYSTORE_PASSWORD, TEMPORARY_DOMAIN
from tests.constants import (
FAKE_PASSWORD_CONFIRMED,
INSECURE_DEVELOPMENT_PASSWORD,
@ -36,7 +36,7 @@ from tests.constants import (
CONFIG_CLASSES = (AliceConfiguration, BobConfiguration, UrsulaConfiguration)
ENV = {NUCYPHER_ENVVAR_KEYRING_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD}
ENV = {NUCYPHER_ENVVAR_KEYSTORE_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD}
@pytest.mark.parametrize('config_class', CONFIG_CLASSES)
@ -64,7 +64,7 @@ def test_initialize_via_cli(config_class, custom_filepath, click_runner, monkeyp
# Files and Directories
assert os.path.isdir(custom_filepath), 'Configuration file does not exist'
assert os.path.isdir(os.path.join(custom_filepath, 'keyring')), 'Keyring does not exist'
assert os.path.isdir(os.path.join(custom_filepath, 'keystore')), 'KEYSTORE does not exist'
assert os.path.isdir(os.path.join(custom_filepath, 'known_nodes')), 'known_nodes directory does not exist'

View File

@ -32,7 +32,7 @@ from nucypher.characters.chaotic import Felix
from nucypher.cli.literature import SUCCESSFUL_DESTRUCTION
from nucypher.cli.main import nucypher_cli
from nucypher.config.characters import FelixConfiguration
from nucypher.config.constants import NUCYPHER_ENVVAR_KEYRING_PASSWORD, TEMPORARY_DOMAIN
from nucypher.config.constants import NUCYPHER_ENVVAR_KEYSTORE_PASSWORD, TEMPORARY_DOMAIN
from tests.constants import (INSECURE_DEVELOPMENT_PASSWORD, MOCK_CUSTOM_INSTALLATION_PATH_2, TEST_PROVIDER_URI)
@ -58,7 +58,7 @@ def test_run_felix(click_runner, testerchain, agency_local_registry):
os.environ['NUCYPHER_FELIX_DB_SECRET'] = INSECURE_DEVELOPMENT_PASSWORD
# Test subproc (Click)
envvars = {NUCYPHER_ENVVAR_KEYRING_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD,
envvars = {NUCYPHER_ENVVAR_KEYSTORE_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD,
'NUCYPHER_FELIX_DB_SECRET': INSECURE_DEVELOPMENT_PASSWORD,
'NUCYPHER_WORKER_ETH_PASSWORD': INSECURE_DEVELOPMENT_PASSWORD,
'FLASK_DEBUG': '1'}
@ -105,8 +105,7 @@ def test_run_felix(click_runner, testerchain, agency_local_registry):
felix_config = FelixConfiguration.from_configuration_file(filepath=configuration_file_location,
registry_filepath=agency_local_registry.filepath)
felix_config.attach_keyring()
felix_config.keyring.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
felix_config.keystore.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
felix = felix_config.produce()
# Make a flask app

View File

@ -16,19 +16,21 @@
"""
import os
from unittest.mock import patch, PropertyMock
import pytest
import shutil
from pathlib import Path
import pytest
from nucypher.blockchain.eth.actors import Worker
from nucypher.cli.main import nucypher_cli
from nucypher.config.characters import AliceConfiguration, FelixConfiguration, UrsulaConfiguration
from nucypher.config.constants import NUCYPHER_ENVVAR_KEYRING_PASSWORD, TEMPORARY_DOMAIN, \
NUCYPHER_ENVVAR_ALICE_ETH_PASSWORD, NUCYPHER_ENVVAR_BOB_ETH_PASSWORD
from nucypher.config.keyring import NucypherKeyring
from nucypher.crypto.umbral_adapter import SecretKey
from nucypher.config.constants import (
NUCYPHER_ENVVAR_KEYSTORE_PASSWORD,
TEMPORARY_DOMAIN,
NUCYPHER_ENVVAR_ALICE_ETH_PASSWORD,
NUCYPHER_ENVVAR_BOB_ETH_PASSWORD
)
from nucypher.crypto.keystore import Keystore, InvalidPassword
from nucypher.network.nodes import Teacher
from tests.constants import (
INSECURE_DEVELOPMENT_PASSWORD,
@ -63,7 +65,8 @@ def test_destroy_with_no_configurations(click_runner, custom_filepath):
def test_coexisting_configurations(click_runner,
custom_filepath,
testerchain,
agency_local_registry):
agency_local_registry,
mocker):
#
# Setup
#
@ -76,12 +79,12 @@ def test_coexisting_configurations(click_runner,
# TODO: Is testerchain & Full contract deployment needed here (causes massive slowdown)?
alice, ursula, another_ursula, felix, staker, *all_yall = testerchain.unassigned_accounts
envvars = {NUCYPHER_ENVVAR_KEYRING_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD,
envvars = {NUCYPHER_ENVVAR_KEYSTORE_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD,
NUCYPHER_ENVVAR_ALICE_ETH_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD,
NUCYPHER_ENVVAR_BOB_ETH_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD}
# Future configuration filepaths for assertions...
public_keys_dir = custom_filepath / 'keyring' / 'public'
public_keys_dir = custom_filepath / 'keystore' / 'public'
known_nodes_dir = custom_filepath / 'known_nodes'
# ... Ensure they do not exist to begin with.
@ -106,7 +109,6 @@ def test_coexisting_configurations(click_runner,
felix_file_location = custom_filepath / FelixConfiguration.generate_filename()
alice_file_location = custom_filepath / AliceConfiguration.generate_filename()
ursula_file_location = custom_filepath / UrsulaConfiguration.generate_filename()
another_ursula_configuration_file_location = custom_filepath / UrsulaConfiguration.generate_filename(modifier=another_ursula)
# Felix creates a system configuration
felix_init_args = ('felix', 'init',
@ -123,8 +125,6 @@ def test_coexisting_configurations(click_runner,
# All configuration files still exist.
assert os.path.isdir(custom_filepath)
assert os.path.isfile(felix_file_location)
assert os.path.isdir(public_keys_dir)
assert len(os.listdir(public_keys_dir)) == 3
# Use a custom local filepath to init a persistent Alice
alice_init_args = ('alice', 'init',
@ -140,7 +140,6 @@ def test_coexisting_configurations(click_runner,
# All configuration files still exist.
assert os.path.isfile(felix_file_location)
assert os.path.isfile(alice_file_location)
assert len(os.listdir(public_keys_dir)) == 5
# Use the same local filepath to init a persistent Ursula
init_args = ('ursula', 'init',
@ -155,36 +154,34 @@ def test_coexisting_configurations(click_runner,
assert result.exit_code == 0, result.output
# All configuration files still exist.
assert len(os.listdir(public_keys_dir)) == 8
assert os.path.isfile(felix_file_location)
assert os.path.isfile(alice_file_location)
assert os.path.isfile(ursula_file_location)
# keyring signing key
signing_public_key = SecretKey.random().public_key()
with patch('nucypher.config.keyring.NucypherKeyring.signing_public_key',
PropertyMock(return_value=signing_public_key)):
# Use the same local filepath to init another persistent Ursula
init_args = ('ursula', 'init',
'--network', TEMPORARY_DOMAIN,
'--worker-address', another_ursula,
'--rest-host', MOCK_IP_ADDRESS_2,
'--registry-filepath', agency_local_registry.filepath,
'--provider', TEST_PROVIDER_URI,
'--config-root', custom_filepath)
key_spy = mocker.spy(Keystore, 'generate')
result = click_runner.invoke(nucypher_cli, init_args, catch_exceptions=False, env=envvars)
assert result.exit_code == 0
# keystore signing key
# Use the same local filepath to init another persistent Ursula
init_args = ('ursula', 'init',
'--network', TEMPORARY_DOMAIN,
'--worker-address', another_ursula,
'--rest-host', MOCK_IP_ADDRESS_2,
'--registry-filepath', agency_local_registry.filepath,
'--provider', TEST_PROVIDER_URI,
'--config-root', custom_filepath)
another_ursula_configuration_file_location = custom_filepath / UrsulaConfiguration.generate_filename(
modifier=bytes(signing_public_key).hex()[:8])
result = click_runner.invoke(nucypher_cli, init_args, catch_exceptions=False, env=envvars)
assert result.exit_code == 0
# All configuration files still exist.
assert os.path.isfile(felix_file_location)
assert os.path.isfile(alice_file_location)
kid = key_spy.spy_return.id[:8]
another_ursula_configuration_file_location = custom_filepath / UrsulaConfiguration.generate_filename(modifier=kid)
assert os.path.isfile(another_ursula_configuration_file_location)
assert os.path.isfile(ursula_file_location)
assert len(os.listdir(public_keys_dir)) == 11
#
# Run
@ -211,7 +208,6 @@ def test_coexisting_configurations(click_runner,
assert os.path.isfile(alice_file_location)
assert os.path.isfile(another_ursula_configuration_file_location)
assert os.path.isfile(ursula_file_location)
assert len(os.listdir(public_keys_dir)) == 11
# Check that the proper Ursula console is attached
assert another_ursula in result.output
@ -225,26 +221,22 @@ def test_coexisting_configurations(click_runner,
'--config-file', another_ursula_configuration_file_location)
result = click_runner.invoke(nucypher_cli, another_ursula_destruction_args, catch_exceptions=False, env=envvars)
assert result.exit_code == 0
assert len(os.listdir(public_keys_dir)) == 8
assert not os.path.isfile(another_ursula_configuration_file_location)
ursula_destruction_args = ('ursula', 'destroy', '--config-file', ursula_file_location)
result = click_runner.invoke(nucypher_cli, ursula_destruction_args, input='Y', catch_exceptions=False, env=envvars)
assert result.exit_code == 0
assert 'y/N' in result.output
assert len(os.listdir(public_keys_dir)) == 5
assert not os.path.isfile(ursula_file_location)
alice_destruction_args = ('alice', 'destroy', '--force', '--config-file', alice_file_location)
result = click_runner.invoke(nucypher_cli, alice_destruction_args, catch_exceptions=False, env=envvars)
assert result.exit_code == 0
assert len(os.listdir(public_keys_dir)) == 3
assert not os.path.isfile(alice_file_location)
felix_destruction_args = ('felix', 'destroy', '--force', '--config-file', felix_file_location)
result = click_runner.invoke(nucypher_cli, felix_destruction_args, catch_exceptions=False, env=envvars)
assert result.exit_code == 0
assert len(os.listdir(public_keys_dir)) == 0
assert not os.path.isfile(felix_file_location)
@ -277,9 +269,9 @@ def test_corrupted_configuration(click_runner,
)
# Fails because password is too short and the command uses incomplete args (needs either -F or blockchain details)
envvars = {NUCYPHER_ENVVAR_KEYRING_PASSWORD: ''}
envvars = {NUCYPHER_ENVVAR_KEYSTORE_PASSWORD: ''}
with pytest.raises(NucypherKeyring.AuthenticationFailed):
with pytest.raises(InvalidPassword):
result = click_runner.invoke(nucypher_cli, init_args, catch_exceptions=False, env=envvars)
assert result.exit_code != 0
@ -288,8 +280,8 @@ def test_corrupted_configuration(click_runner,
assert 'ursula.config' not in top_level_config_root # no config file was created
assert Path(custom_filepath).exists()
keyring = custom_filepath / 'keyring'
assert not keyring.exists()
keystore = custom_filepath / 'keystore'
assert not keystore.exists()
known_nodes = 'known_nodes'
path = custom_filepath / known_nodes
@ -304,7 +296,7 @@ def test_corrupted_configuration(click_runner,
'--registry-filepath', agency_local_registry.filepath,
'--config-root', custom_filepath)
envvars = {NUCYPHER_ENVVAR_KEYRING_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD}
envvars = {NUCYPHER_ENVVAR_KEYSTORE_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD}
result = click_runner.invoke(nucypher_cli, init_args, catch_exceptions=False, env=envvars)
assert result.exit_code == 0
@ -313,8 +305,7 @@ def test_corrupted_configuration(click_runner,
# Ensure configuration creation
top_level_config_root = os.listdir(custom_filepath)
assert default_filename in top_level_config_root, "JSON configuration file was not created"
assert len(os.listdir(custom_filepath / 'keyring' / 'private')) == 4 # keys were created
for field in ['known_nodes', 'keyring', default_filename]:
for field in ['known_nodes', 'keystore', default_filename]:
assert field in top_level_config_root
# "Corrupt" the configuration by removing the contract registry
@ -328,5 +319,4 @@ def test_corrupted_configuration(click_runner,
# Ensure character destruction
top_level_config_root = os.listdir(custom_filepath)
assert default_filename not in top_level_config_root # config file was destroyed
assert len(os.listdir(custom_filepath / 'keyring' / 'private')) == 0 # keys were destroyed
assert default_filename not in top_level_config_root # config file was destroyed

View File

@ -19,12 +19,16 @@ import json
from json import JSONDecodeError
import os
from unittest.mock import PropertyMock
import pytest
from nucypher.cli.literature import SUCCESSFUL_DESTRUCTION, COLLECT_NUCYPHER_PASSWORD
from nucypher.cli.main import nucypher_cli
from nucypher.config.base import CharacterConfiguration
from nucypher.config.characters import UrsulaConfiguration
from nucypher.config.constants import APP_DIR, DEFAULT_CONFIG_ROOT, NUCYPHER_ENVVAR_KEYRING_PASSWORD, TEMPORARY_DOMAIN
from nucypher.config.constants import APP_DIR, DEFAULT_CONFIG_ROOT, NUCYPHER_ENVVAR_KEYSTORE_PASSWORD, TEMPORARY_DOMAIN
from nucypher.crypto.keystore import Keystore
from tests.constants import (
FAKE_PASSWORD_CONFIRMED, INSECURE_DEVELOPMENT_PASSWORD,
MOCK_CUSTOM_INSTALLATION_PATH,
@ -32,12 +36,16 @@ from tests.constants import (
from tests.utils.ursula import MOCK_URSULA_STARTING_PORT, select_test_port
def test_initialize_ursula_defaults(click_runner, mocker):
def test_initialize_ursula_defaults(click_runner, mocker, tmpdir):
# Mock out filesystem writes
mocker.patch.object(UrsulaConfiguration, 'initialize', autospec=True)
mocker.patch.object(UrsulaConfiguration, 'to_configuration_file', autospec=True)
# Mock Keystore init
keystore = Keystore.generate(keystore_dir=tmpdir, password=INSECURE_DEVELOPMENT_PASSWORD)
mocker.patch.object(CharacterConfiguration, 'keystore', return_value=keystore, new_callable=PropertyMock)
# Use default ursula init args
init_args = ('ursula', 'init', '--network', TEMPORARY_DOMAIN, '--federated-only')
@ -73,7 +81,7 @@ def test_initialize_custom_configuration_root(custom_filepath, click_runner):
# Files and Directories
assert os.path.isdir(custom_filepath), 'Configuration file does not exist'
assert os.path.isdir(os.path.join(custom_filepath, 'keyring')), 'Keyring does not exist'
assert os.path.isdir(os.path.join(custom_filepath, 'keystore')), 'KEYSTORE does not exist'
assert os.path.isdir(os.path.join(custom_filepath, 'known_nodes')), 'known_nodes directory does not exist'
custom_config_filepath = os.path.join(custom_filepath, UrsulaConfiguration.generate_filename())
@ -182,7 +190,7 @@ def test_ursula_destroy_configuration(custom_filepath, click_runner):
result = click_runner.invoke(nucypher_cli, destruction_args,
input='Y\n'.format(INSECURE_DEVELOPMENT_PASSWORD),
catch_exceptions=False,
env={NUCYPHER_ENVVAR_KEYRING_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD})
env={NUCYPHER_ENVVAR_KEYSTORE_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD})
# CLI Output
assert not os.path.isfile(custom_config_filepath), 'Configuration file still exists'

View File

@ -28,7 +28,7 @@ from nucypher.blockchain.eth.token import StakeList
from nucypher.cli.main import nucypher_cli
from nucypher.config.characters import StakeHolderConfiguration, UrsulaConfiguration
from nucypher.config.constants import (
NUCYPHER_ENVVAR_KEYRING_PASSWORD,
NUCYPHER_ENVVAR_KEYSTORE_PASSWORD,
NUCYPHER_ENVVAR_WORKER_ETH_PASSWORD,
TEMPORARY_DOMAIN,
)
@ -121,7 +121,7 @@ def test_ursula_and_local_keystore_signer_integration(click_runner,
'--signer', mock_signer_uri)
cli_env = {
NUCYPHER_ENVVAR_KEYRING_PASSWORD: password,
NUCYPHER_ENVVAR_KEYSTORE_PASSWORD: password,
NUCYPHER_ENVVAR_WORKER_ETH_PASSWORD: password,
}
result = click_runner.invoke(nucypher_cli, init_args, catch_exceptions=False, env=cli_env)
@ -138,10 +138,9 @@ def test_ursula_and_local_keystore_signer_integration(click_runner,
ursula_config = UrsulaConfiguration.from_configuration_file(ursula_config_path)
assert ursula_config.signer_uri == mock_signer_uri
# Mock decryption of web3 client keyring
# Mock decryption of web3 client keystore
mocker.patch.object(Account, 'decrypt', return_value=worker_account.privateKey)
ursula_config.attach_keyring(checksum_address=worker_account.address)
ursula_config.keyring.unlock(password=password)
ursula_config.keystore.unlock(password=password)
# Produce an Ursula with a Keystore signer correctly derived from the signer URI, and don't do anything else!
mocker.patch.object(StakeList, 'refresh', autospec=True)

View File

@ -29,7 +29,7 @@ from nucypher.characters.base import Learner
from nucypher.cli.literature import NO_CONFIGURATIONS_ON_DISK
from nucypher.cli.main import nucypher_cli
from nucypher.config.characters import UrsulaConfiguration
from nucypher.config.constants import NUCYPHER_ENVVAR_KEYRING_PASSWORD, TEMPORARY_DOMAIN
from nucypher.config.constants import NUCYPHER_ENVVAR_KEYSTORE_PASSWORD, TEMPORARY_DOMAIN
from nucypher.network.nodes import Teacher
from nucypher.utilities.networking import LOOPBACK_ADDRESS, UnknownIPAddress
from tests.constants import (
@ -162,8 +162,7 @@ def test_federated_ursula_learns_via_cli(click_runner, federated_ursulas):
assert deploy_port not in reserved_ports
# Check that CLI Ursula reports that it remembers the teacher and saves the TLS certificate
assert teacher.checksum_address in result.output
assert f"Saved TLS certificate for {teacher.nickname}" in result.output
assert f"Saved TLS certificate for {LOOPBACK_ADDRESS}" in result.output
federated_ursulas.clear()
@ -188,7 +187,7 @@ def test_persistent_node_storage_integration(click_runner,
'--registry-filepath', agency_local_registry.filepath,
)
envvars = {NUCYPHER_ENVVAR_KEYRING_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD}
envvars = {NUCYPHER_ENVVAR_KEYSTORE_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD}
result = click_runner.invoke(nucypher_cli, init_args, catch_exceptions=False, env=envvars)
assert result.exit_code == 0
@ -248,7 +247,7 @@ def test_ursula_run_ip_checkup(testerchain, custom_filepath, click_runner, mocke
staker = blockchain_ursulas.pop()
def set_staker_address(worker, *args, **kwargs):
worker._checksum_address = staker.checksum_address
worker.checksum_address = staker.checksum_address
return True
monkeypatch.setattr(Worker, 'block_until_ready', set_staker_address)

View File

@ -169,7 +169,7 @@ def test_staker_divide_stakes(click_runner,
result = click_runner.invoke(nucypher_cli,
divide_args,
catch_exceptions=False,
env=dict(NUCYPHER_KEYRING_PASSWORD=INSECURE_DEVELOPMENT_PASSWORD))
env=dict(NUCYPHER_KEYSTORE_PASSWORD=INSECURE_DEVELOPMENT_PASSWORD))
assert result.exit_code == 0
stake_args = ('stake', 'list', '--config-file', stakeholder_configuration_file_location)
@ -377,7 +377,7 @@ def test_ursula_init(click_runner,
# Files and Directories
assert os.path.isdir(custom_filepath), 'Configuration file does not exist'
assert os.path.isdir(os.path.join(custom_filepath, 'keyring')), 'Keyring does not exist'
assert os.path.isdir(os.path.join(custom_filepath, 'keystore')), 'KEYSTORE does not exist'
assert os.path.isdir(os.path.join(custom_filepath, 'known_nodes')), 'known_nodes directory does not exist'
custom_config_filepath = os.path.join(custom_filepath, UrsulaConfiguration.generate_filename())

View File

@ -140,7 +140,7 @@ def test_alice_refuses_to_make_arrangement_unless_ursula_is_valid(blockchain_ali
vladimir._Ursula__substantiate_stamp()
vladimir._Teacher__interface_signature = signature
vladimir.node_storage.store_node_certificate(certificate=target.certificate)
vladimir.node_storage.store_node_certificate(certificate=target.certificate, port=vladimir.rest_interface.port)
# Ideally, a fishy node shouldn't be present in `known_nodes`,
# but I guess we're testing the case when it became fishy somewhere between we learned about it

View File

@ -19,6 +19,7 @@ from collections import defaultdict
import lmdb
import pytest
from eth_utils.crypto import keccak
from nucypher.characters.control.emitters import WebEmitter
from nucypher.crypto.powers import TransactingPower
@ -45,7 +46,7 @@ Learner._DEBUG_MODE = False
@pytest.fixture(autouse=True, scope='session')
def __very_pretty_and_insecure_scrypt_do_not_use():
def __very_pretty_and_insecure_scrypt_do_not_use(request):
"""
# WARNING: DO NOT USE THIS CODE ANYWHERE #
@ -57,13 +58,10 @@ def __very_pretty_and_insecure_scrypt_do_not_use():
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
original_derivation_function = Scrypt.derive
# One-Time Insecure Password
insecure_password = bytes(INSECURE_DEVELOPMENT_PASSWORD, encoding='utf8')
# Patch Method
def __insecure_derive(*args, **kwargs):
def __insecure_derive(_scrypt, key_material: bytes):
"""Temporarily replaces Scrypt.derive for mocking"""
return insecure_password
return keccak(key_material)
# Disable Scrypt KDF
Scrypt.derive = __insecure_derive

View File

@ -24,7 +24,7 @@ from random import SystemRandom
from web3 import Web3
from nucypher.blockchain.eth.token import NU
from nucypher.config.constants import NUCYPHER_ENVVAR_KEYRING_PASSWORD, NUCYPHER_ENVVAR_WORKER_ETH_PASSWORD
from nucypher.config.constants import NUCYPHER_ENVVAR_KEYSTORE_PASSWORD, NUCYPHER_ENVVAR_WORKER_ETH_PASSWORD
#
# Ursula
@ -76,7 +76,7 @@ NUMBER_OF_ALLOCATIONS_IN_TESTS = 50 # TODO: Move to constants
__valid_password_chars = string.ascii_uppercase + string.ascii_lowercase + string.digits
INSECURE_DEVELOPMENT_PASSWORD = ''.join(SystemRandom().choice(__valid_password_chars) for _ in range(16))
INSECURE_DEVELOPMENT_PASSWORD = ''.join(SystemRandom().choice(__valid_password_chars) for _ in range(32))
#
# Temporary Directories and Files
@ -137,7 +137,7 @@ NO_ENTER = NO + '\n'
FAKE_PASSWORD_CONFIRMED = '{password}\n{password}\n'.format(password=INSECURE_DEVELOPMENT_PASSWORD)
CLI_TEST_ENV = {NUCYPHER_ENVVAR_KEYRING_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD}
CLI_TEST_ENV = {NUCYPHER_ENVVAR_KEYSTORE_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD}
CLI_ENV = {NUCYPHER_ENVVAR_KEYRING_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD,
CLI_ENV = {NUCYPHER_ENVVAR_KEYSTORE_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD,
NUCYPHER_ENVVAR_WORKER_ETH_PASSWORD: INSECURE_DEVELOPMENT_PASSWORD}

View File

@ -23,7 +23,7 @@ from web3.contract import Contract
from nucypher.blockchain.economics import BaseEconomics
from nucypher.blockchain.eth.constants import NULL_ADDRESS
from nucypher.crypto.api import sha256_digest
from nucypher.crypto.utils import sha256_digest
from nucypher.crypto.signing import SignatureStamp
from nucypher.crypto.umbral_adapter import SecretKey, Signer
from nucypher.utilities.ethereum import to_32byte_hex

View File

@ -17,8 +17,9 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import os
import pytest
import coincurve
import pytest
from cryptography.hazmat.backends.openssl import backend
from cryptography.hazmat.primitives import hashes
from eth_account.account import Account
@ -27,9 +28,12 @@ from eth_keys import KeyAPI as EthKeyAPI
from eth_tester.exceptions import TransactionFailed
from eth_utils import to_canonical_address, to_checksum_address, to_normalized_address
from nucypher.crypto.api import keccak_digest, verify_eip_191
from nucypher.crypto.umbral_adapter import SecretKey, PublicKey, Signer, Signature
from nucypher.crypto.utils import canonical_address_from_umbral_key
from nucypher.crypto.utils import (
canonical_address_from_umbral_key,
keccak_digest,
verify_eip_191
)
ALGORITHM_KECCAK256 = 0
ALGORITHM_SHA256 = 1

View File

@ -58,6 +58,7 @@ from nucypher.config.characters import (
UrsulaConfiguration
)
from nucypher.config.constants import TEMPORARY_DOMAIN
from nucypher.crypto.keystore import Keystore
from nucypher.crypto.powers import TransactingPower
from nucypher.datastore import datastore
from nucypher.network.nodes import TEACHER_NODES
@ -174,7 +175,7 @@ def bob_federated_test_config():
@pytest.fixture(scope="module")
def ursula_decentralized_test_config(test_registry):
def ursula_decentralized_test_config(test_registry, temp_dir_path):
config = make_ursula_test_configuration(federated=False,
provider_uri=TEST_PROVIDER_URI,
test_registry=test_registry,
@ -201,8 +202,7 @@ def bob_blockchain_test_config(testerchain, test_registry):
config = make_bob_test_configuration(federated=False,
provider_uri=TEST_PROVIDER_URI,
test_registry=test_registry,
checksum_address=testerchain.bob_account,
)
checksum_address=testerchain.bob_account)
yield config
config.cleanup()
@ -366,8 +366,9 @@ def blockchain_bob(bob_blockchain_test_config, testerchain):
@pytest.fixture(scope="module")
def federated_ursulas(ursula_federated_test_config):
if MOCK_KNOWN_URSULAS_CACHE:
# raise RuntimeError("Ursulas cache was unclear at fixture loading time. Did you use one of the ursula maker functions without cleaning up?")
MOCK_KNOWN_URSULAS_CACHE.clear()
raise RuntimeError("Ursulas cache was unclear at fixture loading time. "
"Did you use one of the ursula maker functions without cleaning up?")
# MOCK_KNOWN_URSULAS_CACHE.clear()
_ursulas = make_federated_ursulas(ursula_config=ursula_federated_test_config,
quantity=NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK)
@ -1050,3 +1051,9 @@ def stakeholder_configuration_file_location(custom_filepath):
def mock_teacher_nodes(mocker):
mock_nodes = tuple(u.rest_url() for u in MOCK_KNOWN_URSULAS_CACHE.values())[0:2]
mocker.patch.dict(TEACHER_NODES, {TEMPORARY_DOMAIN: mock_nodes}, clear=True)
@pytest.fixture(autouse=True)
def disable_interactive_keystore_generation(mocker):
# Do not notify or confirm mnemonic seed words during tests normally
mocker.patch.object(Keystore, '_confirm_generate')

View File

@ -20,7 +20,7 @@ import maya
import pytest
from nucypher.characters.lawful import Enrico
from nucypher.crypto.api import keccak_digest
from nucypher.crypto.utils import keccak_digest
from nucypher.datastore.models import PolicyArrangement
from nucypher.policy.collections import Revocation

View File

@ -16,9 +16,10 @@
"""
import os
import pytest
from constant_sorrow.constants import NO_PASSWORD
from nacl.exceptions import CryptoError
from mnemonic.mnemonic import Mnemonic
from nucypher.blockchain.eth.decorators import InvalidChecksumAddress
from nucypher.characters.control.emitters import StdoutEmitter
@ -26,16 +27,18 @@ from nucypher.cli.actions.auth import (
get_client_password,
get_nucypher_password,
get_password_from_prompt,
unlock_nucypher_keyring
unlock_nucypher_keystore
)
from nucypher.cli.literature import (
COLLECT_ETH_PASSWORD,
COLLECT_NUCYPHER_PASSWORD,
DECRYPTING_CHARACTER_KEYRING,
DECRYPTING_CHARACTER_KEYSTORE,
GENERIC_PASSWORD_PROMPT
)
from nucypher.config.keyring import NucypherKeyring
from nucypher.config.base import CharacterConfiguration
from nucypher.crypto import passwords
from nucypher.crypto.keystore import Keystore
from nucypher.crypto.passwords import SecretBoxAuthenticationError
from tests.constants import INSECURE_DEVELOPMENT_PASSWORD
@ -97,55 +100,55 @@ def test_get_nucypher_password(mock_stdin, mock_account, confirm, capsys):
captured = capsys.readouterr()
assert COLLECT_NUCYPHER_PASSWORD in captured.out
if confirm:
prompt = COLLECT_NUCYPHER_PASSWORD + f" ({NucypherKeyring.MINIMUM_PASSWORD_LENGTH} character minimum)"
prompt = COLLECT_NUCYPHER_PASSWORD + f" ({Keystore._MINIMUM_PASSWORD_LENGTH} character minimum)"
assert prompt in captured.out
def test_unlock_nucypher_keyring_invalid_password(mocker, test_emitter, alice_blockchain_test_config, capsys):
def test_unlock_nucypher_keystore_invalid_password(mocker, test_emitter, alice_blockchain_test_config, capsys, tmpdir):
# Setup
keyring_attach_spy = mocker.spy(CharacterConfiguration, 'attach_keyring')
mocker.patch.object(NucypherKeyring, 'unlock', side_effect=CryptoError)
mocker.patch.object(passwords, 'secret_box_decrypt', side_effect=SecretBoxAuthenticationError)
mocker.patch.object(CharacterConfiguration,
'dev_mode',
return_value=False,
new_callable=mocker.PropertyMock)
keystore = Keystore.generate(password=INSECURE_DEVELOPMENT_PASSWORD, keystore_dir=tmpdir)
alice_blockchain_test_config.attach_keystore(keystore)
# Test
with pytest.raises(NucypherKeyring.AuthenticationFailed):
unlock_nucypher_keyring(emitter=test_emitter,
password=INSECURE_DEVELOPMENT_PASSWORD+'typo',
character_configuration=alice_blockchain_test_config)
keyring_attach_spy.assert_called_once()
with pytest.raises(Keystore.AuthenticationFailed):
unlock_nucypher_keystore(emitter=test_emitter,
password=INSECURE_DEVELOPMENT_PASSWORD+'typo',
character_configuration=alice_blockchain_test_config)
captured = capsys.readouterr()
assert DECRYPTING_CHARACTER_KEYRING.format(name=alice_blockchain_test_config.NAME.capitalize()) in captured.out
assert DECRYPTING_CHARACTER_KEYSTORE.format(name=alice_blockchain_test_config.NAME.capitalize()) in captured.out
def test_unlock_nucypher_keyring_dev_mode(mocker, test_emitter, capsys, alice_blockchain_test_config):
def test_unlock_nucypher_keystore_dev_mode(mocker, test_emitter, capsys, alice_blockchain_test_config, tmpdir):
# Setup
unlock_spy = mocker.spy(NucypherKeyring, 'unlock')
attach_spy = mocker.spy(CharacterConfiguration, 'attach_keyring')
unlock_spy = mocker.spy(Keystore, 'unlock')
mocker.patch.object(CharacterConfiguration,
'dev_mode',
return_value=True,
new_callable=mocker.PropertyMock)
# Test
result = unlock_nucypher_keyring(emitter=test_emitter,
password=INSECURE_DEVELOPMENT_PASSWORD,
character_configuration=alice_blockchain_test_config)
keystore = Keystore.generate(password=INSECURE_DEVELOPMENT_PASSWORD, keystore_dir=tmpdir)
alice_blockchain_test_config.attach_keystore(keystore)
result = unlock_nucypher_keystore(emitter=test_emitter,
password=INSECURE_DEVELOPMENT_PASSWORD,
character_configuration=alice_blockchain_test_config)
assert result
output = capsys.readouterr().out
message = DECRYPTING_CHARACTER_KEYRING.format(name=alice_blockchain_test_config.NAME.capitalize())
message = DECRYPTING_CHARACTER_KEYSTORE.format(name=alice_blockchain_test_config.NAME.capitalize())
assert message in output
unlock_spy.assert_not_called()
attach_spy.assert_not_called()
def test_unlock_nucypher_keyring(mocker,
def test_unlock_nucypher_keystore(mocker,
test_emitter,
capsys,
alice_blockchain_test_config,
@ -154,21 +157,22 @@ def test_unlock_nucypher_keyring(mocker,
# Setup
# Do not test "real" unlocking here, just the plumbing
unlock_spy = mocker.patch.object(NucypherKeyring, 'unlock', return_value=True)
attach_spy = mocker.spy(CharacterConfiguration, 'attach_keyring')
unlock_spy = mocker.patch.object(Keystore, 'unlock', return_value=True)
mocker.patch.object(CharacterConfiguration,
'dev_mode',
return_value=False,
new_callable=mocker.PropertyMock)
# Test
result = unlock_nucypher_keyring(emitter=test_emitter,
password=INSECURE_DEVELOPMENT_PASSWORD,
character_configuration=alice_blockchain_test_config)
mocker.patch.object(Mnemonic, 'detect_language', return_value='english')
keystore = Keystore.generate(password=INSECURE_DEVELOPMENT_PASSWORD, keystore_dir=tmpdir)
alice_blockchain_test_config.attach_keystore(keystore)
result = unlock_nucypher_keystore(emitter=test_emitter,
password=INSECURE_DEVELOPMENT_PASSWORD,
character_configuration=alice_blockchain_test_config)
assert result
captured = capsys.readouterr()
message = DECRYPTING_CHARACTER_KEYRING.format(name=alice_blockchain_test_config.NAME.capitalize())
message = DECRYPTING_CHARACTER_KEYSTORE.format(name=alice_blockchain_test_config.NAME.capitalize())
assert message in captured.out
unlock_spy.assert_called_once_with(password=INSECURE_DEVELOPMENT_PASSWORD)
attach_spy.assert_called_once()

View File

@ -26,7 +26,7 @@ from nucypher.blockchain.eth.token import StakeList
from nucypher.cli.main import nucypher_cli
from nucypher.config.characters import UrsulaConfiguration
from nucypher.config.constants import (
NUCYPHER_ENVVAR_KEYRING_PASSWORD,
NUCYPHER_ENVVAR_KEYSTORE_PASSWORD,
NUCYPHER_ENVVAR_WORKER_ETH_PASSWORD,
TEMPORARY_DOMAIN
)
@ -73,7 +73,7 @@ def test_ursula_init_with_local_keystore_signer(click_runner,
'--signer', mock_signer_uri)
cli_env = {
NUCYPHER_ENVVAR_KEYRING_PASSWORD: password,
NUCYPHER_ENVVAR_KEYSTORE_PASSWORD: password,
NUCYPHER_ENVVAR_WORKER_ETH_PASSWORD: password,
}
result = click_runner.invoke(nucypher_cli,
@ -93,10 +93,9 @@ def test_ursula_init_with_local_keystore_signer(click_runner,
ursula_config = UrsulaConfiguration.from_configuration_file(custom_config_filepath, config_root=custom_filepath)
assert ursula_config.signer_uri == mock_signer_uri
# Mock decryption of web3 client keyring
# Mock decryption of web3 client keystore
mocker.patch.object(Account, 'decrypt', return_value=worker_account.privateKey)
ursula_config.attach_keyring(checksum_address=worker_account.address)
ursula_config.keyring.unlock(password=password)
ursula_config.keystore.unlock(password=password)
# Produce an ursula with a Keystore signer correctly derived from the signer URI, and dont do anything else!
mocker.patch.object(StakeList, 'refresh', autospec=True)

View File

@ -16,18 +16,17 @@
"""
import os
from unittest.mock import Mock
import tempfile
import pytest
import tempfile
from constant_sorrow.constants import CERTIFICATE_NOT_SAVED, NO_KEYRING_ATTACHED
from constant_sorrow.constants import CERTIFICATE_NOT_SAVED, NO_KEYSTORE_ATTACHED
from tests.constants import MOCK_IP_ADDRESS
from nucypher.blockchain.eth.actors import StakeHolder
from nucypher.characters.chaotic import Felix
from nucypher.characters.lawful import Alice, Bob, Ursula
from nucypher.cli.actions.configure import destroy_configuration
from nucypher.cli.literature import SUCCESSFUL_DESTRUCTION
from nucypher.config.base import CharacterConfiguration
from nucypher.config.characters import (
AliceConfiguration,
BobConfiguration,
@ -36,11 +35,11 @@ from nucypher.config.characters import (
UrsulaConfiguration
)
from nucypher.config.constants import TEMPORARY_DOMAIN
from nucypher.config.keyring import NucypherKeyring
from nucypher.config.base import CharacterConfiguration
from nucypher.config.storages import ForgetfulNodeStorage
from nucypher.crypto.keystore import Keystore
from nucypher.crypto.umbral_adapter import SecretKey
from tests.constants import INSECURE_DEVELOPMENT_PASSWORD
from tests.constants import MOCK_IP_ADDRESS
# Main Cast
configurations = (AliceConfiguration, BobConfiguration, UrsulaConfiguration)
@ -66,7 +65,7 @@ def test_federated_development_character_configurations(character, configuration
assert config.is_me is True
assert config.dev_mode is True
assert config.keyring == NO_KEYRING_ATTACHED
assert config.keystore == NO_KEYSTORE_ATTACHED
assert config.provider_uri is None
# Production
@ -106,7 +105,7 @@ def test_federated_development_character_configurations(character, configuration
# TODO: This test is unnecessarily slow due to the blockchain configurations. Perhaps we should mock them -- See #2230
@pytest.mark.parametrize('configuration_class', all_configurations)
def test_default_character_configuration_preservation(configuration_class, testerchain, test_registry_source_manager):
def test_default_character_configuration_preservation(configuration_class, testerchain, test_registry_source_manager, mocker, tmpdir):
configuration_class.DEFAULT_CONFIG_ROOT = '/tmp'
fake_address = '0xdeadbeef'
@ -127,13 +126,13 @@ def test_default_character_configuration_preservation(configuration_class, teste
elif configuration_class == UrsulaConfiguration:
# special case for rest_host & dev mode
# use keyring
keyring = Mock(spec=NucypherKeyring)
keyring.signing_public_key = SecretKey.random().public_key()
# use keystore
keystore = Keystore.generate(password=INSECURE_DEVELOPMENT_PASSWORD, keystore_dir=tmpdir)
keystore.signing_public_key = SecretKey.random().public_key()
character_config = configuration_class(checksum_address=fake_address,
domain=network,
rest_host=MOCK_IP_ADDRESS,
keyring=keyring)
keystore=keystore)
else:
character_config = configuration_class(checksum_address=fake_address, domain=network)
@ -166,7 +165,7 @@ def test_ursula_development_configuration(federated_only=True):
config = UrsulaConfiguration(dev_mode=True, federated_only=federated_only)
assert config.is_me is True
assert config.dev_mode is True
assert config.keyring == NO_KEYRING_ATTACHED
assert config.keystore == NO_KEYSTORE_ATTACHED
# Produce an Ursula
ursula_one = config()
@ -209,9 +208,9 @@ def test_destroy_configuration(config,
config_file = config.filepath
# Isolate from filesystem and Spy on the methods we're testing here
spy_keyring_attached = mocker.spy(CharacterConfiguration, 'attach_keyring')
spy_keystore_attached = mocker.spy(CharacterConfiguration, 'attach_keystore')
mock_config_destroy = mocker.patch.object(CharacterConfiguration, 'destroy')
spy_keyring_destroy = mocker.spy(NucypherKeyring, 'destroy')
spy_keystore_destroy = mocker.spy(Keystore, 'destroy')
mock_os_remove = mocker.patch('os.remove')
# Test
@ -221,8 +220,8 @@ def test_destroy_configuration(config,
captured = capsys.readouterr()
assert SUCCESSFUL_DESTRUCTION in captured.out
spy_keyring_attached.assert_called_once()
spy_keyring_destroy.assert_called_once()
spy_keystore_attached.assert_called_once()
spy_keystore_destroy.assert_called_once()
mock_os_remove.assert_called_with(str(config_file))
# Ensure all destroyed files belong to this Ursula

View File

@ -43,8 +43,8 @@ def test_alices_powers_are_persistent(federated_ursulas, tmpdir):
# Generate keys and write them the disk
alice_config.initialize(password=INSECURE_DEVELOPMENT_PASSWORD)
# Unlock Alice's keyring
alice_config.keyring.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
# Unlock Alice's keystore
alice_config.keystore.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
# Produce an Alice
alice = alice_config() # or alice_config.produce()
@ -98,9 +98,8 @@ def test_alices_powers_are_persistent(federated_ursulas, tmpdir):
config_root=config_root
)
# Alice unlocks her restored keyring from disk
new_alice_config.attach_keyring()
new_alice_config.keyring.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
# Alice unlocks her restored keystore from disk
new_alice_config.keystore.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
new_alice = new_alice_config()
# First, we check that her public keys are correctly restored

View File

@ -26,7 +26,7 @@ from flask import Flask
from nucypher.characters.lawful import Alice, Bob, Ursula
from nucypher.config.constants import TEMPORARY_DOMAIN
from nucypher.config.keyring import NucypherKeyring
from nucypher.crypto.keystore import Keystore
from nucypher.crypto.powers import DecryptingPower, DelegatingPower
from nucypher.crypto.umbral_adapter import SecretKey, Signer
from nucypher.datastore.datastore import Datastore
@ -36,30 +36,22 @@ from tests.constants import INSECURE_DEVELOPMENT_PASSWORD
from tests.utils.matchers import IsType
def test_generate_alice_keyring(tmpdir):
def test_generate_alice_keystore(tmpdir):
keyring = NucypherKeyring.generate(
checksum_address=FEDERATED_ADDRESS,
keystore = Keystore.generate(
password=INSECURE_DEVELOPMENT_PASSWORD,
encrypting=True,
rest=False,
keyring_root=tmpdir
keystore_dir=tmpdir
)
enc_pubkey = keyring.encrypting_public_key
assert enc_pubkey is not None
with pytest.raises(Keystore.Locked):
_dec_keypair = keystore.derive_crypto_power(DecryptingPower).keypair
with pytest.raises(NucypherKeyring.KeyringLocked):
_dec_keypair = keyring.derive_crypto_power(DecryptingPower).keypair
keyring.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
dec_keypair = keyring.derive_crypto_power(DecryptingPower).keypair
assert enc_pubkey == dec_keypair.pubkey
keystore.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
assert keystore.derive_crypto_power(DecryptingPower).keypair
label = b'test'
delegating_power = keyring.derive_crypto_power(DelegatingPower)
delegating_power = keystore.derive_crypto_power(DelegatingPower)
delegating_pubkey = delegating_power.get_pubkey_from_label(label)
bob_pubkey = SecretKey.random().public_key()
@ -70,26 +62,23 @@ def test_generate_alice_keyring(tmpdir):
assert delegating_pubkey == delegating_pubkey_again
another_delegating_power = keyring.derive_crypto_power(DelegatingPower)
another_delegating_power = keystore.derive_crypto_power(DelegatingPower)
another_delegating_pubkey = another_delegating_power.get_pubkey_from_label(label)
assert delegating_pubkey == another_delegating_pubkey
def test_characters_use_keyring(tmpdir):
keyring = NucypherKeyring.generate(
checksum_address=FEDERATED_ADDRESS,
def test_characters_use_keystore(tmpdir):
keystore = Keystore.generate(
password=INSECURE_DEVELOPMENT_PASSWORD,
encrypting=True,
rest=True,
host=LOOPBACK_ADDRESS,
keyring_root=tmpdir)
keyring.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
alice = Alice(federated_only=True, start_learning_now=False, keyring=keyring)
Bob(federated_only=True, start_learning_now=False, keyring=keyring)
keystore_dir=tmpdir
)
keystore.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
alice = Alice(federated_only=True, start_learning_now=False, keystore=keystore)
Bob(federated_only=True, start_learning_now=False, keystore=keystore)
Ursula(federated_only=True,
start_learning_now=False,
keyring=keyring,
keystore=keystore,
rest_host=LOOPBACK_ADDRESS,
rest_port=12345,
db_filepath=tempfile.mkdtemp(),
@ -97,28 +86,26 @@ def test_characters_use_keyring(tmpdir):
alice.disenchant() # To stop Alice's publication threadpool. TODO: Maybe only start it at first enactment?
@pytest.mark.skip('Do we really though?')
def test_tls_hosting_certificate_remains_the_same(tmpdir, mocker):
keyring = NucypherKeyring.generate(
checksum_address=FEDERATED_ADDRESS,
keystore = Keystore.generate(
password=INSECURE_DEVELOPMENT_PASSWORD,
encrypting=True,
rest=True,
host=LOOPBACK_ADDRESS,
keyring_root=tmpdir)
keyring.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
keystore_dir=tmpdir
)
keystore.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
rest_port = 12345
db_filepath = tempfile.mkdtemp()
ursula = Ursula(federated_only=True,
start_learning_now=False,
keyring=keyring,
keystore=keystore,
rest_host=LOOPBACK_ADDRESS,
rest_port=rest_port,
db_filepath=db_filepath,
domain=TEMPORARY_DOMAIN)
assert ursula.keyring is keyring
assert ursula.keystore is keystore
assert ursula.certificate == ursula._crypto_power.power_ups(TLSHostingPower).keypair.certificate
original_certificate_bytes = ursula.certificate.public_bytes(encoding=Encoding.PEM)
@ -128,13 +115,13 @@ def test_tls_hosting_certificate_remains_the_same(tmpdir, mocker):
spy_rest_server_init = mocker.spy(ProxyRESTServer, '__init__')
recreated_ursula = Ursula(federated_only=True,
start_learning_now=False,
keyring=keyring,
keystore=keystore,
rest_host=LOOPBACK_ADDRESS,
rest_port=rest_port,
db_filepath=db_filepath,
domain=TEMPORARY_DOMAIN)
assert recreated_ursula.keyring is keyring
assert recreated_ursula.keystore is keystore
assert recreated_ursula.certificate.public_bytes(encoding=Encoding.PEM) == original_certificate_bytes
tls_hosting_power = recreated_ursula._crypto_power.power_ups(TLSHostingPower)
spy_rest_server_init.assert_called_once_with(ANY, # self

View File

@ -50,7 +50,7 @@ class BaseTestNodeStorageBackends:
node_storage.store_node_metadata(node=ursula)
# Read Node
node_from_storage = node_storage.get(checksum_address=ursula.checksum_address,
node_from_storage = node_storage.get(stamp=ursula.stamp,
federated_only=True)
assert ursula == node_from_storage, "Node storage {} failed".format(node_storage)
@ -78,34 +78,14 @@ class BaseTestNodeStorageBackends:
# Read random nodes
for i in range(3):
random_node = all_known_nodes.pop()
random_node_from_storage = node_storage.get(checksum_address=random_node.checksum_address,
federated_only=True)
random_node_from_storage = node_storage.get(stamp=random_node.stamp, federated_only=True)
assert random_node.checksum_address == random_node_from_storage.checksum_address
return True
def _write_and_delete_metadata(self, ursula, node_storage):
# Write Node
node_storage.store_node_metadata(node=ursula)
# Delete Node
node_storage.remove(checksum_address=ursula.checksum_address, certificate=False)
# Read Node
with pytest.raises(NodeStorage.UnknownNode):
_node_from_storage = node_storage.get(checksum_address=ursula.checksum_address,
federated_only=True)
# Read all nodes from storage
all_stored_nodes = node_storage.all(federated_only=True)
assert all_stored_nodes == set()
return True
#
# Storage Backend Tests
#
def test_delete_node_in_storage(self, light_ursula):
assert self._write_and_delete_metadata(ursula=light_ursula, node_storage=self.storage_backend)
def test_read_and_write_to_storage(self, light_ursula):
assert self._read_and_write_metadata(ursula=light_ursula, node_storage=self.storage_backend)
@ -133,7 +113,7 @@ class TestTemporaryFileBasedNodeStorage(BaseTestNodeStorageBackends):
file.write(Learner.LEARNER_VERSION.to_bytes(4, 'big') + b'invalid')
with pytest.raises(TemporaryFileBasedNodeStorage.InvalidNodeMetadata):
self.storage_backend.get(checksum_address=some_node[:-5],
self.storage_backend.get(stamp=some_node[:-5],
federated_only=True,
certificate_only=False)
@ -143,7 +123,7 @@ class TestTemporaryFileBasedNodeStorage(BaseTestNodeStorageBackends):
file.write(b'meh') # Versions are expected to be 4 bytes, but this is 3 bytes
with pytest.raises(TemporaryFileBasedNodeStorage.InvalidNodeMetadata):
self.storage_backend.get(checksum_address=another_node[:-5],
self.storage_backend.get(stamp=another_node[:-5],
federated_only=True,
certificate_only=False)

View File

@ -14,14 +14,17 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import contextlib
import maya
import pytest
import time
from datetime import datetime
from flask import Response
from unittest.mock import patch
import maya
import pytest
from flask import Response
from nucypher.characters.lawful import Ursula
from nucypher.crypto.umbral_adapter import PublicKey, encrypt
from nucypher.datastore.base import RecordField

View File

@ -18,7 +18,7 @@
import pytest
from nucypher.characters.lawful import Ursula
from nucypher.crypto.api import keccak_digest
from nucypher.crypto.utils import keccak_digest
from nucypher.datastore.models import TreasureMap as DatastoreTreasureMap
from nucypher.policy.collections import TreasureMap as FederatedTreasureMap
from tests.utils.middleware import MockRestMiddleware

View File

@ -177,7 +177,7 @@ def make_alice(known_nodes: Optional[Set[Ursula]] = None):
)
alice_config.initialize(password=INSECURE_PASSWORD)
alice_config.keyring.unlock(password=INSECURE_PASSWORD)
alice_config.keystore.unlock(password=INSECURE_PASSWORD)
alice = alice_config.produce()
alice.signer.unlock(account=ALICE_ADDRESS, password=SIGNER_PASSWORD)
alice.start_learning_loop(now=True)

View File

@ -15,7 +15,6 @@
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import tempfile
from contextlib import contextmanager
from unittest.mock import patch
@ -200,7 +199,7 @@ class VerificationTracker:
cls.metadata_verifications += 1
mock_cert_generation = patch("nucypher.crypto.api.generate_self_signed_certificate", new=do_not_create_cert)
mock_cert_generation = patch("nucypher.crypto.tls.generate_self_signed_certificate", new=do_not_create_cert)
mock_rest_app_creation = patch("nucypher.characters.lawful.make_rest_app",
new=NotARestApp.create_with_not_a_datastore)

View File

@ -21,7 +21,6 @@ from constant_sorrow import constants
from cryptography.exceptions import InvalidSignature
from nucypher.characters.lawful import Alice, Bob, Character
from nucypher.crypto import api
from nucypher.crypto.powers import (CryptoPower, NoSigningPower, SigningPower)
"""

View File

@ -87,14 +87,14 @@ def test_echo_config_root(click_runner):
version_args = ('--config-path', )
result = click_runner.invoke(nucypher_cli, version_args, catch_exceptions=False)
assert result.exit_code == 0
assert DEFAULT_CONFIG_ROOT in result.output, 'Configuration path text was not produced.'
assert str(DEFAULT_CONFIG_ROOT.resolve()) in result.output, 'Configuration path text was not produced.'
def test_echo_logging_root(click_runner):
version_args = ('--logging-path', )
result = click_runner.invoke(nucypher_cli, version_args, catch_exceptions=False)
assert result.exit_code == 0
assert USER_LOG_DIR in result.output, 'Log path text was not produced.'
assert str(USER_LOG_DIR.resolve()) in result.output, 'Log path text was not produced.'
def test_contacts_help(click_runner):

View File

@ -1,213 +0,0 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from functools import partial
from pathlib import Path
import pytest
from constant_sorrow.constants import FEDERATED_ADDRESS
from cryptography.hazmat.primitives.serialization import Encoding
from nucypher.config.keyring import (
_assemble_key_data,
_generate_tls_keys,
_serialize_private_key,
_deserialize_private_key,
_serialize_private_key_to_pem,
_deserialize_private_key_from_pem,
_write_private_keyfile,
_read_keyfile, NucypherKeyring
)
from nucypher.crypto.api import _TLS_CURVE
from nucypher.crypto.powers import DecryptingPower, SigningPower
from nucypher.network.server import TLSHostingPower
from nucypher.utilities.networking import LOOPBACK_ADDRESS
from tests.constants import INSECURE_DEVELOPMENT_PASSWORD
def test_keyring_invalid_password(tmpdir):
with pytest.raises(NucypherKeyring.AuthenticationFailed):
_generate_keyring(tmpdir, password='tobeornottobe') # password less than 16 characters
def test_keyring_lock_unlock(tmpdir):
keyring = _generate_keyring(tmpdir)
assert not keyring.is_unlocked
keyring.unlock(INSECURE_DEVELOPMENT_PASSWORD)
assert keyring.is_unlocked
keyring.unlock(INSECURE_DEVELOPMENT_PASSWORD) # unlock when already unlocked
assert keyring.is_unlocked
keyring.lock()
assert not keyring.is_unlocked
keyring.lock() # lock when already locked
assert not keyring.is_unlocked
def test_keyring_derive_crypto_power_without_unlock(tmpdir):
keyring = _generate_keyring(tmpdir)
with pytest.raises(NucypherKeyring.KeyringLocked):
keyring.derive_crypto_power(power_class=DecryptingPower)
def test_keyring_restoration(tmpdir):
keyring = _generate_keyring(tmpdir)
keyring.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
account = keyring.account
checksum_address = keyring.checksum_address
certificate_filepath = keyring.certificate_filepath
encrypting_public_key_hex = bytes(keyring.encrypting_public_key).hex()
signing_public_key_hex = bytes(keyring.signing_public_key).hex()
# tls power
tls_hosting_power = keyring.derive_crypto_power(power_class=TLSHostingPower, host=LOOPBACK_ADDRESS)
tls_hosting_power_public_key_numbers = tls_hosting_power.public_key().public_numbers()
tls_hosting_power_certificate_public_bytes = \
tls_hosting_power.keypair.certificate.public_bytes(encoding=Encoding.PEM)
tls_hosting_power_certificate_filepath = tls_hosting_power.keypair.certificate_filepath
# decrypting power
decrypting_power = keyring.derive_crypto_power(power_class=DecryptingPower)
decrypting_power_public_key_hex = bytes(decrypting_power.public_key()).hex()
decrypting_power_fingerprint = decrypting_power.keypair.fingerprint()
# signing power
signing_power = keyring.derive_crypto_power(power_class=SigningPower)
signing_power_public_key_hex = bytes(signing_power.public_key()).hex()
signing_power_fingerprint = signing_power.keypair.fingerprint()
# get rid of object, but not persistent data
del keyring
restored_keyring = NucypherKeyring(keyring_root=tmpdir, account=account)
restored_keyring.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
assert restored_keyring.account == account
assert restored_keyring.checksum_address == checksum_address
assert restored_keyring.certificate_filepath == certificate_filepath
assert bytes(restored_keyring.encrypting_public_key).hex() == encrypting_public_key_hex
assert bytes(restored_keyring.signing_public_key).hex() == signing_public_key_hex
# tls power
restored_tls_hosting_power = restored_keyring.derive_crypto_power(power_class=TLSHostingPower,
host=LOOPBACK_ADDRESS)
assert restored_tls_hosting_power.public_key().public_numbers() == tls_hosting_power_public_key_numbers
assert restored_tls_hosting_power.keypair.certificate.public_bytes(encoding=Encoding.PEM) == \
tls_hosting_power_certificate_public_bytes
assert restored_tls_hosting_power.keypair.certificate_filepath == tls_hosting_power_certificate_filepath
# decrypting power
restored_decrypting_power = restored_keyring.derive_crypto_power(power_class=DecryptingPower)
assert bytes(restored_decrypting_power.public_key()).hex() == decrypting_power_public_key_hex
assert restored_decrypting_power.keypair.fingerprint() == decrypting_power_fingerprint
# signing power
restored_signing_power = restored_keyring.derive_crypto_power(power_class=SigningPower)
assert bytes(restored_signing_power.public_key()).hex() == signing_power_public_key_hex
assert restored_signing_power.keypair.fingerprint() == signing_power_fingerprint
def test_keyring_destroy(tmpdir):
keyring = _generate_keyring(tmpdir)
keyring.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
keyring.destroy()
with pytest.raises(FileNotFoundError):
keyring.encrypting_public_key
def test_private_key_serialization():
key_data = _assemble_key_data(key_data=b'peanuts, get your peanuts',
master_salt=b'sea salt',
wrap_salt=b'red salt')
key_bytes = _serialize_private_key(key_data)
deserialized_key_data = _deserialize_private_key(key_bytes)
assert key_data == deserialized_key_data
def test_write_read_private_keyfile(temp_dir_path):
temp_filepath = Path(temp_dir_path) / "test_private_key_serialization_file"
key_data = _assemble_key_data(key_data=b'peanuts, get your peanuts',
master_salt=b'sea salt',
wrap_salt=b'red salt')
_write_private_keyfile(keypath=temp_filepath,
key_data=key_data,
serializer=_serialize_private_key)
deserialized_key_data_from_file = _read_keyfile(keypath=temp_filepath,
deserializer=_deserialize_private_key)
assert key_data == deserialized_key_data_from_file
def test_tls_private_key_serialization():
host = LOOPBACK_ADDRESS
checksum_address = '0xdeadbeef'
private_key, _ = _generate_tls_keys(host=host,
checksum_address=checksum_address,
curve=_TLS_CURVE)
password = b'serialize_deserialized'
key_bytes = _serialize_private_key_to_pem(private_key, password=password)
deserialized_private_key = _deserialize_private_key_from_pem(key_bytes, password=password)
assert private_key.private_numbers() == deserialized_private_key.private_numbers()
# sanity check just to be certain that a different key doesn't have the same private numbers
other_private_key, _ = _generate_tls_keys(host=host,
checksum_address=checksum_address,
curve=_TLS_CURVE)
assert other_private_key.private_numbers() != deserialized_private_key.private_numbers()
def test_tls_write_read_private_keyfile(temp_dir_path):
temp_filepath = Path(temp_dir_path) / "test_tls_private_key_serialization_file"
host = LOOPBACK_ADDRESS
checksum_address = '0xdeadbeef'
private_key, _ = _generate_tls_keys(host=host,
checksum_address=checksum_address,
curve=_TLS_CURVE)
password = b'serialize_deserialized'
tls_serializer = partial(_serialize_private_key_to_pem, password=password)
_write_private_keyfile(keypath=temp_filepath,
key_data=private_key,
serializer=tls_serializer)
tls_deserializer = partial(_deserialize_private_key_from_pem, password=password)
deserialized_private_key_from_file = _read_keyfile(keypath=temp_filepath,
deserializer=tls_deserializer)
assert private_key.private_numbers() == deserialized_private_key_from_file.private_numbers()
def _generate_keyring(root,
checksum_address=FEDERATED_ADDRESS,
password=INSECURE_DEVELOPMENT_PASSWORD,
encrypting=True,
rest=True,
host=LOOPBACK_ADDRESS):
keyring = NucypherKeyring.generate(
checksum_address=checksum_address,
password=password,
encrypting=encrypting,
rest=rest,
host=host,
keyring_root=root)
return keyring

View File

@ -19,14 +19,18 @@ import unittest
import sha3
from nucypher.crypto import api
from nucypher.crypto.utils import (
secure_random_range,
secure_random,
keccak_digest
)
class TestCrypto(unittest.TestCase):
def test_secure_random(self):
rand1 = api.secure_random(10)
rand2 = api.secure_random(10)
rand1 = secure_random(10)
rand2 = secure_random(10)
self.assertNotEqual(rand1, rand2)
self.assertEqual(bytes, type(rand1))
@ -35,13 +39,13 @@ class TestCrypto(unittest.TestCase):
self.assertEqual(10, len(rand2))
def test_secure_random_range(self):
output = [api.secure_random_range(1, 3) for _ in range(20)]
output = [secure_random_range(1, 3) for _ in range(20)]
# Test that highest output can be max-1
self.assertNotIn(3, output)
# Test that min is present
output = [api.secure_random_range(1, 2) for _ in range(20)]
output = [secure_random_range(1, 2) for _ in range(20)]
self.assertNotIn(2, output)
self.assertIn(1, output)
@ -49,7 +53,7 @@ class TestCrypto(unittest.TestCase):
data = b'this is a test'
digest1 = sha3.keccak_256(data).digest()
digest2 = api.keccak_digest(data)
digest2 = keccak_digest(data)
self.assertEqual(digest1, digest2)
@ -57,6 +61,6 @@ class TestCrypto(unittest.TestCase):
data = data.split()
digest1 = sha3.keccak_256(b''.join(data)).digest()
digest2 = api.keccak_digest(*data)
digest2 = keccak_digest(*data)
self.assertEqual(digest1, digest2)

View File

@ -14,7 +14,6 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import base64
import sha3
from constant_sorrow.constants import PUBLIC_ONLY

View File

@ -0,0 +1,277 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import os
import random
import string
from pathlib import Path
import pytest
from constant_sorrow.constants import KEYSTORE_LOCKED
from cryptography.hazmat.primitives._serialization import Encoding
from mnemonic.mnemonic import Mnemonic
from nucypher.crypto.keystore import (
Keystore,
InvalidPassword,
validate_keystore_filename,
_MNEMONIC_LANGUAGE,
_DELEGATING_INFO,
)
from nucypher.crypto.keystore import (
_assemble_keystore,
_serialize_keystore,
_deserialize_keystore,
_write_keystore,
_read_keystore
)
from nucypher.crypto.powers import DecryptingPower, SigningPower, DelegatingPower
from nucypher.crypto.umbral_adapter import (
secret_key_factory_from_seed,
secret_key_factory_from_secret_key_factory
)
from nucypher.network.server import TLSHostingPower
from nucypher.utilities.networking import LOOPBACK_ADDRESS
from tests.constants import INSECURE_DEVELOPMENT_PASSWORD
def test_invalid_keystore_path_parts(tmp_path, tmp_path_factory):
# Setup
not_hex = 'h' + ''.join(random.choice(string.ascii_letters) for _ in range(Keystore._ID_SIZE))
invalid_paths = (
'nosuffix', # missing suffix
'deadbeef.priv', # missing created epoch
f'123-{not_hex[:3]}.priv', # too short
f'123-{not_hex}.priv', # not hex
)
# Test
for invalid_path in invalid_paths:
invalid_path = Path(invalid_path)
with pytest.raises(Keystore.Invalid, match=f'{invalid_path} is not a valid keystore filename'):
validate_keystore_filename(path=invalid_path)
def test_invalid_keystore_file_type(tmp_path, tmp_path_factory):
# Not a file
invalid_path = Path()
with pytest.raises(ValueError, match="Keystore path must be a file."):
_keystore = Keystore(invalid_path)
invalid_path = Path(tmp_path)
with pytest.raises(ValueError, match="Keystore path must be a file."):
_keystore = Keystore(invalid_path)
# Not an existing file
invalid_path = Path('does-not-exist')
with pytest.raises(Keystore.NotFound, match=f"Keystore '{str(invalid_path)}' does not exist."):
_keystore = Keystore(invalid_path)
def test_keystore_instantiation_defaults(tmp_path_factory):
# Setup
parent = Path(tmp_path_factory.mktemp('test-keystore-'))
parent.touch(exist_ok=True)
keystore_id = ''.join(random.choice(string.hexdigits.lower()) for _ in range(Keystore._ID_SIZE))
path = parent / f'123-{keystore_id}.priv'
path.touch()
# Test
keystore = Keystore(path)
assert keystore.keystore_path == path # retains the correct keystore path
assert keystore.id == keystore_id # accurately parses filename for ID
assert not keystore.is_unlocked # defaults to locked
assert keystore._Keystore__secret is KEYSTORE_LOCKED
assert parent in keystore.keystore_path.parents # created in the correct directory
def test_keystore_generation_defaults(tmp_path_factory):
# Setup
parent = Path(tmp_path_factory.mktemp('test-keystore-'))
parent.touch(exist_ok=True)
# Test
keystore = Keystore.generate(INSECURE_DEVELOPMENT_PASSWORD, keystore_dir=parent)
assert not keystore.is_unlocked # defaults to locked
assert keystore._Keystore__secret is KEYSTORE_LOCKED
assert parent in keystore.keystore_path.parents # created in the correct directory
def test_keystore_invalid_password(tmpdir):
with pytest.raises(InvalidPassword):
_keystore = Keystore.generate('short', keystore_dir=tmpdir)
def test_keystore_derive_crypto_power_without_unlock(tmpdir):
keystore = Keystore.generate(INSECURE_DEVELOPMENT_PASSWORD, keystore_dir=tmpdir)
with pytest.raises(Keystore.Locked):
keystore.derive_crypto_power(power_class=DecryptingPower)
def test_keystore_serializer():
encrypted_secret, psalt, wsalt = b'peanuts! Get your peanuts!', b'sea salt', b'bath salt'
payload = _assemble_keystore(encrypted_secret=encrypted_secret, password_salt=psalt, wrapper_salt=wsalt)
serialized_payload = _serialize_keystore(payload)
deserialized_key_data = _deserialize_keystore(serialized_payload)
assert deserialized_key_data['key'] == encrypted_secret
assert deserialized_key_data['password_salt'] == psalt
assert deserialized_key_data['wrapper_salt'] == wsalt
def test_keystore_lock_unlock(tmpdir):
keystore = Keystore.generate(INSECURE_DEVELOPMENT_PASSWORD, keystore_dir=tmpdir)
# locked by default
assert not keystore.is_unlocked
assert keystore._Keystore__secret is KEYSTORE_LOCKED
# incorrect password
with pytest.raises(Keystore.AuthenticationFailed):
keystore.unlock('opensaysme')
# unlock
keystore.unlock(INSECURE_DEVELOPMENT_PASSWORD)
assert keystore.is_unlocked
assert keystore._Keystore__secret != KEYSTORE_LOCKED
assert isinstance(keystore._Keystore__secret, bytes)
# unlock when already unlocked
keystore.unlock(INSECURE_DEVELOPMENT_PASSWORD)
assert keystore.is_unlocked
# incorrect password when already unlocked
with pytest.raises(Keystore.AuthenticationFailed):
keystore.unlock('opensaysme')
# lock
keystore.lock()
assert not keystore.is_unlocked
# lock when already locked
keystore.lock()
assert not keystore.is_unlocked
def test_write_keystore_file(temp_dir_path):
temp_filepath = Path(temp_dir_path) / "test_private_key_serialization_file"
encrypted_secret, psalt, wsalt = b'peanuts! Get your peanuts!', b'sea salt', b'bath_salt'
payload = _assemble_keystore(encrypted_secret=encrypted_secret, password_salt=psalt, wrapper_salt=wsalt)
_write_keystore(path=temp_filepath, payload=payload, serializer=_serialize_keystore)
deserialized_payload_from_file = _read_keystore(path=temp_filepath, deserializer=_deserialize_keystore)
assert deserialized_payload_from_file['key'] == encrypted_secret
assert deserialized_payload_from_file['password_salt'] == psalt
assert deserialized_payload_from_file['wrapper_salt'] == wsalt
def test_decrypt_keystore(tmpdir, mocker):
# Setup
spy = mocker.spy(Mnemonic, 'generate')
# Decrypt post-generation
keystore = Keystore.generate(INSECURE_DEVELOPMENT_PASSWORD, keystore_dir=tmpdir)
keystore.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
mnemonic = Mnemonic(_MNEMONIC_LANGUAGE)
words = spy.spy_return
secret = bytes(mnemonic.to_entropy(words))
assert keystore._Keystore__secret == secret
# Decrypt from keystore file
keystore_path = keystore.keystore_path
del words
del keystore
keystore = Keystore(keystore_path=keystore_path)
keystore.unlock(INSECURE_DEVELOPMENT_PASSWORD)
assert keystore._Keystore__secret == secret
def test_keystore_persistence(tmpdir):
"""Regression test for keystore file persistence"""
keystore = Keystore.generate(INSECURE_DEVELOPMENT_PASSWORD, keystore_dir=tmpdir)
keystore.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
path = keystore.keystore_path
del keystore
assert path.exists()
def test_restore_keystore_from_mnemonic(tmpdir, mocker):
# Setup
spy = mocker.spy(Mnemonic, 'generate')
# Decrypt post-generation
keystore = Keystore.generate(INSECURE_DEVELOPMENT_PASSWORD, keystore_dir=tmpdir)
keystore.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
mnemonic = Mnemonic(_MNEMONIC_LANGUAGE)
words = spy.spy_return
secret = bytes(mnemonic.to_entropy(words))
keystore_path = keystore.keystore_path
# remove local and disk references, simulating a
# lost keystore or forgotten password.
del keystore
os.unlink(keystore_path)
# prove the keystore is lost or missing
assert not keystore_path.exists()
with pytest.raises(Keystore.NotFound):
_keystore = Keystore(keystore_path=keystore_path)
# Restore with user-supplied words and a new password
keystore = Keystore.restore(words=words, password='ANewHope')
keystore.unlock(password='ANewHope')
assert keystore._Keystore__secret == secret
def test_derive_signing_power(tmpdir):
keystore = Keystore.generate(INSECURE_DEVELOPMENT_PASSWORD, keystore_dir=tmpdir)
keystore.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
signing_power = keystore.derive_crypto_power(power_class=SigningPower)
assert bytes(signing_power.public_key()).hex()
assert signing_power.keypair.fingerprint()
def test_derive_decrypting_power(tmpdir):
keystore = Keystore.generate(INSECURE_DEVELOPMENT_PASSWORD, keystore_dir=tmpdir)
keystore.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
decrypting_power = keystore.derive_crypto_power(power_class=DecryptingPower)
assert bytes(decrypting_power.public_key()).hex()
assert decrypting_power.keypair.fingerprint()
def test_derive_delegating_power(tmpdir):
keystore = Keystore.generate(INSECURE_DEVELOPMENT_PASSWORD, keystore_dir=tmpdir)
keystore.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
delegating_power = keystore.derive_crypto_power(power_class=DelegatingPower)
parent_skf = secret_key_factory_from_seed(keystore._Keystore__secret)
child_skf = secret_key_factory_from_secret_key_factory(skf=parent_skf, label=_DELEGATING_INFO)
assert bytes(delegating_power._DelegatingPower__secret_key_factory) == bytes(child_skf)
assert delegating_power._get_privkey_from_label(label=b'some-label')
def test_derive_hosting_power(tmpdir):
keystore = Keystore.generate(INSECURE_DEVELOPMENT_PASSWORD, keystore_dir=tmpdir)
keystore.unlock(password=INSECURE_DEVELOPMENT_PASSWORD)
hosting_power = keystore.derive_crypto_power(power_class=TLSHostingPower, host=LOOPBACK_ADDRESS)
assert hosting_power.public_key().public_numbers()
assert hosting_power.keypair.certificate.public_bytes(encoding=Encoding.PEM)
rederived_hosting_power = keystore.derive_crypto_power(power_class=TLSHostingPower, host=LOOPBACK_ADDRESS)
assert hosting_power.public_key().public_numbers() == rederived_hosting_power.public_key().public_numbers()

View File

@ -14,14 +14,9 @@ GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
from bytestring_splitter import BytestringSplitter, BytestringSplittingError
from nucypher.characters.lawful import Enrico
from nucypher.crypto.api import secure_random
from nucypher.crypto.kits import UmbralMessageKit
from nucypher.crypto.signing import Signature
from nucypher.crypto.splitters import signature_splitter
def test_message_kit_serialization_via_enrico(enacted_federated_policy, federated_alice):

View File

@ -17,7 +17,6 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import os
import pytest
from bytestring_splitter import VariableLengthBytestring
from eth_utils import to_canonical_address
from nucypher.blockchain.eth.constants import ETH_HASH_BYTE_LENGTH, LENGTH_ECDSA_SIGNATURE_WITH_RECOVERY

View File

@ -14,14 +14,15 @@
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import tempfile
from typing import List
from tests.constants import MOCK_IP_ADDRESS
from nucypher.blockchain.eth.registry import BaseContractRegistry
from nucypher.characters.lawful import Ursula
from nucypher.config.characters import AliceConfiguration, BobConfiguration, UrsulaConfiguration
from nucypher.config.constants import TEMPORARY_DOMAIN
from nucypher.crypto.keystore import Keystore
from tests.constants import INSECURE_DEVELOPMENT_PASSWORD
from tests.utils.middleware import MockRestMiddleware
from tests.utils.ursula import MOCK_URSULA_STARTING_PORT

View File

@ -18,9 +18,10 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
import contextlib
import socket
from cryptography.x509 import Certificate
from typing import Iterable, List, Optional, Set
from cryptography.x509 import Certificate
from nucypher.blockchain.eth.actors import Staker
from nucypher.blockchain.eth.interfaces import BlockchainInterface
from nucypher.characters.lawful import Bob
@ -64,9 +65,7 @@ def make_federated_ursulas(ursula_config: UrsulaConfiguration,
starting_port = max(MOCK_KNOWN_URSULAS_CACHE.keys()) + 1
federated_ursulas = set()
for port in range(starting_port, starting_port+quantity):
ursula = ursula_config.produce(rest_port=port + 100,
db_filepath=MOCK_DB,
**ursula_overrides)