Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Use multiple yubikeys at same time #503

Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
dfb673f
feat: use multiple yubikeys at same time
ronantakizawa Aug 13, 2024
7293210
feat: build keys and repos with multiple yubikeys
ronantakizawa Aug 14, 2024
956311f
fix: fix security issues
ronantakizawa Aug 14, 2024
e5773f6
fix: updated yubikey-manager to 5.5*
ronantakizawa Aug 14, 2024
40e91d7
fix: changed ci.yml to run smart card reader for linux
ronantakizawa Aug 14, 2024
1670ce8
fix: check dependencies in ci.yml for yubikeys
ronantakizawa Aug 14, 2024
7622adf
fix: fix ci.yml issues
ronantakizawa Aug 14, 2024
1d4d318
fix: fix ci.yml 2
ronantakizawa Aug 14, 2024
ca0501c
fix: fix ci.yml 3
ronantakizawa Aug 14, 2024
15aa12c
fix: updated test yubikey functions
ronantakizawa Aug 15, 2024
00c1194
fix: fix linting issues
ronantakizawa Aug 15, 2024
f68ec8f
fix: fix ci.yml issues
ronantakizawa Aug 15, 2024
24738ca
fix: remove unused functions
ronantakizawa Aug 15, 2024
0c990d3
fix: fix repository_tool tests
ronantakizawa Aug 15, 2024
d84c53d
fix: fix repository_tool tests
ronantakizawa Aug 15, 2024
84c8b56
fix: fix test issues
ronantakizawa Aug 15, 2024
dde9b7b
fix: fixed is_valid_metadata_yubikey
ronantakizawa Aug 15, 2024
f283f7b
fix: fixed check_yubikey_count()
ronantakizawa Aug 15, 2024
d355f96
fix: try using original ci.yml
ronantakizawa Aug 21, 2024
7ed01c1
fix: add try catch to setup_signing_yubikey
ronantakizawa Aug 21, 2024
ad6bbfd
Merge branch 'master' into ronantakizawa/1+yubikeys
ronantakizawa Aug 21, 2024
fc7b990
fix: fix merging issues
ronantakizawa Aug 21, 2024
ed16878
feat: automate key search
ronantakizawa Aug 22, 2024
738d6af
fix: fixed some tests
ronantakizawa Aug 22, 2024
a956c1b
resolved tests
ronantakizawa Aug 22, 2024
93d6129
fix: fix linting issues
ronantakizawa Aug 22, 2024
d61cfae
fix: fix linux test errors
ronantakizawa Aug 22, 2024
35284ad
feat: sign repos without needing keys-description
ronantakizawa Aug 23, 2024
c9dbac7
Merge branch 'master' into ronantakizawa/1+yubikeys
ronantakizawa Aug 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,35 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install libhdf5-serial-dev zlib1g-dev libatlas-base-dev lcov swig3.0 libpcsclite-dev
sudo apt-get install -y pcscd git python-setuptools swig gcc libpcsclite-dev python-dev-is-python3
mkdir -p ~/bin/ && ln -s /usr/bin/swig3.0 ~/bin/swig && export PATH=~/bin/:$PATH
pip install wheel # Ensure wheel is installed
pip install -e .[ci,test,yubikey]

- name: Blacklist conflicting drivers
run: |
echo "install nfc /bin/false" | sudo tee -a /etc/modprobe.d/blacklist.conf
echo "install pn533 /bin/false" | sudo tee -a /etc/modprobe.d/blacklist.conf

- name: Build and install pyscard from source
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember having to build pyscard from source a while back, but at some point, it was no longer necessary. If this is a temporary workaround, please create an issue and provide some details, so that we don't forget about it.

run: |
cd ~
git clone https://github.com/LudovicRousseau/pyscard.git
cd pyscard
sudo python setup.py build_ext install

- name: Restart pcscd service
run: |
sudo service pcscd restart

- name: Fix library paths for pcsc-lite
run: |
sudo ln -sf /usr/lib/libpcsclite.so.1.0.0 /lib/libpcsclite.so.1.0.0

- name: Start pcscd service (manual method)
run: |
sudo pcscd -f -d &

- name: Setup GitHub user
run: |
git config --global user.name oll-bot
Expand All @@ -59,7 +83,7 @@ jobs:
- name: Run pre-commit and test with pytest
run: |
pre-commit run --all-files
pytest taf/tests
pytest taf/tests
ronantakizawa marked this conversation as resolved.
Show resolved Hide resolved

