pyca / cryptography

cryptography is a package designed to expose cryptographic primitives and recipes to Python developers.
https://cryptography.io
Other
6.58k stars 1.51k forks source link

Allow verifying an x509 cert chain without making assertions about the subject name #10276

Closed kislyuk closed 6 months ago

kislyuk commented 8 months ago

Thanks to all who worked on the X.509 verification support in version 42.

I am trying to use this API for verifying a signing certificate, and realizing that the API requires me to assert a subject name (DNS name or IP address) to get the validation output. The subject name is not defined/not relevant in this application.

How can I verify that a certificate is in the chain of trust without asserting on the subject name?

alex commented 8 months ago

We don't yet support this use case. We intend to in the future.

woodruffw commented 8 months ago

I've been thinking a bit about how to do this (since I'll also likely need it at some point for sigstore-python). Some ideas:

  1. Make the current build_server_verifier(subject: Subject) take an optional subject instead (maybe explicitly None?), which would then disable any SAN checks during chain building.
  2. Add a new build_generic_verifier(subject: Subject | None), which would then be an explicit path for chain building operations that are more "generic" than server authentication (presumed by build_server_identifier).
alex commented 8 months ago

Well, we should not be changing the build_server_verifier function. You should never verify a server cert without checking the subject is valid.

This needs to be a new type of verifier.

woodruffw commented 8 months ago

That makes sense. How does build_generic_verifier/GenericVerifier sound?

(Other meh names: AnonymousVerifier/SubjectlessVerifier for explicit non-subject checking.)

alex commented 8 months ago

What's teh difference between that and a client cert verifier?

woodruffw commented 8 months ago

Hmm, not sure. My idea of a ClientVerifier was something that takes a nonempty list[Subject] to match against so it wouldn't work for the "explicitly not checking the subject" case. But maybe I'm thinking about that the wrong way.

alex commented 8 months ago

I don't think that's how a clientverifier would work -- imagine a webserver using client certs for auth.

woodruffw commented 8 months ago

So in that case ClientVerifier.verify() would return the Subject for subsequent AuthZ/handling, right? In that case yeah, I think this is indistinguishable from the client cert verifier case and can be handled through it.

alex commented 8 months ago

Yes, I think that's what you'd want, indeed.

woodruffw commented 8 months ago

Just to give a public update: I've offered to do the work here. I'll try and get a PR up for this tonight.

exFalso commented 7 months ago

Just noting: there are other x509 certificates that don't actually have anything to do with webPKI. In our case we're looking for a generic way to verify certificate chains, with various constraints on the contents. It'd be nice if the project had a generic verifier as well. As-is, we're stuck with oscrypto/certvalidator or a fork.

woodruffw commented 7 months ago

Just noting: there are other x509 certificates that don't actually have anything to do with webPKI. In our case we're looking for a generic way to verify certificate chains, with various constraints on the contents. It'd be nice if the project had a generic verifier as well. As-is, we're stuck with oscrypto/certvalidator or a fork.

Could you say a bit more about your use case? There isn't exactly anything called "generic" chain verification; all chain verifiers perform verification modulo some kind of certificate profile (sometimes that profile is poorly defined or not well documented, but it's always there).

In other words: are you verifying code-signing certs, S/MIME, etc.? Each of these has a profile (or profiles), and supporting each without exposing footguns requires some design consideration 🙂

vEpiphyte commented 7 months ago

If you look at the PyOpenSSL X509Store / X509StoreContext APIs ( https://www.pyopenssl.org/en/latest/api/crypto.html#x509store-objects ) those are probably the most flexible examples of asking "Does this certificate validate with this CA/CRL chain using the [openssl] polices I choose to set?". That level of flexibility does mean its full of footguns for someone to use and potentially whiff on ( for example, validating a server certificate is signed but not taking the extra step to validate the name is as expected ).

@exFalso If you've got good examples you can share I bet that would be helpful for the PYCA devs in their feature planning :)

exFalso commented 7 months ago

@woodruffw @vEpiphyte sure I can expand. We're working with Intel SGX remote attestation, which requires a client of a remotely executing code to verify certain cryptographic evidence (it's essentially Intel signing off on the fact that a piece of code has been isolated and is running using encrypted memory). The remote code will present a certificate chain with a signature over a byteblob which contains further data about that running code. Part of the verification is the chain verification (there are quite a few more steps to it).

Example API: https://api.portal.trustedservices.intel.com/content/documentation.html#pcs-certificate-v4-response

Example certvalidator/oscrypto chain verification (obv. partial code):

        trust_roots = [spec_dcap.dcapRootCaDer]
        validation_context = ValidationContext(trust_roots)
        validator = CertificateValidator(
            pck_certs[0].as_bytes(),
            intermediate_certs=map(lambda cert: cert.as_bytes(), pck_certs[1:]),
            validation_context=validation_context,
        )
        validator.validate_usage({"digital_signature"})

(sidenote: I know the camelcase field is an eyesore, that comes from generated code ;) )

