Skip to content

feat(rust): read the client identity from a PKCS#12 bundle - #730

Draft
wkirschenmann wants to merge 1 commit into
wk/feat/rust-config-schema-v2from
wk/feat/rust-pkcs12-v2
Draft

wkirschenmann wants to merge 1 commit into
wk/feat/rust-config-schema-v2from
wk/feat/rust-pkcs12-v2

Conversation

@wkirschenmann

Copy link
Copy Markdown
Contributor

Lets the client's mTLS identity come from a PKCS#12 bundle instead of a PEM certificate/key pair.
CertP12 names a certificate and its key bundled together, the form Windows and most certificate
authorities hand out, optionally protected by CertP12Password. It is an alternative to
CertPem/KeyPem, not an addition: setting both spellings is refused while the configuration is
read, as is a password naming no bundle.

How it works

The bundle is loaded through p12-keystore (pure Rust, so no OpenSSL joins the build) under
Pkcs12ImportPolicy::Strict, and its whole chain is kept leaf first, re-encoded into the DER
shapes the PEM pair already produces, so a server that trusts only the root can still build its
path and nothing past Identity sees two identity formats. Loading happens while the configuration
is read, like every other file-naming option, so a mistyped path fails where the error can name the
option rather than surfacing later as a refused handshake.

CertP12Password is a SecretString: redacted by Debug, zeroized on drop, and never echoed in
an error. It deserialises through a secret_text reader added to config_utils, naming no option
of its own. The proxy-options PR adds the same reader; whichever lands second drops its copy.

RawIdentity gains a Pkcs12 shape, first so it wins over the PEM one and ahead of Bare, which
requires nothing and so stays last. Every shape carries every identity option and differs only in
which it requires: that is what lets load see both spellings at once, and what keeps a PEM pair
readable next to a CertP12 that is present but empty. The generated schema spells the same
distinction, as an anyOf whose branches differ by required; tests/schema.rs pins both new
option names and the bundle branch, in place of the assertion that CertP12 is absent.

C# parity

Worth knowing before this merges: the C# client declares CertP12
(packages/csharp/ArmoniK.Api.Client/Options/GrpcClient.cs:62) but has no CertP12Password -
GrpcChannelFactory.cs:536 opens the file with new X509Certificate2(path), and no spelling of a
p12 password exists anywhere under packages/csharp/. So CertP12Password is a name only the Rust
client knows, and a password-protected bundle is not portable to C#. The alternative is to refuse
password-protected bundles instead, keeping the vocabularies identical; this PR takes the first
road and documents the divergence in armonik-transport/README.md.

Tests

13 new tests. The PKCS#12 material: a bundle round-tripping into the same identity a PEM pair
produces, a CA-signed chain keeping its intermediates leaf first, an unprotected bundle opening
without a password, a wrong password naming the path and not itself, an identity-less bundle, a
file that is not PKCS#12, a path that leads nowhere, and the CertP12 option reading the file it
names. The option rules: mutual exclusion in all three combinations, an orphan password not echoed,
empty options reading as unset, an empty CertP12 leaving the PEM pair alone, and a bundle path
that leads nowhere failing while the configuration is read. Fixtures are generated with rcgen
rather than committed, so nothing can expire, and because p12-keystore rebuilds a bundle's chain
by issuer, proving the intermediates survive needs material that really signed the leaf.

Gates

From packages/rust, all green:

cargo test -p armonik-transport --all-features                  69 + 13 + 5 + 6 passed
cargo clippy -p armonik-transport --all-features --all-targets  0 warnings
cargo clippy -p armonik --all-features --all-targets            0 warnings
cargo check -p armonik-transport --no-default-features          0 warnings
cargo fmt --check                                               clean
cargo run -p armonik-transport --features schema --example generate_schema

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

☂️ Python Coverage

current status: ✅

Overall Coverage

Lines Covered Coverage Threshold Status
1478 1247 84% 0% 🟢

New Files

No new covered files...

Modified Files

No covered modified files...

updated for commit: 3ea509e by action🐍

@wkirschenmann
wkirschenmann force-pushed the wk/feat/rust-pkcs12-v2 branch from 4f2f993 to f418ee4 Compare August 8, 2026 14:23
@wkirschenmann
wkirschenmann marked this pull request as ready for review August 8, 2026 18:55
@wkirschenmann
wkirschenmann force-pushed the wk/feat/rust-pkcs12-v2 branch from f418ee4 to 37ce70a Compare August 9, 2026 08:37
@wkirschenmann
wkirschenmann force-pushed the wk/feat/rust-pkcs12-v2 branch from 37ce70a to 6493367 Compare August 9, 2026 09:28
`CertP12` names a certificate and its key bundled together, the form
Windows and most certificate authorities hand out, optionally protected
by `CertP12Password`. It is an alternative to `CertPem`/`KeyPem`, not an
addition: setting both spellings is refused while the configuration is
read, as is a password naming no bundle. Errors name the file, never the
password, which is a `SecretString` throughout.

The bundle is loaded through `p12-keystore` (pure Rust, so no OpenSSL
joins the build) under `Pkcs12ImportPolicy::Strict`, and its whole chain
is kept leaf first, re-encoded into the DER shapes the PEM pair already
produces, so a server that trusts only the root can still build its path
and nothing past `Identity` sees two identity formats.

`RawIdentity` gains a `Pkcs12` shape, first so it wins over the PEM one.
Every shape carries every identity option and differs only in which it
requires: that is what lets `load` see both spellings at once, and what
keeps a PEM pair readable next to a `CertP12` that is present but empty.
The generated schema spells the same distinction, as an `anyOf` whose
branches differ by `required`.

ArmoniK's C# client reads `CertP12` with no password counterpart
(`GrpcClient.cs` declares the option, `GrpcChannelFactory.cs` opens the
file with `new X509Certificate2(path)`), so `CertP12Password` is a name
only this client knows.

  cargo test -p armonik-transport --all-features
    test result: ok. 69 passed  (lib)
    test result: ok. 13 passed  (proxy)
    test result: ok. 5 passed   (schema)
    test result: ok. 6 passed   (timeout)
  cargo clippy -p armonik-transport --all-features --all-targets
    Finished `dev` profile, 0 warnings
  cargo clippy -p armonik --all-features --all-targets
    Finished `dev` profile, 0 warnings
  cargo check -p armonik-transport --no-default-features
    Finished `dev` profile, 0 warnings
  cargo fmt --check
    clean
  cargo run -p armonik-transport --features schema --example generate_schema
    the identity `anyOf` now carries a branch requiring `CertP12` and
    declaring `CertP12Password`

The 13 new tests are the PKCS#12 material (a bundle round-tripping, a
CA-signed chain keeping its intermediates leaf first, an unprotected
bundle opening without a password, a wrong password naming the path and
not itself, an identity-less bundle, a file that is not PKCS#12, a path
that leads nowhere, and the option reading the file it names) and the
option rules (mutual exclusion in all three combinations, an orphan
password not echoed, empty options reading as unset, an empty `CertP12`
leaving the PEM pair alone, a bundle path that leads nowhere).
`tests/schema.rs` pins both new option names and the bundle branch of
the identity `anyOf`, in place of the assertion that `CertP12` is absent.
@wkirschenmann
wkirschenmann force-pushed the wk/feat/rust-pkcs12-v2 branch from 6493367 to 3ea509e Compare August 9, 2026 10:40
@sonarqubecloud

sonarqubecloud Bot commented Aug 9, 2026

Copy link
Copy Markdown

@wkirschenmann
wkirschenmann marked this pull request as draft August 13, 2026 18:51
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.

1 participant