-
Notifications
You must be signed in to change notification settings - Fork 1.7k
[RFC] Implement X.509 certificate verification #5093
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
Conversation
Does this mean that the verify callback will be called sequentially on each certificate in a chain? If you are passing the full chain each time, why not have the callback verify the whole chain rather than one certificate at a time? If multiple chains can be built, I could see calling the callback on each of those possible chains until the callback returns that one of them is valid, but I'm not sure why you'd want to call the callback multiple times for each chain, passing in the index of the cert being verified, rather than letting the callback do this loop. Is the callback expected to do all the checks itself, even for basic stuff like KU/EKU, CA flag, valid before/after? If so, I think we're definitely going to want cryptography to export implementations of these common checks, so people don't have to implement all of them themselves (and most likely get them wrong). While I can see the argument that trying to provide arguments for all the possible verification overrides could get very complicated, I don't think providing only "example" callback functions the right place to land there. Even if the caller is required to provide a callback, cryptography should at least provide solid implementations of each check as a library call that the callbacks can use for common things like checking basic constraints, key usage, expiration, revocation, and subject alt name checks. Something like EKU or subject alt name checks will require the caller to specify the values they want to match against, but there'd still be value in providing a standard function here I think, particularly for SAN where you have to deal with matching against IPv4 and IPv6 addresses in binary form along with string hostnames in many cases. In AsyncSSH today, I'm actually using a mix of PyCA X509 code and pyOpenSSL code, with cert validation specifically requiring me to convert from a PyCA cert to a pyOpenSSL one (via crypto.X509.from_cryptography). I do this only to get at pyOpenSSL's chain building (in crypto.X509Store/X509StoreContext). It looks like this new API will provide that, and I'd love to get rid of the pyOpenSSL dependency, but right now I'm expecting pyOpenSSL to do the checking of attributes on all the intermediate certs during the chain building, and I don't have any callback defined for that. I do check other attributes of the end-entity certificate myself, but that's not done through a callback. |
| issuer = issuer_candidate | ||
| break | ||
| if issuer: | ||
| g.add_edge(cert, issuer) |
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 might be missing something, but I believe we do not check the signature of the current cert with the issuer public key here which I believe should guard the add_edge call.
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'd think that would do the job instead (roughly tested):
for cert in all_certs:
issuer_name = cert.issuer
for issuer_candidate in all_certs:
if issuer_candidate.subject == issuer_name:
pkey = issuer_candidate.public_key()
try:
if isinstance(pkey, rsa.RSAPublicKeyWithSerialization):
pkey.verify(
signature=cert.signature,
data=cert.tbs_certificate_bytes,
padding=padding.PKCS1v15(),
algorithm=cert.signature_hash_algorithm,
)
elif isinstance(
pkey,
ec.EllipticCurvePublicKeyWithSerialization):
pkey.verify(
signature=cert.signature,
data=cert.tbs_certificate_bytes,
signature_algorithm=cert
.signature_hash_algorithm,
)
elif isinstance(
pkey, dsa.DSAPublicKeyWithSerialization):
pkey.verify(
signature=cert.signature,
data=cert.tbs_certificate_bytes,
algorithm=cert.signature_hash_algorithm,
)
else:
raise UnsupportedAlgorithm(
'{} verification is not implemented'
.format(pkey.__class__)
)
g.add_edge(cert, issuer_candidate)
except InvalidSignature:
passThere 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.
Definitely so! Thank you for the code sketch!
I decided that this check deserves function on its own and implemented it in #5116. Will update this PR when the latter (hopefully) will be merged. (The code requried minor change for EC key, verify() in that case requires signature_algorithm being instance of ec.ECDSA.)
The function returns True if the "issuer" field of `Certificate` is the same as the "subject" of `issuer`, and the signature on `Certificate` validates with the public key of `issuer`. If any of these conditions fails, corresponding exception is raised. This code will be used in pyca#5093.
The function returns True if the "issuer" field of `Certificate` is the same as the "subject" of `issuer`, and the signature on `Certificate` validates with the public key of `issuer`. If any of these conditions fails, corresponding exception is raised. This code will be used in pyca#5093.
The function returns True if the "issuer" field of `Certificate` is the same as the "subject" of `issuer`, and the signature on `Certificate` validates with the public key of `issuer`. If any of these conditions fails, appropriate exception is raised. This code will be used in pyca#5093.
The function returns True if the "issuer" field of `Certificate` is the same as the "subject" of `issuer`, and the signature on `Certificate` validates with the public key of `issuer`. If any of these conditions fails, appropriate exception is raised. This code will be used in pyca#5093.
|
@ronf Thank you for the deep comments!
You are correct. There is no reason to call the callback for each certificate, not for the whole chain. My thought was that "API users are more used to work with certificates, not with chains", but now this looks like a moot justification as the API user will receive the chain anyway (and very likely will have to handle it, as for example requirements on KU/EKU bits are probably different for leaf certificates, intermediate certificates and root certificates). The next version of the PR will pass the whole chain to the callback as you suggest.
Absolutely so! This is work-in-progress. (I'm especially thrilled by CRL/OCSP checks 🙄 Pointers to "industry practices" to related decision making examples are welcome.) |
|
@baloo What is the practice in the project - I'd love to cleanup the commits, "hide the sausage making" by squashing the review comments into their "parents", and force-push. This will, naturally, require additional work from the reviewer. Happy to follow the conventions. |
|
@KonstantinShemyak I never contributed to this project, I'm afraid I can't help you with that :( I was in need of x509 chain validation and merely passing by :) |
722aa90 to
2e25fd7
Compare
|
Branch force-pushed with the following changes:
|
|
I haven't reviewed this yet (and probably won't be able to until after I find the time to fix the absurdity of our CI yet again, sigh), but you may find https://github.com/alex/x509-validator interesting as it represents a partial attempt at solving this that we never finished. One big item: validation like this is obnoxiously tricky so any validator should pull in very large test vector sets to gain some confidence in the edges. |
|
@reaperhulk would be great if someone could review this - it's blocking those who want to move off of PyOpenSSL. |
The function returns True if the "issuer" field of `Certificate` is the same as the "subject" of `issuer`, and the signature on `Certificate` validates with the public key of `issuer`. If any of these conditions fails, appropriate exception is raised. This code will be used in pyca#5093.
…tificate Two booleans leaf_cert.is_issued_by(ca_cert) and ca_cert.is_issuer_of(leaf_cert), mirroring each other, are implemented. The functions return True if the "issuer" field of `leaf_cert` is the same as the "subject" of `ca_cert`, and the signature on `leaf_cert` validates with the public key of `ca_cert`. If any of these conditions fails, appropriate exception is raised. This code will be used in pyca#5093.
…tificate Two booleans leaf_cert.is_issued_by(ca_cert) and ca_cert.is_issuer_of(leaf_cert), mirroring each other, are implemented. The functions return True if the "issuer" field of `leaf_cert` is the same as the "subject" of `ca_cert`, and the signature on `leaf_cert` validates with the public key of `ca_cert`. If any of these conditions fails, appropriate exception is raised. This code will be used in pyca#5093.
|
I'm not sure if you are still working on this or not. If so, it would be great if this were able to handle partial chains, like For example, if a certificate chain is: OpenSSL commandline supports: Ideally, the following test cases would pass, without needing the self-signed root |
|
@jvanasco I have not updated the branch since long time, but it is supposed to do exactly what you expect:
[...]
Both sketched tests would pass. It is nowhere required that the trusted root be self-signed. (It could be required in |
Pet peeve: The term "certificate chain" is an oversimplification and only applies in the most trivial case. Path building is much more complicated and messy. For Let's Encrypt uses cross-signing with multiple possible trust paths. https://tools.ietf.org/html/rfc4158 has some great ASCII art for cross-signing and trust meshes.
Actually it depends on the implementation and configuration. For example OpenSSL never trusts intermediate certificate in the trust store unless X509_VERIFY_PARAM_set_flags flag |
In my example, the intent is that we are testing the integrity of an identified chain. A lot of other developers have fought with this same problem for many years. Path building and validating trust are discrete needs which are unrelated to verifying the integrity of chains or partial chains. If you want to make your letsEncrypt example |
Definitely so, and the proposed implementation accounts for this. It's the reason to build the directed graph.
@tiran The proposed implementation, as I said, does not care is the trusted certificate self-signed or not. I think it makes more sense than the described behavior of OpenSSL. If you see the reasons why the latter behavior should be the default, please tell. (But "because OpenSSL does so" would not be accepted as valid argument 😄) |
|
@reaperhulk @KonstantinShemyak is there anything I can do to help this along? It looks like the PR needs to be rebased. |
…tificate Two booleans leaf_cert.is_issued_by(ca_cert) and ca_cert.is_issuer_of(leaf_cert), mirroring each other, are implemented. The functions return True if the "issuer" field of `leaf_cert` is the same as the "subject" of `ca_cert`, and the signature on `leaf_cert` validates with the public key of `ca_cert`. If any of these conditions fails, appropriate exception is raised. This code will be used in pyca#5093.
2e25fd7 to
04702de
Compare
…tificate Two booleans leaf_cert.is_issued_by(ca_cert) and ca_cert.is_issuer_of(leaf_cert), mirroring each other, are implemented. The functions return True if the "issuer" field of `leaf_cert` is the same as the "subject" of `ca_cert`, and the signature on `leaf_cert` validates with the public key of `ca_cert`. If any of these conditions fails, appropriate exception is raised. This code will be used in pyca#5093.
…tificate Two booleans leaf_cert.is_issued_by(ca_cert) and ca_cert.is_issuer_of(leaf_cert), mirroring each other, are implemented. The functions return True if the "issuer" field of `leaf_cert` is the same as the "subject" of `ca_cert`, and the signature on `leaf_cert` validates with the public key of `ca_cert`. If any of these conditions fails, appropriate exception is raised. This code will be used in pyca#5093.
…#2381) This is the first step of implementing verification of a certificate against trusted roots. To complete the implementation, an interface must be provided to pass a policy (such as, for example, require correct validity time, particular KU/EKU fields and CRL checks).
… valid (pyca#2381) Formerly unused 'cert_verif_cb' is called now. Caller of X509.verify() must pass a callable which would return True for the certificate in the chain which complies with the user's policy. A minimal reasonalbe callback which can be used as default is provided.
04702de to
3f7ca22
Compare
|
The branch is rebased on top of (also rebased and updated) #5116 . The latter is valuable by itself (not only as a prerequisite for this PR). In fact yet another PR doing the same has been submitted (#5367), I have justified my decisions in the parts where they differ. This PR is still RFC, my plan is to implement suggestions by @ronf, add tests and add a number of example policy callbacks for verification. |
…tificate Two booleans leaf_cert.is_issued_by(ca_cert) and ca_cert.is_issuer_of(leaf_cert), mirroring each other, are implemented. The functions return True if the "issuer" field of `leaf_cert` is the same as the "subject" of `ca_cert`, and the signature on `leaf_cert` validates with the public key of `ca_cert`. If any of these conditions fails, appropriate exception is raised. This code will be used in pyca#5093.
…tificate Two booleans leaf_cert.is_issued_by(ca_cert) and ca_cert.is_issuer_of(leaf_cert), mirroring each other, are implemented. The functions return True if the "issuer" field of `leaf_cert` is the same as the "subject" of `ca_cert`, and the signature on `leaf_cert` validates with the public key of `ca_cert`. If any of these conditions fails, appropriate exception is raised. This code will be used in pyca#5093.
…tificate Two booleans leaf_cert.is_issued_by(ca_cert) and ca_cert.is_issuer_of(leaf_cert), mirroring each other, are implemented. The functions return True if the "issuer" field of `leaf_cert` is the same as the "subject" of `ca_cert`, and the signature on `leaf_cert` validates with the public key of `ca_cert`. If any of these conditions fails, appropriate exception is raised. This code will be used in pyca#5093.
…tificate Two booleans leaf_cert.is_issued_by(ca_cert) and ca_cert.is_issuer_of(leaf_cert), mirroring each other, are implemented. The functions return True if the "issuer" field of `leaf_cert` is the same as the "subject" of `ca_cert`, and the signature on `leaf_cert` validates with the public key of `ca_cert`. If any of these conditions fails, appropriate exception is raised. This code will be used in pyca#5093.
…tificate Two booleans leaf_cert.is_issued_by(ca_cert) and ca_cert.is_issuer_of(leaf_cert), mirroring each other, are implemented. The functions return True if the "issuer" field of `leaf_cert` is the same as the "subject" of `ca_cert`, and the signature on `leaf_cert` validates with the public key of `ca_cert`. If any of these conditions fails, appropriate exception is raised. This code will be used in pyca#5093.
…tificate Two booleans leaf_cert.is_issued_by(ca_cert) and ca_cert.is_issuer_of(leaf_cert), mirroring each other, are implemented. The functions return True if the "issuer" field of `leaf_cert` is the same as the "subject" of `ca_cert`, and the signature on `leaf_cert` validates with the public key of `ca_cert`. If any of these conditions fails, appropriate exception is raised. This code will be used in pyca#5093.
…tificate Two booleans leaf_cert.is_issued_by(ca_cert) and ca_cert.is_issuer_of(leaf_cert), mirroring each other, are implemented. The functions return True if the "issuer" field of `leaf_cert` is the same as the "subject" of `ca_cert`, and the signature on `leaf_cert` validates with the public key of `ca_cert`. If any of these conditions fails, appropriate exception is raised. This code will be used in pyca#5093.
|
This is in progress and there are several PRs that have already landed (with many more to come). Thanks for the work you did originally on this -- even though we didn't ultimately land this it helped us understand the demand for this! |
@reaperhulk Would it be possible to create an Issue that correlates all these things together? For those of us that need this functionality, that would make it much easier to monitor for when a fix lands. |
|
@reaperhulk Thank you so much! |
This is a Request-For-Comments implementation of verifying X.509 certificate against set of intermediate CA certificates and set of trusted CA certificates.
It provides the following API:
If a trusted chain cannot be built,
NoValidTrustChainexception is raised. Otherwise, the method returns list of trust chains (lists ofCertificateobjects), each starting withselfand ending in one oftrusted_roots.The interface is an attempt to get the best of "the new" and "the old" worlds:
The goal is to balance between principles "do not provide unsafe defaults" and "do not make The Right Thing too complicated". Differently from Go, the method:
cryptographyprovide an easy way for the user to do so if needed, like OpenSSL'sSSL_CTX_set_default_verify_paths()?Differently from OpenSSL, the method:
X509_STORE_CTX_set_verify_cb()).int verify_callback(int ok, X509_STORE_CTX *ctx).Remaining tasks which should be done:
backend, but quickly looking I have not seen a way to implement the method inx509.base.Certificate. So I have put it the quick-and-dirty way intohazmat.backends.openssl.x509._Certificate, which is probably not the way it should be done.Closes #2381 .