Skip to content

[TLS 1.3] Support signature_algorithm_cert Extension#3125

Merged
reneme merged 3 commits intomasterfrom
tls13/sigalgo_cert
Jan 26, 2023
Merged

[TLS 1.3] Support signature_algorithm_cert Extension#3125
reneme merged 3 commits intomasterfrom
tls13/sigalgo_cert

Conversation

@reneme
Copy link
Copy Markdown
Collaborator

@reneme reneme commented Dec 15, 2022

Pull Request Dependencies

Description

This introduces support for the signature_algorithm_cert extension 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 the CertificateVerify message).

To communicate the peer's restrictions to the application, CredentialsManager::find_cert_chain() (and friends) will now additionally get a vector of AlgorithmIdentifiers 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::nullopt meaning that the restrictions from the existing ::tls_acceptable_signature_schemes() should also apply for signatures in certificates. If anything other than std::nullopt is returned, clients will add an explicit signature_algorithm_cert extension to the Client Hello; servers likewise in the CertificateRequest message.

Comment on lines +270 to +299
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();
}
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. :)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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!

@reneme reneme force-pushed the tls13/sigalgo_cert branch 2 times, most recently from ebde083 to 3f5ff8f Compare December 16, 2022 08:26
@reneme
Copy link
Copy Markdown
Collaborator Author

reneme commented Dec 16, 2022

Whoops, that almost broke the code examples. @lieser thanks for the CI job checking their successful compilation. 😅

@reneme reneme mentioned this pull request Dec 16, 2022
14 tasks
@reneme reneme force-pushed the tls13/sigalgo_cert branch from 3f5ff8f to e9397c0 Compare December 30, 2022 19:25
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Dec 30, 2022

Codecov Report

Base: 88.09% // Head: 88.08% // Decreases project coverage by -0.01% ⚠️

Coverage data is based on head (0f993a9) compared to base (f1ec760).
Patch coverage: 68.08% of modified lines in pull request are covered.

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     
Impacted Files Coverage Δ
src/bogo_shim/bogo_shim.cpp 72.99% <ø> (ø)
src/fuzzer/tls_server.cpp 38.88% <ø> (ø)
src/lib/tls/msg_cert_verify.cpp 90.36% <0.00%> (ø)
src/lib/tls/tls12/tls_client_impl_12.cpp 83.38% <ø> (ø)
src/lib/tls/tls_extensions.cpp 81.99% <0.00%> (ø)
src/tests/unit_tls.cpp 90.10% <ø> (ø)
src/lib/tls/msg_client_hello.cpp 77.42% <20.00%> (-1.11%) ⬇️
src/lib/tls/tls13/msg_certificate_req_13.cpp 67.39% <55.55%> (-5.78%) ⬇️
src/cli/tls_helpers.h 69.76% <100.00%> (+0.71%) ⬆️
src/lib/tls/credentials_manager.cpp 87.50% <100.00%> (-1.39%) ⬇️
... and 23 more

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.
📢 Do you have feedback about the report comment? Let us know in this issue.

Copy link
Copy Markdown
Owner

@randombit randombit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a rebase in order to (hopefully) make use of #3142, but otherwise lgtm.

@reneme reneme force-pushed the tls13/sigalgo_cert branch from e9397c0 to 0d91c99 Compare January 16, 2023 07:51
Comment on lines +243 to +250
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);
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now using EMSA::config_for_x509() instead of copy-pasting most of its scattered implementations.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@reneme reneme force-pushed the tls13/sigalgo_cert branch 4 times, most recently from 085eb32 to 881a852 Compare January 18, 2023 09:26
... 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.
@reneme reneme force-pushed the tls13/sigalgo_cert branch from 881a852 to 0f993a9 Compare January 19, 2023 13:25
@reneme
Copy link
Copy Markdown
Collaborator Author

reneme commented Jan 19, 2023

Rebased and cleaned up after #3186 was merged. This should be ready for prime time.

@randombit randombit mentioned this pull request Jan 22, 2023
29 tasks
@reneme reneme merged commit c6733da into master Jan 26, 2023
@randombit randombit deleted the tls13/sigalgo_cert branch January 27, 2023 23:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants