Skip to content

Seed cipher lists and version bounds from the system crypto policy - #3503

Open
WillChilds-Klein wants to merge 3 commits into
feat/crypto-policies-2-parserfrom
feat/crypto-policies-3-seed-ciphers-versions
Open

Seed cipher lists and version bounds from the system crypto policy#3503
WillChilds-Klein wants to merge 3 commits into
feat/crypto-policies-2-parserfrom
feat/crypto-policies-3-seed-ciphers-versions

Conversation

@WillChilds-Klein

@WillChilds-Klein WillChilds-Klein commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Stack, split out of #3442, to merge bottom-up:

  1. Add ERR_num_errors and ERR_pop_to_count #3501 -- error-queue primitives
  2. Add a reader for the system crypto-policies OpenSSL back-end #3502 -- policy file reader
  3. Seed cipher lists and version bounds from the system crypto policy #3503 -- cipher lists and version bounds (this PR)
  4. Seed groups and signature algorithms from the system crypto policy #3504 -- groups and signature algorithms
  5. Keep AWS-LC's post-quantum defaults unless the policy opts out #3505 -- post-quantum defaults

Description

  • SSL_CTX_new now applies the policy's cipher lists and protocol bounds when built with the flag, taking the TLS or DTLS directives according to the context's method.
  • Seeding is best-effort and has no way to report failure, so it restores the caller's error queue to exactly what it was handed.
  • Both kinds of setter fail unhelpfully, so each value is resolved before it is applied and the built-in default stands otherwise: SSL_CTX_set_cipher_list installs its empty result before reporting failure, and the version setters check each bound only against the method's full range, so an inverted pair is accepted and then fails every handshake.
  • The @SECLEVEL=N prefix of CipherString is parsed and dropped, since AWS-LC has no security levels; the key-size and hash constraints a level implies are therefore not enforced.
  • The parse is cached per process, keyed on the resolved path, matching how OpenSSL reads openssl.cnf: a policy change takes effect only in processes started afterward, while a changed override path still misses.

Testing / verification

  • A new Amazon Linux 2023 CI job runs the whole suite with seeding compiled in, in debug and release, pointed at a path that does not exist so the thousands of existing tests still see AWS-LC's built-in defaults. A third build turns libssl off.
  • A further run drops that override and reads the file crypto-policies itself renders, comparing the automatically seeded context against one seeded by hand from the same path; an absent file fails the job rather than skipping.
  • The fixture policy is the Amazon Linux 2023 DEFAULT file copied byte for byte, so it carries the spellings, modifiers, and unimplemented algorithm names the code has to cope with.
  • The caller's error-queue state is checked three ways: its entries, a mark it had set, and the data pointer from its last ERR_get_error_line_data.
  • Unsatisfiable cipher rules, inverted bounds, a one-sided bound below an existing floor, and a version-locked method are each checked to leave the context as it was.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license and the ISC license.

Comment thread ssl/ssl_crypto_policy_test.cc Outdated
Comment thread ssl/ssl_crypto_policy_test.cc
Comment thread ssl/ssl_lib.cc
Comment thread ssl/ssl_crypto_policy_test.cc
# temporarily clears this variable to read the real system policy file, so the
# feature is still validated end-to-end on Amazon Linux 2023 (and skips
# elsewhere).
export AWSLC_CRYPTO_POLICY_FILE=/nonexistent/aws-lc-crypto-policy-disabled

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

all well and good, but we still need an integration test against an actual policy file on the filesystem. let's have a test run only the crypto policy tests against AL23's default crypto policy file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, and it found a real bug.

CryptoPolicySystemTest now runs with no environment override, so SSL_CTX_new reads whatever policy file the host actually has, and it compares the auto-seeded context against one the test seeds by hand from the same path. The Amazon Linux 2023 job sets AWSLC_CRYPTO_POLICY_TEST_REQUIRE_SYSTEM=1, which turns a missing file into a failure; elsewhere the suite skips. The CI script re-runs --gtest_filter='CryptoPolicy*' against the live /etc/crypto-policies/back-ends/opensslcnf.config after the bulk suite, and dumps the policy name and file first so a failure is diagnosable from the log.

The fixture in this PR is also now the AL2023 DEFAULT file copied byte for byte instead of a hand-written approximation.

What that turned up: every policy AL2023 ships writes Groups = *X25519:.... The * is the OpenSSL 3.5 key-share marker, which AWS-LC's name lookup does not handle, so X25519 -- the first and most-preferred group -- was silently dropped from every seeded context on AL2023. Fixed in #3504, which strips * and ? and honors - as removal, with the group assertions the new suite needs to catch it.