Example root certificate, this is the Intel SGX DCAP root:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            d1:07:76:5d:32:a3:b0:94
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = US, ST = CA, L = Santa Clara, O = Intel Corporation, CN = Intel SGX Attestation Report Signing CA
        Validity
            Not Before: Nov 14 15:37:31 2016 GMT
            Not After : Dec 31 23:59:59 2049 GMT
        Subject: C = US, ST = CA, L = Santa Clara, O = Intel Corporation, CN = Intel SGX Attestation Report Signing CA
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (3072 bit)
                Modulus:
                    00:9f:3c:64:7e:b5:77:3c:bb:51:2d:27:32:c0:d7:
                    ...
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 CRL Distribution Points:
                Full Name:
                  URI:http://trustedservices.intel.com/content/CRL/SGX/AttestationReportSigningCA.crl
            X509v3 Subject Key Identifier:
                78:43:7B:76:A6:7E:BC:D0:AF:7E:42:37:EB:35:7C:3B:87:01:51:3C
            X509v3 Authority Key Identifier:
                78:43:7B:76:A6:7E:BC:D0:AF:7E:42:37:EB:35:7C:3B:87:01:51:3C
            X509v3 Key Usage: critical
                Certificate Sign, CRL Sign
            X509v3 Basic Constraints: critical
                CA:TRUE, pathlen:0
    Signature Algorithm: sha256WithRSAEncryption
    Signature Value:
        78:5f:2d:60:c5:c8:0a:f4:2a:79:76:10:21:39:15:da:82:c9:
        ...

The full verification requires a lot more steps.

As you can see, this is a highly specialized use case, and implementing a separate specialized validator in an x509 library is unwarranted. Instead, a verifier for the overall chain integrity would suffice, and any additional checks we want to implement would be separate. However, this is currently not possible with cryptography iiuc.

Another completely different use-case is SPIFFE: https://github.com/spiffe/spiffe/blob/main/standards/X509-SVID.md, where again, the webPKI-specialized validator will not work. I would highly recommend not implementing these verifiers, but rather exposing the primitives (probably under hazmat) that allow library users to verify.

woodruffw commented 7 months ago

Thanks for the additional context @exFalso!

It looks like the Intel SGX certificate profile is defined here: https://download.01.org/intel-sgx/latest/dcap-latest/linux/docs/SGX_PCK_Certificate_CRL_Spec-1.4.pdf

However, I agree that it probably doesn't make a ton of sense to expose a dedicated API for every X.509 profile on the planet :slightly_smiling_face:. Having a building block validator under hazmat (or extending the existing configuration knobs in a way that doesn't encourage user misuse) would probably be more straightforward.

exFalso commented 7 months ago

If you look at the PyOpenSSL X509Store / X509StoreContext APIs ( https://www.pyopenssl.org/en/latest/api/crypto.html#x509store-objects ) those are probably the most flexible examples of asking "Does this certificate validate with this CA/CRL chain using the [openssl] polices I choose to set?". That level of flexibility does mean its full of footguns for someone to use and potentially whiff on ( for example, validating a server certificate is signed but not taking the extra step to validate the name is as expected ).

@exFalso If you've got good examples you can share I bet that would be helpful for the PYCA devs in their feature planning :)

This worked, thank you!

vEpiphyte commented 7 months ago

@exFalso If you're able to share some of the code you used from pyopenssl that could be useful for the devs here. I shared an example here #10393 for their reference purposes.

exFalso commented 7 months ago

It's very similar to that code.

        root_x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_ASN1, spec_dcap.dcapRootCaDer)
        trust_store = OpenSSL.crypto.X509Store()
        trust_store.add_cert(root_x509)
        pck_x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, pck_certs[0].as_bytes())
        intermediate_certs = map(lambda cert: OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert.as_bytes()), pck_certs[1:])
        context = OpenSSL.crypto.X509StoreContext(trust_store, pck_x509, list(intermediate_certs))
        context.verify_certificate()

It is a lot more cumbersome to do additional checks like on keyUsage, effectively need to deserialize from the ASN1-level

shane-kearns commented 6 months ago

Another non web use case is in verification of TPM certificates (e.g. Endorsement Key, which is signed by the TPM manufacturer; and Attestation Keys signed by the system manufacturer or owner). https://trustedcomputinggroup.org/wp-content/uploads/TPM-2p0-Keys-for-Device-Identity-and-Attestation_v1_r12_pub10082021.pdf

I think similar to the cases described by exFalso, path validation is relatively standard but the application needs to do additional checks on the leaf certificate e.g. checking the certificate policy and key usage extensions for the appropriate OIDs.

The primitives needed to implement path validation on top of cryptography are all there, but there are a lot of opportunities to get it wrong too.

kislyuk commented 6 months ago

FYI, for completeness, my use case for this functionality is certificate verification for XML Signature (which is used in SAML 2.0 and XAdES), and TSP (RFC 3161), also used in XAdES and other applications.

woodruffw commented 6 months ago

Just to give a brief update here: the work in #10345 is pretty much done, and just needs to be deconflicted (and reviewed, of course). Once merged, that will expose Python APIs for "client" verification (i.e. path validation without subject assertions, with the expectation that the validating party will check the subjects or otherwise handle the chain appropriately subsequently).