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

Add HSMSigner #472

Merged
merged 16 commits into from
Dec 13, 2022
Merged

Add HSMSigner #472

merged 16 commits into from
Dec 13, 2022

Conversation

lukpueh
Copy link
Member

@lukpueh lukpueh commented Dec 1, 2022

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

  • If we want to support schemes that allow arbitrary token reader slots, e.g.
    hsm:YubiKey%20PIV%20%2315835999?piv_slot=9c we need to change the HSMSigner constructor, to e.g. add an hsm_label argument. This could be an optional non-API-breaking addition.
  • If we want to export public keys automatically, e.g. if no key is passed to 'from_priv_key_uri', we also need to pass the sslib keyid via uri parameter, or use generate a random/canonical keyid (the choice should maybe also be parameterizable

Notes to reviewer

  • Keeping this as draft until #442 is merged (please ignore Jussi's commits until then)
  • The first of my commits is what started this PR. Everything afterwards is new.
  • I don't recommend reviewing commit by commit. They tell an engineering story, but are not all relevant for reviewers.
  • If desired, I can spend some time on rewriting the commit history, which will likely be just squashing everything. :)
  • Ping me if you have questions.

@lukpueh
Copy link
Member Author

lukpueh commented Dec 1, 2022

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")

@lukpueh lukpueh force-pushed the hsm-signer branch 2 times, most recently from 3f84bcb to c0f56a5 Compare December 1, 2022 13:48
@lukpueh lukpueh mentioned this pull request Dec 1, 2022
3 tasks
@jku
Copy link
Collaborator

jku commented Dec 1, 2022

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):

  • locating the pkcs11 module path: If PYKCS11LIB (or some other) environment variable works, I think it would be fine to just document that
  • identifying the correct private key (aka what needs to be in private key URI for this signer). I'm really not sure what these are exactly so have no opinions... Have you found good docs on what these really are?
    • id tuple?
    • slot?
    • label?
  • key import: I think this should be a Signer feature. This way Key can be a common SSLibKey:
    • first create a Signer -- maybe no need for new functions even: if your constructor gets a public_key, use that. If public_key is None, construct it from the HSM content
    • then imported_pubkey = signer.public_key
  • pkcs11 session: this seems like it should be a HSMSigner implementation detail?
  • pin entry: SecretHandler in Signer should work for this

@lukpueh
Copy link
Member Author

lukpueh commented Dec 1, 2022

  • locating the pkcs11 module path: If PYKCS11LIB (or some other) environment variable works, I think it would be fine to just document that

Yes, I think that documenting PYKCS11LIB is good enough, plus some hints on how to locate the path.

  • identifying the correct private key (aka what needs to be in private key URI for this signer). I'm really not sure what these are exactly so have no opinions... Have you found good docs on what these really are?

    • id tuple?
    • slot?

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.

  • key import: I think this should be a Signer feature. This way Key can be a common SSLibKey:

    • first create a Signer -- maybe no need for new functions even: if your constructor gets a public_key, use that. If public_key is None, construct it from the HSM content
    • then imported_pubkey = signer.public_key

Great idea!

  • pkcs11 session: this seems like it should be a HSMSigner implementation detail?

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.

  • pin entry: SecretHandler in Signer should work for this

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?

Copy link
Member Author

@lukpueh lukpueh left a 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
Copy link
Member Author

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).

Copy link
Member Author

Choose a reason for hiding this comment

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

This still needs to be 95%, but the reason changed. The PR now also pulls in #442, which is excluded from aggregate_tests. I should be able to increase the number was that #442 is merged.

tox.ini Outdated Show resolved Hide resolved
.github/workflows/hsm.yml Outdated Show resolved Hide resolved
pyproject.toml Outdated Show resolved Hide resolved
@jku
Copy link
Collaborator

jku commented Dec 1, 2022

identifying the correct private key (aka what needs to be in private key URI for this signer). I'm really not sure what these are exactly so have no opinions... Have you found good docs on what these really are?

  • id tuple?
  • slot?
  • label?

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

  • token (but a good first implemenation would be to assume only one token is available)
  • the key within the token (by CKA_ID or CKA_LABEL?) which maybe maps to yubico slots like "9c" . Can the user see CKA_ID or CKA_LABEL anywhere? (Potential first implementation: If we can tell which CKA_ID is "9c" we could start by only supporting "9c"?)

@jku
Copy link
Collaborator

jku commented Dec 1, 2022

The HSMSigner could still receive the pin as constructor argument. Or would you just pass on the handler?

🤷 I think handler as argument to constructor makes sense?

@lukpueh
Copy link
Member Author

lukpueh commented Dec 1, 2022

identifying the correct private key (aka what needs to be in private key URI for this signer). I'm really not sure what these are exactly so have no opinions... Have you found good docs on what these really are?

  • id tuple?
  • slot?
  • label?

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?

Yeah, YubiKey docs refer to PIV certificate slots (PIV is a separate standard). In PKCS11 slots and tokens are defined as:

  • Slot -- A logical reader that potentially contains a token.
  • Token -- The logical view of a cryptographic device defined by Cryptoki.