Verified against all four real policy files (DEFAULT, FIPS, FUTURE, LEGACY) pulled out of the AL2023 container image.

@WillChilds-Klein
WillChilds-Klein force-pushed the feat/crypto-policies-3-seed-ciphers-versions branch from 952a912 to 6c7c325 Compare September 9, 2026 21:08
@WillChilds-Klein
WillChilds-Klein added this pull request to stack #3509 September 9, 2026 22:50
skmcgrail
skmcgrail previously approved these changes Sep 9, 2026
Comment thread ssl/crypto_policy.cc
Comment on lines +370 to +378
// Seeding is best-effort, so the errors its failures queue must not reach the
// caller. Neither may the caller's own queue be disturbed, since this runs
// inside |SSL_CTX_new|, which no caller expects to touch the error queue at
// all. Cutting the queue back to the length it had leaves everything below the
// cut alone: the caller's entries, the data pointer their last
// |ERR_get_error_line_data| handed out, and any mark they set. It allocates
// nothing, so it has no failure mode, and if seeding queued nothing it does
// nothing.
const size_t num_errors = ERR_num_errors();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reason not to use the ERR_save_state?

@WillChilds-Klein WillChilds-Klein Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That was the approach i took initially, preserving mark state across copies. Claude found a few issues with that:

  1. ERR_save_state null return value indicates both queue empty + OOM
  2. saving the mark across copies (effectively re-setting the mark on every restore) had some unintended consequences elsewhere in the code base
  3. performance -- 2 additional malloc's on every SSL_CTX_new if error queue non-empty

Comment thread ssl/internal.h
Comment on lines 3728 to +3762
@@ -3732,7 +3737,29 @@ bool ssl_crypto_policy_parse_file(const char *path, CryptoPolicyConfig *out);
// The environment override is ignored in processes running with elevated
// privileges, where the environment sits on the far side of a privilege boundary
// from the root-owned default path.
const char *ssl_crypto_policy_default_path(void);
//
// Marked with OPENSSL_EXPORT to make it available for unit tests.
OPENSSL_EXPORT const char *ssl_crypto_policy_default_path(void);

// ssl_ctx_apply_crypto_policy seeds |ctx| from the crypto-policies OpenSSL
// back-end file at |path|. It is best-effort and never fails: a missing or
// malformed file, or a directive AWS-LC rejects, leaves the corresponding
// built-in default in place. Errors already queued by the caller are preserved;
// errors this function provokes are not.
//
// |is_dtls| selects the TLS.* vs DTLS.* protocol directives. |version_locked|
// must be true when |ctx| came from one of the legacy version-locked
// |SSL_METHOD|s (|ssl_method_st.version| non-zero), in which case the policy's
// protocol floor and ceiling are skipped: the caller pinned a single version and
// a system-wide default must not silently widen it.
//
// The parsed file is cached process-wide, keyed on |path|, so repeated
// |SSL_CTX_new| calls do not re-read it.
//
// Marked with OPENSSL_EXPORT to make it available for unit tests.
OPENSSL_EXPORT void ssl_ctx_apply_crypto_policy(SSL_CTX *ctx, const char *path,
bool is_dtls,
bool version_locked);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With -DENABLE_DIST_PKG=ON -DBUILD_SHARED_LIBS=ON -DENABLE_CRYPTO_POLICIES=ON, ssl_test fails to link on Linux with undefined references to all three helpers.

  • OPENSSL_EXPORT is not sufficient and ssl/libssl.map's local: *; hides anything missing from the registry. (Disabling symbol versioning makes the same build link.)
  • You should register these as PRIVATE_CXX in ssl/libssl.txt and regenerate the map (or keep them private and test through public APIs?). The extractor also needs an AWSLC_CRYPTO_POLICIES configuration; its default scan misses these declarations.

Comment thread ssl/crypto_policy.cc

ApplyPolicyToCtx(ctx, path, is_dtls, version_locked);

ERR_pop_to_count(num_errors);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Restoring the count cannot restore entries evicted by overflow. I reproduced this through SSL_CTX_new with 14 caller errors (capacity 15) and a policy whose CipherString and Ciphersuites both match nothing: the two probes evict the oldest caller error, and ERR_pop_to_count(14) leaves one probe error behind. An already-full queue also fails.