build_and_upload_wheel:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -104,7 +128,6 @@ jobs:
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}


build-and-test-executables:
needs: [set_python_versions, run_tests]
if: github.event_name == 'release'
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning][semver].

### Added

- Use multiple yubikeys at same time (Key generation, repo generation, target signing) ([503])
- Allow for the displaying of varied levels of log and debug information based on the verbosity level ([493])
- Added new tests to test out of sync repositories and manual updates [488]
- Added lazy loading to CLI [481]
Expand Down Expand Up @@ -37,6 +38,7 @@ and this project adheres to [Semantic Versioning][semver].

### Fixed

[503]: https://github.com/openlawlibrary/taf/pull/503
[493]: https://github.com/openlawlibrary/taf/pull/493
[489]: https://github.com/openlawlibrary/taf/pull/489
[488]: https://github.com/openlawlibrary/taf/pull/488
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"jinja2==3.1.*",
]

yubikey_require = ["yubikey-manager==5.1.*"]
yubikey_require = ["yubikey-manager==5.5.*"]

# Determine the appropriate version of pygit2 based on the Python version
if sys.version_info >= (3, 11):
Expand Down
43 changes: 27 additions & 16 deletions taf/api/yubikey.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,16 @@
on_exceptions=TAFError,
reraise=True,
)
def export_yk_public_pem(path: Optional[str] = None) -> None:
def export_yk_public_pem(
path: Optional[str] = None, serial: Optional[int] = None
) -> None:
"""
Export public key from a YubiKey and save it to a file or print to console.

Arguments:
path (optional): Path to a file to which the public key should be written.
The key is printed to console if file path is not provided.
serial (optional): The serial number of the YubiKey to use.
ronantakizawa marked this conversation as resolved.
Show resolved Hide resolved

Side Effects:
Write public key to a file if path is specified
Expand All @@ -38,7 +41,7 @@ def export_yk_public_pem(path: Optional[str] = None) -> None:
None
"""
try:
pub_key_pem = yk.export_piv_pub_key().decode("utf-8")
pub_key_pem = yk.export_piv_pub_key(serial=serial).decode("utf-8")
except Exception:
print("Could not export the public key. Check if a YubiKey is inserted")
return
Expand All @@ -62,25 +65,28 @@ def export_yk_public_pem(path: Optional[str] = None) -> None:
on_exceptions=TAFError,
reraise=True,
)
def export_yk_certificate(path: Optional[str] = None) -> None:
def export_yk_certificate(
path: Optional[str] = None, serial: Optional[int] = None
) -> None:
"""
Export certificate from the YubiKey.

Arguments:
path (optional): Path to a file to which the certificate key should be written.
Will be written to the user's home directory by default
Will be written to the user's home directory by default.
serial (optional): The serial number of the YubiKey to use.

Side Effects:
Write certificate to a file
Write certificate to a file.

Returns:
None
"""
try:
pub_key_pem = yk.export_piv_pub_key().decode("utf-8")
pub_key_pem = yk.export_piv_pub_key(serial=serial).decode("utf-8")
scheme = DEFAULT_RSA_SIGNATURE_SCHEME
key = import_rsakey_from_pem(pub_key_pem, scheme)
yk.export_yk_certificate(path, key)
yk.export_yk_certificate(path, key, serial=serial)
except Exception:
print("Could not export certificate. Check if a YubiKey is inserted")
return
Expand All @@ -94,21 +100,23 @@ def export_yk_certificate(path: Optional[str] = None) -> None:
on_exceptions=TAFError,
reraise=True,
)
def get_yk_roles(path: str) -> Dict:
def get_yk_roles(path: str, serial: Optional[int] = None) -> Dict:
"""
List all roles that the inserted YubiKey whose metadata files can be signed by this YubiKey.
In case of delegated targets roles, include the delegation paths.

Arguments:
path: Authentication repository's path.
serial (optional): The serial number of the YubiKey to use.

Side Effects:
None

Returns:
A dictionary containing roles and delegated paths in case of delegated target roles
A dictionary containing roles and delegated paths in case of delegated target roles.
"""
auth = AuthenticationRepository(path=path)
pub_key = yk.get_piv_public_key_tuf()
pub_key = yk.get_piv_public_key_tuf(serial=serial)
return get_roles_and_paths_of_key(pub_key, auth)