Not sure what you mean by reboot, but I saw that slot ids can change even between pkcs11 sessions.

So we want to be able to potentially select

  • token (but a good first implemenation would be to assume only one token is available)

👍

  • the key within the token (by CKA_ID or CKA_LABEL?) which maybe maps to yubico slots like "9c" . Can the user see CKA_ID or CKA_LABEL anywhere? (Potential first implementation: If we can tell which CKA_ID is "9c" we could start by only supporting "9c"?)

In my example I just used "9c" when creating the key, because YubiKey docs suggest that it is semantically the correct slot for a signing key. I don't think our API needs to care about the semantics of PIV slots. They are not related to the hsm keyid.

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.

@jku
Copy link
Collaborator

jku commented Dec 2, 2022

When I use pkcs-tool -lO, the results depend quite a bit on the pkcs module but both opensc and yubico seem to have a Digital Signing key in ID 02. This seems to match the table at https://developers.yubico.com/yubico-piv-tool/YKCS11/:

ykcs11 id   PIV
1           9a
2           9c
3           9d
4           9e
5 - 24      82 - 95
25          f9

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"

@lukpueh
Copy link
Member Author

lukpueh commented Dec 2, 2022

Thanks for digging, @jku! Just tried pkcs11-tool with libykcs11.dylib instead of the default opensc-pkcs11.so module and now I see a lot more keys on the yubikey, and the mapping seems to match the table. 🎉

$ pkcs11-tool --module /usr/local/lib/libykcs11.dylib -O
[...]
Public Key Object; EC  EC_POINT 256 bits
  label:      Public key for Digital Signature
  ID:         02
  [...]
Public Key Object; EC  EC_POINT 256 bits
  label:      Public key for Key Management
  ID:         03
  [...]

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.

@jku jku mentioned this pull request Dec 2, 2022
@jku
Copy link
Collaborator

jku commented Dec 2, 2022

So then the version 1 implementation could

  • assume first token is the one we want
  • assume key id is 02
  • document that this has been tested with libykcs and PIV slot "9c"

then the first version would need no path or query params for the private key URI: pkcs11: or hsm: would be enough. Then later we could add optional paths/params if needed: hsm:YubiKey%20PIV%20%2315835999?piv_slot=9c (here the path is the url encoded token label and piv_slot is a query param)

@lukpueh
Copy link
Member Author

lukpueh commented Dec 5, 2022

Just rebased on top of #445 to pull in 16734bb (for sub-modules) and added a bunch of WIP-commits based on our latest discussion. Will ping for re-review when ready.

@lukpueh lukpueh force-pushed the hsm-signer branch 4 times, most recently from 2bbe44e to ac677aa Compare December 6, 2022 15:02
@lukpueh lukpueh changed the title Add HSMSigner and HSMKey Add HSMSigner Dec 7, 2022
@lukpueh
Copy link
Member Author

lukpueh commented Dec 7, 2022

Okay, this is ready for re-review. See updated PR description for what changed.

@lukpueh lukpueh requested a review from jku December 7, 2022 14:48
Copy link
Collaborator

@jku jku left a 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

Comment on lines 263 to 264
pin = secrets_handler(cls.SECRETS_HANDLER_MSG)
return cls(cls.SCHEME_KEYID, public_key, pin)
Copy link
Collaborator

@jku jku Dec 8, 2022

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed in 8283650

Comment on lines +188 to +191
@classmethod
def pubkey_from_hsm(
cls, sslib_keyid: str, hsm_keyid: Optional[int] = None
) -> SSlibKey:
Copy link
Collaborator

@jku jku Dec 8, 2022

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?

Copy link
Collaborator

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

Copy link
Member Author

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?

Copy link
Collaborator

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

securesystemslib/signer/_hsm_signer.py Outdated Show resolved Hide resolved
from securesystemslib.signer._signer import SecretsHandler, Signer

# pylint: disable=wrong-import-position
CRYPTO_IMPORT_ERROR = None
Copy link
Collaborator

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)?

Copy link
Member Author

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.

securesystemslib/signer/_hsm_signer.py Show resolved Hide resolved
securesystemslib/signer/_hsm_signer.py Outdated Show resolved Hide resolved
Comment on lines +50 to +51
asn1 = ["asn1crypto"]
pykcs11 = ["PyKCS11"]
Copy link
Collaborator

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?

Copy link
Member Author

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.

Copy link
Member Author

@lukpueh lukpueh Dec 12, 2022

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

Copy link
Member Author

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>
@lukpueh lukpueh marked this pull request as ready for review December 12, 2022 08:57
@lukpueh
Copy link
Member Author

lukpueh commented Dec 12, 2022

Just force-pushed a pure rebase without changes and marked as non-draft.

lukpueh and others added 2 commits December 12, 2022 11:26
- 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>
@lukpueh
Copy link
Member Author

lukpueh commented Dec 12, 2022

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.

@jku
Copy link
Collaborator

jku commented Dec 12, 2022

I'm happy with this, LGTM.

@lukpueh lukpueh merged commit 438c273 into secure-systems-lab:master Dec 13, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants