feat: add core keypair crate with PKCS#8/SPKI helpers#20
Conversation
- Introduced `uselesskey-core-keypair` crate for reusable key material helpers. - Implemented `Pkcs8SpkiKeyMaterial` struct with methods for accessing and manipulating PKCS#8 and SPKI key formats. - Added tests for key material accessors and corruption methods. feat: add deterministic key-id helpers - Created `uselesskey-core-kid` crate for generating deterministic key IDs. - Implemented `kid_from_bytes` function for deriving key IDs from byte arrays. - Added tests to ensure deterministic behavior of key ID generation. feat: introduce negative fixture builders - Added `uselesskey-core-negative` crate for generating corrupted PEM/DER data. - Implemented various corruption strategies for PEM and DER formats. - Added tests for corruption functions to ensure stability and uniqueness. feat: implement tempfile-backed artifact sinks - Created `uselesskey-core-sink` crate for managing temporary file artifacts. - Implemented `TempArtifact` struct for creating and managing temporary files. - Added tests to verify file creation, reading, and cleanup behavior. feat: define canonical feature and matrix definitions - Introduced `uselesskey-feature-grid` crate for feature flag and matrix definitions. - Defined core and BDD feature matrices for automation consistency. - Added tests to ensure uniqueness and correctness of feature definitions. feat: add compatibility façade for feature grid - Created `uselesskey-test-grid` crate as a compatibility layer for `uselesskey-feature-grid`. - Re-exported feature matrices and constants for backwards compatibility.
Summary by CodeRabbitRelease Notes
WalkthroughAdds multiple new workspace crates (core-id, core-kid, core-keypair, core-negative, core-sink, feature/test-grid, bdd-steps), moves id/derive/negative/sink/keypair logic into those crates via re-exports/wrappers, introduces BDD step implementations and feature matrices, and updates manifests, tests, and consumers to use the new modules. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello @EffortlessSteven, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly refactors the project's core infrastructure by decomposing a monolithic core crate into several specialized micro-crates. This enhances modularity, promotes code reuse, and centralizes key cryptographic utility functions. The changes also introduce a robust framework for managing feature flags and BDD tests, improving the overall testability and maintainability of the project. These foundational improvements streamline future development and ensure consistent behavior across various cryptographic implementations. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Pull request overview
This is a major refactoring PR that decomposes uselesskey-core into focused sub-crates and extracts BDD infrastructure into a reusable library crate. The changes improve modularity, enable better code reuse, and add comprehensive test coverage for new core microcrates.
Changes:
- Extracts core primitives into focused microcrates (
core-id,core-kid,core-keypair,core-negative,core-sink) - Creates
uselesskey-core-keypairto deduplicate PKCS#8/SPKI helpers across RSA, ECDSA, and Ed25519 crates - Moves BDD step definitions into
uselesskey-bdd-stepslibrary crate with new feature files for core microcrates - Extracts feature matrix definitions into
uselesskey-feature-gridwith compatibility facadeuselesskey-test-grid - Enhances X.509 tests to verify key usage bits and Extended Key Usage (EKU) extensions
Reviewed changes
Copilot reviewed 60 out of 63 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
crates/uselesskey-core-id/ |
New crate: Core identity and seed derivation primitives |
crates/uselesskey-core-kid/ |
New crate: Deterministic key-ID generation helpers |
crates/uselesskey-core-keypair/ |
New crate: Shared PKCS#8/SPKI key material abstraction |
crates/uselesskey-core-negative/ |
New crate: PEM/DER corruption utilities |
crates/uselesskey-core-sink/ |
New crate: Tempfile artifact sinks |
crates/uselesskey-feature-grid/ |
New crate: Canonical feature matrix definitions |
crates/uselesskey-test-grid/ |
New crate: Compatibility facade for feature-grid |
crates/uselesskey-bdd-steps/ |
New crate: Reusable BDD step library with feature flags |
crates/uselesskey-core/ |
Refactored to use extracted sub-crates via re-exports |
crates/uselesskey-{rsa,ecdsa,ed25519}/ |
Refactored to use Pkcs8SpkiKeyMaterial from core-keypair |
crates/uselesskey-hmac/ |
Updated to use kid_from_bytes from core-kid |
crates/uselesskey-x509/ |
Enhanced tests for key usage and certificate stability |
crates/uselesskey-bdd/ |
Simplified to proxy to uselesskey-bdd-steps |
xtask/ |
Updated to use feature-grid; added dependency tracking for new crates |
Cargo.toml |
Added workspace entries for all new crates |
.gitattributes |
New file enforcing LF line endings |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 77c5a90439
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| uselesskey-core-id = { workspace = true } | ||
| uselesskey-core-negative = { workspace = true } |
There was a problem hiding this comment.
Preserve no_std by disabling std defaults on core deps
uselesskey-core still advertises no_std via #![cfg_attr(not(feature = "std"), no_std)], but these new dependency entries enable uselesskey-core-id and uselesskey-core-negative with their default features, which include std. As a result, cargo build -p uselesskey-core --no-default-features can no longer work on no_std targets because transitive std is always pulled in; please set these deps to default-features = false and gate their std features from uselesskey-core’s std feature.
Useful? React with 👍 / 👎.
| documentation = "https://docs.rs/uselesskey-core-negative" | ||
|
|
||
| [dependencies] | ||
| uselesskey-core-id = { path = "../uselesskey-core-id", version = "0.3.0" } |
There was a problem hiding this comment.
Keep core-negative no_std by disabling core-id defaults
uselesskey-core-negative declares #![cfg_attr(not(feature = "std"), no_std)], but its uselesskey-core-id dependency is added with default features enabled, and core-id defaults to std. This means uselesskey-core-negative --no-default-features still transitively requires std, breaking the crate’s advertised no-std mode for embedded/no-std targets.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Code Review
This pull request introduces a significant refactoring of the uselesskey-core crate, decomposing it into several specialized micro-crates (core-id, core-kid, core-negative, core-sink) and adding a new core-keypair crate for shared PKCS#8/SPKI helpers. This modular approach improves maintainability and reusability across the workspace. The addition of BDD step extraction and enhanced X.509 tests further strengthens the project's testing infrastructure. Overall, the changes are well-structured and align with the project's goals of deterministic fixture generation and avoiding secret leakage.
| fn derived_offset(len: usize, selector: u8) -> usize { | ||
| if len == 0 { | ||
| return 0; | ||
| } | ||
| selector as usize % len | ||
| } |
There was a problem hiding this comment.
The derived_offset function uses a single u8 selector to determine the offset. This limits the deterministic corruption to the first 256 bytes of the DER payload. For larger artifacts (like 4096-bit RSA keys), this means corruption will never occur in the latter parts of the structure. Consider using a u16 or u32 derived from multiple bytes of the digest to support larger payloads.
| fn derived_truncate_len_bytes(len: usize, digest: &[u8; 32]) -> usize { | ||
| if len <= 1 { | ||
| return 0; | ||
| } | ||
| let span = len - 1; | ||
| digest[2] as usize % span | ||
| } |
There was a problem hiding this comment.
Similar to derived_offset, derived_truncate_len_bytes only uses one byte from the digest (digest[2]). This results in truncation lengths that are always small (less than 256) regardless of the total length of the DER. For large artifacts, it might be more useful to have a wider range of possible truncation points. Consider using u16::from_be_bytes as seen in derived_truncate_len for PEMs.
There was a problem hiding this comment.
Actionable comments posted: 35
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
crates/uselesskey-ecdsa/tests/jwk_private.rs (1)
90-103: 🧹 Nitpick | 🔵 TrivialSame magic-number observation as in the Ed25519 test: consider replacing
12withDEFAULT_KID_PREFIX_BYTESif it ispub.Lines 100–101 hardcode
12as the expected decoded length. The suggestion and trade-off noted forcrates/uselesskey-ed25519/tests/jwk_private.rsapply equally here.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/uselesskey-ecdsa/tests/jwk_private.rs` around lines 90 - 103, The test kid_matches_public_key_hash_prefix hardcodes the decoded KID length as 12; update it to use the shared constant DEFAULT_KID_PREFIX_BYTES instead (use DEFAULT_KID_PREFIX_BYTES in the assertion comparing decoded.len() to avoid the magic number) — if DEFAULT_KID_PREFIX_BYTES is not pub from its crate, either make it public or import the value via the crate's public API (or document why the test must keep a literal), and update the assertion in the test function and any similar test (e.g., the counterpart in the Ed25519 tests) to compare against DEFAULT_KID_PREFIX_BYTES rather than 12.crates/uselesskey-ed25519/tests/jwk_private.rs (1)
82-89: 🧹 Nitpick | 🔵 TrivialReplace the magic number
12with theDEFAULT_KID_PREFIX_BYTESconstant.The constant is exported as
pubfromuselesskey-core-kid(already a dev-dependency), and using it here removes the hardcoded value and explicitly documents the coupling. The test file already imports fromuselesskey_core_kid, so adding the constant import is straightforward.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@crates/uselesskey-ed25519/tests/jwk_private.rs` around lines 82 - 89, In the kid_matches_public_key_hash_prefix test (function kid_matches_public_key_hash_prefix) replace the hard-coded literal 12 with the exported constant DEFAULT_KID_PREFIX_BYTES from uselesskey_core_kid and add the appropriate import; i.e., import DEFAULT_KID_PREFIX_BYTES from uselesskey_core_kid and use it in the assert_eq!(decode_b64url(&kid).len(), DEFAULT_KID_PREFIX_BYTES) assertion so the test uses the canonical constant rather than a magic number.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@crates/uselesskey-bdd-steps/Cargo.toml`:
- Line 9: The crate's Cargo.toml currently has publish = true but this BDD
step-definitions crate appears to be test-only; either change publish = false in
Cargo.toml or, if you intend to publish, add version fields to the path-only
dependencies referenced in the manifest (the path deps currently missing
version) and add the crate name to the PUBLISH_CRATES list in xtask/src/main.rs
so it is validated by the publish preflight; update the publish flag or add the
required version attributes and PUBLISH_CRATES entry accordingly to make the
manifest consistent with publishing requirements.
In `@crates/uselesskey-bdd-steps/src/steps/core_id_steps.rs`:
- Line 31: Add an inline comment above the let domain: &'static str =
Box::leak(domain.into_boxed_str()); line in core_id_steps.rs documenting that
Box::leak is intentionally used to convert the runtime String to &'static str
because the library API (ArtifactDomain) requires a 'static lifetime; note that
this leaks memory per scenario but is bounded and acceptable in BDD tests, and
include a brief mention that changing the API to accept owned String or a
shorter lifetime would be an alternative if leaks were unacceptable.
In `@crates/uselesskey-bdd-steps/src/steps/core_keypair_steps.rs`:
- Around line 7-14: The DER and PEM constants (CORE_KEYPAIR_PKCS8_DER,
CORE_KEYPAIR_PKCS8_PEM, CORE_KEYPAIR_SPKI_DER, CORE_KEYPAIR_SPKI_PEM) are
synthetic and not binary-equivalent; add a short clarifying comment above these
constants stating that the PEM bodies do not decode to the shown DER values and
that these are independent dummy/sample blobs for testing only, so maintainers
won't assume they are paired encodings.
- Around line 99-103: The current assertion in core_keypair_kids_identical can
pass when both world.core_kid_first and world.core_kid_second are None; change
the test to first assert both Option fields are Some (fail with a clear message
if not), then compare their inner values for equality (e.g., unwrap or
pattern-match after the presence checks) so the step fails when KIDs were not
actually derived; reference function core_keypair_kids_identical and fields
core_kid_first/core_kid_second.
In `@crates/uselesskey-bdd-steps/src/steps/core_negative_steps.rs`:
- Around line 6-8: The two fixture constants CORE_NEGATIVE_PEM_FIXTURE and
CORE_NEGATIVE_DER_FIXTURE are only used under the uk-core-negative feature gate
so add #[cfg(feature = "uk-core-negative")] above each constant definition to
prevent dead-code warnings; locate the const declarations for
CORE_NEGATIVE_PEM_FIXTURE and CORE_NEGATIVE_DER_FIXTURE and annotate both with
the same feature attribute so they are compiled only when the uk-core-negative
feature is enabled.
In `@crates/uselesskey-bdd/Cargo.toml`:
- Line 19: The uk-all feature currently lists every individual step feature
explicitly; simplify it by keeping only the umbrella feature reference so uk-all
= ["uselesskey-bdd-steps/uk-all"] (remove the long list of individual features
such as "uselesskey-bdd-steps/uk-rsa", "uselesskey-bdd-steps/uk-ecdsa",
"uselesskey-bdd-steps/uk-ed25519", etc.), updating the uk-all entry to reference
only "uselesskey-bdd-steps/uk-all".
In `@crates/uselesskey-bdd/features/core-id.feature`:
- Around line 14-17: The Scenario title "domain and variant changes alter
output" is misleading because only the variant differs in the two When steps;
update the Scenario title in the core-id.feature to reflect what's actually
tested (for example change it to "variant change alters output" or "variant
changes alter output"), or alternatively add a new Scenario that varies the
domain (and use a clear title) so the original title remains accurate; locate
the scenario by its current title and the When steps that call derive a core-id
seed with domain "uselesskey:rsa:keypair" and variants "v1"/"v2" to make the
change.
In `@crates/uselesskey-bdd/features/core-keypair.feature`:
- Around line 20-21: The duplicated step text "When I derive a core-keypair kid"
should be made distinct to avoid implicit invocation-order coupling; change the
second occurrence to use the same "again" suffix pattern used elsewhere (e.g.,
"When I derive a core-keypair kid again") so the step definitions map
unambiguously to the first and second derivations and avoid fragile
counter-based behavior.
In `@crates/uselesskey-bdd/features/core-negative.feature`:
- Line 3: Fix the typo in the feature description by replacing the nonstandard
word "deterministicish" with a clearer term such as "deterministic-ish" (or
"deterministic-like") so the line reads e.g. "I want deterministic and
deterministic-ish PEM/DER corruption utilities"; update that phrase in the
feature text to ensure consistent, standard wording.
- Line 9: Rename the misleading step phrase to reflect the generic PKCS#8
fixture: change the feature step text "the deterministic RSA PEM artifact should
fail to parse" to something like "the corrupted PEM artifact should fail to
parse" and update the matching step definition string in the step implementation
so it still reads world.deterministic_text_1 (no code logic change). Ensure any
other scenarios referencing the old step name are updated to the new phrase so
the step binding (the step definition that reads world.deterministic_text_1)
continues to match.
In `@crates/uselesskey-bdd/tests/feature_contract.rs`:
- Around line 43-45: The condition in the loop uses a redundant check: inside
the block referencing trimmed.starts_with('[') the additional
!trimmed.is_empty() is unnecessary because starts_with implies non-empty; update
the condition in the code that contains trimmed.starts_with('[') to remove the
!trimmed.is_empty() guard so the if becomes simply trimmed.starts_with('[') to
simplify and clarify intent.
In `@crates/uselesskey-core-id/src/lib.rs`:
- Around line 144-148: The length-prefixing in write_len_prefixed is incorrect
because unwrap_or(u32::MAX) silently misrepresents sizes >= 2^32; change it to
enforce failure instead of clamping: validate that bytes.len() fits in a u32
(e.g., with debug_assert!(bytes.len() <= u32::MAX as usize) and then use
u32::try_from(bytes.len()).expect("...") or plain panic with a clear message),
then cast to u32 and write to_be_bytes as before; update the function
write_len_prefixed to perform this explicit check and fail fast on overflow
rather than using u32::MAX.
- Around line 119-125: Replace the stderr print in the unknown-derivation branch
with a structured logging call or remove it: instead of eprintln! in the match
arm that falls back to derive_seed_v1(master, id) (the branch matching `other`),
emit a warn-level log via the chosen logging crate (e.g., log::warn! or
tracing::warn!) behind a feature flag (e.g., feature = "log") or drop the
runtime diagnostic entirely and rely on docs; update Cargo features and
conditional compilation accordingly so the crate does not print to stderr from
library code.
- Around line 222-243: Replace the manual two-case test
`derive_seed_version_affects_output` with an rstest-parameterized test that
iterates different DerivationVersion values; locate the test function and change
it to use rstest's #[rstest] attribute (or #[rstest(param)] style) to supply
multiple DerivationVersion instances, construct the corresponding ArtifactId via
ArtifactId::new (keeping the same domain/label/spec/variant), call
derive_seed(&master, &id) for each version, and assert that outputs differ
between versions (e.g., by comparing bytes of different parameter pairs) so new
versions can be added by simply extending the rstest parameter list.
In `@crates/uselesskey-core-keypair/Cargo.toml`:
- Around line 16-18: The Cargo.toml is missing a [features] section to expose a
"std" feature that can be disabled and propagate to the transitive dependencies,
and it's missing dev-dependencies needed for tests; add a [features] section
that sets default = ["std"] and defines "std" =
["uselesskey-core/std","uselesskey-core-kid/std"] (so consumers can opt out of
std for uselesskey-core and uselesskey-core-kid), and add a [dev-dependencies]
section including proptest and rstest entries so tests that reference those
crates will build.
In `@crates/uselesskey-core-keypair/README.md`:
- Line 9: Update the README bullet to clarify that uselesskey-core-keypair
applies (does not provide) the deterministic negative fixture helpers from the
other crate: change the line describing "deterministic negative fixture helpers"
to something like "applies deterministic negative fixtures from
`uselesskey-core-negative` to key material" so it explicitly references
`uselesskey-core-negative` and states that core-keypair uses those corruption
primitives rather than owning them.
In `@crates/uselesskey-core-keypair/src/lib.rs`:
- Around line 105-178: Add property-based/parameterized tests using proptest or
rstest to increase coverage for functions like private_key_pkcs8_der_truncated
and private_key_pkcs8_pem_corrupt_deterministic; specifically, replace or
augment the fixed-value tests in the tests module (e.g.,
truncation_respects_requested_length and deterministic_corruption_is_stable)
with proptest strategies that generate arbitrary truncation lengths and
arbitrary corruption variant strings, assert invariants (requested length
respected, corruption deterministic across same variant), and also add
randomized checks for kid() stability and that
write_private_key_pkcs8_pem()/write_public_key_spki_pem() round-trip PEM markers
for a variety of sample_material inputs produced by parameterized fixtures or
proptest-generated byte/pem combinations.
In `@crates/uselesskey-core-negative/Cargo.toml`:
- Around line 24-26: Update the crate's feature propagation so the local "std"
feature enables the downstream dependency's std too: change the [features]
section to make std = ["uselesskey-core-id/std"] (so toggling
default-features=false correctly prevents the dependency's std feature), keeping
default = ["std"] as needed.
In `@crates/uselesskey-core-negative/src/lib.rs`:
- Around line 21-29: Add /// doc comments to the public API functions
corrupt_pem, corrupt_pem_deterministic, truncate_der, flip_byte, and
corrupt_der_deterministic explaining their purpose, parameters, return values,
and any determinism guarantees (i.e., which functions are deterministic vs
non-deterministic and how determinism is achieved). For each function include a
short example or note about the corruption semantics (e.g., what CorruptPem
variants do for corrupt_pem, how bytes/count are interpreted for truncate_der
and flip_byte) and mention error or boundary behavior (e.g., what happens if
bytes exceed input length). Ensure the comments use Rust doc style (///) placed
immediately above each function signature.
- Line 17: CorruptPem::Truncate { bytes } is misnamed because corrupt_pem uses
pem.chars().take(bytes) (counts Unicode scalar values); rename the enum variant
field to chars (e.g., CorruptPem::Truncate { chars }) and update corrupt_pem to
use the new field name, and update all callsites and pattern matches accordingly
so the name accurately reflects character-counting behavior; alternatively, if
you want true byte-truncation, change corrupt_pem to operate on pem.as_bytes()
and truncate by bytes and reconstruct a valid String (handling invalid UTF-8)
instead—pick one and apply consistently to CorruptPem::Truncate and corrupt_pem.
- Around line 174-265: Add proptest-based property tests per the guideline: add
proptest to dev-dependencies and in the tests module create property tests
(using proptest! or #[proptest]) that generate arbitrary non-trivial PEM strings
and variant keys and assert corrupt_pem_deterministic(pem, variant) != pem (and
similarly ensure corrupt_der_deterministic(&der, variant) != der for generated
DER byte vectors of length > 0), plus a property asserting determinism for the
same (pem/der, variant) pair; import and use proptest strategies for strings and
byte vectors and reference the existing functions corrupt_pem_deterministic,
corrupt_der_deterministic, and corrupt_pem in those tests.
In `@crates/uselesskey-core-sink/Cargo.toml`:
- Around line 1-21: Add an explicit publish = true field to the package manifest
for the crate named "uselesskey-core-sink" so the intent to allow publishing is
explicit and matches the other new crates (e.g., "uselesskey-feature-grid");
update the [package] section in Cargo.toml for that crate by adding publish =
true.
In `@crates/uselesskey-core-sink/src/lib.rs`:
- Around line 156-159: read_to_string currently uses String::from_utf8_lossy
which silently replaces invalid UTF-8; change it to perform strict UTF-8
conversion to match std semantics by calling String::from_utf8(bytes) and map
the Utf8Error into a std::io::Error (InvalidData) so read_to_string returns an
error on invalid UTF-8; update the implementation in the read_to_string function
to use the bytes from read_to_bytes and propagate conversion errors as
io::Error::new(std::io::ErrorKind::InvalidData, err) (alternatively, rename to
read_to_string_lossy if you prefer preserving lossy behavior, but prefer
matching std::fs::read_to_string).
In `@crates/uselesskey-core/src/id.rs`:
- Line 1: Remove the crate-level attribute #![cfg_attr(not(feature = "std"),
no_std)] from submodule files (the attribute appears at the top of the id module
and the negative::mod module); this attribute must only live in the root crate
module (lib.rs) so delete those lines in the id module (id.rs) and in
negative::mod (mod.rs) and then verify the crate still compiles with
--no-default-features to confirm the root-level no_std in lib.rs is used.
In `@crates/uselesskey-core/src/negative/mod.rs`:
- Line 1: Remove the crate-level attribute from module files: delete the line
`#![cfg_attr(not(feature = "std"), no_std)]` found at the top of the modules
(negative module in negative/mod.rs and the id module in id.rs) because this
attribute belongs only in the crate root (lib.rs); simply remove that attribute
from the negative::mod and the id module files and keep any necessary crate-root
attribute in lib.rs.
In `@crates/uselesskey-ecdsa/tests/jwk_private.rs`:
- Around line 12-14: Replace the fully-qualified call to
uselesskey_core_kid::kid_from_bytes(...) inside the expected_kid function with a
crate-level use import: add `use uselesskey_core_kid::kid_from_bytes;` near the
other imports and change the body of expected_kid(spki_der: &[u8]) to call
kid_from_bytes(spki_der); this keeps imports consistent with the rest of the
module and avoids the inline fully-qualified path.
In `@crates/uselesskey-ed25519/tests/jwk_private.rs`:
- Around line 12-14: Replace the inline fully-qualified call
uselesskey_core_kid::kid_from_bytes in the expected_kid function with a
top-level use import; add a line like use uselesskey_core_kid::kid_from_bytes;
at the top of the module and then call kid_from_bytes(spki_der) inside fn
expected_kid to match the other imports and keep consistency.
In `@crates/uselesskey-feature-grid/Cargo.toml`:
- Around line 17-20: Remove the empty [dependencies] and [dev-dependencies]
sections from Cargo.toml because they are unnecessary when no entries exist;
locate the literal section headers "[dependencies]" and "[dev-dependencies]" in
the file and delete those lines (and any blank lines directly associated) so the
manifest is cleaner and contains only populated sections.
In `@crates/uselesskey-feature-grid/README.md`:
- Around line 5-6: Update the README sentence that claims the crate contains
"only compile-time constants" to accurately reflect that it also exports a
struct type; specifically edit the description to mention both the compile-time
constants and the exported FeatureSet type (struct) used to keep xtask and BDD
automation aligned so readers know the crate provides constants plus the
FeatureSet API.
In `@crates/uselesskey-feature-grid/src/lib.rs`:
- Around line 114-124: BDD_FEATURE_MATRIX uses inline comma-separated feature
string literals in FeatureSet::new calls (e.g., "uk-all,uk-rustls") which won't
pick up changes to the UK_FEATURE_* constants; either add a brief comment above
BDD_FEATURE_MATRIX explaining Rust cannot concat &'static str at compile time so
these literals are intentional, or add a compile-time/unit test that parses each
FeatureSet string in BDD_FEATURE_MATRIX, splits on commas and asserts every
token is one of the canonical constants (UK_FEATURE_ALL, UK_FEATURE_RUSTLS,
UK_FEATURE_TONIC, UK_FEATURE_RING, UK_FEATURE_RUSTCRYPTO, UK_FEATURE_AWS_LC_RS)
to ensure literals remain in sync. Ensure references use the FeatureSet::new
entries and the BDD_FEATURE_MATRIX symbol so future edits are easy to locate.
- Around line 112-135: BDD_FEATURE_SETS is a manually maintained parallel list
that can drift from BDD_FEATURE_MATRIX and BDD_FEATURE_MATRIX also lacks a
uniqueness test; add a test that collects the names from BDD_FEATURE_MATRIX (by
iterating FeatureSet::name or equivalent accessor) and asserts they equal the
slice BDD_FEATURE_SETS (order and contents match), and add a test that asserts
all names in BDD_FEATURE_MATRIX are unique (similar to the existing
CORE_FEATURE_MATRIX uniqueness test) so the two cannot diverge or contain
duplicates; reference BDD_FEATURE_MATRIX, BDD_FEATURE_SETS, and the existing
CORE_FEATURE_MATRIX uniqueness test as the model for placement and behavior.
In `@crates/uselesskey-hmac/src/secret.rs`:
- Around line 71-73: Add a golden (snapshot) test that asserts the exact KID
string produced by kid_from_bytes/kid() for a fixed, hard-coded seed/secret
matches a stored expected value so future algorithm or parameter changes are
caught; create a new test (e.g., alongside kid_is_deterministic) that derives
secret_bytes from the same deterministic seed used in existing tests, calls
Secret::kid() or kid_from_bytes(secret_bytes()), and compares the result to an
explicit string literal (the golden value) instead of comparing two results from
the same algorithm.
In `@crates/uselesskey-x509/src/cert.rs`:
- Around line 598-654: Extract the repeated OID-find-and-parse logic into a test
helper (e.g. get_key_usage) and call it from test_leaf_cert_has_eku,
test_leaf_key_usage_bits, and test_ca_key_usage_bits to reduce boilerplate: the
helper should take cert_der: &[u8], parse with X509Certificate::from_der, locate
the extension matching OID_X509_EXT_KEY_USAGE, and return the
ParsedExtension::KeyUsage value (or panic on mismatch); be mindful of lifetimes
since KeyUsage borrows from the parsed certificate—either return an owned/cloned
KeyUsage or keep the parsed certificate in scope in the tests so the borrowed
KeyUsage remains valid.
In `@crates/uselesskey-x509/src/chain_spec.rs`:
- Around line 281-289: Add an isolated assertion that changing leaf_cn affects
stable_bytes: inside test_stable_bytes_field_sensitivity, clone base, call
.with_leaf_cn("other.example.com".to_string()) on the clone (similar to the
existing leaf_sans subcase), compute changed.stable_bytes() and
assert_ne!(changed.stable_bytes(), base_bytes, "leaf_cn must affect
stable_bytes"). This mirrors the SAN test and ensures leaf_cn's independent
contribution to stable_bytes is covered.
In `@crates/uselesskey-x509/src/spec.rs`:
- Around line 408-415: The test test_stable_bytes_field_sensitivity repeats the
same "clone → mutate one field → assert_ne" pattern for multiple fields;
refactor it to a parameterized rstest so each case is a separate test case with
its own name and failure message. Replace the monolithic assertions with an
rstest that takes parameters (a description/name, a closure or function that
given base returns the mutated object, and the expected inequality check) and
call changed.stable_bytes() vs base_bytes inside the parameterized test body;
reference the existing test name test_stable_bytes_field_sensitivity and the
methods used to mutate fields (e.g., with_subject, with_issuer, with_sans, etc.)
so you can swap each mutation into the rstest parameter list. Ensure you add the
rstest attribute and individual case names to produce targeted failures per
field.
---
Outside diff comments:
In `@crates/uselesskey-ecdsa/tests/jwk_private.rs`:
- Around line 90-103: The test kid_matches_public_key_hash_prefix hardcodes the
decoded KID length as 12; update it to use the shared constant
DEFAULT_KID_PREFIX_BYTES instead (use DEFAULT_KID_PREFIX_BYTES in the assertion
comparing decoded.len() to avoid the magic number) — if DEFAULT_KID_PREFIX_BYTES
is not pub from its crate, either make it public or import the value via the
crate's public API (or document why the test must keep a literal), and update
the assertion in the test function and any similar test (e.g., the counterpart
in the Ed25519 tests) to compare against DEFAULT_KID_PREFIX_BYTES rather than
12.
In `@crates/uselesskey-ed25519/tests/jwk_private.rs`:
- Around line 82-89: In the kid_matches_public_key_hash_prefix test (function
kid_matches_public_key_hash_prefix) replace the hard-coded literal 12 with the
exported constant DEFAULT_KID_PREFIX_BYTES from uselesskey_core_kid and add the
appropriate import; i.e., import DEFAULT_KID_PREFIX_BYTES from
uselesskey_core_kid and use it in the assert_eq!(decode_b64url(&kid).len(),
DEFAULT_KID_PREFIX_BYTES) assertion so the test uses the canonical constant
rather than a magic number.
| rust-version.workspace = true | ||
| repository.workspace = true | ||
| authors.workspace = true | ||
| publish = true |
There was a problem hiding this comment.
publish = true on a BDD step-definitions crate looks unintentional.
This crate is test infrastructure, yet it is marked as publishable. It also isn't listed in PUBLISH_CRATES in xtask/src/main.rs (line 242), so publish-preflight checks won't validate it. If it is truly intended for crates.io, the path-only dependencies on lines 13, 19, and 20 need version fields (required by cargo publish). Otherwise, set publish = false.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-bdd-steps/Cargo.toml` at line 9, The crate's Cargo.toml
currently has publish = true but this BDD step-definitions crate appears to be
test-only; either change publish = false in Cargo.toml or, if you intend to
publish, add version fields to the path-only dependencies referenced in the
manifest (the path deps currently missing version) and add the crate name to the
PUBLISH_CRATES list in xtask/src/main.rs so it is validated by the publish
preflight; update the publish flag or add the required version attributes and
PUBLISH_CRATES entry accordingly to make the manifest consistent with publishing
requirements.
| .as_ref() | ||
| .expect("core-id master seed not set"); | ||
|
|
||
| let domain: &'static str = Box::leak(domain.into_boxed_str()); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
# Find ArtifactDomain definition and its usage
rg -n "ArtifactDomain" --type rust -B 2 -A 2Repository: EffortlessMetrics/uselesskey
Length of output: 2402
🏁 Script executed:
# Look at the specific file and context around line 31
head -50 crates/uselesskey-bdd-steps/src/steps/core_id_steps.rs | cat -nRepository: EffortlessMetrics/uselesskey
Length of output: 1923
🏁 Script executed:
# Find ArtifactId::new signature to understand the domain parameter requirements
rg -n "impl.*ArtifactId|fn new.*domain" --type rust -A 3Repository: EffortlessMetrics/uselesskey
Length of output: 331
🏁 Script executed:
# Check if there are other uses of Box::leak in the codebase to see if this is a documented pattern
rg -n "Box::leak" --type rustRepository: EffortlessMetrics/uselesskey
Length of output: 196
🏁 Script executed:
# Check if the BDD steps could use string literals instead of Box::leak
rg -n "core_id_derive_seed" --type rust -A 20Repository: EffortlessMetrics/uselesskey
Length of output: 1803
🏁 Script executed:
# Look at the ArtifactId struct and field usage to understand why &'static str is required
rg -n "struct ArtifactId|domain:" --type rust -B 2 -A 2Repository: EffortlessMetrics/uselesskey
Length of output: 14446
Document the intentional Box::leak usage with an explanatory comment.
ArtifactDomain is defined as &'static str in the library API, requiring a static lifetime. Since Cucumber provides the domain as a runtime String, Box::leak converts it to &'static str. While this leaks memory per scenario invocation, the leak is bounded in BDD tests. Add a comment explaining why the leak is necessary and acceptable in this test context.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-bdd-steps/src/steps/core_id_steps.rs` at line 31, Add an
inline comment above the let domain: &'static str =
Box::leak(domain.into_boxed_str()); line in core_id_steps.rs documenting that
Box::leak is intentionally used to convert the runtime String to &'static str
because the library API (ArtifactDomain) requires a 'static lifetime; note that
this leaks memory per scenario but is bounded and acceptable in BDD tests, and
include a brief mention that changing the API to accept owned String or a
shorter lifetime would be an alternative if leaks were unacceptable.
| const CORE_KEYPAIR_PKCS8_DER: &[u8] = &[0x30, 0x82, 0x01, 0x22, 0x04, 0x20, 0xAA, 0xBB]; | ||
| #[cfg(feature = "uk-core-keypair")] | ||
| const CORE_KEYPAIR_SPKI_DER: &[u8] = &[0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2A, 0x86]; | ||
| #[cfg(feature = "uk-core-keypair")] | ||
| const CORE_KEYPAIR_PKCS8_PEM: &str = | ||
| "-----BEGIN PRIVATE KEY-----\nMHg=\n-----END PRIVATE KEY-----\n"; | ||
| #[cfg(feature = "uk-core-keypair")] | ||
| const CORE_KEYPAIR_SPKI_PEM: &str = "-----BEGIN PUBLIC KEY-----\nMFk=\n-----END PUBLIC KEY-----\n"; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
DER and PEM fixture constants represent different byte sequences.
The PEM body MHg= decodes to [0x30, 0x78] (2 bytes), while CORE_KEYPAIR_PKCS8_DER is [0x30, 0x82, 0x01, 0x22, 0x04, 0x20, 0xAA, 0xBB] (8 bytes). Similarly for the SPKI pair. These are intentionally synthetic, but for a "sample PKCS8/SPKI fixture" the inconsistency between DER and PEM forms could confuse future maintainers. Consider adding a brief comment clarifying they are independent dummy blobs, not encoding-paired representations.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-bdd-steps/src/steps/core_keypair_steps.rs` around lines 7 -
14, The DER and PEM constants (CORE_KEYPAIR_PKCS8_DER, CORE_KEYPAIR_PKCS8_PEM,
CORE_KEYPAIR_SPKI_DER, CORE_KEYPAIR_SPKI_PEM) are synthetic and not
binary-equivalent; add a short clarifying comment above these constants stating
that the PEM bodies do not decode to the shown DER values and that these are
independent dummy/sample blobs for testing only, so maintainers won't assume
they are paired encodings.
| #[cfg(feature = "uk-core-keypair")] | ||
| #[then("the first and second core-keypair kids should be identical")] | ||
| fn core_keypair_kids_identical(world: &mut crate::UselessWorld) { | ||
| assert_eq!(world.core_kid_first, world.core_kid_second); | ||
| } |
There was a problem hiding this comment.
Assertion passes vacuously if both core_kid_first and core_kid_second are None.
If the preceding "derive kid" steps didn't execute (e.g., feature mismatch or scenario wiring error), both Option fields remain None, and assert_eq!(None, None) succeeds silently — hiding a test gap.
🐛 Proposed fix
fn core_keypair_kids_identical(world: &mut crate::UselessWorld) {
+ assert!(world.core_kid_first.is_some(), "core_kid_first was never set");
+ assert!(world.core_kid_second.is_some(), "core_kid_second was never set");
assert_eq!(world.core_kid_first, world.core_kid_second);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| #[cfg(feature = "uk-core-keypair")] | |
| #[then("the first and second core-keypair kids should be identical")] | |
| fn core_keypair_kids_identical(world: &mut crate::UselessWorld) { | |
| assert_eq!(world.core_kid_first, world.core_kid_second); | |
| } | |
| #[cfg(feature = "uk-core-keypair")] | |
| #[then("the first and second core-keypair kids should be identical")] | |
| fn core_keypair_kids_identical(world: &mut crate::UselessWorld) { | |
| assert!(world.core_kid_first.is_some(), "core_kid_first was never set"); | |
| assert!(world.core_kid_second.is_some(), "core_kid_second was never set"); | |
| assert_eq!(world.core_kid_first, world.core_kid_second); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-bdd-steps/src/steps/core_keypair_steps.rs` around lines 99
- 103, The current assertion in core_keypair_kids_identical can pass when both
world.core_kid_first and world.core_kid_second are None; change the test to
first assert both Option fields are Some (fail with a clear message if not),
then compare their inner values for equality (e.g., unwrap or pattern-match
after the presence checks) so the step fails when KIDs were not actually
derived; reference function core_keypair_kids_identical and fields
core_kid_first/core_kid_second.
| const CORE_NEGATIVE_PEM_FIXTURE: &str = | ||
| "-----BEGIN PRIVATE KEY-----\nMHg=\n-----END PRIVATE KEY-----\n"; | ||
| const CORE_NEGATIVE_DER_FIXTURE: &[u8] = &[0x30, 0x82, 0x01, 0x22, 0x30, 0x0D, 0x06, 0x09]; |
There was a problem hiding this comment.
Add #[cfg(feature = "uk-core-negative")] to both constants to avoid dead-code warnings.
CORE_NEGATIVE_PEM_FIXTURE and CORE_NEGATIVE_DER_FIXTURE are only consumed inside #[cfg(feature = "uk-core-negative")] functions. Without the same gate, the compiler emits dead-code warnings (or errors when deny(dead_code) / deny(warnings) is in force).
🛠️ Proposed fix
+#[cfg(feature = "uk-core-negative")]
const CORE_NEGATIVE_PEM_FIXTURE: &str =
"-----BEGIN PRIVATE KEY-----\nMHg=\n-----END PRIVATE KEY-----\n";
+#[cfg(feature = "uk-core-negative")]
const CORE_NEGATIVE_DER_FIXTURE: &[u8] = &[0x30, 0x82, 0x01, 0x22, 0x30, 0x0D, 0x06, 0x09];📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const CORE_NEGATIVE_PEM_FIXTURE: &str = | |
| "-----BEGIN PRIVATE KEY-----\nMHg=\n-----END PRIVATE KEY-----\n"; | |
| const CORE_NEGATIVE_DER_FIXTURE: &[u8] = &[0x30, 0x82, 0x01, 0x22, 0x30, 0x0D, 0x06, 0x09]; | |
| #[cfg(feature = "uk-core-negative")] | |
| const CORE_NEGATIVE_PEM_FIXTURE: &str = | |
| "-----BEGIN PRIVATE KEY-----\nMHg=\n-----END PRIVATE KEY-----\n"; | |
| #[cfg(feature = "uk-core-negative")] | |
| const CORE_NEGATIVE_DER_FIXTURE: &[u8] = &[0x30, 0x82, 0x01, 0x22, 0x30, 0x0D, 0x06, 0x09]; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-bdd-steps/src/steps/core_negative_steps.rs` around lines 6
- 8, The two fixture constants CORE_NEGATIVE_PEM_FIXTURE and
CORE_NEGATIVE_DER_FIXTURE are only used under the uk-core-negative feature gate
so add #[cfg(feature = "uk-core-negative")] above each constant definition to
prevent dead-code warnings; locate the const declarations for
CORE_NEGATIVE_PEM_FIXTURE and CORE_NEGATIVE_DER_FIXTURE and annotate both with
the same feature attribute so they are compiled only when the uk-core-negative
feature is enabled.
| FeatureSet::new("all-features+rustls", &["--features", "uk-all,uk-rustls"]), | ||
| FeatureSet::new("all-features+tonic", &["--features", "uk-all,uk-tonic"]), | ||
| FeatureSet::new("all-features+ring", &["--features", "uk-all,uk-ring"]), | ||
| FeatureSet::new( | ||
| "all-features+rustcrypto", | ||
| &["--features", "uk-all,uk-rustcrypto"], | ||
| ), | ||
| FeatureSet::new( | ||
| "all-features+aws-lc-rs", | ||
| &["--features", "uk-all,uk-aws-lc-rs"], | ||
| ), |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Inconsistent use of feature-name constants within BDD_FEATURE_MATRIX.
Line 113 uses UK_FEATURE_ALL correctly, but lines 114–124 inline string literals like "uk-all,uk-rustls". This is understandable since Rust can't concatenate &'static str at compile time, but it means changes to UK_FEATURE_* constants won't propagate here. Add a comment noting the limitation, or consider a compile-time test that verifies each literal contains only known feature names.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-feature-grid/src/lib.rs` around lines 114 - 124,
BDD_FEATURE_MATRIX uses inline comma-separated feature string literals in
FeatureSet::new calls (e.g., "uk-all,uk-rustls") which won't pick up changes to
the UK_FEATURE_* constants; either add a brief comment above BDD_FEATURE_MATRIX
explaining Rust cannot concat &'static str at compile time so these literals are
intentional, or add a compile-time/unit test that parses each FeatureSet string
in BDD_FEATURE_MATRIX, splits on commas and asserts every token is one of the
canonical constants (UK_FEATURE_ALL, UK_FEATURE_RUSTLS, UK_FEATURE_TONIC,
UK_FEATURE_RING, UK_FEATURE_RUSTCRYPTO, UK_FEATURE_AWS_LC_RS) to ensure literals
remain in sync. Ensure references use the FeatureSet::new entries and the
BDD_FEATURE_MATRIX symbol so future edits are easy to locate.
| pub fn kid(&self) -> String { | ||
| use base64::Engine as _; | ||
| use base64::engine::general_purpose::URL_SAFE_NO_PAD; | ||
|
|
||
| let h = blake3::hash(self.secret_bytes()); | ||
| let short = &h.as_bytes()[..12]; // 96 bits is plenty for tests. | ||
| URL_SAFE_NO_PAD.encode(short) | ||
| kid_from_bytes(self.secret_bytes()) | ||
| } |
There was a problem hiding this comment.
Add a golden test to protect KID stability after the algorithm migration.
The kid_is_deterministic test (lines 192–199) only verifies that two invocations with the same seed produce the same KID in the same binary. It does not guard against the algorithm change introduced by this refactoring: if kid_from_bytes's BLAKE3 parameters (prefix size, domain separator, base64url encoding length) differ from the previous inline computation, any system that persisted KIDs (e.g., stored in a JWKS endpoint or a database) will silently break.
The DOMAIN_HMAC_SECRET constant has an explicit stability comment; the KID derivation output deserves the same protection via a snapshot test:
🛡️ Proposed golden test
+ #[test]
+ #[cfg(feature = "jwk")]
+ fn kid_golden_value() {
+ // Seed fixed so the secret bytes are reproducible; kid must be stable across refactors.
+ let fx = Factory::deterministic(Seed::from_env_value("hmac-kid").unwrap());
+ let secret = fx.hmac("issuer", HmacSpec::hs512());
+ // Update this constant only after a deliberate, documented breaking change.
+ assert_eq!(
+ secret.kid(),
+ "<insert expected base64url value here>",
+ "KID derivation algorithm changed — this is a breaking change for stored JWKs",
+ );
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-hmac/src/secret.rs` around lines 71 - 73, Add a golden
(snapshot) test that asserts the exact KID string produced by
kid_from_bytes/kid() for a fixed, hard-coded seed/secret matches a stored
expected value so future algorithm or parameter changes are caught; create a new
test (e.g., alongside kid_is_deterministic) that derives secret_bytes from the
same deterministic seed used in existing tests, calls Secret::kid() or
kid_from_bytes(secret_bytes()), and compares the result to an explicit string
literal (the golden value) instead of comparing two results from the same
algorithm.
| #[test] | ||
| fn test_leaf_key_usage_bits() { | ||
| use x509_parser::prelude::*; | ||
|
|
||
| let factory = fx(); | ||
| let spec = X509Spec::self_signed("ku-leaf.example.com"); | ||
| let cert = factory.x509_self_signed("ku-leaf", spec); | ||
|
|
||
| let (_, parsed) = X509Certificate::from_der(cert.cert_der()).expect("parse cert"); | ||
|
|
||
| let ku_ext = parsed | ||
| .extensions() | ||
| .iter() | ||
| .find(|ext| ext.oid == x509_parser::oid_registry::OID_X509_EXT_KEY_USAGE) | ||
| .expect("leaf cert should have KeyUsage extension"); | ||
|
|
||
| let ku = match ku_ext.parsed_extension() { | ||
| x509_parser::extensions::ParsedExtension::KeyUsage(ku) => ku, | ||
| other => panic!("expected KeyUsage, got {:?}", other), | ||
| }; | ||
|
|
||
| // Leaf defaults: digital_signature=true, key_encipherment=true, | ||
| // key_cert_sign=false, crl_sign=false | ||
| assert!(ku.digital_signature(), "leaf must have DigitalSignature"); | ||
| assert!(ku.key_encipherment(), "leaf must have KeyEncipherment"); | ||
| assert!(!ku.key_cert_sign(), "leaf must NOT have KeyCertSign"); | ||
| assert!(!ku.crl_sign(), "leaf must NOT have CrlSign"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_ca_key_usage_bits() { | ||
| use x509_parser::prelude::*; | ||
|
|
||
| let factory = fx(); | ||
| let spec = X509Spec::self_signed_ca("ku-ca.example.com"); | ||
| let cert = factory.x509_self_signed("ku-ca", spec); | ||
|
|
||
| let (_, parsed) = X509Certificate::from_der(cert.cert_der()).expect("parse cert"); | ||
|
|
||
| let ku_ext = parsed | ||
| .extensions() | ||
| .iter() | ||
| .find(|ext| ext.oid == x509_parser::oid_registry::OID_X509_EXT_KEY_USAGE) | ||
| .expect("CA cert should have KeyUsage extension"); | ||
|
|
||
| let ku = match ku_ext.parsed_extension() { | ||
| x509_parser::extensions::ParsedExtension::KeyUsage(ku) => ku, | ||
| other => panic!("expected KeyUsage, got {:?}", other), | ||
| }; | ||
|
|
||
| // CA defaults: digital_signature=true, key_encipherment=false, | ||
| // key_cert_sign=true, crl_sign=true | ||
| assert!(ku.digital_signature(), "CA must have DigitalSignature"); | ||
| assert!(!ku.key_encipherment(), "CA must NOT have KeyEncipherment"); | ||
| assert!(ku.key_cert_sign(), "CA must have KeyCertSign"); | ||
| assert!(ku.crl_sign(), "CA must have CrlSign"); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Optional: extract a test helper for extension lookup + parse.
The OID-find-then-pattern-match sequence is repeated across test_leaf_cert_has_eku, test_leaf_key_usage_bits, and test_ca_key_usage_bits. A small test-only helper would reduce the boilerplate:
♻️ Example helper (add inside `mod tests`)
fn get_key_usage(cert_der: &[u8]) -> x509_parser::extensions::KeyUsage<'_> {
use x509_parser::prelude::*;
let (_, parsed) = X509Certificate::from_der(cert_der).expect("parse cert");
let ext = parsed
.extensions()
.iter()
.find(|e| e.oid == x509_parser::oid_registry::OID_X509_EXT_KEY_USAGE)
.expect("cert should have KeyUsage extension");
match ext.parsed_extension() {
x509_parser::extensions::ParsedExtension::KeyUsage(ku) => ku.clone(),
other => panic!("expected KeyUsage, got {:?}", other),
}
}Note: this would need care with lifetimes since KeyUsage borrows from the parsed certificate. If the borrow makes this awkward, keeping the inline pattern is fine.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-x509/src/cert.rs` around lines 598 - 654, Extract the
repeated OID-find-and-parse logic into a test helper (e.g. get_key_usage) and
call it from test_leaf_cert_has_eku, test_leaf_key_usage_bits, and
test_ca_key_usage_bits to reduce boilerplate: the helper should take cert_der:
&[u8], parse with X509Certificate::from_der, locate the extension matching
OID_X509_EXT_KEY_USAGE, and return the ParsedExtension::KeyUsage value (or panic
on mismatch); be mindful of lifetimes since KeyUsage borrows from the parsed
certificate—either return an owned/cloned KeyUsage or keep the parsed
certificate in scope in the tests so the borrowed KeyUsage remains valid.
| // Changing leaf_sans | ||
| let changed = base | ||
| .clone() | ||
| .with_sans(vec!["extra.example.com".to_string()]); | ||
| assert_ne!( | ||
| changed.stable_bytes(), | ||
| base_bytes, | ||
| "leaf_sans must affect stable_bytes" | ||
| ); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
LGTM — but consider adding an isolated leaf_cn sensitivity assertion.
The new sub-case is correct: replacing leaf_sans from ["test.example.com"] to ["extra.example.com"] produces a different stable_bytes output because both the SAN count and the SAN bytes differ after the sort+dedup step.
One coverage gap worth noting: leaf_cn is independently encoded in stable_bytes (lines 112–114) but has no isolated mutation test in test_stable_bytes_field_sensitivity. The existing test_stable_bytes_determinism changes leaf_cn, root_cn, intermediate_cn, and leaf_sans all at once via new(), so it doesn't isolate leaf_cn's contribution. A direct field mutation would cover this:
🧪 Suggested addition to test_stable_bytes_field_sensitivity
+ // Changing leaf_cn
+ let mut changed = base.clone();
+ changed.leaf_cn = "other.example.com".to_string();
+ assert_ne!(
+ changed.stable_bytes(),
+ base_bytes,
+ "leaf_cn must affect stable_bytes"
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Changing leaf_sans | |
| let changed = base | |
| .clone() | |
| .with_sans(vec!["extra.example.com".to_string()]); | |
| assert_ne!( | |
| changed.stable_bytes(), | |
| base_bytes, | |
| "leaf_sans must affect stable_bytes" | |
| ); | |
| // Changing leaf_sans | |
| let changed = base | |
| .clone() | |
| .with_sans(vec!["extra.example.com".to_string()]); | |
| assert_ne!( | |
| changed.stable_bytes(), | |
| base_bytes, | |
| "leaf_sans must affect stable_bytes" | |
| ); | |
| // Changing leaf_cn | |
| let mut changed = base.clone(); | |
| changed.leaf_cn = "other.example.com".to_string(); | |
| assert_ne!( | |
| changed.stable_bytes(), | |
| base_bytes, | |
| "leaf_cn must affect stable_bytes" | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-x509/src/chain_spec.rs` around lines 281 - 289, Add an
isolated assertion that changing leaf_cn affects stable_bytes: inside
test_stable_bytes_field_sensitivity, clone base, call
.with_leaf_cn("other.example.com".to_string()) on the clone (similar to the
existing leaf_sans subcase), compute changed.stable_bytes() and
assert_ne!(changed.stable_bytes(), base_bytes, "leaf_cn must affect
stable_bytes"). This mirrors the SAN test and ensures leaf_cn's independent
contribution to stable_bytes is covered.
|
|
||
| // Changing sans changes output | ||
| let changed = base.clone().with_sans(vec!["san.example.com".into()]); | ||
| assert_ne!( | ||
| changed.stable_bytes(), | ||
| base_bytes, | ||
| "sans must affect stable_bytes" | ||
| ); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider refactoring test_stable_bytes_field_sensitivity to use rstest.
The new block follows the same "clone → mutate one field → assert_ne" pattern repeated for every field. This is a classic parameterized test case: the assertion logic is identical, only the changed expression and the failure message vary. Using rstest would give each variant its own named test entry and a targeted failure message, rather than one monolithic test that stops at the first failing assertion.
♻️ Example rstest refactor sketch
- #[test]
- fn test_stable_bytes_field_sensitivity() {
- let base = X509Spec::self_signed("test");
- let base_bytes = base.stable_bytes();
-
- let changed = base.clone().with_validity_days(999);
- assert_ne!(changed.stable_bytes(), base_bytes, "validity_days must affect stable_bytes");
- // ... (all other sub-cases) ...
- let changed = base.clone().with_sans(vec!["san.example.com".into()]);
- assert_ne!(changed.stable_bytes(), base_bytes, "sans must affect stable_bytes");
- }
+ use rstest::rstest;
+
+ fn base_spec() -> X509Spec { X509Spec::self_signed("test") }
+
+ #[rstest]
+ #[case(base_spec().with_validity_days(999), "validity_days")]
+ #[case(base_spec().with_is_ca(true), "is_ca")]
+ #[case(base_spec().with_rsa_bits(4096), "rsa_bits")]
+ #[case(base_spec().with_not_before(NotBeforeOffset::DaysFromNow(7)), "not_before_offset")]
+ #[case(base_spec().with_key_usage(KeyUsage::ca()), "key_usage")]
+ #[case({ let mut s = base_spec(); s.issuer_cn = "Other".into(); s }, "issuer_cn")]
+ #[case(base_spec().with_sans(vec!["san.example.com".into()]), "sans")]
+ fn test_stable_bytes_field_sensitivity(#[case] changed: X509Spec, #[case] field: &str) {
+ assert_ne!(
+ changed.stable_bytes(),
+ base_spec().stable_bytes(),
+ "{field} must affect stable_bytes",
+ );
+ }As per coding guidelines, rstest should be used for parameterized tests in crates/**/*.rs.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-x509/src/spec.rs` around lines 408 - 415, The test
test_stable_bytes_field_sensitivity repeats the same "clone → mutate one field →
assert_ne" pattern for multiple fields; refactor it to a parameterized rstest so
each case is a separate test case with its own name and failure message. Replace
the monolithic assertions with an rstest that takes parameters (a
description/name, a closure or function that given base returns the mutated
object, and the expected inequality check) and call changed.stable_bytes() vs
base_bytes inside the parameterized test body; reference the existing test name
test_stable_bytes_field_sensitivity and the methods used to mutate fields (e.g.,
with_subject, with_issuer, with_sans, etc.) so you can swap each mutation into
the rstest parameter list. Ensure you add the rstest attribute and individual
case names to produce targeted failures per field.
Add default-features = false on microcrate dependency edges and forward std feature to uselesskey-core-id and uselesskey-core-negative. Remove invalid module-level #![no_std] attributes from id.rs and negative/mod.rs (only valid at crate root).
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@crates/uselesskey-core-negative/Cargo.toml`:
- Line 18: Replace the direct path/version dependency for uselesskey-core-id
with a workspace dependency for consistency: update the dependency entry for
"uselesskey-core-id" in the Cargo.toml from the current "{ path =
\"../uselesskey-core-id\", version = \"0.3.0\", default-features = false }" form
to use "workspace = true" (you can omit repeating default-features if the root
workspace already sets it) so version/workspace inheritance matches other
crates.
| documentation = "https://docs.rs/uselesskey-core-negative" | ||
|
|
||
| [dependencies] | ||
| uselesskey-core-id = { path = "../uselesskey-core-id", version = "0.3.0", default-features = false } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Prefer workspace = true for uselesskey-core-id to stay consistent with the rest of the workspace.
crates/uselesskey-core/Cargo.toml (and others) reference this same crate via { workspace = true, default-features = false }. Using the direct path here bypasses workspace inheritance and means version bumps must be updated in two places.
♻️ Proposed refactor
-uselesskey-core-id = { path = "../uselesskey-core-id", version = "0.3.0", default-features = false }
+uselesskey-core-id = { workspace = true }The root workspace entry already declares default-features = false, so no override is needed here.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| uselesskey-core-id = { path = "../uselesskey-core-id", version = "0.3.0", default-features = false } | |
| uselesskey-core-id = { workspace = true } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-core-negative/Cargo.toml` at line 18, Replace the direct
path/version dependency for uselesskey-core-id with a workspace dependency for
consistency: update the dependency entry for "uselesskey-core-id" in the
Cargo.toml from the current "{ path = \"../uselesskey-core-id\", version =
\"0.3.0\", default-features = false }" form to use "workspace = true" (you can
omit repeating default-features if the root workspace already sets it) so
version/workspace inheritance matches other crates.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@crates/uselesskey-core-id/src/lib.rs`:
- Line 8: The function hash32 currently exposes blake3::Hash in the public API;
change hash32 to return a plain [u8; 32] instead of blake3::Hash (convert the
internal blake3 hash to a fixed-size array via .as_bytes() ->
try_into().unwrap() or equivalent), remove the public use/import of
blake3::Hash, and update all call sites (including tests referencing hash32 and
places that call .as_bytes() on its result) to accept the [u8; 32] return value
(dropping the extra .as_bytes() calls). Ensure any signatures or exports
referencing blake3::Hash (e.g., in docs or other public functions) are updated
to avoid leaking blake3 types.
- Around line 150-278: Add property-based tests using proptest in the existing
tests module to satisfy the guideline: write a proptest for idempotency that
generates arbitrary Seed (or [u8;32]) and ArtifactId and asserts
derive_seed(&master, &id).bytes() equals a second call; write an injectivity
proptest that generates a base ArtifactId then mutates exactly one field
(domain, label, variant, or DerivationVersion) via strategies and asserts
derive_seed results differ; and write a parse_hex_32 roundtrip proptest that
generates arbitrary [u8;32], hex-encodes it (with/without 0x and mixed
case/whitespace variants), calls parse_hex_32 and verifies the bytes match. Use
the existing symbols Seed, ArtifactId::new, derive_seed, parse_hex_32, and
DerivationVersion in the tests and add the proptest crate to dev-dependencies if
missing.
---
Duplicate comments:
In `@crates/uselesskey-core-id/src/lib.rs`:
- Around line 268-277: The two tests derive_seed_label_affects_output and
derive_seed_version_affects_output both repeat the same pattern; replace them
with a single parameterized rstest that takes variants (e.g., ("label",
"label-a", "label-b") and ("version", DerivationVersion::V1,
DerivationVersion::V2)) and asserts derive_seed(&master, &id1).bytes() !=
derive_seed(&master, &id2).bytes(); construct ArtifactId instances using
ArtifactId::new and Seed::new([5u8;32]) inside the fixture, add the #[rstest]
attribute and parameter list, and remove the duplicated standalone tests so the
behavior is covered by the single parameterized test using the derive_seed and
ArtifactId symbols.
- Around line 222-243: The test derive_seed_version_affects_output duplicates
version checks; convert it into a single parameterized rstest that iterates over
the DerivationVersion values (e.g., DerivationVersion::V1 and
DerivationVersion(2)) to avoid duplicated test bodies — keep usage of Seed::new,
ArtifactId::new, and derive_seed, and assert that derived bytes differ between
the two provided versions by comparing results from derive_seed for each version
pair; ensure the test function name and referenced symbols (derive_seed, Seed,
ArtifactId, DerivationVersion) remain so they are easy to locate.
- Around line 119-125: The library is printing to stderr with eprintln! in the
match arm (other => ...) which is not appropriate for a library; replace the
eprintln! call with a library-friendly logging call such as log::warn! (or
tracing::warn! if you use tracing) that includes the unknown derivation version
{other}, keep the fallback to derive_seed_v1(master, id) in that branch, and
ensure you add/enable the chosen logging crate (or gate the call behind an
optional feature) so the code compiles in your crate configuration; reference
the match arm containing derive_seed_v1 to locate the change.
- Around line 144-148: The write_len_prefixed function currently uses
u32::try_from(bytes.len()).unwrap_or(u32::MAX), which silently corrupts the
length prefix when the slice is larger than u32::MAX; change write_len_prefixed
to explicitly handle that overflow instead of defaulting to u32::MAX: perform
u32::try_from(bytes.len()) and propagate an error (change the signature to
return Result<(), E> and map the conversion failure into a clear error) or else
panic with a descriptive expect message (e.g.,
u32::try_from(...).expect("write_len_prefixed: input too large for u32 length
prefix")), and update any callers to handle the new Result if you choose the
error-return approach so no silent corruption can occur.
|
|
||
| use alloc::string::String; | ||
|
|
||
| use blake3::Hash; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
hash32 leaks blake3::Hash into the public API surface.
Every internal call site immediately calls .as_bytes() (lines 34, 104, and 186 in tests). Returning [u8; 32] directly decouples the public API from the blake3 crate and removes the need for all callers to .as_bytes().
♻️ Proposed refactor
-use blake3::Hash;
-
...
-pub fn hash32(bytes: &[u8]) -> Hash {
- blake3::hash(bytes)
+pub fn hash32(bytes: &[u8]) -> [u8; 32] {
+ *blake3::hash(bytes).as_bytes()
}Then update call sites:
-spec_fingerprint: *hash32(spec_bytes).as_bytes(),
+spec_fingerprint: hash32(spec_bytes),-Ok(Self(*hash32(v.as_bytes()).as_bytes()))
+Ok(Self(hash32(v.as_bytes())))Also applies to: 111-113
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-core-id/src/lib.rs` at line 8, The function hash32
currently exposes blake3::Hash in the public API; change hash32 to return a
plain [u8; 32] instead of blake3::Hash (convert the internal blake3 hash to a
fixed-size array via .as_bytes() -> try_into().unwrap() or equivalent), remove
the public use/import of blake3::Hash, and update all call sites (including
tests referencing hash32 and places that call .as_bytes() on its result) to
accept the [u8; 32] return value (dropping the extra .as_bytes() calls). Ensure
any signatures or exports referencing blake3::Hash (e.g., in docs or other
public functions) are updated to avoid leaking blake3 types.
| #[cfg(test)] | ||
| mod tests { | ||
| use super::{ArtifactId, DerivationVersion, Seed, derive_seed, hash32, parse_hex_32}; | ||
|
|
||
| #[test] | ||
| fn seed_debug_is_redacted() { | ||
| let seed = Seed::new([7u8; 32]); | ||
| assert_eq!(format!("{:?}", seed), "Seed(**redacted**)"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn parse_hex_32_rejects_wrong_length() { | ||
| let err = parse_hex_32("abcd").unwrap_err(); | ||
| assert!(err.contains("expected 64 hex chars")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn parse_hex_32_rejects_invalid_char() { | ||
| let mut s = "0".repeat(64); | ||
| s.replace_range(10..11, "g"); | ||
|
|
||
| let err = parse_hex_32(&s).unwrap_err(); | ||
| assert!(err.contains("invalid hex char")); | ||
| } | ||
|
|
||
| #[test] | ||
| fn artifact_id_fingerprints_spec_bytes() { | ||
| let spec = [1u8, 2, 3, 4, 5]; | ||
| let id = ArtifactId::new( | ||
| "domain:test", | ||
| "label", | ||
| &spec, | ||
| "variant", | ||
| DerivationVersion::V1, | ||
| ); | ||
|
|
||
| let expected = *hash32(&spec).as_bytes(); | ||
| assert_eq!(id.spec_fingerprint, expected); | ||
| } | ||
|
|
||
| #[test] | ||
| fn artifact_id_preserves_fields() { | ||
| let id = ArtifactId::new( | ||
| "domain:test", | ||
| "my-label", | ||
| b"spec", | ||
| "my-variant", | ||
| DerivationVersion::V1, | ||
| ); | ||
|
|
||
| assert_eq!(id.domain, "domain:test"); | ||
| assert_eq!(id.label, "my-label"); | ||
| assert_eq!(id.variant, "my-variant"); | ||
| assert_eq!(id.derivation_version, DerivationVersion::V1); | ||
| } | ||
|
|
||
| #[test] | ||
| fn derive_seed_unknown_version_is_deterministic() { | ||
| let master = Seed::new([9u8; 32]); | ||
| let id = ArtifactId::new( | ||
| "domain:test", | ||
| "label", | ||
| b"spec", | ||
| "variant", | ||
| DerivationVersion(999), | ||
| ); | ||
|
|
||
| let first = derive_seed(&master, &id); | ||
| let second = derive_seed(&master, &id); | ||
| assert_eq!(first.bytes(), second.bytes()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn derive_seed_version_affects_output() { | ||
| let master = Seed::new([3u8; 32]); | ||
| let id_v1 = ArtifactId::new( | ||
| "domain:test", | ||
| "label", | ||
| b"spec", | ||
| "variant", | ||
| DerivationVersion::V1, | ||
| ); | ||
| let id_v2 = ArtifactId::new( | ||
| "domain:test", | ||
| "label", | ||
| b"spec", | ||
| "variant", | ||
| DerivationVersion(2), | ||
| ); | ||
|
|
||
| let v1 = derive_seed(&master, &id_v1); | ||
| let v2 = derive_seed(&master, &id_v2); | ||
| assert_ne!(v1.bytes(), v2.bytes()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn seed_from_env_value_parses_hex_with_prefix_and_whitespace() { | ||
| let hex = "0x0000000000000000000000000000000000000000000000000000000000000001"; | ||
| let seed = Seed::from_env_value(&format!(" {hex} ")).unwrap(); | ||
| assert_eq!(seed.bytes()[31], 1); | ||
| assert!(seed.bytes()[..31].iter().all(|b| *b == 0)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn seed_from_env_value_parses_uppercase_hex() { | ||
| let hex = "F".repeat(64); | ||
| let seed = Seed::from_env_value(&hex).unwrap(); | ||
| assert!(seed.bytes().iter().all(|b| *b == 0xFF)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn parse_hex_32_lowercase_values() { | ||
| // "0a" repeated 32 times → each byte should be 0x0a = 10 | ||
| let hex = "0a".repeat(32); | ||
| let parsed = parse_hex_32(&hex).unwrap(); | ||
| assert!(parsed.iter().all(|b| *b == 0x0a)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn derive_seed_label_affects_output() { | ||
| let master = Seed::new([5u8; 32]); | ||
| let id_a = ArtifactId::new("d", "label-a", b"spec", "v", DerivationVersion::V1); | ||
| let id_b = ArtifactId::new("d", "label-b", b"spec", "v", DerivationVersion::V1); | ||
| assert_ne!( | ||
| derive_seed(&master, &id_a).bytes(), | ||
| derive_seed(&master, &id_b).bytes() | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Missing proptest property-based tests — coding guideline violation.
The test module contains only deterministic unit tests. For a seed derivation primitive, proptest should cover at minimum:
- Idempotency:
derive_seedon the same(master, id)pair is always equal over arbitrary inputs. - Injectivity: changing any single
ArtifactIdfield (domain, label, variant, or version) produces a distinct seed. parse_hex_32roundtrip: encoding an arbitrary[u8; 32]to hex and round-tripping throughparse_hex_32recovers the original bytes.
As per coding guidelines: "Use proptest for property-based tests."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-core-id/src/lib.rs` around lines 150 - 278, Add
property-based tests using proptest in the existing tests module to satisfy the
guideline: write a proptest for idempotency that generates arbitrary Seed (or
[u8;32]) and ArtifactId and asserts derive_seed(&master, &id).bytes() equals a
second call; write an injectivity proptest that generates a base ArtifactId then
mutates exactly one field (domain, label, variant, or DerivationVersion) via
strategies and asserts derive_seed results differ; and write a parse_hex_32
roundtrip proptest that generates arbitrary [u8;32], hex-encodes it
(with/without 0x and mixed case/whitespace variants), calls parse_hex_32 and
verifies the bytes match. Use the existing symbols Seed, ArtifactId::new,
derive_seed, parse_hex_32, and DerivationVersion in the tests and add the
proptest crate to dev-dependencies if missing.
Add content assertion to truncation test to catch argument-swap mutations. All 25 mutants now caught or unviable.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@crates/uselesskey-core-keypair/src/lib.rs`:
- Around line 133-139: The test debug_does_not_include_key_pem currently only
checks that the private PEM header is absent; update it to also assert that the
public-key PEM header is absent (e.g., check dbg does not contain "BEGIN PUBLIC
KEY") so the Debug output does not leak any key material from
Pkcs8SpkiKeyMaterial.
- Around line 70-77: The debug_does_not_include_key_pem test only checks that
"BEGIN PRIVATE KEY" is not present; update the test so it also asserts that
"BEGIN PUBLIC KEY" is absent to ensure neither private nor public key PEM
material is printed. Locate the test named debug_does_not_include_key_pem and
add an assertion that the debug string does not contain "BEGIN PUBLIC KEY" (in
addition to the existing "BEGIN PRIVATE KEY" check); you can reference the
key-producing methods write_private_key_pkcs8_pem and write_public_key_spki_pem
to ensure both PEM types are covered by the test.
---
Duplicate comments:
In `@crates/uselesskey-core-keypair/src/lib.rs`:
- Around line 110-117: Add gitleaks suppression annotations to the synthetic PEM
fixtures: insert a comment like "gitleaks:allow" immediately above the PEM
strings in sample_material() (the private/public key literals in
Pkcs8SpkiKeyMaterial::new) and likewise add the same "gitleaks:allow" comment
above the second fixture inside kid_depends_on_spki_bytes to suppress the
false-positive scanner hits on these fake PEM blocks.
| pub fn write_private_key_pkcs8_pem(&self) -> Result<TempArtifact, Error> { | ||
| TempArtifact::new_string("uselesskey-", ".pkcs8.pem", self.private_key_pkcs8_pem()) | ||
| } | ||
|
|
||
| /// Write the SPKI PEM public key to a tempfile and return the handle. | ||
| pub fn write_public_key_spki_pem(&self) -> Result<TempArtifact, Error> { | ||
| TempArtifact::new_string("uselesskey-", ".spki.pem", self.public_key_spki_pem()) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find Error type definition in uselesskey-core crate
echo "=== Searching for Error type in uselesskey-core ==="
find crates/uselesskey-core*/src -name "*.rs" -type f | head -20Repository: EffortlessMetrics/uselesskey
Length of output: 626
🏁 Script executed:
#!/bin/bash
# Find the uselesskey-core crate and locate Error definition
fd -e rs crates/uselesskey-core/srcRepository: EffortlessMetrics/uselesskey
Length of output: 481
🏁 Script executed:
#!/bin/bash
# Search for 'type Error' or 'enum Error' in uselesskey-core
rg "type Error|enum Error" crates/uselesskey-core/src --type=rust -B 1 -A 2Repository: EffortlessMetrics/uselesskey
Length of output: 347
🏁 Script executed:
#!/bin/bash
# Check the exact import and usage in the keypair file
echo "=== Lines 1-10 of keypair lib.rs ==="
head -10 crates/uselesskey-core-keypair/src/lib.rs
echo ""
echo "=== Lines 70-77 of keypair lib.rs ==="
sed -n '70,77p' crates/uselesskey-core-keypair/src/lib.rsRepository: EffortlessMetrics/uselesskey
Length of output: 834
🏁 Script executed:
#!/bin/bash
# Check error.rs for From<std::io::Error> impl
cat crates/uselesskey-core/src/error.rsRepository: EffortlessMetrics/uselesskey
Length of output: 1500
🏁 Script executed:
#!/bin/bash
# Also check if the file compiles by looking for any obvious compilation barriers
echo "=== Check if the methods can compile ==="
echo "Looking at full Error enum definition and any From implementations:"
rg -A 20 "pub enum Error" crates/uselesskey-core/src/error.rsRepository: EffortlessMetrics/uselesskey
Length of output: 727
🏁 Script executed:
#!/bin/bash
# Find TempArtifact::new_string signature
rg -B 2 -A 5 "fn new_string" crates/uselesskey-core-sink/src/lib.rsRepository: EffortlessMetrics/uselesskey
Length of output: 604
🏁 Script executed:
#!/bin/bash
# Check if there's evidence of successful builds or CI configuration
echo "=== Check for CI/build configuration ==="
find . -maxdepth 2 -name "*.yml" -o -name "*.yaml" | head -10Repository: EffortlessMetrics/uselesskey
Length of output: 182
🏁 Script executed:
#!/bin/bash
# Check the Cargo.toml of uselesskey-core-keypair to understand the crate setup
cat crates/uselesskey-core-keypair/Cargo.tomlRepository: EffortlessMetrics/uselesskey
Length of output: 767
🏁 Script executed:
#!/bin/bash
# Check the CI workflow to see if tests/builds are actually run
ls -la .github/workflows/ 2>/dev/null || echo "No workflows directory"Repository: EffortlessMetrics/uselesskey
Length of output: 238
🏁 Script executed:
#!/bin/bash
# Look for any CI/build workflow file
find . -path "./.github/workflows/*.yml" -o -path "./.github/workflows/*.yaml"Repository: EffortlessMetrics/uselesskey
Length of output: 98
🏁 Script executed:
#!/bin/bash
# Try to see if there are GitHub Actions workflows
cat .github/workflows/*.yml 2>/dev/null | head -50Repository: EffortlessMetrics/uselesskey
Length of output: 1200
🏁 Script executed:
#!/bin/bash
# Check if the actual code at lines 70-77 has been modified or if there's a build step
echo "=== Verify current code compiles or has issues ==="
# Look at the actual function signatures more carefully
rg -B 5 -A 3 "pub fn write_private_key_pkcs8_pem" crates/uselesskey-core-keypair/src/lib.rsRepository: EffortlessMetrics/uselesskey
Length of output: 456
🏁 Script executed:
#!/bin/bash
# Check the xtask to see what build/test steps are run
fd -e rs xtask crates/ | head -5Repository: EffortlessMetrics/uselesskey
Length of output: 54
🏁 Script executed:
#!/bin/bash
# Try to understand if there's something that auto-converts errors
# Check if there's any custom build or macro magic
grep -r "macro_rules\|#\[derive" crates/uselesskey-core-keypair/src/lib.rs | head -5Repository: EffortlessMetrics/uselesskey
Length of output: 88
🏁 Script executed:
#!/bin/bash
# Check if the methods might use a custom Result type instead of std Result
rg "type Result" crates/uselesskey-core-keypair/src/lib.rsRepository: EffortlessMetrics/uselesskey
Length of output: 54
🏁 Script executed:
#!/antml:noninteractive_/bin/bash
# Final check: look at the full function implementation context
sed -n '1,80p' crates/uselesskey-core-keypair/src/lib.rs | tail -40Repository: EffortlessMetrics/uselesskey
Length of output: 1385
🏁 Script executed:
#!/bin/bash
# One more critical check: does the code actually use the ? operator?
# Let me get the EXACT bytes/characters at lines 70-77
echo "=== Exact lines 70-77 with all whitespace visible ==="
sed -n '70,77p' crates/uselesskey-core-keypair/src/lib.rs | od -c | head -30Repository: EffortlessMetrics/uselesskey
Length of output: 183
🏁 Script executed:
#!/bin/bash
# Also check if there's any way this Result type is being rebound
grep -n "Result<" crates/uselesskey-core-keypair/src/lib.rs | head -10Repository: EffortlessMetrics/uselesskey
Length of output: 233
🏁 Script executed:
#!/bin/bash
# One final verification: check if there might be a blanket From impl somewhere
rg "impl.*From" crates/uselesskey-core/src/error.rsRepository: EffortlessMetrics/uselesskey
Length of output: 54
🏁 Script executed:
#!/bin/bash
# Get the exact content of lines 70-77 with cat to be absolutely sure
echo "=== Lines 70-77 exact content ==="
sed -n '70,77p' crates/uselesskey-core-keypair/src/lib.rs
echo ""
echo "=== Check if there's a trailing ? ==="
sed -n '71p' crates/uselesskey-core-keypair/src/lib.rs | grep -o '?'
sed -n '76p' crates/uselesskey-core-keypair/src/lib.rs | grep -o '?'Repository: EffortlessMetrics/uselesskey
Length of output: 569
🏁 Script executed:
#!/bin/bash
# Final definitive check: search for any hidden characters or multi-line continuation
echo "=== Lines 70-77 with hex dump to catch any hidden characters ==="
sed -n '70,77p' crates/uselesskey-core-keypair/src/lib.rs | xxd | head -20Repository: EffortlessMetrics/uselesskey
Length of output: 195
🏁 Script executed:
#!/bin/bash
# Check if maybe there's implicit conversion happening through some other mechanism
# Look at all usages of TempArtifact::new_string in the entire codebase
rg "TempArtifact::new_string" --type=rust -B 1 -A 1Repository: EffortlessMetrics/uselesskey
Length of output: 6057
🏁 Script executed:
#!/bin/bash
# KEY FINDING: There are TWO TempArtifact implementations!
# One in uselesskey-core-sink/src/lib.rs (returns std::io::Result)
# One in uselesskey-core/src/sink/mod.rs (should return uselesskey_core::Result)
# Let me check which one is being used by keypair
echo "=== Which TempArtifact is imported in keypair? ==="
head -12 crates/uselesskey-core-keypair/src/lib.rsRepository: EffortlessMetrics/uselesskey
Length of output: 426
🏁 Script executed:
#!/bin/bash
# Check the actual implementation in uselesskey-core
cat crates/uselesskey-core/src/sink/mod.rs | head -100Repository: EffortlessMetrics/uselesskey
Length of output: 2056
🏁 Script executed:
#!/bin/bash
# Check the Gitleaks concern - are there fake keys that might trigger false positives?
sed -n '113,115p' crates/uselesskey-core-keypair/src/lib.rs
sed -n '194,196p' crates/uselesskey-core-keypair/src/lib.rsRepository: EffortlessMetrics/uselesskey
Length of output: 487
🏁 Script executed:
#!/antml:noninteractive_/bin/bash
# Check the debug test at lines 134-139
sed -n '133,139p' crates/uselesskey-core-keypair/src/lib.rsRepository: EffortlessMetrics/uselesskey
Length of output: 324
🏁 Script executed:
#!/bin/bash
# Final verification: check the full Debug implementation to understand what it outputs
sed -n '22,31p' crates/uselesskey-core-keypair/src/lib.rsRepository: EffortlessMetrics/uselesskey
Length of output: 501
Strengthen the debug test to verify both private and public key material is excluded.
The debug_does_not_include_key_pem test (lines 133–139) only asserts that "BEGIN PRIVATE KEY" is absent. Since the guideline is to never print any key material in debug output, the test should also verify that "BEGIN PUBLIC KEY" is absent:
Suggested enhancement
#[test]
fn debug_does_not_include_key_pem() {
let material = sample_material();
let dbg = format!("{material:?}");
assert!(dbg.contains("Pkcs8SpkiKeyMaterial"));
assert!(!dbg.contains("BEGIN PRIVATE KEY"));
+ assert!(!dbg.contains("BEGIN PUBLIC KEY"));
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-core-keypair/src/lib.rs` around lines 70 - 77, The
debug_does_not_include_key_pem test only checks that "BEGIN PRIVATE KEY" is not
present; update the test so it also asserts that "BEGIN PUBLIC KEY" is absent to
ensure neither private nor public key PEM material is printed. Locate the test
named debug_does_not_include_key_pem and add an assertion that the debug string
does not contain "BEGIN PUBLIC KEY" (in addition to the existing "BEGIN PRIVATE
KEY" check); you can reference the key-producing methods
write_private_key_pkcs8_pem and write_public_key_spki_pem to ensure both PEM
types are covered by the test.
| #[test] | ||
| fn debug_does_not_include_key_pem() { | ||
| let material = sample_material(); | ||
| let dbg = format!("{material:?}"); | ||
| assert!(dbg.contains("Pkcs8SpkiKeyMaterial")); | ||
| assert!(!dbg.contains("BEGIN PRIVATE KEY")); | ||
| } |
There was a problem hiding this comment.
debug_does_not_include_key_pem should also assert the public key PEM is absent.
The test currently only verifies BEGIN PRIVATE KEY is absent. The Debug impl is correct for both, but the test leaves the public-key side uncovered. Per the guideline "Never print key material in debug output," the assertion should cover all key material fields.
✅ Proposed addition
assert!(!dbg.contains("BEGIN PRIVATE KEY"));
+ assert!(!dbg.contains("BEGIN PUBLIC KEY"));As per coding guidelines: "Never print key material in debug output."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| #[test] | |
| fn debug_does_not_include_key_pem() { | |
| let material = sample_material(); | |
| let dbg = format!("{material:?}"); | |
| assert!(dbg.contains("Pkcs8SpkiKeyMaterial")); | |
| assert!(!dbg.contains("BEGIN PRIVATE KEY")); | |
| } | |
| #[test] | |
| fn debug_does_not_include_key_pem() { | |
| let material = sample_material(); | |
| let dbg = format!("{material:?}"); | |
| assert!(dbg.contains("Pkcs8SpkiKeyMaterial")); | |
| assert!(!dbg.contains("BEGIN PRIVATE KEY")); | |
| assert!(!dbg.contains("BEGIN PUBLIC KEY")); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@crates/uselesskey-core-keypair/src/lib.rs` around lines 133 - 139, The test
debug_does_not_include_key_pem currently only checks that the private PEM header
is absent; update it to also assert that the public-key PEM header is absent
(e.g., check dbg does not contain "BEGIN PUBLIC KEY") so the Debug output does
not leak any key material from Pkcs8SpkiKeyMaterial.
Summary
uselesskey-core-keypair): Extracts PKCS#8/SPKI key pair helpers into a standalone crate, enabling reuse across RSA, ECDSA, and Ed25519 extension cratesuselesskey-coreinternals into focused sub-crates (core-id,core-kid,core-negative,core-sink) for better modularityuselesskey-bdd-stepslibrary crate with new feature files for core-id, core-keypair, core-kid, and core-negativeTest plan
cargo xtask fmtpassescargo xtask clippypassescargo xtask prpasses (4/5 targets;aws-lc-rsskipped due to missing NASM on local machine — CI has NASM)aws-lc-rsand BDD tests