Andrey Yarovoy created HDDS-17353:
-------------------------------------

             Summary: Ozone client registers the BouncyCastle security provider 
at start-up although it doesn't need it
                 Key: HDDS-17353
                 URL: https://issues.apache.org/jira/browse/HDDS-17353
             Project: Apache Ozone
          Issue Type: Bug
          Components: Ozone Client
            Reporter: Andrey Yarovoy


*Problem*

Every Ozone client registers the BouncyCastle JCE provider at start-up, and a 
plain client uses almost none of it.
- Client code creates a `SecurityConfig` just to read flags such as "is 
security enabled":
`BasicRootedOzoneClientAdapterImpl`, `BasicOzoneClientAdapterImpl`, 
`XceiverClientGrpc`, `GrpcOmTransport`.
It also calls `OzoneSecurityUtil.isSecurityEnabled(conf)`, from `RpcClient` and 
`XceiverClientCreator`.
- Both of these call `SecurityConfig.initSecurityProvider(conf)`. With the 
default `hdds.security.provider=BC`,
that runs `Security.addProvider(new BouncyCastleProvider())`.
- BouncyCastle's jar is signed, so the JVM first verifies the jar, then loads 
the provider's classes.

What uses the provider:
- **Services:** key generation (`HDDSKeyGenerator`), certificate requests and 
signing (`CertificateSignRequest`,
`SelfSignedCertificate`, `DefaultApprover`), and reading certificate paths from 
PEM files
(`CertificateCodec.generateCertPathFromInputStream`, which uses 
`generateCertPath(in, "PEM")`). Only BouncyCastle
supports that encoding.
- **Clients:** one call. `ServiceInfoEx.getCaCertificates()` → 
`OzoneSecurityUtil.convertToX509()` →
`CertificateCodec.readX509Certificate()` parses each CA certificate OM returns 
with
`CertificateFactory.getInstance("X.509", "BC")`. Each is a single PEM 
certificate, which the JDK's own X.509
factory parses as well.
- **Other lookups:** `addProvider` appends BouncyCastle after the JDK 
providers, so lookups that don't name a
provider already resolve to a JDK implementation. `STSTokenEncryption` in OM 
uses its own `BouncyCastleProvider`
instance and doesn't depend on the registration.

The cost shows up wherever many short-lived client JVMs start: YARN 
`ContainerLocalizer` JVMs, MapReduce tasks, CLI
commands. A MapReduce job starts about four cold clients one after another on 
its critical path. Measured on a
secure compose cluster (Kerberos + gRPC TLS, JDK 25), in fresh JVMs:
- **On its own,** after loading the configuration: registering the provider 
takes **182 ms** median (176–224 ms,
9 JVMs) and loads about 1,000 extra classes.
- **Inside a real cold client** (`FileSystem.get()` on `ofs://`, getFileStatus, 
a 64 KB read), with
`-Xlog:class+load`: the stretch from loading `SecurityConfig` to the next 
Hadoop class takes **140–176 ms**. It
starts with about 60 ms of jar verification and loads 498 BouncyCastle classes. 
Another 117 BouncyCastle classes
load later, when the CA certificates are parsed.

That's roughly 0.6 s per MapReduce job, from a client that generates no keys 
and signs nothing.

*Proposed change*

Register the provider only where it's needed:
1. `SecurityConfig`'s constructor and `OzoneSecurityUtil.isSecurityEnabled()` 
no longer call
`initSecurityProvider()`.
2. `SecurityConfig.getProvider()` calls `initSecurityProvider()` before 
returning the provider name. Every service
path that names the provider goes through it: key generation, certificate 
requests, signing. The explicit calls
in `HddsServerUtil.getScmSecurityClient*` stay.
3. `CertificateCodec`:
- `readX509Certificate()` / `getX509Certificate()` parse with the JDK's X.509 
factory
(`CertificateFactory.getInstance("X.509")`);
- `getCertFactory()` stays BouncyCastle-backed for `generateCertPath(in, 
"PEM")`, and registers the provider
itself before the lookup, instead of throwing "BouncyCastle JCE provider not 
loaded".
4. Optional, for safety: every service registers the provider once at start-up 
(SCM, OM, datanode, Recon, S3
Gateway). Then any service-side lookup that relies on the provider being 
registered, without going through
`getProvider()` or `getCertFactory()`, keeps working.

*Compatibility*

- Services see no difference if step 4 is done. Without it, the risk is a 
service-side lookup that names BouncyCastle
without going through `getProvider()` or `getCertFactory()`; none was found in 
`src/main`.
- Clients parse CA certificates with the JDK instead of BouncyCastle. Ozone's 
certificates are standard X.509
issued by SCM. Equality and trust checks compare encoded certificates, so the 
parser's provider doesn't matter. A
certificate that BouncyCastle accepts but the JDK rejects would now fail on the 
client.
- Applications that relied on Ozone registering BouncyCastle as a side effect, 
and then asked for it by name, would
need to register it themselves. That was never documented behavior.
- `hdds.security.provider` keeps its meaning for services.

*Tests*

- Unit: after creating a `SecurityConfig` and calling 
`OzoneSecurityUtil.isSecurityEnabled()`,
`Security.getProvider("BC")` is null. This needs a fresh JVM or a test that 
removes the provider first, because
registration is JVM-wide.
- Unit: `CertificateCodec.readX509Certificate()` on an SCM-issued PEM 
certificate gives a certificate equal to the
one BouncyCastle parses, `getEncoded()` included. `getCertFactory()` works when 
the provider isn't registered yet.
- Unit: `SecurityConfig.getProvider()` leaves the provider registered.
- Existing secure integration tests (key and certificate generation, CA 
rotation, gRPC TLS) pass unchanged.
- Measurement: the cold-client probe on a secure compose cluster shows 
`FileSystem.get()` about 150 ms shorter, and
no BouncyCastle classes in a client's class-load log.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to