Expand All @@ -121,13 +129,16 @@ def get_yk_roles(path: str) -> Dict:
on_exceptions=TAFError,
reraise=True,
)
def setup_signing_yubikey(certs_dir: Optional[str] = None) -> None:
def setup_signing_yubikey(
certs_dir: Optional[str] = None, serial: Optional[int] = None
) -> None:
"""
Delete everything from the inserted YubiKey, generate a new key and copy it to the YubiKey.
Optionally export and save the certificate to a file.

Arguments:
certs_dir (optional): Path to a directory where the exported certificate should be stored.
serial (optional): The serial number of the YubiKey to use.

Side Effects:
None
Expand All @@ -147,7 +158,7 @@ def setup_signing_yubikey(certs_dir: Optional[str] = None) -> None:
prompt_message="Please insert the new Yubikey and press ENTER",
)
key = yk.setup_new_yubikey(serial_num)
yk.export_yk_certificate(certs_dir, key)
yk.export_yk_certificate(certs_dir, key, serial=serial)
ronantakizawa marked this conversation as resolved.
Show resolved Hide resolved


@log_on_start(DEBUG, "Setting up a new test YubiKey", logger=taf_logger)
Expand All @@ -159,13 +170,13 @@ def setup_signing_yubikey(certs_dir: Optional[str] = None) -> None:
on_exceptions=TAFError,
reraise=True,
)
def setup_test_yubikey(key_path: str) -> None:
def setup_test_yubikey(key_path: str, serial: Optional[int] = None) -> None:
"""
Reset the inserted yubikey, set default pin and copy the specified key
to it.
Reset the inserted YubiKey, set default pin, and copy the specified key to it.

Arguments:
key_path: Path to a key which should be copied to a YubiKey.
serial (optional): The serial number of the YubiKey to use.

Side Effects:
None
Expand All @@ -181,7 +192,7 @@ def setup_test_yubikey(key_path: str) -> None:
print(f"Importing RSA private key from {key_path} to Yubikey...")
pin = yk.DEFAULT_PIN

pub_key = yk.setup(pin, "Test Yubikey", private_key_pem=key_pem)
pub_key = yk.setup(pin, "Test Yubikey", private_key_pem=key_pem, serial=serial)
print("\nPrivate key successfully imported.\n")
print("\nPublic key (PEM): \n{}".format(pub_key.decode("utf-8")))
print("Pin: {}\n".format(pin))
30 changes: 19 additions & 11 deletions taf/repository_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,27 +146,35 @@ def yubikey_signature_provider(name, key_id, key, data): # pylint: disable=W061

def _check_key_and_get_pin(expected_key_id):
try:
inserted_key = yk.get_piv_public_key_tuf()
if expected_key_id != inserted_key["keyid"]:
return None
serial_num = yk.get_serial_num(inserted_key)
pin = yk.get_key_pin(serial_num)
yubikey_count = yk.check_yubikey_count()
if yubikey_count < 2:
inserted_key = yk.get_piv_public_key_tuf()
if expected_key_id != inserted_key["keyid"]:
return None, None
serial = yk.get_serial_num(inserted_key)
else:
print(f"Final confirmation for {name} key")
serial = yk.verify_yubikey_serial()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am currently unable to test this since I am not able to insert two yubikeys at the same time, but based on the code, it looks like you have to manually enter each of the yubikey's serial number. This is probably less user-friendly compared to having to remove and insert the devices. A bigger issue, however, is that we want to be able to sign metadata using YubiKeys plugged into our server. So, this should iterate over all inserted YubiKeys and simply try to sign using all of them, instead of asking for any input. So, let's say that you have timestamp and snapshot keys inserted and you need to sign timestamp metadata. We need to automatically detect the key that can sign the timestamp metadata. At the moment, the user will have to enter pins too, but we don't need to address that in this PR.

inserted_key = yk.get_piv_public_key_tuf(serial=serial)
if expected_key_id != inserted_key["keyid"]:
return None, None
pin = yk.get_key_pin(serial)
if pin is None:
pin = yk.get_and_validate_pin(name)
return pin
pin = yk.get_and_validate_pin(name, serial=serial)
return pin, serial
except Exception:
return None
return None, None

while True:
# check if the needed YubiKey is inserted before asking the user to do so
# this allows us to use this signature provider inside an automated process
# assuming that all YubiKeys needed for signing are inserted
pin = _check_key_and_get_pin(key_id)
if pin is not None:
pin, serial = _check_key_and_get_pin(key_id)
if pin is not None and serial is not None:
break
input(f"\nInsert {name} and press enter")

signature = yk.sign_piv_rsa_pkcs1v15(data, pin)
signature = yk.sign_piv_rsa_pkcs1v15(data, pin, serial=serial)
return {"keyid": key_id, "sig": hexlify(signature).decode()}


Expand Down
47 changes: 31 additions & 16 deletions taf/tools/yubikey/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@
from taf.api.yubikey import export_yk_certificate, export_yk_public_pem, get_yk_roles, setup_signing_yubikey, setup_test_yubikey
from taf.exceptions import YubikeyError
from taf.repository_utils import find_valid_repository
from taf.yubikey import list_connected_yubikeys
from taf.tools.cli import catch_cli_exception


def check_pin_command():
@click.command(help="Checks if the specified pin is valid")
@click.argument("pin")
def check_pin(pin):
@click.option("--serial", type=int, help="YubiKey serial number to use")
def check_pin(pin, serial):
try:
from taf.yubikey import is_valid_pin
valid, retries = is_valid_pin(pin)
valid, retries = is_valid_pin(pin, serial)
inserted = True
except YubikeyError:
valid = False
Expand All @@ -27,20 +29,22 @@ def check_pin(pin):


def export_pub_key_command():
@click.command(help="Export the inserted Yubikey's public key and save it to the specified location.")
@click.command(help="Export the inserted YubiKey's public key and save it to the specified location.")
@click.option("--output", help="File to which the exported public key will be written. The result will be written to the console if path is not specified")
def export_pub_key(output):
export_yk_public_pem(output)
@click.option("--serial", type=int, help="YubiKey serial number to use")
def export_pub_key(output, serial):
export_yk_public_pem(output, serial)
return export_pub_key


def get_roles_command():
@click.command(help="Export the inserted Yubikey's public key and save it to the specified location.")
@click.command(help="Export the inserted YubiKey's public key and save it to the specified location.")
@catch_cli_exception(handle=YubikeyError, print_error=True)
@click.option("--path", default=".", help="Authentication repository's location. If not specified, set to the current directory")
def get_roles(path):
@click.option("--serial", type=int, help="YubiKey serial number to use")
def get_roles(path, serial):
path = find_valid_repository(path)
roles_with_paths = get_yk_roles(path)
roles_with_paths = get_yk_roles(path, serial)
for role, paths in roles_with_paths.items():
print(f"\n{role}")
for path in paths:
Expand All @@ -49,32 +53,42 @@ def get_roles(path):


def export_certificate_command():
@click.command(help="Export the inserted Yubikey's public key and save it to the specified location.")
@click.command(help="Export the inserted YubiKey's public key and save it to the specified location.")
@click.option("--output", help="File to which the exported certificate key will be written. The result will be written to the user's home directory by default")
def export_certificate(output):
export_yk_certificate(output)
@click.option("--serial", type=int, help="YubiKey serial number to use")
def export_certificate(output, serial):
export_yk_certificate(output, serial)
return export_certificate


def setup_signing_key_command():
@click.command(help="""Generate a new key on the yubikey and set the pin. Export the generated certificate
@click.command(help="""Generate a new key on the YubiKey and set the pin. Export the generated certificate
to the specified directory.
WARNING - this will delete everything from the inserted key.""")
@click.option("--certs-dir", help="Path of the directory where the exported certificate will be saved. Set to the user home directory by default")
def setup_signing_key(certs_dir):
setup_signing_yubikey(certs_dir)
@click.option("--serial", type=int, help="YubiKey serial number to use")
def setup_signing_key(certs_dir, serial):
setup_signing_yubikey(certs_dir, serial)
return setup_signing_key


def setup_test_key_command():
@click.command(help="""Copies the specified key onto the inserted YubiKey
WARNING - this will reset the inserted key.""")
@click.argument("key-path")
def setup_test_key(key_path):
setup_test_yubikey(key_path)
@click.option("--serial", type=int, help="YubiKey serial number to use")
def setup_test_key(key_path, serial):
setup_test_yubikey(key_path, serial) # Only pass key_path and serial
return setup_test_key


def list_keys_command():
@click.command(help="List All Connected Keys and their information")
def list_keys():
list_connected_yubikeys()
return list_keys


def attach_to_group(group):

group.add_command(check_pin_command(), name='check-pin')
Expand All @@ -83,3 +97,4 @@ def attach_to_group(group):
group.add_command(export_certificate_command(), name='export-certificate')
group.add_command(setup_signing_key_command(), name='setup-signing-key')
group.add_command(setup_test_key_command(), name='setup-test-key')
group.add_command(list_keys_command(), name='list-keys')
Loading