Skip to content

[Bug]: step-ca does not verify that yubikey-stored SSH CA public / private keys match #2521

@kaysond

Description

@kaysond

Steps to Reproduce

Run step ca init --ssh

# step ca init --ssh
✔ Deployment Type: Standalone
What would you like to name your new PKI?
✔ (e.g. Smallstep): test
What DNS names or IP addresses will clients use to reach your CA?
✔ (e.g. ca.example.com[,10.1.2.3,etc.]): test.test.com
What IP and port will your new CA bind to? (:443 will bind to 0.0.0.0:443)
✔ (e.g. :443 or 127.0.0.1:443): :443
What would you like to name the CA's first provisioner?
✔ (e.g. [email protected]): test
Choose a password for your CA keys and first provisioner.
✔ [leave empty and we'll generate one]:
✔ Password: RmLf5ZnCTpCNN0e8sdyoavgjxphiiJSD█

Generating root certificate... done!
Generating intermediate certificate... done!
Generating user and host SSH certificate signing keys... done!

✔ Root certificate: /home/step/certs/root_ca.crt
✔ Root private key: /home/step/secrets/root_ca_key
✔ Root fingerprint: 2efe7c3e048dc2c09ff2ae6af1c278d885073dbc3ba9fb22641899bdf3f206c1
✔ Intermediate certificate: /home/step/certs/intermediate_ca.crt
✔ Intermediate private key: /home/step/secrets/intermediate_ca_key
✔ SSH user public key: /home/step/certs/ssh_user_ca_key.pub
✔ SSH user private key: /home/step/secrets/ssh_user_ca_key
✔ SSH host public key: /home/step/certs/ssh_host_ca_key.pub
✔ SSH host private key: /home/step/secrets/ssh_host_ca_key
✔ Database folder: /home/step/db
✔ Templates folder: /home/step/templates
✔ Default configuration: /home/step/config/defaults.json
✔ Certificate Authority configuration: /home/step/config/ca.json

Your PKI is ready to go. To generate certificates for individual services see 'step help ca'.

FEEDBACK 😍 🍻
  The step utility is not instrumented for usage statistics. It does not phone
  home. But your feedback is extremely valuable. Any information you can provide
  regarding how you’re using `step` helps. Please send us a sentence or two,
  good or bad at [email protected] or join GitHub Discussions
  https://github.com/smallstep/certificates/discussions and our Discord
  https://u.step.sm/discord.

Import intermediate CA on the yubikey:

# ykman piv keys import --touch-policy never --pin-policy always 9c secrets/intermediate_ca_key
Enter password to decrypt key:
Enter a management key [blank to use default key]:
Private key imported into slot SIGNATURE.
# ykman piv certificates import 9c certs/intermediate_ca.crt
Enter a management key [blank to use default key]:
Certificate imported into slot SIGNATURE

Import the SSH host CA on the yubikey:

# ykman piv keys import --touch-policy never --pin-policy always 9d secrets/ssh_host_ca_key
Enter password to decrypt key:
Enter a management key [blank to use default key]:
Private key imported into slot KEY_MANAGEMENT.
# ssh-keygen -f certs/ssh_host_ca_key.pub -e -m pem > certs/ssh_host_ca_key.pem
# ykman piv certificates generate --hash-algorithm SHA512 --valid-days 3650 --subject "My SSH Host CA" 9d certs/ssh_host_ca_key.pem
Enter a management key [blank to use default key]:
Enter PIN:
Certificate generated in slot KEY_MANAGEMENT.

Note that you need to import a certificate in the same slot because the PIV/PKCS#11 library extracts the public key from the certificate. See: https://developers.yubico.com/PIV/Guides/SSH_with_PIV_and_PKCS11.html (Step 2).

Check the SSH public key:

# cat certs/ssh_host_ca_key.pub
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPONIAYOEPirXbAksQGRm9mzoiy4kvOBxsVuhFBbAVxV7hMVg6bVaHOk19t+BkrttA9jZBxrtRLaJbVpfSsB9lo=

Update config/ca.json to use the yubikey:

        "key": "yubikey:slot-id=9c",

...

        "ssh": {
                "hostKey": "yubikey:slot-id=9d",
                "userKey": "yubikey:slot-id=9d"
        },
        "kms": {"type": "yubikey", "pin": "123456"},

(Lazily using the same key for both)

Start step-ca and notice the SSH public key is correct:

badger 2026/01/07 19:25:08 INFO: All 0 tables opened in 0s
badger 2026/01/07 19:25:08 INFO: Replaying file id: 0 at offset: 0
badger 2026/01/07 19:25:08 INFO: Replay took: 882.984µs
2026/01/07 19:25:08 Building new tls configuration using step-ca x509 Signer Interface
2026/01/07 19:25:08 Starting Smallstep CA/0.29.0 (linux/arm64)
2026/01/07 19:25:08 Documentation: https://u.step.sm/docs/ca
2026/01/07 19:25:08 Community Discord: https://u.step.sm/discord
2026/01/07 19:25:08 Config file: /home/step/config/ca.json
2026/01/07 19:25:08 The primary server URL is https://test.test.com:443
2026/01/07 19:25:08 Root certificates are available at https://test.test.com:443/roots.pem
2026/01/07 19:25:08 X.509 Root Fingerprint: 2efe7c3e048dc2c09ff2ae6af1c278d885073dbc3ba9fb22641899bdf3f206c1
2026/01/07 19:25:08 SSH Host CA Key: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPONIAYOEPirXbAksQGRm9mzoiy4kvOBxsVuhFBbAVxV7hMVg6bVaHOk19t+BkrttA9jZBxrtRLaJbVpfSsB9lo=
2026/01/07 19:25:08 SSH User CA Key: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPONIAYOEPirXbAksQGRm9mzoiy4kvOBxsVuhFBbAVxV7hMVg6bVaHOk19t+BkrttA9jZBxrtRLaJbVpfSsB9lo=
2026/01/07 19:25:08 Serving HTTPS on :443 ...

Create a new self-signed cert and import it over slot 9d:

# step certificate create --profile root-ca --no-password --insecure Dummy dummy.crt dummy.key
Your certificate has been saved in dummy.crt.
Your private key has been saved in dummy.key.
# ykman piv certificates import 9d dummy.crt
Enter a management key [blank to use default key]:
Certificate imported into slot KEY_MANAGEMENT

(Interestingly, ykman also doesn't verify the cert public key matches)

Now start step-ca again. The public key is incorrect but it doesn't complain!

badger 2026/01/07 19:38:03 INFO: All 1 tables opened in 1ms
badger 2026/01/07 19:38:03 INFO: Replaying file id: 0 at offset: 7631
badger 2026/01/07 19:38:03 INFO: Replay took: 110.739µs
2026/01/07 19:38:03 Building new tls configuration using step-ca x509 Signer Interface
2026/01/07 19:38:04 Starting Smallstep CA/0.29.0 (linux/arm64)
2026/01/07 19:38:04 Documentation: https://u.step.sm/docs/ca
2026/01/07 19:38:04 Community Discord: https://u.step.sm/discord
2026/01/07 19:38:04 Config file: /home/step/config/ca.json
2026/01/07 19:38:04 The primary server URL is https://test.test.com:443
2026/01/07 19:38:04 Root certificates are available at https://test.test.com:443/roots.pem
2026/01/07 19:38:04 X.509 Root Fingerprint: 2efe7c3e048dc2c09ff2ae6af1c278d885073dbc3ba9fb22641899bdf3f206c1
2026/01/07 19:38:04 SSH Host CA Key: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGK75296fTwL377zQNoq6/WYsjLQi/cHSCPIC5xDXchOsHeo+0EXAbPMAnn5nSCgadWKVFYA+B8YFjP1tHDdTA8=
2026/01/07 19:38:04 SSH User CA Key: ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGK75296fTwL377zQNoq6/WYsjLQi/cHSCPIC5xDXchOsHeo+0EXAbPMAnn5nSCgadWKVFYA+B8YFjP1tHDdTA8=
2026/01/07 19:38:04 Serving HTTPS on :443 ...

Your Environment

Docker was used for step commands (smallstep/step-ca:0.29.0-hsm)

Otherwise using ykman 5.6.1 running on Debian 13 on a Raspberry Pi 4.

Expected Behavior

To be honest, I'm not sure if this is actually a problem. Does the public key as extracted from the cert get used for anything besides logging? Or do certificates get signed with the private key and the public key is derived from the private key instead?

Actual Behavior

step-ca starts without any issue

Additional Context

I ran into this while trying to debug why importing my step ca init-generated SSH CA key was preventing step-ca startup, but generating one on the yubikey was not. (Turns out, it was not having the certificate imported). Interestingly, if you generate the key on the yubikey itself then import a dummy cert, it doesn't actually use the dummy cert, and step-ca logs the correct public key. I'm guessing the yubikey must make a distinction between imported and generated keys, separately storing/tracking the public key for the latter.

This may be a very unlikely scenario, but probably worth having a sanity check anyways.

Contributing

Vote on this issue by adding a 👍 reaction.
To contribute a fix for this issue, leave a comment (and link to your pull request, if you've opened one already).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugneeds triageWaiting for discussion / prioritization by team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions