-
Notifications
You must be signed in to change notification settings - Fork 53
Add HSMSigner #472
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
Add HSMSigner #472
Conversation
Usage example (tried on YubiKey 5 Nano)# Create and sign key
yubico-piv-tool -a generate -s 9c -A ECCP256 -k -o public.pem
yubico-piv-tool -a verify-pin -a selfsign -s 9c -i public.pem -S /CN=a.b.c/OU=abc/O=b.c/ -o cert.pem
yubico-piv-tool -k -a import-certificate -s 9c -i cert.pem from PyKCS11 import PyKCS11
from securesystemslib.signer import HSMKey, HSMSigner
lib = "<PATH/TO/PKCS11/LIBRARY>"
# e.g. "/usr/local/Cellar/yubico-piv-tool/2.3.0/lib/libykcs11.dylib" (via `brew`)
hsm_keyid = "<HSM KEY IDTUPLE>"
# e.g. `(2,)`; see `pkcs15-tool --list-key`
pin = "<YOUR USER PIN>"
sslib_keyid = "0"*64
pkcs11 = PyKCS11.PyKCS11Lib()
pkcs11.load(lib)
slot = pkcs11.getSlotList(tokenPresent=True)[0]
session = pkcs11.openSession(slot)
pubkey = HSMKey.from_hsm(session, hsm_keyid, sslib_keyid)
session.login(pin)
signer = HSMSigner(session, hsm_keyid, pubkey)
sig = signer.sign(b"DATA")
session.logout()
session.closeSession()
pubkey.verify_signature(sig, b"DATA") |
3f84bcb
to
c0f56a5
Compare
Very cool So I suppose the issues to solve are following (and they don't all need to be solved here: we can merge new signers without issues now since they don't really affect the existing one):
|
Yes, I think that documenting
Haven't found any good docs, but I think id should be fine for the key. For the slot/token, I am not sure. The slot ID can change between sessions. So maybe label would work.
Great idea!
Agreed, this is just a crutch until I have figured out a good way to identify the token. That's also why the PR is still a draft.
Yes, but as an argument to `from_priv_key_uri akin to SSlibSigner, right? The HSMSigner could still receive the pin as constructor argument. Or would you just pass on the handler? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some comments about testing, CI and dependencies...
@@ -18,7 +18,7 @@ deps = | |||
commands = | |||
python -m tests.check_gpg_available | |||
coverage run tests/aggregate_tests.py | |||
coverage report -m --fail-under 97 | |||
coverage report -m --fail-under 95 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had to lower this because aggregate_tests
does not include tests for new HSM code, thus the coverage of this run drops, even though that code is tested (see [testenv:hsm]
below).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So first confusion source is that pkcs11 slot seems to be completely different from what yubikey docs call a slot??? I suppose PKCS11 slot is something that can e.g. change over reboots etc? So we want to be able to potentially select
|
🤷 I think handler as argument to constructor makes sense? |
Yeah, YubiKey docs refer to PIV certificate slots (PIV is a separate standard). In PKCS11 slots and tokens are defined as:
Not sure what you mean by reboot, but I saw that slot ids can change even between pkcs11 sessions.
👍
In my example I just used The hsm keyid can be determined e.g. with $ pkcs11-tool -lO
Using slot 0 with a present token (0x0)
Logging in to "host.example.com".
Please enter User PIN:
Private Key Object; EC
[...]
ID: 03
[...]
Public Key Object; EC EC_POINT 256 bits
[...]
ID: 03
[...] It's the same for public and private key and remains stable. |
When I use
I was hoping this would be true, that the key ids would be predictable... Are you seeing the signing key at id 3 or was that just an example of the output? Does the ID depend on the module you use? I was really hoping we could avoid handwave documentation like "now find out which CKA_ID your key ended up in and insert that in the configuration file" |
Thanks for digging, @jku! Just tried
I was already wondering, why I can pass IDs 2 and 3, to sign, when, according to pkcs11-tool, I only have one signing key at ID 3. |
So then the version 1 implementation could
then the first version would need no path or query params for the private key URI: |
2bbe44e
to
ac677aa
Compare
Okay, this is ready for re-review. See updated PR description for what changed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to merge to me. Left comments but I don't think any of them are blockers -- although the optional-dependencies discussion might be good to have before merge
pin = secrets_handler(cls.SECRETS_HANDLER_MSG) | ||
return cls(cls.SCHEME_KEYID, public_key, pin) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think calling secrets_handler() from the constructor might make more sense but this is more like a nit since the constructor is "less public api" than this method is...
The idea behind SecretsHandler is that "pin" or other secrets don't have to be part of the API: that at least theoretically the code can dynamically decide if it needs a pin or not (variable authentication methods are definitely a feature in systems like this, although I don't know if it makes sense with yubikeys), and when it needs it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 8283650
@classmethod | ||
def pubkey_from_hsm( | ||
cls, sslib_keyid: str, hsm_keyid: Optional[int] = None | ||
) -> SSlibKey: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a different idea than what I had planned for GCPSigner but I think that's fine for now... I don't think we are sure yet what really works.
My design was
signer = GCPSigner(uri) # when public_key arg is not given, import it
pubkey = signer.public_key
with the logic that the constructor is implementation specific already, but we could make the public_key accessor a part of Signer API: that way implementation specific API does not grow (and import errors are only handled once).
Originally I also thought I would always need a Signer and Key at same time anyway, but I think this is not necessarily true. This would support your classmethod design: if I sometimes only need a Key, why do I need to construct a Signer first?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If signer construction was required to import a Key, then signer construction should not trigger a pin request meaning this would mean a bit of refactoring: I'm not asking for that, just thinking out loud
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with you. Let's fix this with #466. Until then, do you think it's better to merge as non-public _pubkey_from_hsm
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's fine as is: if we fix before next release it's a non-issue; if we fix in some later release it's a minor API break
from securesystemslib.signer._signer import SecretsHandler, Signer | ||
|
||
# pylint: disable=wrong-import-position | ||
CRYPTO_IMPORT_ERROR = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
all three of the imports get their own errors, own try-except blocks and own error handling. Since only one error is ever reported, this duplication isn't really that useful.
How about just a single HSM_IMPORT_ERROR to simplify the error handling (the separate try-except blocks might still be nice for readability)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the separation makes sense wrt current extra dependency handling. Let's fix those together.
asn1 = ["asn1crypto"] | ||
pykcs11 = ["PyKCS11"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
feels like this isn't the correct way to use this system. pykcs11 without asn1 doesn't make sense so why offer that as option?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
asn1crypto
is only an immediate dependency for pubkey export. For signing we don't use it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But I agree, telling users to run
pip install securesystemslib[crypto, asn1, pykcs11]
is not a lot better, than
pip install cryptography asn1crypto pykcs11 securesystemslib
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Listing extra dependencies by feature, otoh, seems prone to redundancy. cryptography
, for instance, is used for many different features.
Adds basic Signer and Key implementations to generate signatures on a hardware security module (HSMSigner) and to export the corresponding public key (HSMKey). Supported keys are ecdsa on SECG curves secp256r1 (NIST P-256) or secp384r1 (NIST P-384), which correspond to securesystemslib signing schemes "ecdsa-sha2-nistp256" and "ecdsa-sha2-nistp384". Tests are performed on SoftHSM (virtual hsm). **Caveat** HSMSigner and HSMKey use the token from a passed PyKCS11 session. This means that users must identify the correct slot, token and key, open a session, optionally log in (for signing), and also log out and close the session afterwards. This is not user-friendly. Ideally, the user only identifies the correct slot, token and key out-of-band (e.g. with pkcs11-tool, yubico-piv-tool or ykman) and then passes a stable identifier to HSMSigner. Maybe labels? Slot id is not stable. **Other ideas** - HSMKey is an SSlibKey with an *import key from HSM* method. The method could be moved to different import API (see secure-systems-lab#466), and HSMKey could be removed. - HSMSigner could live in a dedicated _hsm_signer.py, this would better hide the conditional imports. Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
HSMKey is only useful for its from_hsm method. This method is temporarily moved to HSM test module, as verbatim copy with some error handling commented out, and will likely be moved to the HSMSigner in a subsequent commit. Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
This allows to hide all the import case-handling for optional dependencies from the _signer module. Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
PyKCS11 for expects a tuple. For UX sake we take an int and create the tuple before passing it to PyKCS11. Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
Asking callers to handle the PyKCS11 session was a crutch to punt the not quite straight-forward decision of how to identify the correct token reader, but is no nice UX. This commit moves session handling to the signer and just uses the first reader slot that has a token. This commit, move the session to the signer also requires loading the pkcs11 library in the hsm module, and to take a login pin (as secrets handler). Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
Running HSM tests in a separate workflow on all Python version and os combinations requires 15 additional runners. This is a bit of an overkill. This patch integrates the HSMTests with regular CI: - Make hsm test module discoverable for aggregate_tests by changing the module name. - Skip HSM tests if PYKCS11LIB is not set, so aggregate_tests can be executed without SoftHSM installed. (e.g. locally by devs) - Remove dedicated HSM test env from tox.ini. Tests now run as part of default testenv. - Require PYKCS11LIB to be set for that testenv. - Remove dedicated HSM requirements files and add them to default requirement file, which already includes optional requirements. - Re-compile pinned requirements file. Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
Add ugly helper function to not load pkcs11 library more than once, while allowing to load it explicitly. This important for tests, where we need to setup a softhsm config file before we load the library, and need to load the library before we use the Signer. **Alternative approachs**: Load and unload for each use, just like we create and close session for each use: - unload is implemented on `PyKCS11Lib.__del__` - performance? Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
Implement basic hsm scheme in from_priv_key_uri, which cannot be parametrized. It just uses the first token it finds and the key on piv slot 9c. Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
Move pub key export, previously on the no longer existing HSMKey class, and then temporarily a test method, to HSMSigner. This should not be testing code but public API. Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
Listing multiple dependencies per feature (e.g. hsm) is inconsistent with other extras and also prone to redundancy. Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
- define constants on module level not in function bodies - add context manager for more ergonomic and safer session handling - move some hsm api calls to helpers - make hsm keyid optional in pubkey export function - minor renames and refactors - polish docstrings Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
When storing the secrets handler function as instance variable, invocation will automatically pass self, but the secrets handler only takes one argument. There might be a workaround, but the easier solution is to just call the secrets handler in from_priv_key_uri and pass the pin to the constructor. Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
- separate direct and from_priv_ke_uri -signer instantiation - move key generation to helper - add sign wrapper, which can patch mechanism and pre-hash data, to accommodate SoftHSM lack of capabilities Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
Just force-pushed a pure rebase without changes and marked as non-draft. |
- show only HSMSigner.from_priv_key_uri instantiation example (calling HSMSigner constructor directly should not be encouraged) - disambiguate PIV slot id vs. PKCS11 key id Co-authored-by: Jussi Kukkonen <jku@goto.fi> Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
Store the secrets handler as instance variable and call when needed to not make the pin part of the API. This reverts 5cb6a69, which was a fix to a no longer reproducible issue. Signed-off-by: Lukas Puehringer <lukas.puehringer@nyu.edu>
Thanks for the review, @jku! I addressed your comments wrt docs and secrets_handler. Would you mind if we take a look at extra dependency definition (and related error handling) in a follow-up? It's not obvious to me, what the best solution is. |
I'm happy with this, LGTM. |
based on #229
updated PR title and description on 12/7/2022
un-draft on 12/12/2022
--
Description
Adds basic HSMSigner implementation to generate signatures on a hardware security module and to export the corresponding public key (as SSlibKey).
HSMSigner uses the first token it finds, if multiple tokens are available, and can be instantiated directly or with
Signer.from_priv_key_uri("hsm:", public_key, secret_handler)
. The former can be passed a keyid, the latter uses the key on the digital signature default piv slot 9c.Supported keys are ecdsa on SECG curves secp256r1 (NIST P-256) or secp384r1 (NIST P-384), which correspond to securesystemslib signing schemes "ecdsa-sha2-nistp256" and "ecdsa-sha2-nistp384".
Tests are performed on SoftHSM (virtual hsm).
Future work
hsm:YubiKey%20PIV%20%2315835999?piv_slot=9c
we need to change the HSMSigner constructor, to e.g. add anhsm_label
argument. This could be an optional non-API-breaking addition.Notes to reviewer
Keeping this as draft until #442 is merged (please ignore Jussi's commits until then)