Skip to content

[improve][pip] PIP-489: FIPS 140-3 compliance mode for Apache Pulsar - #26155

Open
david-streamlio wants to merge 5 commits into
apache:masterfrom
david-streamlio:fips-pip-489
Open

david-streamlio wants to merge 5 commits into
apache:masterfrom
david-streamlio:fips-pip-489

Conversation

@david-streamlio

@david-streamlio david-streamlio commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

PIP: this PR

Motivation

Pulsar has no supported FIPS 140-3 deployment path today, despite being most of the way there at the JCA layer (SecurityUtility already resolves BC vs. BC-FIPS reflectively). A full audit of master found the gaps documented in the proposal: the BC/BC-FIPS swap lost its packaging story in the Gradle migration; the broker binary listener cannot be steered off BoringSSL (tlsProvider is declared but never wired); Conscrypt is the shipped web-TLS default; several security paths use non-approved algorithms (SHA-1 OAEP and ECIES in MessageCryptoBc, MD5-crypt/DES-crypt in Basic auth, a non-HMAC construction in SaslRoleTokenSigner); and there is no FIPS documentation or meaningful FIPS test coverage.

Modifications

Adds pip/pip-489.md, proposing a supported, tested, documented FIPS mode deployment profile: TLS provider wiring for every listener (matching the proxy's existing pattern), a FIPS distribution variant / documented jar swap, a fipsMode fail-fast startup validator, metadata-negotiated migration to RSA_OAEP_SHA256 key wrapping (with ECDH_AES_KW replacing ECIES in a later phase), SHA-2-crypt Basic auth, a dual-verify HMAC migration for the SASL role-token signer, a name-stable fix for the SHA-1-derived Kubernetes resource names, and a real FIPS TLS integration test group.

Companion quick-win PRs already open (intentionally outside the PIP): #26152, #26153, #26154.

A DISCUSS thread has been started on [email protected] referencing this PR.

@lhotari

lhotari commented Jul 7, 2026

Copy link
Copy Markdown
Member

I wanted to flag some overlap so we can keep the efforts aligned. PIP-478 has been in progress and, besides the asynchronous authentication interfaces on the client side, it covers the TLS transport aspects of Pulsar 5.0 on both the client and the server side. The dependency between the auth and the TLS parts is explained in the PIP document (PIP-478: #25890, discussion thread: https://lists.apache.org/thread/s9n9jksr9vqgn9o982zmnnkcxdcncy3f).

On the TLS transport side there are more concerns than configuring the JCA provider. Stricter security compliance -- for example FIPS 140-3 Level 3 -- requires an HSM: the key material is stored in a hardware device and kept outside the application, so it never crosses into the Pulsar process. This can be achieved with the PulsarTlsFactory plugin interface introduced in PIP-478: a plugin implementation can integrate with a security solution that meets such requirements (for instance building the TLS context against an HSM via a PKCS#11 token, so the private key never leaves the device). For the simpler FIPS 140-3 Level 1 case, PIP-478 also lets you configure the JCA provider directly (for example a FIPS-validated provider such as BC-FIPS on the JDK TLS engine), and it wires the engine/provider selection through every server component and the client.

PIP-478 also adds a PulsarHttpClient API. The Pulsar client today uses several HTTP clients -- for example authentication plugins such as OAuth2 that call a token endpoint -- which currently don't share a centralized configuration. The PulsarHttpClient API is added for authentication plugin implementations so that the TLS configuration of those HTTP clients can be controlled by the PulsarTlsFactory when there are special requirements, or handled through the Pulsar v5 client's TlsPolicy configuration when there are none. This keeps the TLS transport (and FIPS) configuration consistent across the client's outbound TLS, not only the binary protocol and the web/admin listeners.

Given that overlap, I'd suggest that PIP-489 builds upon PIP-478 for the TLS transport aspects rather than defining a separate TLS transport configuration path. That would let PIP-489 concentrate on the broader FIPS-compliance concerns that PIP-478 intentionally leaves out of scope -- FIPS-approved algorithms in message encryption and authentication, the FIPS distribution/packaging, and a fail-fast FIPS-mode validation switch -- while reusing the TLS transport foundation.

On the packaging point specifically: I don't think the Gradle migration actually lost anything essential there. BC and BC-FIPS can't co-exist on the classpath, so switching between them is really a matter of replacing the non-FIPS BC jars with the FIPS ones. The old bouncy-castle/bcfips module wasn't especially useful for that swap in practice -- a client can simply exclude the non-FIPS BC dependencies and add the FIPS ones, and on the server side it can be handled by keeping the non-FIPS and FIPS jars in separate directories and choosing which one to put on the classpath based on configuration. So the FIPS packaging story seems fairly tractable without reintroducing a dedicated swappable module.

@lhotari

lhotari commented Jul 7, 2026

Copy link
Copy Markdown
Member

One more bit of history that may help scope PIP-489: FIPS / BouncyCastle-FIPS support in Pulsar has been partial and handled case by case in the past, rather than as a coherent, tested capability -- which is a big part of why a proper end-to-end FIPS design is worth doing.

Two concrete examples:

  1. Pulsar has never actually used BouncyCastle's JSSE (TLS) provider. A pulsar-site correction (Remove TLS/JSSE statement in BouncyCastle intro pulsar-site#974) removed a docs statement that implied otherwise, noting "Pulsar does not implement bouncy castle jsse; there is no dependency on bc jsse in Pulsar." So even with the BC-FIPS jars on the classpath, the TLS transport did not route through a BouncyCastle/FIPS TLS provider -- the FIPS story for the transport itself was never wired.

  2. On the message-encryption side, [improve][misc] Improve AES-GCM cipher performance #23122 switched AES-GCM in MessageCryptoBc from the BouncyCastle provider to SunJCE for performance. In reviewing it ([improve][misc] Improve AES-GCM cipher performance #23122 (review)) I raised the FIPS concern -- when the FIPS library is enabled, SunJCE should not become the default -- and the finding was that the existing code already ignored the presence of BouncyCastleFipsProvider. In other words FIPS compliance was already a separate, not-fully-handled concern rather than something the crypto paths consistently respected.

The takeaway is that FIPS support in Pulsar has been incomplete and piecemeal: the TLS transport was never routed through a FIPS provider, and the crypto paths didn't consistently honor the FIPS provider when it was present. That's exactly the gap worth closing, and it splits naturally: PIP-478 covers the TLS-transport slice (a configurable engine plus any JCA provider via TlsPolicy.jcaProvider, and the PulsarTlsFactory plugin for HSM-backed / FIPS 140-3 Level 3 cases), and PIP-489 can cover the broader FIPS-mode concerns -- FIPS-approved algorithms in message encryption and authentication, packaging, and a fail-fast validation switch -- so the crypto paths consistently respect FIPS rather than silently falling back to non-validated providers.

…ore/TLS-version/cipher validation and java.security guidance
@david-streamlio

Copy link
Copy Markdown
Contributor Author

Thanks @lhotari — this is exactly the alignment I was hoping to get before the VOTE, and I agree with the split you propose. I've updated the PIP (95ac5af) accordingly:

  • TLS transport now builds on PIP-478. Design section (a) is rewritten: PIP-489 no longer defines its own TLS provider wiring. FIPS mode consumes PIP-478's TlsPolicy (JDK engine + JCA provider), PulsarTlsFactory covers the HSM/PKCS#11 (140-3 Level 3) case, and PulsarHttpClient brings the auth plugins' outbound HTTPS under the same configuration — that last one closes a gap I hadn't addressed (OAuth2/OIDC token-endpoint calls going through the default SSLContext). The per-listener one-line .tlsProvider(...) pass-throughs are kept only as a documented interim option for maintenance branches, explicitly not a deliverable here. PIP-489 concentrates on what PIP-478 leaves out of scope: approved algorithms in message crypto/auth, packaging, and the fail-fast fipsMode validator.
  • Packaging stays a jar-swap story. I've noted in General Notes that flavor selection is a classpath-assembly concern (client-side dependency exclusion/replacement; server-side separate directories or the fipsRuntimeClasspath variant) rather than reintroducing the Maven-era swappable modules.
  • Your history points are captured in Background — no BC JSSE dependency ever existed (pulsar-site#974), and the crypto paths haven't consistently honored an installed FIPS provider ([improve][misc] Improve AES-GCM cipher performance #23122) — as motivation for making FIPS a coherent, validated capability instead of case-by-case fixes.

While revising I also tightened the fipsMode validator to cover deployment posture, not just provider selection: keystore/truststore types (JKS rejected; PKCS12 documented unsupported — FIPS-conformant PKCS#12 needs the RFC 9879 KDF/PBMAC profile), a TLS 1.2 floor, cipher-suite and certificate-algorithm screening, and the java.security / BCFKS-converted-cacerts guidance in the deployment docs.

One sequencing question for you: given PIP-489's phase 1 depends on PIP-478's TlsPolicy landing, do you see any issue with the two PIPs proceeding through DISCUSS/VOTE in parallel, with 489's TLS-dependent items gated on 478's implementation?

… not missing machinery

The first gap bullet claimed the BC/BC-FIPS swap "lost its packaging story in
the Gradle migration", which reads as though the Maven-era swappable
bouncy-castle-bc / bouncy-castle-bcfips NAR modules need resurrecting. They do
not, and this PIP never proposed to: BC and BC-FIPS are mutually exclusive on a
classpath, so the swap is a matter of shipping one set of jars instead of the
other, and SecurityUtility's reflective loading already accepts either flavor.
Raised on the DISCUSS thread.

The bullet now concedes that mechanics up front, states explicitly that the NAR
modules are not coming back, and locates the actual gap where it belongs: no
supported route for the FIPS artifacts to reach a server runtime (the server
distribution excludes bc-fips outright), no server-side coverage (the only
consumer is a client-side JCA-swap test), and no documentation telling an
operator which jars to swap.

Also drops the pinned FIPS version numbers and the libs.versions.toml line
reference, both of which go stale when the BouncyCastle upgrade in apache#26349
lands; the surviving citation is the distribution/server exclusion, which is
the load-bearing evidence.
PIP-478 has landed since this proposal was written, and it moved or closed several of the things
the Motivation section cites as current state. Reviewers who check the first claim and find it
already fixed will discount the rest, so the audit is done against master rather than left to them.

SecurityUtility is gone (apache#26322 deleted it with the rest of the PIP-337 TLS stack), and the PIP
leaned on it in six places -- the Background paragraph establishing that Pulsar is already
FIPS-capable at the JCA layer, the packaging jar-swap rationale, the EC curve allowlist, the
fipsMode validator's first assertion, the High Level Design, and the integration test. Each now
names JcaProviders, whose ResolvedBouncyCastleProvider(Provider, boolean fips) reports the flavour
as a value rather than a static predicate. Its registration-first resolution order is recorded too:
a provider an operator registered in java.security is preserved rather than shadowed by a
classpath-constructed one, which is the property a FIPS deployment depends on.

Motivation item 2 is closed. PulsarSslConfiguration, PulsarSslFactory and DefaultPulsarSslFactory
no longer exist; every listener builds from a PulsarTlsFactory, and the broker binary listener
carries engineProvider(TlsFactorySupport.engineProvider(conf.getTlsProvider())). Rewritten to what
survives -- an unset tlsProvider still resolves to the native engine, because tcnative remains an
unconditional pulsar-common dependency, so FIPS mode must pin the engine and drop the jar.

Motivation item 3 is stale in form: the declared defaults are now empty rather than the literal
Conscrypt, and unset *means* Conscrypt only through resolveWebJsseProvider, on the web listeners.
The concern survives; the citations did not.

Items 1, 4, 5 and 6 were re-verified and hold, with line numbers corrected. Item 1 gains the
version catalog's own statement of the gap (libs.versions.toml:62-63).

Adds the gap the PIP did not list: DefaultBrokerTlsFactory resolves its JSSE provider as
resolveJsseProvider(null, ...), so the broker's jsseProvider and brokerClientJsseProvider keys are
declared but ignored -- item 2's shape in a newer key. apache#26326 fixes it and adds the jcaProvider
configuration surface that design (a) assumes, so the dependency is now stated rather than implied.

Housekeeping: apache#26152 and apache#26153 are merged, not open; apache#26154 no longer claims the KeyManagerProxy
half, which PIP-478 obsoleted; and the Configuration table's four pre-PIP-478 provider rows become
two rows naming the keys fipsMode actually evaluates.
@david-streamlio

Copy link
Copy Markdown
Contributor Author

@lhotari picking this back up now that its dependency has landed.

The sequencing question I left you in July — whether 489 could go through DISCUSS/VOTE in parallel
with 478, gated on 478's implementation — answered itself when PIP-478 merged on 3 August. So
there's nothing outstanding on you from that thread; I should have said so at the time rather than
leaving it hanging.

I have since reconciled the document against post-478 master rather than leaving it describing the
tree as it was in July:

  • 1912da5 — reworks the gap list against what 478 actually landed. The substantive correction is
    that SecurityUtility.isBCFIPS() no longer exists: [improve][misc] PIP-478: remove the superseded PIP-337 TLS stack #26322 deleted it with the rest of the PIP-337
    TLS stack, and the equivalent is now JcaProviders returning a
    ResolvedBouncyCastleProvider(provider, fips). The registration-first ordering there is the
    FIPS-relevant half — a provider an operator registered in java.security is preserved rather than
    shadowed by a classpath-constructed one — so the PIP now builds on that rather than on a class
    that has been removed.
  • 4990047 — reframes the packaging item as missing support rather than as regression from the
    Gradle migration, which is the framing you argued for and I think you were right.

The scope split you proposed is intact: 489 takes the TLS transport from 478's TlsPolicy /
PulsarTlsFactory / PulsarHttpClient and confines itself to what 478 left out — approved
algorithms in message crypto and auth, the packaging story, and the fail-fast fipsMode validator.

What it needs now is a review. It has none, and I have not opened a VOTE thread yet — starting one
cold against a document nobody has read since July seemed like the way to get a thread with no
replies in it. If you have the time to look at the reconciled version I will start the vote after
that; if you would rather someone else take it, a steer on who would help just as much.

No urgency on my side — it has waited a month, another week will not hurt it.

@lhotari lhotari left a comment

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.

Read the reconciled document at 1912da5dfa end to end and checked its current-state claims against master at bef7b8f9. This is the first review the PIP has had, so it is a long one; the short version is that the correction you asked me to check is right, the scope split holds, and there is more to do before a vote than the reconciliation itself.

A concrete classpath proposal for the standard distribution. I'd like Pulsar to ship the dependencies for both modes out of the box, with PULSAR_FIPS_MODE=true selecting the FIPS dependencies at startup. General Notes already mentions separate directories and startup selection (pip-489.md:633-637); I would make that the supported packaging design rather than requiring a manual jar swap or a separate tarball.

The distribution would contain three sibling directories:

  • lib/: dependencies shared by both modes, with neither BC flavor nor the mode-specific native TLS jars here.
  • lib-fips/: bc-fips, bcpkix-fips, bcutil-fips, and any other dependencies selected for FIPS mode.
  • lib-non-fips/: ordinary BC jars, netty-tcnative-boringssl-static, and Conscrypt (conscrypt-openjdk-uber).

To match the actual launcher: bin/pulsar currently appends $PULSAR_HOME/lib/* to PULSAR_CLASSPATH in its installed-distribution branch (pulsar:212-216). That block could become:

if [ -d "$PULSAR_HOME/lib" ]; then
    PULSAR_CLASSPATH="$PULSAR_CLASSPATH:$PULSAR_HOME/lib/*"
    if [ "${PULSAR_FIPS_MODE:-false}" = "true" ]; then
        PULSAR_CLASSPATH="$PULSAR_CLASSPATH:$PULSAR_HOME/lib-fips/*"
    else
        PULSAR_CLASSPATH="$PULSAR_CLASSPATH:$PULSAR_HOME/lib-non-fips/*"
    fi
else
    add_gradle_deps_to_classpath
fi

The dependency portion of the Java classpath would then be:

PULSAR_FIPS_MODE=true:
  -cp "lib/*:lib-fips/*"

PULSAR_FIPS_MODE unset (or false):
  -cp "lib/*:lib-non-fips/*"

These are dependency-only shorthand, not replacements for the entire -cp argument. The script still adds PULSAR_JAR, PULSAR_EXTRA_CLASSPATH, and the logging-configuration directory (pulsar:262-263), and passes the resulting PULSAR_CLASSPATH through OPTS="-cp $PULSAR_CLASSPATH $OPTS" (pulsar:321). With the distribution installed at /pulsar in Docker (Dockerfile:203), the selected dependency segment is /pulsar/lib/*:/pulsar/lib-fips/* or /pulsar/lib/*:/pulsar/lib-non-fips/*.

This also handles tcnative/BoringSSL and Conscrypt: package their jars in lib-non-fips/, and they remain in the distribution but are absent from the selected FIPS classpath. They must not also be copied into common lib/ or reintroduced through extra classpath settings. The PIP's grpc-netty-shaded/grpc-netty choice should follow the same separation so the shaded native copy is not left in the common dependencies. (We should replace grpc-netty-shaded with unshaded grpc-netty dependency in any case since the reasons to have it no longer exist.)

This sketch covers the installed distribution. The existing Gradle fallback, used when lib/ is absent, needs equivalent dependency selection for source-tree launches. The script also supplies this classpath as pulsar.functions.instance.classpath (pulsar:368); test both modes from the assembled archive, including a function runtime and any additional dependencies it loads. The provider configuration and approved-only settings described elsewhere in the PIP still apply.

The SecurityUtility.isBCFIPS() correction is right, in full. SecurityUtility is gone from the tree entirely - #26322 removed it on 2026-08-15 - ResolvedBouncyCastleProvider(Provider provider, boolean fips) is a record at JcaProviders.java:98, and the registration-first ordering is exactly what the new Background paragraph describes: an already-registered BC or BCFIPS provider wins before anything is constructed from the classpath.

private static ResolvedBouncyCastleProvider loadBouncyCastleProvider() {
Provider installed = Security.getProvider(BC);
if (installed == null) {
installed = Security.getProvider(BC_FIPS);
}
if (installed != null) {
log.debug().attr("provider", installed.getName()).log("Already instantiated Bouncy Castle provider");
return toResolvedProvider(installed);
}
// Not installed, try to load from the classpath. Absence is not an error here — it is only an error
// for a caller that needs Bouncy Castle, which requireBouncyCastleProvider() reports.
try {
return toResolvedProvider(getBCProviderFromClassPath());
} catch (Exception e) {
log.debug().exception(e)

One sentence worth adding while you are in that paragraph, because it turns two of your items from precautionary into necessary: the classpath fallback prefers the non-FIPS artifact - BouncyCastleProvider first, BouncyCastleFipsProvider only as a fallback. A deployment that assembles a FIPS classpath but still pulls bcprov in transitively therefore resolves silently to the non-validated provider, with nothing in the log to say so. That is the single strongest argument for both the packaging work in (b) and the fips() assertion in (g), and right now it is left implicit.

The scope split holds. Design (a) consumes TlsPolicy / PulsarTlsFactory / PulsarHttpClient and defines no TLS configuration of its own; the Configuration table is explicit that its two provider rows add no keys. The only residue is the Upgrade section's mention of the interim maintenance-branch keys (:572-573) and Alternative 6 deferring a webServiceTlsProvider default (:621-623) - both are PIP-478's to change rather than this PIP's, which is worth a clause so nobody reads them as deliverables here.

Other current-state claims I checked rather than took on trust, and that hold: the "false-confidence FIPS test" characterisation of tests/pulsar-client-test-bcfips is accurate (the module excludes the non-FIPS BouncyCastle jars but excludes neither netty-tcnative-boringssl-static nor Conscrypt, and sets no org.bouncycastle.fips.approved_only); the General Notes claim that JcaKeyStores never asks for JKS is right, with a qualifier that turns out to matter; the &s=v2: dispatch in (e) is unambiguous, for a better reason than the one given; and the full-hash-as-salt semantics in (d) are correct even though the method named does not exist.

The findings, in the order I would work them. Details are in the inline comments.

  1. (c)'s stated failure mode is wrong, and the opt-in is irreversible for the retention window. Old consumers do not land in "retry/DLQ paths" - the default action is FAIL, which discards the message and never delivers or acks it. And rollback is conditioned on producer state when the constraint is really about persisted backlog, in every replicated cluster.
  2. Phase 1's validator asserts things phase 2 delivers. On Basic auth it does worse than fail: it green-lights a broker whose password file it has just declared FIPS-clean and which cannot authenticate anyone.
  3. fipsMode screens the configured keystore types, not the carrier the TLS stack builds - which on a PEM deployment is JKS or PKCS12, the two types the PIP rejects.
  4. (d) collapses to deleting one .substring(0, 2), and the API it builds its design around does not exist.
  5. The packaging deliverable never says which bc-fips jar, and certified-versus-patched is the first question an auditor asks. The catalog already documents the tradeoff. The classpath example above makes startup selection concrete; inline 11 covers the packaging implications.
  6. The reconciliation is itself about two weeks behind master - #26326 and #26154 both merged before this commit was pushed, so the PIP no longer has any dependency on unmerged work, but five passages still say it does.
  7. Shape questions I would settle before the key is public: fipsMode as a boolean with an open-ended, unversioned check list; saslRoleTokenSignerAlgorithm encoding two axes in one enum.
  8. (f) contradicts its own Motivation, and adds a hand-written SHA-1 for a hypothetical.
  9. Two missing Alternatives, one of which would change the shape of the whole proposal.
  10. Line-citation drift, plus a suggestion about citing symbols instead.

None of this is an argument against the proposal - the goal is right and most of the design is sound. Answers to the two questions at the end of your comment are in a reply on the conversation thread.

Comment thread pip/pip-489.md
|---|---|
| (a) TLS transport | Follows PIP-478's compatibility contract (`TlsPolicy` and the legacy keys' migration path). No default behavior change from this PIP; `fipsMode=true` only adds validation. |
| (b) Packaging | Standard distribution unchanged. FIPS variant is additive. |
| (c) Message crypto | Producer default remains SHA-1 OAEP → zero wire change until opted in. **Opt-in ordering constraint: upgrade all consumers of a topic before any producer sets `RSA_OAEP_SHA256`** — old consumers cannot unwrap SHA-256-wrapped keys (they will land in retry/DLQ paths per existing crypto-failure handling). Decrypt side reads both formats forever. |

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.

[CORRECTNESS] (c)'s stated failure mode is not what the client does, and the rollback precondition cannot be satisfied

"they will land in retry/DLQ paths per existing crypto-failure handling" is not what the client does, and the difference matters for the operational contract in (c).

The default failure action is FAIL - ConsumerBuilderImpl.java:161-163 sets it when neither cryptoFailureAction nor decryptFailListener is configured - and the FAIL branch does this:

case FAIL:
if (cryptoReaderNotExist) {
log.error().attr("messageId", messageId)
.log("Message delivery failed since"
+ " CryptoKeyReader interface is not"
+ " implemented to consume encrypted"
+ " message");
} else {
log.error().attr("messageId", messageId)
.log("Message delivery failed since unable to decrypt incoming message");
}
MessageId m = new MessageIdImpl(messageId.getLedgerId(), messageId.getEntryId(), partitionIndex);
unAckedMessageTracker.add(m, redeliveryCount);
return DecryptResult.discard();

DecryptResult.discard() then returns out of messageReceived before any delivery, negative-ack, retry-topic or DLQ logic runs (ConsumerImpl.java:1478-1484). So a consumer that cannot unwrap a RSA_OAEP_SHA256 key does not dead-letter the message: it never sees it, never acks it, and the subscription's mark-delete position stops advancing past it. With ackTimeout set, unAckedMessageTracker.add turns that into an unbounded redelivery loop for the whole retention period. Neither outcome is visible as a DLQ an operator can drain.

Second, and I think more important: :585-587 makes rollback conditional on producer state, but the constraint is about persisted data. "provided no producer is still emitting RSA_OAEP_SHA256-wrapped keys" is not a condition anyone can establish - the wrapped keys are in the topic, so retention, backlog, replay from an earlier position, and (per :594-596) every geo-replicated copy keep SHA-256-wrapped messages unreadable to a rolled-back consumer for the whole retention window. There is no operational step that makes it true and no way to check it.

Both are fixable in prose, and the honest version is stronger than the current one:

  • in the compatibility table at :562, say what actually happens (the message is not delivered and the subscription stalls at it; with an ack timeout, it redelivers indefinitely) instead of "retry/DLQ paths";
  • in Downgrade/Rollback, state the precondition in terms of backlog and retention across all replicated clusters, and say plainly that opting a producer into RSA_OAEP_SHA256 is not reversible until the last SHA-256-wrapped message has aged out everywhere.

If that is too sharp an edge for an opt-in, the alternative is a consumer capability signal rather than an ordering instruction - but I would rather see the ordering instruction stated accurately than replaced with machinery, given that it is opt-in and off by default.

Comment thread pip/pip-489.md
(P-256/P-384/P-521), RSA ≥ 2048 bits, no SHA-1 signatures — converting deep handshake
failures into actionable startup errors;
- Basic-auth password file (if that provider is enabled) contains only `$5$`/`$6$` entries;
- `saslRoleTokenSignerAlgorithm` is `HmacSHA256`/`HmacSHA256-strict` (if SASL is enabled).

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.

[DESIGN] the phase-1 validator asserts phase-2 features, and on Basic auth it green-lights a broker that cannot authenticate anyone

These two checks assert features that phase 2 delivers, while (g) itself is phase 1 (:647-652).

  • saslRoleTokenSignerAlgorithm is introduced by (e), phase 2. In a phase-1 release the key does not exist, so this check is either dead or unsatisfiable - fipsMode=true with SASL enabled could never start.
  • The Basic-auth check is worse than unsatisfiable, because it passes. (d) - phase 2 - is what teaches AuthenticationProviderBasic to verify $5$/$6$ entries. In a phase-1 release a $6$-only password file satisfies this bullet, the broker starts and logs the security-posture summary at :527-532 saying the FIPS check passed, and every login then fails, because authenticate still routes the entry to the DES branch:

// For md5 algorithm
if ((users.get(userId).startsWith("$apr1"))) {
List<String> splitEncryptedPassword = Arrays.asList(encryptedPassword.split("\\$"));
if (splitEncryptedPassword.size() != 4 || !encryptedPassword
.equals(Md5Crypt.apr1Crypt(password.getBytes(), splitEncryptedPassword.get(2)))) {
errorCode = ErrorCode.INVALID_TOKEN;
throw new AuthenticationException(msg);
}
// For crypt algorithm
} else if (!encryptedPassword.equals(Crypt.crypt(password.getBytes(), encryptedPassword.substring(0, 2)))) {
errorCode = ErrorCode.INVALID_TOKEN;
throw new AuthenticationException(msg);

A validator that green-lights a broker which cannot authenticate anyone is the same class of outcome as the silent non-compliance the fail-fast design exists to prevent (:543-545) - it just fails in the other direction.

Either move these two checks into phase 2 beside the features they assert, or pull (d) and (e) into phase 1. Either way, "Each phase is independently shippable" at :652 needs replacing with the actual dependency statement - the phases are shippable in order, which is a weaker and true claim.

Comment thread pip/pip-489.md
- every configured keystore/truststore type is FIPS-capable — BCFKS, PKCS11, or PEM. `JKS` is
rejected (its integrity/privacy algorithms are not FIPS-approved), and `PKCS12` is rejected
and documented as unsupported (FIPS-conformant PKCS#12 requires the RFC 9879 KDF/PBMAC
profile, which the current parsing stack does not enforce);

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.

[DESIGN] fipsMode screens the configured keystore types, but the carrier keystore the TLS stack builds for PEM material is JKS or PKCS12

This bullet screens the operator-facing tlsKeyStoreType / tlsTrustStoreType family. A PEM deployment - which the High Level Design explicitly blesses at :216-217 ("Keystores may be PEM, BCFKS, or PKCS11") - configures no store type at all, so it satisfies the check vacuously. But the JDK TLS path still materialises PEM material into a process-local carrier keystore, and that carrier's type is decided here:

public static String inMemoryStoreType(Provider jcaProvider, String defaultType) throws KeyStoreException {
if (jcaProvider == null) {
return defaultType;
}
for (String candidate : IN_MEMORY_STORE_TYPE_PREFERENCE) {
if (jcaProvider.getService("KeyStore", candidate) != null) {
return candidate;
}
}
throw new KeyStoreException("jcaProvider='" + jcaProvider.getName() + "' supplies none of the in-memory "
+ "carrier keystore types " + IN_MEMORY_STORE_TYPE_PREFERENCE + " needed to build the TLS key "
+ "managers. Types this provider registers: " + registeredTypes(jcaProvider, "KeyStore")
+ ". Unset jcaProvider, or pin a provider that supplies BCFKS or PKCS12.");
}

With no jcaProvider pinned, inMemoryStoreType returns the caller's default - and there are two callers with two different defaults:

  • KeyStoreHolder, which JdkSslContexts uses to build the key and trust managers, passes KeyStore.getDefaultType(), controlled by the JVM's keystore.type setting;
  • TlsKeyStoreLoader.toInMemoryKeyStore passes the literal "PKCS12".

A caller default of JKS or PKCS12, and the explicit PKCS12 carrier, would use types this bullet rejects. As written, fipsMode=true can pass on a deployment whose TLS key material is sitting in a JKS carrier - the "FIPS-shaped rather than FIPS-compliant" outcome the switch exists to prevent.

The keystore.type=bcfks guidance in the deployment guide fixes the first, because it changes KeyStore.getDefaultType(); it does not touch the second, which is a hardcoded literal. And a java.security line an operator may or may not have written is not what a fail-fast validator is for.

The fix makes the validator stronger rather than longer: have fipsMode require jcaProvider (and brokerClientJcaProvider) to be pinned to a provider that registers BCFKS, instead of only screening configured store types. Under a pinned BC-FIPS provider inMemoryStoreType selects BCFKS at both call sites and the problem disappears. It also resolves an ambiguity in the current wording: "PKCS12 is rejected" does not say whether it reaches the carrier, and if it does, the PKCS12 entry in the carrier preference list has to be unreachable in FIPS mode rather than merely second choice.

Comment thread pip/pip-489.md
salts are variable-length and may carry a `rounds=N$` prefix (e.g.
`$6$rounds=10000$saltsalt$hash`). The implementation passes the entire stored hash as the salt
argument (`Sha2Crypt.crypt(password, storedHash)` semantics), which commons-codec resolves
correctly, then constant-time-compares the result — mirroring how the `$apr1$` branch works but

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.

[SIMPLIFICATION] (d) collapses to deleting one .substring(0, 2); the API it is designed around does not exist

Worth checking this section against commons-codec before writing any of it: Crypt.crypt(byte[], String) already is the prefix dispatcher this design proposes to build. From the bytecode of the 1.21.0 jar this build resolves (the catalog pins commons-codec = "1.22.1"; the dispatch has been there for many releases):

crypt(byte[] keyBytes, String salt):
    salt == null            -> Sha2Crypt.sha512Crypt(keyBytes)
    salt.startsWith("$6$")  -> Sha2Crypt.sha512Crypt(keyBytes, salt)
    salt.startsWith("$5$")  -> Sha2Crypt.sha256Crypt(keyBytes, salt)
    salt.startsWith("$1$")  -> Md5Crypt.md5Crypt(keyBytes, salt)
    otherwise               -> UnixCrypt.crypt(keyBytes, salt)   // DES

The only reason $5$/$6$ do not work in Pulsar today is that the existing call truncates the salt to two characters:

// For crypt algorithm
} else if (!encryptedPassword.equals(Crypt.crypt(password.getBytes(), encryptedPassword.substring(0, 2)))) {
errorCode = ErrorCode.INVALID_TOKEN;
throw new AuthenticationException(msg);

So the whole of (d)'s new code is: pass encryptedPassword where encryptedPassword.substring(0, 2) is passed now. That is a much better story for the PIP than the one currently written, and it removes the need for the design paragraph at :367-373 entirely.

Two corrections that follow:

  • Sha2Crypt.crypt(password, storedHash) (:372) does not exist. Sha2Crypt's public surface is sha256Crypt(byte[]), sha256Crypt(byte[], String), sha256Crypt(byte[], String, Random) and the three sha512Crypt overloads - no crypt. The semantics the sentence describes are right (Sha2Crypt.SALT_PATTERN ends in .*, so passing the full stored hash as the salt works, rounds=N$ included); only the entry point is wrong, and the right one is Crypt.crypt.
  • The same one-line change also starts accepting $1$ MD5-crypt entries, which today reach the DES branch with a 2-character salt and therefore can never verify. An entry that has always denied every password would start allowing the right one. That is a real behaviour change and belongs in the compatibility table at :563 - and it is a good argument for fipsMode's startup scan rejecting $1$ and $apr1$ explicitly rather than by omission.

What survives from (d) is the part that is genuinely Pulsar's: the fipsMode startup scan of the password file, and the choice to validate at startup rather than per request.

Comment thread pip/pip-489.md
[discussion](https://lists.apache.org/thread/s9n9jksr9vqgn9o982zmnnkcxdcncy3f)) reworks the TLS
transport configuration for Pulsar 5.0 on both the client and the server. **It has since landed on
`master`**, which closes several gaps this PIP originally enumerated (noted per item below); the
remaining follow-ups, including the `jcaProvider` configuration surface, are in

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.

[ACCURACY] every remaining dependency on unmerged work has merged; five passages still describe master as it stood in August

#26326 merged on 2026-08-27 and #26154 merged on 2026-08-28 - both before this reconciliation commit was pushed. The net effect is good for the PIP: it now has no dependency on unmerged work at all. But five passages still describe master as it stood in August, and one of them is a falsifiable claim about code.

  • This line - "the remaining follow-ups, including the jcaProvider configuration surface, are in #26326".
  • :159-162 - "the outstanding piece it does depend on is #26326, which honors the jsseProvider keys and adds the jcaProvider ones."
  • :241-243 - "on master today the axis is reachable only programmatically through the v5 builder". It is not: ServiceConfiguration.getJcaProvider() is read at DefaultBrokerTlsFactory.java:186, and conf/proxy.conf ships a commented-out jcaProvider= key.
  • :257-261 - "DefaultBrokerTlsFactory resolves its JSSE provider as resolveJsseProvider(null, conf.getTlsProvider()), passing null for the explicit axis, so the broker's own jsseProvider / brokerClientJsseProvider keys are declared but ignored". master reads DefaultBrokerTlsFactory.java:148 - resolveJsseProvider(conf.getJsseProvider(), conf.getTlsProvider()) - and the broker-client leg reads conf.getBrokerClientJsseProvider() a few lines below. The whole bullet, including "FIPS mode depends on that axis, so this PIP assumes #26326", can go.
  • :501-502 and :506-508 - the Configuration table's two provider rows ("Declared but ignored on master", "Added by #26326") and the paragraph beneath it.

Worth fixing for exactly the reason the July reconciliation was worth doing: the gap list is the current-state claim a voter checks, and this one is now checkable against a tree that says something else.

Comment thread pip/pip-489.md
or unset; each comparison already uses `MessageDigest.isEqual` (constant-time) and continues to.
- New setting `saslRoleTokenSignerAlgorithm` = `SHA-512` (default, sign legacy / verify both) |
`HmacSHA256` (sign HMAC / verify both) | `HmacSHA256-strict` (sign and verify HMAC only;
implied by `fipsMode=true`).

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.

[DESIGN] saslRoleTokenSignerAlgorithm encodes two axes in one enum, and one of its values duplicates fipsMode

saslRoleTokenSignerAlgorithm encodes two independent axes in one enum: which algorithm to sign with, and whether to accept legacy signatures. That shape does not survive a second algorithm - HmacSHA384 would need both HmacSHA384 and HmacSHA384-strict, and the value space becomes a mix of a digest name (SHA-512, which is not a MAC algorithm name), MAC names, and a policy suffix. A 3-value enum that is really 2xN is the kind of thing worth getting right before the key is public.

-strict also duplicates something the design already expresses: :398 says it is implied by fipsMode=true, and the PIP does not say whether a non-FIPS operator who wants strict mode exists. If they do, name them and give it its own boolean; if they do not, saslRoleTokenSignerAlgorithm = SHA-512 | HmacSHA256 for the signing algorithm and fipsMode for legacy acceptance covers everything with one fewer value and no suffix grammar.

Separately, and as a positive: the &s=v2: dispatch claim at :391-393 is sound, but for a better reason than the one given. The document argues from the Base64 alphabet's "output position", which is a bit indirect. The direct arguments are that SaslRoleToken.toString() emits u=<role>&i=<session>&e=<expires> - the session attribute is i, not s - so &s= occurs exactly once; that verifyAndExtract uses lastIndexOf(SIGNATURE) regardless; and that the legacy signature is Base64.getEncoder() output over a 64-byte digest, so 88 characters from A-Za-z0-9+/=, containing neither : nor &. Stating those three makes the claim robust against a future change to SaslRoleToken's attribute letters, which the current phrasing would not survive.

Comment thread pip/pip-489.md
crypto.** A deployment can pair a validated OpenSSL 3 FIPS provider (driving a native TLS
engine) with BC-FIPS for the Java crypto paths. Viable for a curated image, but for the
upstream distribution it doubles the validated-module boundary to document and keeps a
native TLS engine inside the audit scope; the single-module JDK-over-BC-FIPS path is

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.

[DESIGN] two alternatives are missing, one of which would change the shape of the whole proposal

Two alternatives are missing, and one of them would change the shape of the whole proposal.

A validated JVM or PKCS#11 provider instead of BC-FIPS. SunPKCS11 over a CMVP-validated NSS or OpenSSL FIPS token, or a vendor JDK that ships a validated provider, is a mainstream FedRAMP path. It would avoid the BC jar swap entirely - and with it the shaded-client conflict at :290-294, the second tarball at :283-285, and the certified-versus-patched bc-fips question. The PIP treats BC-FIPS as the only Java-crypto answer without arguing why, and it is already assuming half the mechanism: PIP-478's PulsarTlsFactory PKCS#11 hook is cited at :243-245 for the Level 3 case. Even if the answer is "BC-FIPS, because the jar swap is simpler than provisioning a token", that argument is worth making where a reader can weigh it.

Documentation and tests only - no fipsMode key, no distribution variant. Given Motivation 1's own "the gap is not missing machinery" (:91-92) and "what is missing is that nothing in the project supports, exercises, or documents the swap" (:94), this is the honest minimal competitor, and nothing in the PIP currently argues against it. It is also the baseline every new surface in this proposal should be justified against - which is a useful discipline for the review, not a criticism of the goal.

While here: Alternative 5 (:618-620) reads as a strawman - nobody is proposing a fork with different defaults, and the phase-2 distribution/server-fips plus the grpc-netty substitution at :295-300 already make this PIP a partial instance of the second-artifact cost it rejects there.

Comment thread pip/pip-489.md
2. **Published variant (phase 2).** A `distribution/server-fips` module producing
`apache-pulsar-<version>-fips-bin.tar.gz` from the `fipsRuntimeClasspath`, wired into CI.
Whether to *publish* this to dist.apache.org (vs. providing the build recipe) is a release-
management question flagged for the DISCUSS thread.

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.

[SCOPE] make startup selection of bundled mode-specific dependencies the standard distribution path

General Notes already describes separate provider directories and selecting one at startup (pip-489.md:633-637). Could we promote that to the supported path in the standard distribution? Section (b) still specifies manual jar replacement and an optional second tarball, without committing to bundle both choices.

The review body gives the concrete launcher example: keep common dependencies in lib/, ship BC-FIPS in lib-fips/, and ordinary BC plus netty-tcnative-boringssl-static and Conscrypt in lib-non-fips/. PULSAR_FIPS_MODE=true selects lib/*:lib-fips/*; unset or false selects lib/*:lib-non-fips/*. The change appends the selected directory to PULSAR_CLASSPATH in the existing distribution branch, preserving the launcher's later additions. This supplies both modes in one distribution while keeping the other mode's jars off the selected classpath.

The directory split implements the tcnative/Conscrypt classpath exclusions; the gRPC substitution already specified in (b) needs the same mode-specific placement. Approved-only mode, provider configuration and fipsMode validation still apply. This is packaging support, not a claim that the environment variable alone establishes compliance. Please clarify whether the requirement at pip-489.md:295-308 to ship no BoringSSL object code rules out bundling inactive ordinary-mode dependencies in the same archive.

Both startup modes should be tested from the assembled archive, including provider exclusivity and a function runtime. The proposed tests (pip-489.md:463-472) omit the grpc-netty-shaded to grpc-netty substitution, which couples the function control channel to Pulsar's independently pinned Netty.

Comment thread pip/pip-489.md
exist in `bc-fips` — so on a FIPS classpath the class fails to load
(`NoClassDefFoundError`) even for RSA-only users.
- The `SecureRandom` is pinned to `NativePRNGNonBlocking` (line 128) rather than the BC-FIPS
DRBG (the RNG fix is one of the in-flight quick-win PRs; see *General Notes*).

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.

[ACCURACY] (c)'s SecureRandom bullet describes a gap #26154 closed on 2026-08-28

MessageCryptoBc no longer pins NativePRNGNonBlocking. Since #26154 merged on 2026-08-28 it prefers the BC-FIPS DRBG whenever the BCFIPS provider is registered, and NativePRNGNonBlocking is the fallback for everyone else:

SecureRandom rand;
Provider bcfips = Security.getProvider("BCFIPS");
if (bcfips != null) {
// When the BC-FIPS provider is registered, source randomness from its SP 800-90A
// DRBG so data-key and IV generation stays within the FIPS-validated module.
// Only registered providers are consulted here to avoid triggering BouncyCastle
// classpath resolution during class loading (see BcProviderHolder above).
try {
rand = SecureRandom.getInstance("DEFAULT", bcfips);
} catch (NoSuchAlgorithmException nsa) {
// Deliberately fatal rather than falling back: new SecureRandom() resolves by provider
// search order and may land outside the validated module, which is exactly what this
// branch exists to prevent. Registering BCFIPS is an operator asking for FIPS-approved
// randomness, and a data key or GCM IV drawn from anywhere else leaves no trace at run
// time -- SP 800-38D only permits a random 96-bit GCM IV from an approved DRBG. Failing
// class initialization surfaces the misconfiguration at the point it can still be fixed.
throw new IllegalStateException("The BCFIPS provider is registered but its DEFAULT SP "
+ "800-90A DRBG could not be obtained; refusing to fall back to a non-FIPS "
+ "SecureRandom for data-key and IV generation.", nsa);
}
} else {
try {
rand = SecureRandom.getInstance("NativePRNGNonBlocking");
} catch (NoSuchAlgorithmException nsa) {
// Unchanged: on a JVM without NativePRNGNonBlocking the platform default is the

So this bullet belongs in the "already closed" column rather than the gap list, and the "(open)" beside #26154 in General Notes (:642-643) wants updating too. That shortens (c) rather than lengthening it.

One completeness point while you are in this list: the org.bouncycastle.jce.* bullet above names four imports at lines 74-77; there is a fifth immediately below, MessageCryptoBc.java:78 (org.bouncycastle.jce.spec.IESParameterSpec), equally absent from bc-fips. Since the whole point of that item is that the class fails to load, the complete list is the useful one.

Comment thread pip/pip-489.md
version catalog still declares `bc-fips`, `bcpkix-fips` and `bcutil-fips` — and now says so
in as many words, describing them as "Test-only in this build" because "the server distribution
excludes bc-fips and ships the non-FIPS provider, so a FIPS deployment assembles its own
classpath" (`gradle/libs.versions.toml:62-63`). The distribution bundles the non-FIPS

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.

[NIT] six line citations have drifted; the rest are exact

Checked every file:line in the document against master. Six have drifted:

the PIP says on master
libs.versions.toml 62-63 (this line) the quoted comment is at 63-64; 62 is bouncycastle-bcprov
libs.versions.toml 75-77 (:274) the three version declarations are at 76-78; 75 is a comment
ProxyConfiguration line 343 (:127) webServiceTlsProvider = "" is at 344
conf/proxy.conf line 138 (:128) conf/proxy.conf:145
MessageCryptoBc ECIES "lines 88, 374, 536" (:318) 88, 397, 559
MessageCryptoBc SecureRandom line 128 (:323) the call moved and changed meaning - separate comment

These are exact and unchanged, for what it is worth - the drift is not systematic:

distribution/server/build.gradle.kts:154 and :59
pulsar-common/build.gradle.kts:185-190
DefaultBrokerTlsFactory.java:112            (the sentence quoted from its comment is verbatim)
ServiceConfiguration:217
WebSocketProxyConfiguration:256
MessageCryptoBc:92 and :74-77
AuthenticationProviderBasic:141-149
SaslRoleTokenSigner:92-104                  (and MessageDigest.isEqual at :79, as (e) says)
KubernetesRuntime:1141 and :322, 511, 601, 730, 886
pulsar-functions/runtime/build.gradle.kts:63

A suggestion rather than a request: a PIP outlives its line numbers, and this document is on its second reconciliation pass largely because of them. Citing the symbol - DefaultBrokerTlsFactory.fromServiceConfiguration, JcaKeyStores.inMemoryStoreType, AuthenticationProviderBasic.authenticate - except where the line genuinely is the point would make the next pass much cheaper, and would have survived both #26322 and #26326 untouched.

@lhotari

lhotari commented Sep 18, 2026

Copy link
Copy Markdown
Member

Thanks for picking it back up, and no need to apologise about the July thread - the sequencing question did answer itself, and nothing was sitting with me.

On the isBCFIPS() claim: you are right, and I checked rather than took it. SecurityUtility is gone from the tree entirely, #26322 removed it on 2026-08-15, and JcaProviders returns ResolvedBouncyCastleProvider(provider, fips) with exactly the registration-first ordering you describe. Detail, plus one addition I think strengthens that paragraph, is in the review.

On whether it is ready for a [VOTE]: not quite, though nothing procedural is blocking you. pip/README.md asks for a [DISCUSS] thread on dev@ and consensus before the vote, and the vote itself needs one binding +1 under lazy majority over at least 48 hours; the "three PMC reviews first" in the README's worked example is convention, not a gate. So the argument for one more revision is about content. I have left ten comments on the document; four of them are things I would want settled before a vote rather than during one:

  1. (c)'s failure mode is not what the client does. The default crypto-failure action is FAIL, which discards the message without delivering or acking it - there is no retry/DLQ path. And the rollback precondition is written in terms of producer state when the real constraint is persisted backlog across every replicated cluster, which makes opting in effectively irreversible for the retention window. Both are prose fixes, and the accurate version is a stronger argument for the design than the current one.
  2. Phase 1's validator asserts phase 2's features. For Basic auth that is not just unsatisfiable, it passes: a phase-1 broker with a $6$-only file starts, logs a clean FIPS posture, and then rejects every login, because (d) is what teaches it to verify those entries.
  3. fipsMode screens the configured keystore types, not the carrier the TLS stack actually builds - which on a PEM deployment is JKS or PKCS12, the two types the PIP rejects. The fix is small (require a pinned jcaProvider that registers BCFKS) and makes the switch mean what it says.
  4. The packaging deliverable does not say which bc-fips jar it ships, and certified-versus-patched is the first question an auditor asks. The version catalog already documents that tradeoff; the PIP does not mention it exists.

Worth knowing before you revise, because it makes (d) much smaller: Crypt.crypt(byte[], String) in commons-codec already dispatches on $6$/$5$/$1$. The only reason SHA-2 crypt does not work in Pulsar today is that the existing call truncates the salt to two characters. (d) is one .substring(0, 2) away, and the method it currently names, Sha2Crypt.crypt, does not exist.

The rest are shape questions I would rather raise now than after the key is public - fipsMode as a boolean, the saslRoleTokenSignerAlgorithm enum - plus (f) contradicting its own Motivation, two missing Alternatives, and some line-citation drift. Also worth saying: the reconciliation is itself about two weeks stale. #26326 merged 2026-08-27 and #26154 on 2026-08-28, so the PIP no longer depends on anything unmerged - good news, but five passages still say it does.

I would do that pass and then open the vote without waiting for further reviews. The DISCUSS thread has three messages in it and two of them are mine; a vote is more likely to draw readers out than another month of waiting.

On who else could review - the concrete name is @merlimat: he reviewed PIP-478 (#25890), so he already holds the context this proposal builds on, and his +1 is binding. Past that it is honestly a question of who has time right now rather than one I can answer on anyone's behalf. What I would do in your position is nudge the DISCUSS thread with the two points that most want a second opinion - the packaging tradeoff and the scope of the fipsMode validator - rather than asking for a whole-document read. A specific question tends to get answered where a general one does not.

The goal here is right and most of the design is sound; none of the above is an argument against the proposal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants