unlock unlimited code runs for $79, limited time →
NOSSLETTER.techLIVE
back to OSSpedia
securityerror-handlingtesting-strategy

quic: support P-384 and P-521 EC certificates

Project: envoyproxy/envoy | Language: C++ | Concepts: security, error-handling, testing-strategy | Difficulty: mid
Link: https://github.com/envoyproxy/envoy/pull/46701

The problem

QUIC listeners in Envoy only supported P-256 elliptic curve (EC) certificates. This prevented users from deploying QUIC with P-384 or P-521 EC certificates, which are sometimes required for compliance or security policies.

Why it's non-trivial

A naive fix would simply add support for the new curves, but this could break existing deployments if there are unexpected compatibility issues. It also risks introducing subtle bugs if the new curves are not handled everywhere that curve selection or validation occurs. Additionally, such a change needs to be guarded so operators can opt out if problems arise.

The approach

1. Add a runtime guard for the new curves

A new runtime guard is registered:

+RUNTIME_GUARD(envoy_reloadable_features_quic_support_additional_ecdsa_curves);

This allows the feature (support for P-384 and P-521) to be toggled at runtime, providing a safety net.

2. Update signature algorithm deduction to support new curves

The function that deduces the signature algorithm from the certificate's public key is modified:

-    // We only support P-256 ECDSA today.
     const EC_KEY* ecdsa_public_key = EVP_PKEY_get0_EC_KEY(public_key);
     // Since we checked the key type above, this should be valid.
     ASSERT(ecdsa_public_key != nullptr);
     const EC_GROUP* ecdsa_group = EC_KEY_get0_group(ecdsa_public_key);
-    if (ecdsa_group == nullptr || EC_GROUP_get_curve_name(ecdsa_group) != NID_X9_62_prime256v1) {
-      *error_details = "Invalid leaf cert, only P-256 ECDSA certificates are supported";
-      break;
-    }
-    // QUICHE uses SHA-256 as hash function in cert signature.
-    sign_alg = SSL_SIGN_ECDSA_SECP256R1_SHA256;
+    const int ec_group_curve_name =
+        ecdsa_group == nullptr ? 0 : EC_GROUP_get_curve_name(ecdsa_group);
+    const bool additional_curves_enabled = Runtime::runtimeFeatureEnabled(
+        "envoy.reloadable_features.quic_support_additional_ecdsa_curves");
+    switch (ec_group_curve_name) {
+    case NID_X9_62_prime256v1:
+      sign_alg = SSL_SIGN_ECDSA_SECP256R1_SHA256;
+      break;
+    case NID_secp384r1:
+      sign_alg = additional_curves_enabled ? SSL_SIGN_ECDSA_SECP384R1_SHA384 : 0;
+      break;
+    case NID_secp521r1:
+      sign_alg = additional_curves_enabled ? SSL_SIGN_ECDSA_SECP521R1_SHA512 : 0;
+      break;
+    default:
+      break;
+    }
+    if (sign_alg == 0) {
+      *error_details = additional_curves_enabled
+                           ? "Invalid leaf cert, only P-256, P-384 or P-521 ECDSA certificates "
+                             "are supported"
+                           : "Invalid leaf cert, only P-256 ECDSA certificates are supported";
+    }

This code checks the curve used in the certificate. If the runtime guard is enabled, it accepts P-384 and P-521; otherwise, it only allows P-256. The error message is also tailored based on the guard.

3. Pass supported curves to context selection

When selecting the TLS context, the set of supported curves is determined by the runtime guard:

+  const Ssl::CurveNIDVector supported_curves =
+      Runtime::runtimeFeatureEnabled(
+          "envoy.reloadable_features.quic_support_additional_ecdsa_curves")
+          ? Ssl::CurveNIDVector{NID_X9_62_prime256v1, NID_secp384r1, NID_secp521r1}
+          : Ssl::CurveNIDVector{NID_X9_62_prime256v1};
   auto [tls_context, ocsp_staple_action] =
-      ctx->findTlsContext(sni, Ssl::CurveNIDVector{NID_X9_62_prime256v1} /* TODO: ecdsa_capable */,
-                          false /* TODO: ocsp_capable */, cert_matched_sni);
+      ctx->findTlsContext(sni, supported_curves, false /* TODO: ocsp_capable */, cert_matched_sni);

This ensures that the server only attempts to use certificates for the permitted curves.

4. Add tests for both guard states

Two new tests are added:

  • One verifies that a P-384 certificate is accepted when the guard is enabled (default).
  • The other verifies that the same certificate is rejected when the guard is explicitly disabled at runtime.
+TEST_F(EnvoyQuicProofVerifierTest, VerifyCertChainSuccessP384ECKey) {
+  ...
+  EXPECT_EQ(quic::QUIC_SUCCESS,
+            verifier_->VerifyCertChain(...))
+      << error_details;
+  EXPECT_NE(verify_details, nullptr);
+  EXPECT_TRUE(static_cast<CertVerifyResult&>(*verify_details).isValid());
+}
+
+TEST_F(EnvoyQuicProofVerifierTest, VerifyCertChainFailureP384ECKeyWithRuntimeGuardDisabled) {
+  TestScopedRuntime scoped_runtime;
+  scoped_runtime.mergeValues(
+      {{"envoy.reloadable_features.quic_support_additional_ecdsa_curves", "false"}});
+  ...
+  EXPECT_EQ(quic::QUIC_FAILURE,
+            verifier_->VerifyCertChain(...));
+  EXPECT_EQ("Invalid leaf cert, only P-256 ECDSA certificates are supported", error_details);
+}

This ensures the feature is both correctly enabled and correctly guarded.

From the review thread

This looks good, but we should probably have a runtime guard to protect the behavior change. Let's add that, please.

@RyanTheOptimist I have added the suggested runtime guard.

Takeaways

  • When expanding cryptographic support, use runtime guards to allow safe rollout and rollback of new algorithms or curves.
  • Always update both validation logic and context selection to ensure new cryptographic options are consistently supported.
  • Comprehensive tests should cover both enabled and disabled states of new features, especially when controlled by runtime flags.