[TLS 1.3] Support signature_algorithm_cert Extension#3125
Conversation
src/lib/tls/tls_signature_scheme.cpp
Outdated
| auto pss_params = [](const std::string hash_name) | ||
| { | ||
| const AlgorithmIdentifier hash_id(hash_name, AlgorithmIdentifier::USE_NULL_PARAM); | ||
| const AlgorithmIdentifier mgf_id("MGF1", hash_id.BER_encode()); | ||
|
|
||
| std::vector<uint8_t> parameters; | ||
| DER_Encoder(parameters) | ||
| .start_sequence() | ||
| .start_context_specific(0).encode(hash_id).end_cons() | ||
| .start_context_specific(1).encode(mgf_id).end_cons() | ||
| .start_context_specific(2).encode(HashFunction::create(hash_name)->output_length()).end_cons() | ||
| .start_context_specific(3).encode(size_t(1)).end_cons() // trailer field | ||
| .end_cons(); | ||
|
|
||
| return parameters; | ||
| }; | ||
|
|
||
| switch(m_code) | ||
| { | ||
| // for RSA PKCSv1.5 parameters "SHALL" be NULL | ||
| case RSA_PKCS1_SHA1: | ||
| return AlgorithmIdentifier("RSA/EMSA3(SHA-160)", AlgorithmIdentifier::USE_NULL_PARAM); | ||
| case RSA_PKCS1_SHA256: | ||
| return AlgorithmIdentifier("RSA/EMSA3(SHA-256)", AlgorithmIdentifier::USE_NULL_PARAM); | ||
| case RSA_PKCS1_SHA384: | ||
| return AlgorithmIdentifier("RSA/EMSA3(SHA-384)", AlgorithmIdentifier::USE_NULL_PARAM); | ||
| case RSA_PKCS1_SHA512: | ||
| return AlgorithmIdentifier("RSA/EMSA3(SHA-512)", AlgorithmIdentifier::USE_NULL_PARAM); | ||
|
|
||
| // for DSA, ECDSA, GOST parameters "SHALL" be empty | ||
| case ECDSA_SHA1: | ||
| return AlgorithmIdentifier("ECDSA/EMSA1(SHA-160)", AlgorithmIdentifier::USE_EMPTY_PARAM); | ||
| case ECDSA_SHA256: | ||
| return AlgorithmIdentifier("ECDSA/EMSA1(SHA-256)", AlgorithmIdentifier::USE_EMPTY_PARAM); | ||
| case ECDSA_SHA384: | ||
| return AlgorithmIdentifier("ECDSA/EMSA1(SHA-384)", AlgorithmIdentifier::USE_EMPTY_PARAM); | ||
| case ECDSA_SHA512: | ||
| return AlgorithmIdentifier("ECDSA/EMSA1(SHA-512)", AlgorithmIdentifier::USE_EMPTY_PARAM); | ||
|
|
||
| case RSA_PSS_SHA256: | ||
| return AlgorithmIdentifier("RSA/EMSA4", pss_params("SHA-256")); | ||
| case RSA_PSS_SHA384: | ||
| return AlgorithmIdentifier("RSA/EMSA4", pss_params("SHA-384")); | ||
| case RSA_PSS_SHA512: | ||
| return AlgorithmIdentifier("RSA/EMSA4", pss_params("SHA-512")); | ||
|
|
||
| // for DSA, ECDSA, GOST parameters "SHALL" be empty | ||
| case DSA_SHA1: | ||
| return AlgorithmIdentifier("DSA/EMSA1(SHA-160)", AlgorithmIdentifier::USE_EMPTY_PARAM); | ||
| case DSA_SHA256: | ||
| return AlgorithmIdentifier("DSA/EMSA1(SHA-256)", AlgorithmIdentifier::USE_EMPTY_PARAM); | ||
| case DSA_SHA384: | ||
| return AlgorithmIdentifier("DSA/EMSA1(SHA-384)", AlgorithmIdentifier::USE_EMPTY_PARAM); | ||
| case DSA_SHA512: | ||
| return AlgorithmIdentifier("DSA/EMSA1(SHA-512)", AlgorithmIdentifier::USE_EMPTY_PARAM); | ||
|
|
||
| default: | ||
| return AlgorithmIdentifier(); | ||
| } |
There was a problem hiding this comment.
This is somewhat a duplicate of the implementations of EMSA::config_for_x509() scattered around the different EMSAs. I'm not quite happy with that, to be honest. Simply instantiating the respective EMSA and then actually using ::config_for_x509() doesn't work out-of-the-box, as it would currently require to pass a respective private key. I think it would be helpful to abstract both use cases and centralize this somehow.
Anyway: As is, this is fairly inefficient, as the AlgorithmIdentifiers are re-generated for each invocation. It would be an interesting exercise to constexpr the heck out of this. 🤡
I'm open for better ideas. :)
There was a problem hiding this comment.
Yeah pretty ugly. Also we recompute the PSS params each time when in practice it will always be SHA-{256,512} 😭
Looking at config_for_x509 ... why the heck does this interface take a Private_Key? It could easily get away with a Public_Key? (Not that this change would address the issue here, but still kind of odd).
Anyway this falls in the category of definitely not great and pointing to a place where some refactoring time is needed but doesn't IMO need to block the PR merging.
There was a problem hiding this comment.
After merging #3142, this now became: #3125 (comment)
Thanks for the help in adapting config_for_x509() to make it suitable for this use case!
ebde083 to
3f5ff8f
Compare
|
Whoops, that almost broke the code examples. @lieser thanks for the CI job checking their successful compilation. 😅 |
3f5ff8f to
e9397c0
Compare
Codecov ReportBase: 88.09% // Head: 88.08% // Decreases project coverage by
Additional details and impacted files@@ Coverage Diff @@
## master #3125 +/- ##
==========================================
- Coverage 88.09% 88.08% -0.01%
==========================================
Files 604 604
Lines 67399 67422 +23
Branches 6748 6755 +7
==========================================
+ Hits 59374 59389 +15
- Misses 5211 5216 +5
- Partials 2814 2817 +3
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. ☔ View full report at Codecov. |
e9397c0 to
0d91c99
Compare
| AlgorithmIdentifier Signature_Scheme::algorithm_identifier() const noexcept | ||
| { | ||
| auto emsa = EMSA::create(padding_string()); | ||
| if(!emsa) | ||
| { return AlgorithmIdentifier(); } | ||
| auto hash = hash_function_name(); | ||
| if(hash == "SHA-1") | ||
| { hash = "SHA-160"; } | ||
|
|
||
| return emsa->config_for_x509(algorithm_name(), hash); | ||
| } | ||
|
|
There was a problem hiding this comment.
This is now using EMSA::config_for_x509() instead of copy-pasting most of its scattered implementations.
There was a problem hiding this comment.
The config_for_x509() implementations check that the passed hash function name is equal to the hash function used in the EMSA. This is done by string comparison (e.g. from emsa1.cpp):
if(cert_hash_name != m_hash->name())
throw Invalid_Argument(...);The library can deal with different string-identifiers for hash functions (e.g "SHA-1", "SHA1" and "SHA-160"). But m_hash->name() usually returns a hard-coded identifier. I tried dealing with that using the "SHA-1" to "SHA-160" mapping above. However, this PR's macOS builds fail, because the software-implementation (sha1.cpp) returns "SHA-160" when invoking Hash_Function::name() and the CommonCrypto provider (CommonCrypto_HashFunction) that might or might not be enabled on macOS returns "SHA-1". 😨
In my opinion, these string comparisons are a generic footgun that will cause recurring issues. 😞 Especially when they force us to scatter such mappings around the code base. Lets try to centralize the string-identifier mappings in the algorithm implementations? @randombit
This could be done by requiring a static method ::implements(std::string_view algo_name) in each concrete implementation. Now, that implementations "know" all their string-identifiers, a virtual bool Hash_Function::is(std::string_view algo_name) could allow convenient polymorphic checks as in config_for_x509(). Even the Hash_Function::create() factory could benefit from ::implements() and stop duplicating the string-identifiers. See this godbolt for a sketch.
One might even consider to auto-generate the Hash_Function::create() code based on self-registering sub-classes. But that might take it too far.
Obviously, getting rid of the ambiguous string-identifiers would be even better, but I doubt the massive public API change would be worth it.
There was a problem hiding this comment.
However, this PR's macOS builds fail, because the software-implementation (sha1.cpp) returns "SHA-160" when invoking Hash_Function::name() and the CommonCrypto provider (CommonCrypto_HashFunction) that might or might not be enabled on macOS returns "SHA-1"
Can you take a look at #3184?
In my opinion, these string comparisons are a generic footgun that will cause recurring issues. disappointed Especially when they force us to scatter such mappings around the code base. Lets try to centralize the string-identifier mappings in the algorithm implementations?
They are. It was one of the big design mistakes.
One might even consider to auto-generate the Hash_Function::create() code based on self-registering sub-classes.
This used to be how it worked, actually. But that approach ran into weird problems with static libraries, and ended up having to be removed (in #279)
This could be done by requiring a static method ::implements(std::string_view algo_name) in each concrete implementation. Now, that implementations "know" all their string-identifiers, a virtual bool Hash_Function::is(std::string_view algo_name) could allow convenient polymorphic checks as in config_for_x509().
This might work but I wonder how this handles parameterizations such as output length or personalization flags. Quick example
auto hash = HashFunction::create("BLAKE2b(384)");
assert!(hash->is("BLAKE2b")); // true?
assert!(hash->is("BLAKE2b(384)")); // true?
As far as I can recall the main problematic case for this is SHA-1, largely because we use the idiosyncratic/non-standard "SHA-160" name. Maybe the real fix here is to just remove that entirely as a known name, and relnote it? It's not even a big compatibility issue since "SHA-1" always worked.
Obviously, getting rid of the ambiguous string-identifiers would be even better, but I doubt the massive public API change would be worth it.
We could consider having an enum approach and then implementing the string method on top of that, but I agree that we cannot really remove the string-based interface at this point since it's very widely baked into how the CLI, the tests, the FFI layer, etc all work.
There was a problem hiding this comment.
This might work but I wonder how this handles parameterizations such as output length or personalization flags.
Fair point. A static BLAKE2b::implements("BLAKE2b(384)") should return true. But a specific instance of it probably shouldn't.
Maybe the real fix here is to just remove that entirely as a known name, and relnote it?
I tend to agree. Given that the string-identifiers are such a footgun its probably a good idea to make them as dumb as possible. As in: Don't allow for ambiguities during construction.
We could consider having an enum approach [...]
Though, I don't see how parameterization would work well with enums either. Hash_Types::BLAKE2b_384? That feels rigid to me.
Would it make sense to use class AlgorithmIdentifier for those use cases -- as its name suggests? De facto this is already implemented in the specific algorithm implementations. As a library user I always found it inconvenient/confusing to construct AlgorithmIdentifier out of thin air, but that might be fixable by some user-friendly API.
085eb32 to
881a852
Compare
... and the usage of those to populate the signature_algorithms_cert extension in Client_Hello and Certificate_Request_13 messages
Signature_Scheme::algorithm_identifier() produces the AlgorithmIdentifier of the described signature scheme as it would be used in an ASN.1 object or X.509 certificate.
881a852 to
0f993a9
Compare
|
Rebased and cleaned up after #3186 was merged. This should be ready for prime time. |
Pull Request Dependencies
[TLS 1.3] Basic Server Implementation #3053[TLS 1.3] Server-Side Client Authentication #3081Only allow "SHA-1" as a valid name for SHA-1 #3186Description
This introduces support for the
signature_algorithm_certextension introduced with TLS 1.3. With that, one can create a finer-grained signature algorithm policy; independently specifying acceptable algorithms for signatures in X.509 certificates and in the TLS protocol itself (i.e. in theCertificateVerifymessage).To communicate the peer's restrictions to the application,
CredentialsManager::find_cert_chain()(and friends) will now additionally get a vector ofAlgorithmIdentifiers specifying which algorithms are acceptable.Note that also the TLS 1.2 implementation can process this new extension (as suggested by RFC 8446).
New policy:
tls_acceptable_certificate_signature_schemes()By default, this returns
std::nulloptmeaning that the restrictions from the existing::tls_acceptable_signature_schemes()should also apply for signatures in certificates. If anything other thanstd::nulloptis returned, clients will add an explicitsignature_algorithm_certextension to the Client Hello; servers likewise in theCertificateRequestmessage.