Deterministic cryptographic test fixtures for Rust.
Stop committing PEM/DER/JWK blobs into your repos.
A test-fixture factory that generates cryptographic key material and X.509 certificates at runtime. Not a crypto library.
Secret scanners have changed the game for test fixtures:
- GitGuardian scans each commit in a PR. "Add then remove" still triggers incidents.
- GitHub push protection blocks pushes until the secret is removed from all commits.
- Path ignores exist but require ongoing maintenance and documentation.
Even fake keys that look real cause friction. This crate replaces "security policy + docs + exceptions" with one dev-dependency.
Do not use for production keys. Deterministic keys are predictable by design. Even random-mode keys are intended for tests only.
| Approach | Drawback |
|---|---|
| Check in PEM files | Triggers GitGuardian/GitHub push protection |
| Generate keys ad-hoc in tests | No caching, slow RSA keygen, no determinism |
| Use raw crypto crates directly | Boilerplate for PEM/DER encoding, no negative fixtures |
Use rcgen directly |
Not test-fixture-focused; no deterministic mode, no negative fixtures |
Algorithms:
- RSA (2048, 3072, 4096 bits)
- ECDSA (P-256, P-384)
- Ed25519
- HMAC (HS256, HS384, HS512)
- OpenPGP (RSA 2048/3072, Ed25519)
- Token fixtures (API key, bearer, OAuth access token/JWT shape)
Output formats:
- PKCS#8 PEM/DER (private keys)
- SPKI PEM/DER (public keys)
- OpenPGP armored and binary keyblocks (with
pgpfeature) - JWK/JWKS (with
jwkfeature) - Tempfiles (for libraries that need paths)
- X.509 self-signed certificates and certificate chains (with
x509feature)
Negative fixtures:
- Corrupt PEM (bad base64, wrong headers, truncated)
- Truncated DER
- Mismatched keypairs (valid public key that doesn't match the private key)
- X.509: expired leaf/intermediate, hostname mismatch, unknown CA, revoked leaf (with CRL)
Choose the fixture families you need explicitly. For RSA fixtures:
[dev-dependencies]
uselesskey = { version = "0.4.0", features = ["rsa"] }Generate keys:
use uselesskey::{Factory, RsaFactoryExt, RsaSpec};
// Random mode (different keys each run)
let fx = Factory::random();
// Deterministic mode (stable keys from text)
let fx = Factory::deterministic_from_str("my-test-seed");
// Or fall back to random if env var not set
let fx = Factory::deterministic_from_env("USELESSKEY_SEED")
.unwrap_or_else(|_| Factory::random());
// Generate RSA keypair
let rsa = fx.rsa("issuer", RsaSpec::rs256());
let pkcs8_pem = rsa.private_key_pkcs8_pem();
let spki_der = rsa.public_key_spki_der();For token-only fixtures without pulling RSA:
[dev-dependencies]
uselesskey = { version = "0.4.0", default-features = false, features = ["token"] }use uselesskey::{Factory, RsaSpec, RsaFactoryExt};
let fx = Factory::random();
let rsa = fx.rsa("issuer", RsaSpec::rs256());
let jwk = rsa.public_jwk();
let jwks = rsa.public_jwks();use uselesskey::{Factory, RsaSpec, RsaFactoryExt};
let fx = Factory::random();
let rsa = fx.rsa("server", RsaSpec::rs256());
let keyfile = rsa.write_private_key_pkcs8_pem().unwrap();
assert!(keyfile.path().exists());Self-signed certificates for simple TLS tests:
use uselesskey::{Factory, X509FactoryExt, X509Spec};
let fx = Factory::random();
let cert = fx.x509_self_signed("my-service", X509Spec::self_signed("test.example.com"));
let cert_pem = cert.cert_pem();
let key_pem = cert.private_key_pkcs8_pem();Three-level certificate chains (root CA → intermediate CA → leaf):
use uselesskey::{Factory, X509FactoryExt, ChainSpec};
let fx = Factory::random();
let chain = fx.x509_chain("my-service", ChainSpec::new("test.example.com"));
// Standard TLS server chain (leaf + intermediate, no root)
let chain_pem = chain.chain_pem();
// Individual certs for custom setups
let root_pem = chain.root_cert_pem();
let leaf_key = chain.leaf_private_key_pkcs8_pem();Generate intentionally invalid certificates for testing error-handling paths:
use uselesskey::{Factory, X509FactoryExt, ChainSpec};
let fx = Factory::random();
let chain = fx.x509_chain("my-service", ChainSpec::new("test.example.com"));
// Expired leaf certificate
let expired = chain.expired_leaf();
// Hostname mismatch (SAN doesn't match expected hostname)
let wrong_host = chain.hostname_mismatch("wrong.example.com");
// Signed by an unknown CA (not in your trust store)
let unknown = chain.unknown_ca();
// Revoked leaf with CRL signed by the intermediate CA
let revoked = chain.revoked_leaf();
let crl_pem = revoked.crl_pem().expect("CRL present for revoked variant");use uselesskey::{Factory, RsaSpec, RsaFactoryExt};
use uselesskey::negative::CorruptPem;
let fx = Factory::random();
let rsa = fx.rsa("issuer", RsaSpec::rs256());
let bad_pem = rsa.private_key_pkcs8_pem_corrupt(CorruptPem::BadBase64);
let truncated = rsa.private_key_pkcs8_der_truncated(32);
let mismatched_pub = rsa.mismatched_public_key_spki_der();Generate realistic token-shaped fixtures without committing token blobs:
use uselesskey::{Factory, TokenFactoryExt, TokenSpec};
let fx = Factory::random();
let api_key = fx.token("billing", TokenSpec::api_key());
let bearer = fx.token("gateway", TokenSpec::bearer());
let oauth = fx.token("issuer", TokenSpec::oauth_access_token());
assert!(api_key.value().starts_with("uk_test_"));
assert!(bearer.authorization_header().starts_with("Bearer "));
assert_eq!(oauth.value().split('.').count(), 3);Adapter crates bridge uselesskey fixtures to third-party library types. They are separate crates (not features) to avoid coupling versioning. See the Workspace Crates section for the full list.
With the tls-config feature, build rustls configs in one line:
[dev-dependencies]
uselesskey-rustls = { version = "0.4.0", features = ["tls-config", "rustls-ring"] }use uselesskey_core::Factory;
use uselesskey_x509::{X509FactoryExt, ChainSpec};
use uselesskey_rustls::{RustlsServerConfigExt, RustlsClientConfigExt};
let fx = Factory::random();
let chain = fx.x509_chain("my-service", ChainSpec::new("test.example.com"));
let server_config = chain.server_config_rustls(); // ServerConfig (no client auth)
let client_config = chain.client_config_rustls(); // ClientConfig (trusts root CA)[dev-dependencies]
uselesskey-ring = { version = "0.4.0", features = ["all"] }use uselesskey_core::Factory;
use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
use uselesskey_ring::RingRsaKeyPairExt;
let fx = Factory::random();
let rsa = fx.rsa("signer", RsaSpec::rs256());
let ring_kp = rsa.rsa_key_pair_ring(); // ring::rsa::KeyPair[dev-dependencies]
uselesskey-rustcrypto = { version = "0.4.0", features = ["all"] }use uselesskey_core::Factory;
use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
use uselesskey_rustcrypto::RustCryptoRsaExt;
let fx = Factory::random();
let rsa = fx.rsa("signer", RsaSpec::rs256());
let rsa_pk = rsa.rsa_private_key(); // rsa::RsaPrivateKey[dev-dependencies]
uselesskey-aws-lc-rs = { version = "0.4.0", features = ["native", "all"] }use uselesskey_core::Factory;
use uselesskey_rsa::{RsaFactoryExt, RsaSpec};
use uselesskey_aws_lc_rs::AwsLcRsRsaKeyPairExt;
let fx = Factory::random();
let rsa = fx.rsa("signer", RsaSpec::rs256());
let lc_kp = rsa.rsa_key_pair_aws_lc_rs(); // aws_lc_rs::rsa::KeyPair[dev-dependencies]
uselesskey-tonic = "0.4.0"use uselesskey_core::Factory;
use uselesskey_x509::{X509FactoryExt, ChainSpec};
use uselesskey_tonic::{TonicClientTlsExt, TonicServerTlsExt};
let fx = Factory::random();
let chain = fx.x509_chain("grpc", ChainSpec::new("test.example.com"));
let server_tls = chain.server_tls_config_tonic();
let client_tls = chain.client_tls_config_tonic("test.example.com");The crates/uselesskey/examples/ directory contains standalone programs you can run with cargo run -p uselesskey --example:
| Example | Description |
|---|---|
adapter_jsonwebtoken |
Sign and verify JWTs using jsonwebtoken crate integration |
adapter_rustls |
Convert X.509 fixtures into rustls ServerConfig / ClientConfig |
basic_ecdsa |
Generate ECDSA keypairs for P-256 and P-384 in PEM, DER, JWK |
basic_ed25519 |
Generate Ed25519 keypairs in PEM, DER, and JWK formats |
basic_hmac |
Generate HMAC secrets for HS256, HS384, and HS512 |
basic_rsa |
Generate RSA keypairs in PEM, DER, and JWK formats |
basic_token |
Generate API key, bearer, and OAuth access-token fixtures |
basic_usage |
All-in-one: RSA, ECDSA, and Ed25519 fixture generation |
deterministic |
Reproducible fixtures from seeds — same seed always yields same key |
deterministic_mode |
Order-independent deterministic derivation guarantees |
jwk_generation |
Build JWKs and JWKS with JwksBuilder across key types |
jwk_jwks |
JWK Sets from multiple key types with metadata inspection |
jwks |
Build a JWKS from RSA and ECDSA public keys |
jwks_server_mock |
Generate a JWKS response body for a mock /.well-known/jwks.json endpoint |
jwt_rs256_jwks |
RSA keypairs with JWK/JWKS extraction for JWT verification flows |
jwt_signing |
JWT signing with deterministic RSA, ECDSA, and HMAC keys |
negative_fixtures |
Intentionally invalid certificates and keys for error-path testing |
tempfile_paths |
Write key fixtures to temporary files for path-based APIs |
tempfiles |
Write X.509 cert, key, and identity PEM to temp files |
tls_server |
Certificate chain generation for TLS server testing |
token_generation |
Realistic API keys, bearer tokens, and OAuth tokens for tests |
x509_certificates |
Self-signed certs, cert chains, and negative X.509 fixtures |
uselesskey is a facade crate that re-exports from focused implementation crates.
Depend on the facade for convenience, or on individual crates to minimize compile time.
| Crate | Description |
|---|---|
uselesskey |
Public facade — re-exports all key types and traits behind feature flags |
uselesskey-core |
Factory, deterministic derivation, caching, and negative-fixture helpers |
uselesskey-rsa |
RSA 2048/3072/4096 keypairs (PKCS#8, SPKI, PEM, DER) |
uselesskey-ecdsa |
ECDSA P-256 / P-384 keypairs |
uselesskey-ed25519 |
Ed25519 keypairs |
uselesskey-hmac |
HMAC HS256/HS384/HS512 secrets |
uselesskey-pgp |
OpenPGP key fixtures (armored + binary keyblocks) |
uselesskey-token |
API key, bearer token, and OAuth access-token fixtures |
uselesskey-jwk |
Typed JWK/JWKS models and builders |
uselesskey-x509 |
X.509 self-signed certificates and certificate chains |
| Crate | Integrates with |
|---|---|
uselesskey-jsonwebtoken |
jsonwebtoken EncodingKey / DecodingKey |
uselesskey-rustls |
rustls ServerConfig / ClientConfig builders |
uselesskey-tonic |
tonic::transport TLS identity / config for gRPC |
uselesskey-ring |
ring 0.17 native signing key types |
uselesskey-rustcrypto |
RustCrypto native types (rsa::RsaPrivateKey, etc.) |
uselesskey-aws-lc-rs |
aws-lc-rs native types |
| Feature | Description |
|---|---|
rsa |
RSA keypairs |
ecdsa |
ECDSA P-256/P-384 keypairs |
ed25519 |
Ed25519 keypairs |
hmac |
HMAC secrets |
pgp |
OpenPGP keypairs (armored + binary keyblocks) |
token |
API key, bearer token, and OAuth access token fixtures |
x509 |
X.509 certificate generation (implies rsa) |
jwk |
JWK/JWKS output for enabled key types |
all-keys |
All key algorithms (rsa + ecdsa + ed25519 + hmac + pgp) |
full |
Everything (all-keys + token + x509 + jwk) |
The uselesskey facade default feature set is empty.
Extension traits by feature:
rsa:RsaFactoryExtecdsa:EcdsaFactoryExted25519:Ed25519FactoryExthmac:HmacFactoryExtpgp:PgpFactoryExttoken:TokenFactoryExtx509:X509FactoryExt
| Feature | Extension Trait | Algorithms / Outputs | Implies |
|---|---|---|---|
rsa |
RsaFactoryExt |
RSA 2048/3072/4096 — PKCS#8, SPKI, PEM, DER | — |
ecdsa |
EcdsaFactoryExt |
P-256 (ES256), P-384 (ES384) — PKCS#8, SPKI | — |
ed25519 |
Ed25519FactoryExt |
Ed25519 — PKCS#8, SPKI | — |
hmac |
HmacFactoryExt |
HS256, HS384, HS512 | — |
pgp |
PgpFactoryExt |
OpenPGP RSA 2048/3072, Ed25519 — armored, binary | — |
token |
TokenFactoryExt |
API key, bearer, OAuth access token | — |
x509 |
X509FactoryExt |
Self-signed certs, cert chains, negative certs | rsa |
jwk |
— | JWK/JWKS output for all enabled key types | — |
all-keys |
— | (bundle) | rsa ecdsa ed25519 hmac pgp |
full |
— | (everything) | all-keys token x509 jwk |
Each adapter crate has per-algorithm feature flags (rsa, ecdsa, ed25519, hmac) and an all convenience flag.
| Adapter | RSA | ECDSA | Ed25519 | HMAC | X.509 / TLS | Extra features |
|---|---|---|---|---|---|---|
uselesskey-jsonwebtoken |
✓ | ✓ | ✓ | ✓ | — | — |
uselesskey-ring |
✓ | ✓ | ✓ | — | — | — |
uselesskey-rustcrypto |
✓ | ✓ | ✓ | ✓ | — | — |
uselesskey-aws-lc-rs |
✓ | ✓ | ✓ | — | — | native (enables aws-lc-rs dep) |
uselesskey-rustls |
✓ | ✓ | ✓ | — | ✓ | tls-config, rustls-ring, rustls-aws-lc-rs |
uselesskey-tonic |
— | — | — | — | ✓ | — |
seed + (domain, label, spec, variant) -> derived seed -> artifact
Adding new fixtures doesn't perturb existing ones. Test order doesn't matter.
RSA keygen is expensive. Per-process caching by (domain, label, spec, variant) makes runtime generation cheap enough to replace committed fixtures.
Ask for PKCS#8/SPKI/JWK, not crypto primitives. Users shouldn't need to know which crate does the encoding.
Corrupt PEM, truncated DER, mismatched keys, expired certs, revoked leaves with CRLs. These are annoying to produce manually, which is why teams commit them. This crate makes them cheap and ephemeral.
- Production key generation or certificate management
- Certificate validation logic (use
rustls,x509-parser) - Runtime CA operations (use
rcgendirectly)
Use uselesskey when you need test fixtures that don't trip secret scanners. If you need runtime certificate generation for production (e.g., an internal CA), reach for rcgen directly. If you need certificate validation logic, see rustls or x509-parser.
- CHANGELOG — release history
- CONTRIBUTING — how to build, test, and add new key types
- SECURITY — security policy (this is a test-only crate)
- CODE_OF_CONDUCT — Contributor Covenant
- SUPPORT — how to get help
Derivation stability: Artifacts generated with a given (seed, domain, label, spec, variant) tuple are stable within the same DerivationVersion. We will never change V1 output; if derivation logic changes, a new version (e.g., V2) will be introduced.
MSRV: The minimum supported Rust version is 1.92 (edition 2024).
Licensed under either of:
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.