Skipping only when num_errors == ERR_NUM_ERRORS - 1 is not enough. This should prevent overflow across the whole seeding operation and add tests for both full and one-slot-free caller queues.

@justsmth

Copy link
Copy Markdown
Contributor

Also address the secure-execution and partial-read issues noted on #3502.

WillChilds-Klein added a commit that referenced this pull request Sep 11, 2026
Stack, split out of #3442, to merge bottom-up:

1. **#3501 -- error-queue primitives (this PR)**
2. #3502 -- policy file reader
3. #3503 -- cipher lists and version bounds
4. #3504 -- groups and signature algorithms
5. #3505 -- post-quantum defaults

## Description

- Adds `ERR_num_errors` and `ERR_pop_to_count`, so code that calls into
libcrypto on a caller's behalf can drop the errors it raised and leave
the queue it was handed untouched.
- The existing mark APIs cannot do this. `ERR_set_mark` needs an entry
to mark, so it is a no-op on an empty queue, and popping to a mark
consumes one the caller had already set.
- `ERR_clear_error` and `ERR_restore_state` rebuild the queue, which
dangles the data pointer the caller got from its last
`ERR_get_error_line_data`.
- A count is a position rather than a mark, so it nests inside a
caller's mark without disturbing it.

## Testing / verification

- New error-queue tests cover popping back to a recorded count, a count
taken from an empty queue, and a count at or above the queue's length.
- One case fills the ring past its capacity to confirm a stale count
leaves the caller's errors alone.
- One case wraps a nested call in the caller's own mark and an error
carrying a data string, then checks the mark still pops and the string
is intact.
- One case pins that a mark does not survive
`ERR_save_state`/`ERR_restore_state`, since a snapshot can be restored
many times and would re-arm a mark nobody set.

By submitting this pull request, I confirm that my contribution is made
under the terms of the Apache 2.0 license and the ISC license.
@WillChilds-Klein
WillChilds-Klein force-pushed the feat/crypto-policies-3-seed-ciphers-versions branch from 6c7c325 to e12618d Compare September 11, 2026 15:03
@codecov-commenter

codecov-commenter commented Sep 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (feat/crypto-policies-2-parser@0ffc3e1). Learn more about missing BASE report.

Additional details and impacted files
@@                       Coverage Diff                        @@
##             feat/crypto-policies-2-parser    #3503   +/-   ##
================================================================
  Coverage                                 ?   78.37%           
================================================================
  Files                                    ?      700           
  Lines                                    ?   125744           
  Branches                                 ?    17390           
================================================================
  Hits                                     ?    98557           
  Misses                                   ?    26315           
  Partials                                 ?      872           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@WillChilds-Klein
WillChilds-Klein force-pushed the feat/crypto-policies-3-seed-ciphers-versions branch from e12618d to 390a732 Compare September 11, 2026 19:53
SSL_CTX_new now applies the policy's CipherString, Ciphersuites, and protocol
bounds when built with ENABLE_CRYPTO_POLICIES. Seeding is best-effort, so it
cannot report failure and must leave the caller's error queue as it found it.

Two directives cannot be applied blind. A failing SSL_CTX_set_cipher_list
installs its empty result before returning, so a rule AWS-LC cannot satisfy
would leave the context with no ciphers rather than the defaults. The version
setters validate each bound only against the method's whole range, so an
inverted pair would be accepted and fail every later handshake.
The policy file was re-read on every SSL_CTX_new; cache the parse keyed on the
resolved path so a changed AWSLC_CRYPTO_POLICY_FILE still misses.

The three internal entry points need OPENSSL_EXPORT to reach ssl_test across a
shared libssl, which -fvisibility=hidden otherwise blocks.

A temp-file write failure now fails the test instead of skipping it; the
platform skip moves to the fixtures.
Every existing case composes its own fixture, so nothing noticed that the
framework's Groups value carries a '*' key-share marker and its Ciphersuites
value names a suite AWS-LC lacks. The fixture now copies Amazon Linux 2023's
DEFAULT file verbatim, and a new suite runs against whatever the host installed:
the AL2023 CI job sets AWSLC_CRYPTO_POLICY_TEST_REQUIRE_SYSTEM, so an absent file
fails there and skips elsewhere.
@WillChilds-Klein
WillChilds-Klein force-pushed the feat/crypto-policies-3-seed-ciphers-versions branch from 390a732 to cb08448 Compare September 11, 2026 20:16
@github-actions

Copy link
Copy Markdown
Contributor

🔒 Security ReviewView Report

Please review before merging.

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.

4 participants