mirror of https://github.com/nucypher/nucypher.git
commit
527d4142c6
|
@ -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') }}"
|
||||
|
|
|
@ -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') }}"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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']}}"
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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'
|
||||
)
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Characters use mnemonic seed words to derive deterministic keystore, taking the place of the "keyring".
|
|
@ -0,0 +1 @@
|
|||
Renames enviorment variable `NUCYPHER_KEYRING_PASSWORD` to `NUCYPHER_KEYSTORE_PASSWORD`
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}'
|
||||
|
||||
|
||||
#
|
||||
|
|
|
@ -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!
|
||||
|
||||
""")
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
|
@ -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"""
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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.")
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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': [
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
"""
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue