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

Document generating X.509 certs signed by HSM-maintained keys #12108

Open
amd-isaac opened this issue Dec 6, 2024 · 7 comments
Open

Document generating X.509 certs signed by HSM-maintained keys #12108

amd-isaac opened this issue Dec 6, 2024 · 7 comments

Comments

@amd-isaac
Copy link

I'm using cryptography to generate X.509 certificates, which works wonderfully when my code can access the private key of the issuing certificate. However, in my environment our private keys are stored on Hardware Security Modules (HSMs), and it does not appear that cryptography supports creating certificates (or at least the to-be-signed portions of certificates) and signing certificates as two separate steps. My current flow looks something like this:

from datetime import datetime, timedelta, timezone
from cryptography import x509
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives.serialization import Encoding

subject_private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)

# Build cert for the given public key
builder = x509.CertificateBuilder()
builder = builder.issuer_name(x509.Name([x509.NameAttribute(x509.NameOID.COMMON_NAME, "issuer")]))
builder = builder.subject_name(x509.Name([x509.NameAttribute(x509.NameOID.COMMON_NAME, "subject")]))
builder = builder.not_valid_before(datetime.now(tz=timezone.utc))
builder = builder.not_valid_after(datetime.now(tz=timezone.utc) + timedelta(days=3))
builder = builder.serial_number(x509.random_serial_number())
builder = builder.public_key(subject_private_key.public_key())

# Create cert by signing using an ephemeral fake key
fake_issuer_private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
cert_with_fake_sig = builder.sign(
    private_key=fake_issuer_private_key,
    algorithm=hashes.SHA256(),
    rsa_padding=padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=32),
)

# Get real signature from HSM; using local cryptography-generated key here for illustrative purposes
unavailable_hsm_issuer_private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
signature = unavailable_hsm_issuer_private_key.sign(
    cert_with_fake_sig.tbs_certificate_bytes,
    padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=32),
    hashes.SHA256(),
)

# Remove signature bytes from previously generated cert
der_with_no_sig = cert_with_fake_sig.public_bytes(Encoding.DER)[:-256]

# Rebuild cert using previous cert bytes (sans signature) with new signature appended
cert_with_correct_sig = x509.load_der_x509_certificate(der_with_no_sig + signature)

This process works, but feels very "hacky". Is there a recommended process to use an HSM to sign X.509 certificates (or CRLs, etc)? After searching through the documentation I see references to "opaque keys" and removed support for different backends, but nothing that specifically walks through how to use cryptography to integrate with an "offline" signing system, whether using a custom sign function as an input to the certificate builder, or a multi-step "generate partial certificate, then finish generating with a user-provided signature".

@alex
Copy link
Member

alex commented Dec 6, 2024

We don't currently have native support for HSMs or any other non-in-memory private keys.

However, if you have some Python API to your HSM, it's possible to hook it up to x509!

It's unfortunately not well documented, but https://github.com/reaperhulk/vault-signing lays out the basic pattern and shows how to implement it by example.

@amd-isaac
Copy link
Author

Interesting! So you're using RSAPrivateKey as a (partially-implemented) interface to intercept remote signing requests. I've enabled this using some proof-of-concept code for signing certificates, and it seems to work fine.

One follow-up question; I notice that the repo you linked uses the cryptography.hazmat.primitives._asymmetric and cryptography.hazmat.primitives._serialization modules. Do I need to be concerned that the leading underscores for these modules indicate private/unofficial cryptography APIs that would be likely to change without CHANGELOG notice in the future?

@alex
Copy link
Member

alex commented Dec 7, 2024

I don't know why it uses those private ones, this should be able to be implemented entirely in terms of our public API:

  • cryptography.hazmat.primitives.asymmetric.padding.AsymmetricPadding
  • cryptography.hazmat.primitives.serialization.Encoding
  • cryptography.hazmat.primitives.serialization.PrivateFormat
  • cryptography.hazmat.primitives.serialization.KeySerializationEncryption

Are all available.

@reaperhulk clean up your code plz :D

@reaperhulk
Copy link
Member

I wrote it years ago as a PoC!

we should really officially document this.

@alex
Copy link
Member

alex commented Dec 7, 2024 via email

@amd-isaac
Copy link
Author

I tested it using the public API as recommended and everything works fine. Thanks @alex and @reaperhulk for all the help!

Will leave this issue open in case you want to use it to track a documentation update, otherwise feel free to close it.

@alex alex changed the title Generating X.509 certs signed by HSM-maintained keys Document generating X.509 certs signed by HSM-maintained keys Dec 7, 2024
@alex
Copy link
Member

alex commented Dec 7, 2024

Yes, let's keep this open as a documentation task. Thanks much!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

No branches or pull requests

3 participants