Skip to content

feat(xmldsig): add KeyInfo dispatch parsing#47

Open
polaz wants to merge 10 commits intomainfrom
feat/#46-keyinfo-dispatch
Open

feat(xmldsig): add KeyInfo dispatch parsing#47
polaz wants to merge 10 commits intomainfrom
feat/#46-keyinfo-dispatch

Conversation

@polaz
Copy link
Copy Markdown
Member

@polaz polaz commented Apr 4, 2026

Summary

  • implement ROADMAP P2-001 KeyInfo child dispatch parsing
  • support KeyName, KeyValue, X509Data, and dsig11 DEREncodedKeyValue sources
  • wire KeyInfo parsing into verify pipeline with explicit ParseKeyInfo mapping

Details

  • added parse types and parser in src/xmldsig/parse.rs
  • re-exported new KeyInfo parsing API from src/xmldsig/mod.rs
  • added pipeline and parser tests in parse.rs and verify.rs

Validation

  • cargo fmt -- --check
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo nextest run --workspace
  • cargo test --doc --workspace

Closes #46

- parse KeyInfo child sources: KeyName, KeyValue, X509Data, DEREncodedKeyValue
- wire KeyInfo parsing into signature verification pipeline with explicit ParseKeyInfo error
- add parser and pipeline coverage tests for order and malformed input

Closes #46
Copilot AI review requested due to automatic review settings April 4, 2026 20:44
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

Warning

Rate limit exceeded

@polaz has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 1 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 4 minutes and 1 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5ad2ca85-69e8-474a-9149-09b3a4a648d9

📥 Commits

Reviewing files that changed from the base of the PR and between 9f9917e and 196d7fa.

📒 Files selected for processing (2)
  • src/xmldsig/parse.rs
  • src/xmldsig/verify.rs
📝 Walkthrough

Walkthrough

Parses ds:KeyInfo (KeyName, KeyValue, X509Data, dsig11:DEREncodedKeyValue); adds KeyInfo result types and parse_key_info(); introduces an internal XML-whitespace helper; integrates KeyInfo parsing into the verify pipeline; and tightens child-order and mixed-content validation.

Changes

Cohort / File(s) Summary
KeyInfo Parser & Models
src/xmldsig/parse.rs
Adds XMLDSIG11 namespace and DER size limits; introduces KeyInfo, KeyInfoSource, KeyValueInfo, X509DataInfo; implements parse_key_info with element-wise dispatch, strict structure/whitespace/base64 checks, and extensive unit tests.
Signature Verification Flow
src/xmldsig/verify.rs
Wires parse_key_info into verify_signature_with_context; adds DsigError::ParseKeyInfo; tracks key_info_node; enforces <Signature> element ordering/arity, rejects non-whitespace mixed text and foreign-namespace element children; updates tests.
Module Surface & Whitespace Helper
src/xmldsig/mod.rs, src/xmldsig/whitespace.rs
Exports new parse symbols (KeyInfo, KeyInfoSource, KeyValueInfo, X509DataInfo, parse_key_info), adds pub(crate) mod whitespace;, and implements is_xml_whitespace_only(text: &str) -> bool.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor Caller
    participant Verify as "verify.rs"
    participant ParseSigned as "parse.rs (parse_signed_info)"
    participant ParseKey as "parse.rs (parse_key_info)"
    participant Whitespace as "whitespace.rs"

    Caller->>Verify: verify_signature_with_context(signature_node)
    Verify->>ParseSigned: parse_signed_info(signed_info_node)
    ParseSigned-->>Verify: SignedInfo
    Verify->>Verify: parse_signature_value()
    alt KeyInfo present
        Verify->>ParseKey: parse_key_info(key_info_node)
        ParseKey->>Whitespace: is_xml_whitespace_only(text)
        ParseKey->>ParseKey: dispatch children (KeyName, KeyValue, X509Data, DEREncodedKeyValue)
        ParseKey-->>Verify: KeyInfo
    else KeyInfo absent
        Note over Verify: continue without KeyInfo
    end
    Verify-->>Caller: verification result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title concisely and accurately captures the main change: implementing KeyInfo dispatch parsing with support for multiple source types.
Description check ✅ Passed The description is directly related to the changeset, detailing implementation of KeyInfo parsing, supported sources, pipeline wiring, and validation performed.
Linked Issues check ✅ Passed All requirements from issue #46 are met: parsing types added, child dispatch implemented, all source types supported, lax mode with unknown element ignoring, comprehensive tests added, and pipeline integration completed.
Out of Scope Changes check ✅ Passed All changes are directly aligned with issue #46 objectives. The whitespace module addition and parse_signature_children hardening are supporting implementations for the KeyInfo dispatch parsing feature.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#46-keyinfo-dispatch

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds initial XMLDSig <KeyInfo> child dispatch parsing (ROADMAP P2-001) and wires it into the verification pipeline so malformed/unsupported structures can be surfaced as explicit pipeline parse errors.

Changes:

  • Introduces KeyInfo/KeyInfoSource parsing with dispatch for KeyName, KeyValue, X509Data, and dsig11:DEREncodedKeyValue.
  • Integrates <KeyInfo> parsing into the verify pipeline and enforces <Signature> child ordering with optional KeyInfo as the 3rd element child.
  • Re-exports the new parsing API from the xmldsig module and adds targeted unit tests for parser + pipeline behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/xmldsig/parse.rs Adds KeyInfo model types and a dispatch parser for supported KeyInfo child elements + unit tests.
src/xmldsig/verify.rs Parses optional KeyInfo in the verify pipeline, adds ParseKeyInfo error mapping, and enforces Signature child ordering + pipeline tests.
src/xmldsig/mod.rs Re-exports new KeyInfo parsing types/functions as part of the public API.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/xmldsig/verify.rs (1)

964-1043: ⚠️ Potential issue | 🟠 Major

Reject unexpected <Signature> children after the allowed prefix.

This index tracking only proves where SignedInfo, SignatureValue, and KeyInfo appear; it still accepts "<Signature><SignedInfo/><SignatureValue/><Foo/></Signature>" and "...<KeyInfo/><Foo/>" because any other trailing element is ignored. The content model after the optional KeyInfo is only Object*, so this should fail closed and get a regression test.

As per coding guidelines, Tier 1 - Flag logic bugs: missing validation (unvalidated input from network/disk/external source).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/xmldsig/verify.rs` around lines 964 - 1043, The parser currently ignores
unexpected XMLDSIG element children after the allowed prefix, so
"<Signature><SignedInfo/><SignatureValue/><Foo/></Signature>" wrongly succeeds;
fix parse_signature_children to count only XMLDSIG element children (use a
xml_element_index counter incremented only when child.tag_name().namespace() ==
Some(XMLDSIG_NS)) and enforce the content model: require SignedInfo at xml index
1 and SignatureValue at 2, allow optional KeyInfo at 3, and permit only "Object"
elements after that; treat any other XMLDSIG element name at any position as
SignatureVerificationPipelineError::InvalidStructure (include the element name
in the reason) and fail if an unexpected element appears after the allowed
prefix. Reference: parse_signature_children, signed_info_index,
signature_value_index, key_info_index, and SignatureChildNodes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/xmldsig/parse.rs`:
- Around line 357-383: The loop over element_children(key_info_node) accepts
malformed mixed-content like "<KeyValue>junk<RSAKeyValue/></KeyValue>" because
element_children() and collect_text_content() silently drop unexpected node
kinds; update the KeyInfo parsing to explicitly reject non-whitespace mixed
content and unexpected child elements: for KeyName (in the KeyName arm) assert
the node contains no element children and only text (trim() non-empty allowed,
any embedded element nodes or non-whitespace text before/after elements =>
return Err(ParseError::InvalidStructure(...))); for KeyValue and X509Data ensure
the node has no non-whitespace text nodes mixed with element children before
calling parse_key_value_dispatch or parse_x509_data_dispatch; for
DEREncodedKeyValue ensure there are no element children and the text node is the
only content before calling decode_base64_xml_text; return a clear
ParseError::InvalidStructure on violations. Apply the same validation pattern to
the other parsing locations you mentioned (around the
parse_key_value_dispatch/parse_x509_data_dispatch and DER decoding sites).
- Around line 115-161: Add forward-compatibility by marking the public parse
model as non-exhaustive and tighten field visibility: add #[non_exhaustive] to
KeyInfo, KeyInfoSource, KeyValueInfo, and X509DataInfo; change internal fields
that need not be publicly mutable (KeyInfo.sources and the X509DataInfo *_count
fields) from pub to pub(crate) so consumers cannot match exhaustively or rely on
concrete variants/fields; leave enum variants and public types exported but rely
on non_exhaustive to allow adding new variants/fields in future.

---

Outside diff comments:
In `@src/xmldsig/verify.rs`:
- Around line 964-1043: The parser currently ignores unexpected XMLDSIG element
children after the allowed prefix, so
"<Signature><SignedInfo/><SignatureValue/><Foo/></Signature>" wrongly succeeds;
fix parse_signature_children to count only XMLDSIG element children (use a
xml_element_index counter incremented only when child.tag_name().namespace() ==
Some(XMLDSIG_NS)) and enforce the content model: require SignedInfo at xml index
1 and SignatureValue at 2, allow optional KeyInfo at 3, and permit only "Object"
elements after that; treat any other XMLDSIG element name at any position as
SignatureVerificationPipelineError::InvalidStructure (include the element name
in the reason) and fail if an unexpected element appears after the allowed
prefix. Reference: parse_signature_children, signed_info_index,
signature_value_index, key_info_index, and SignatureChildNodes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f04a3c91-55a7-4840-8c05-39edae4ff650

📥 Commits

Reviewing files that changed from the base of the PR and between dad900e and 7063ba0.

📒 Files selected for processing (3)
  • src/xmldsig/mod.rs
  • src/xmldsig/parse.rs
  • src/xmldsig/verify.rs

@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 4, 2026

Addressed the outside-diff item as well in commit 2004cf8: now fails closed for unexpected XMLDSIG children after the allowed SignedInfo/SignatureValue/(optional KeyInfo) prefix, and the regression suite passes.

@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 4, 2026

Correction: outside-diff item is fixed in commit 2004cf8. Function parse_signature_children now rejects unexpected XMLDSIG children after the allowed SignedInfo/SignatureValue/(optional KeyInfo) prefix, and full regression tests are green.

@polaz polaz requested a review from Copilot April 4, 2026 21:27
@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 4, 2026

@coderabbitai re-review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

@polaz: Thanks for the follow-up and for confirming that commit 2004cf8 hardens parse_signature_children to fail closed on unexpected XMLDSIG children after the allowed SignedInfo/SignatureValue/(optional KeyInfo) prefix. I'll kick off a fresh review of the PR now.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T15:50:07.840Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), `push_normalized_signature_text` strips only the four XML whitespace characters (space 0x20, tab 0x09, CR 0x0D, LF 0x0A) from `SignatureValue` text before base64 decoding, and explicitly rejects any other ASCII whitespace (e.g., form feed 0x0C, vertical tab 0x0B) as `SignatureVerificationPipelineError::SignatureValueBase64(base64::DecodeError::InvalidByte(...))`. The regression test `push_normalized_signature_text_rejects_form_feed` covers the form-feed boundary case.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 32
File: src/xmldsig/verify.rs:34-51
Timestamp: 2026-03-28T22:00:29.444Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), the `VerifyingKey` and `KeyResolver` public traits intentionally omit `Send + Sync` supertraits to preserve lightweight single-threaded implementations. The single-threaded-only constraint is documented on both trait definitions. Do not flag the absence of `Send + Sync` on these traits as a review issue.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 41
File: tests/donor_full_verification_suite.rs:165-178
Timestamp: 2026-04-04T06:14:34.735Z
Learning: In `tests/donor_full_verification_suite.rs` (xml-sec Rust project), the `Expectation::Skip` arm must exercise skipped vectors through `VerifyContext::new().verify(xml)` (no injected PEM key) so that the real key-resolution path runs, and must assert a concrete blocker outcome per case: `Invalid(KeyNotFound)` for vectors with unresolved KeyValue/X.509 key material, and `ParseSignedInfo(UnsupportedAlgorithm)` for DSA-signature vectors. Accepting any `Err` or non-Valid result as a valid skip is insufficient and can mask stale skip reasons or unrelated failures.
✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 4, 2026

@coderabbitai Outside-diff item (verify.rs 964-1043) is already addressed: parse_signature_children now fails closed on unexpected XMLDSIG children after SignedInfo/SignatureValue/(optional KeyInfo), and workspace tests are green after latest commit 5238c92.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

Tip

For best results, initiate chat on the files or code changes.

@polaz: Confirmed — thanks for the pointer to commit 5238c92. With parse_signature_children now failing closed on any unexpected XMLDSIG-namespace children beyond the allowed SignedInfo / SignatureValue / (optional KeyInfo) prefix, and the full workspace suite green, the outside-diff concern is fully resolved.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T15:50:07.840Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), `push_normalized_signature_text` strips only the four XML whitespace characters (space 0x20, tab 0x09, CR 0x0D, LF 0x0A) from `SignatureValue` text before base64 decoding, and explicitly rejects any other ASCII whitespace (e.g., form feed 0x0C, vertical tab 0x0B) as `SignatureVerificationPipelineError::SignatureValueBase64(base64::DecodeError::InvalidByte(...))`. The regression test `push_normalized_signature_text_rejects_form_feed` covers the form-feed boundary case.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 41
File: tests/donor_full_verification_suite.rs:165-178
Timestamp: 2026-04-04T06:14:34.735Z
Learning: In `tests/donor_full_verification_suite.rs` (xml-sec Rust project), the `Expectation::Skip` arm must exercise skipped vectors through `VerifyContext::new().verify(xml)` (no injected PEM key) so that the real key-resolution path runs, and must assert a concrete blocker outcome per case: `Invalid(KeyNotFound)` for vectors with unresolved KeyValue/X.509 key material, and `ParseSignedInfo(UnsupportedAlgorithm)` for DSA-signature vectors. Accepting any `Err` or non-Valid result as a valid skip is insufficient and can mask stale skip reasons or unrelated failures.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T18:51:49.919Z
Learning: In `src/xmldsig/parse.rs` (xml-sec Rust project), the private function `n` (formerly `base64_decode_digest`) strips only the four XML whitespace characters (space 0x20, tab 0x09, CR 0x0D, LF 0x0A) from `DigestValue` base64 text before decoding, and returns `ParseError::Base64(format!("invalid XML whitespace U+{:04X} ...", ...))` for any other `is_ascii_whitespace()` character (e.g. form feed 0x0C). Regression tests `n_accepts_xml_whitespace_chars` and `n_rejects_non_xml_ascii_whitespace` cover these cases.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 32
File: src/xmldsig/verify.rs:34-51
Timestamp: 2026-03-28T22:00:29.444Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), the `VerifyingKey` and `KeyResolver` public traits intentionally omit `Send + Sync` supertraits to preserve lightweight single-threaded implementations. The single-threaded-only constraint is documented on both trait definitions. Do not flag the absence of `Send + Sync` on these traits as a review issue.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T20:47:05.798Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), the ASCII-whitespace branch in `push_normalized_signature_text` uses `u8::try_from(u32::from(ch)).expect("ASCII whitespace always fits into u8")` for the explicit narrowing conversion, producing `base64::DecodeError::InvalidByte(normalized.len(), invalid_byte)` as the error payload. The conversion is provably infallible because `ch.is_ascii_whitespace()` guarantees the character is in the 0x00–0x7F range.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 12
File: src/c14n/serialize.rs:382-398
Timestamp: 2026-03-16T17:41:41.053Z
Learning: Repo: structured-world/xml-sec (Rust)
Context: src/c14n/serialize.rs::collect_inherited_xml_attrs
Learning: For inclusive C14N subsets, collect_inherited_xml_attrs must stop at the nearest included ancestor (A in set → B out → C in) to avoid inheriting xml:* through an included ancestor. A unit test named `no_inheritance_past_included_ancestor` covers this scenario. Exclusive C14N should skip this inheritance step entirely.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 12
File: src/c14n/xml_base.rs:61-93
Timestamp: 2026-03-16T17:23:26.715Z
Learning: In the xml-sec Rust project, the xml:base URI resolver in `src/c14n/xml_base.rs` correctly uses RFC 3986 §5 for IRI resolution in C14N 1.1 mode, as C14N 1.1 §2.4 explicitly delegates to RFC 3986 §5. When the base URI has no scheme, `parse_base()` returns `None` and the reference is returned as-is — this is the correct fallback behavior. The W3C C14N 1.1 interop test suite notes "modifications to RFC 3986 dot segment processing for C14N11" which may represent edge cases worth verifying against the `remove_dot_segments()` implementation.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 16
File: tests/fixtures_smoke.rs:104-104
Timestamp: 2026-03-17T15:17:20.125Z
Learning: In structured-world/xml-sec (Rust), `expect()` on `fs::read_to_string()` and other I/O calls is acceptable and idiomatic inside `#[test]` functions. The Tier 2 rule (use `Result` propagation instead of `expect()` on I/O paths) applies only to production code paths, not test code. Repo instructions (line 51) state: "Test code style: Tests prioritize readability and explicitness over DRY."

Learnt from: polaz
Repo: structured-world/xml-sec PR: 6
File: src/c14n/ns_exclusive.rs:81-107
Timestamp: 2026-03-15T19:26:11.246Z
Learning: In the xml-sec project (Rust), roxmltree 0.21 does not expose lexical prefixes from parsed QNames — `ExpandedName` only provides `namespace()` and `name()`. As a result, `visibly_utilized_prefixes()` in `src/c14n/ns_exclusive.rs` and `write_qualified_name()` in `src/c14n/serialize.rs` must use `lookup_prefix(namespace_uri)` for prefix resolution. This is a known limitation: when multiple prefixes bind the same namespace URI, the reverse-mapping is ambiguous. The ambiguity is documented in code comments and tracked as a ROADMAP gap; the edge case is considered rare in SAML/XMLDSig use cases.

Learnt from: CR
Repo: structured-world/xml-sec PR: 0
File: .github/instructions/rust.instructions.md:0-0
Timestamp: 2026-03-17T12:12:28.146Z
Learning: Review ONLY code within the PR's diff; for issues outside the diff, suggest opening a separate issue

Learnt from: CR
Repo: structured-world/xml-sec PR: 0
File: .github/instructions/rust.instructions.md:0-0
Timestamp: 2026-03-17T12:12:28.146Z
Learning: Applies to **/*.rs : Tier 1 - Flag logic bugs: checksum/hash mismatches (computing over wrong byte range, verifying against stale value)

Learnt from: CR
Repo: structured-world/xml-sec PR: 0
File: .github/instructions/code-review.instructions.md:0-0
Timestamp: 2026-03-17T12:12:07.790Z
Learning: Applies to **/*.rs : Corruption/validation tests: tamper the relevant field (e.g., digest value, signature bytes, base64 encoding) and assert the error

@polaz polaz requested a review from Copilot April 4, 2026 22:48
@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 4, 2026

@coderabbitai re-review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

@polaz: Kicking off a fresh review of PR #47 now. I'll examine all the latest changes including the parse_key_info dispatcher, the KeyInfo/KeyInfoSource/KeyValueInfo/X509DataInfo data model, the pipeline wiring in verify.rs, and the hardened parse_signature_children from commits 2004cf8/5238c92.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T15:50:07.840Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), `push_normalized_signature_text` strips only the four XML whitespace characters (space 0x20, tab 0x09, CR 0x0D, LF 0x0A) from `SignatureValue` text before base64 decoding, and explicitly rejects any other ASCII whitespace (e.g., form feed 0x0C, vertical tab 0x0B) as `SignatureVerificationPipelineError::SignatureValueBase64(base64::DecodeError::InvalidByte(...))`. The regression test `push_normalized_signature_text_rejects_form_feed` covers the form-feed boundary case.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 41
File: tests/donor_full_verification_suite.rs:165-178
Timestamp: 2026-04-04T06:14:34.735Z
Learning: In `tests/donor_full_verification_suite.rs` (xml-sec Rust project), the `Expectation::Skip` arm must exercise skipped vectors through `VerifyContext::new().verify(xml)` (no injected PEM key) so that the real key-resolution path runs, and must assert a concrete blocker outcome per case: `Invalid(KeyNotFound)` for vectors with unresolved KeyValue/X.509 key material, and `ParseSignedInfo(UnsupportedAlgorithm)` for DSA-signature vectors. Accepting any `Err` or non-Valid result as a valid skip is insufficient and can mask stale skip reasons or unrelated failures.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 32
File: src/xmldsig/verify.rs:34-51
Timestamp: 2026-03-28T22:00:29.444Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), the `VerifyingKey` and `KeyResolver` public traits intentionally omit `Send + Sync` supertraits to preserve lightweight single-threaded implementations. The single-threaded-only constraint is documented on both trait definitions. Do not flag the absence of `Send + Sync` on these traits as a review issue.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T18:51:49.919Z
Learning: In `src/xmldsig/parse.rs` (xml-sec Rust project), the private function `n` (formerly `base64_decode_digest`) strips only the four XML whitespace characters (space 0x20, tab 0x09, CR 0x0D, LF 0x0A) from `DigestValue` base64 text before decoding, and returns `ParseError::Base64(format!("invalid XML whitespace U+{:04X} ...", ...))` for any other `is_ascii_whitespace()` character (e.g. form feed 0x0C). Regression tests `n_accepts_xml_whitespace_chars` and `n_rejects_non_xml_ascii_whitespace` cover these cases.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T20:47:05.798Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), the ASCII-whitespace branch in `push_normalized_signature_text` uses `u8::try_from(u32::from(ch)).expect("ASCII whitespace always fits into u8")` for the explicit narrowing conversion, producing `base64::DecodeError::InvalidByte(normalized.len(), invalid_byte)` as the error payload. The conversion is provably infallible because `ch.is_ascii_whitespace()` guarantees the character is in the 0x00–0x7F range.

Learnt from: CR
Repo: structured-world/xml-sec PR: 0
File: .github/instructions/rust.instructions.md:0-0
Timestamp: 2026-03-17T12:12:28.146Z
Learning: Applies to **/*.rs : Tier 1 - Flag logic bugs: checksum/hash mismatches (computing over wrong byte range, verifying against stale value)

Learnt from: polaz
Repo: structured-world/xml-sec PR: 16
File: tests/fixtures_smoke.rs:104-104
Timestamp: 2026-03-17T15:17:20.125Z
Learning: In structured-world/xml-sec (Rust), `expect()` on `fs::read_to_string()` and other I/O calls is acceptable and idiomatic inside `#[test]` functions. The Tier 2 rule (use `Result` propagation instead of `expect()` on I/O paths) applies only to production code paths, not test code. Repo instructions (line 51) state: "Test code style: Tests prioritize readability and explicitness over DRY."

Learnt from: CR
Repo: structured-world/xml-sec PR: 0
File: .github/instructions/code-review.instructions.md:0-0
Timestamp: 2026-03-17T12:12:07.790Z
Learning: Applies to **/*.rs : Corruption/validation tests: tamper the relevant field (e.g., digest value, signature bytes, base64 encoding) and assert the error

Learnt from: CR
Repo: structured-world/xml-sec PR: 0
File: .github/instructions/rust.instructions.md:0-0
Timestamp: 2026-03-17T12:12:28.146Z
Learning: Applies to **/*.rs : Tier 2 - Flag safety issues: constant-time comparison must be used for cryptographic MACs/checksums

Learnt from: polaz
Repo: structured-world/xml-sec PR: 12
File: src/c14n/serialize.rs:382-398
Timestamp: 2026-03-16T17:41:41.053Z
Learning: Repo: structured-world/xml-sec (Rust)
Context: src/c14n/serialize.rs::collect_inherited_xml_attrs
Learning: For inclusive C14N subsets, collect_inherited_xml_attrs must stop at the nearest included ancestor (A in set → B out → C in) to avoid inheriting xml:* through an included ancestor. A unit test named `no_inheritance_past_included_ancestor` covers this scenario. Exclusive C14N should skip this inheritance step entirely.
✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/xmldsig/verify.rs (1)

976-984: ⚠️ Potential issue | 🟠 Major

Reject foreign and mixed-content children under <Signature>.

This scan only tracks XMLDSig element children and silently skips everything else, so malformed inputs like <foo:Bar/> or stray non-whitespace text after the allowed prefix still pass the new suffix checks. If this function is meant to enforce the SignedInfo, SignatureValue, optional KeyInfo, then Object* shape, it needs to fail closed on any non-XMLDSig element child and any non-whitespace mixed content under <Signature>.

🔒 Suggested hardening
-    for (zero_based_index, child) in signature_node
-        .children()
-        .filter(|node| node.is_element())
-        .enumerate()
-    {
-        let element_index = zero_based_index + 1;
-        if child.tag_name().namespace() != Some(XMLDSIG_NS) {
-            continue;
-        }
+    let mut element_index = 0usize;
+    for child in signature_node.children() {
+        if child.is_text()
+            && child.text().is_some_and(|text| {
+                text.chars().any(|c| !matches!(c, ' ' | '\t' | '\n' | '\r'))
+            })
+        {
+            return Err(SignatureVerificationPipelineError::InvalidStructure {
+                reason: "Signature must not contain non-whitespace mixed content",
+            });
+        }
+        if !child.is_element() {
+            continue;
+        }
+        element_index += 1;
+        if child.tag_name().namespace() != Some(XMLDSIG_NS) {
+            return Err(SignatureVerificationPipelineError::InvalidStructure {
+                reason: "Signature must contain only ds:SignedInfo, ds:SignatureValue, optional ds:KeyInfo, and ds:Object element children",
+            });
+        }

As per coding guidelines, Tier 1 - Flag logic bugs: missing validation (unchecked index, unvalidated input from network/disk/external source).

Also applies to: 1050-1072

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/xmldsig/verify.rs` around lines 976 - 984, The loop over signature_node
children in verify.rs currently ignores non-XMLDSIG elements and mixed content;
change it to fail-closed: inside the iteration over signature_node.children()
(the block around the for ... in signature_node.children().filter(|node|
node.is_element()).enumerate()), remove the filter that hides non-element nodes
and instead inspect every child node; if a child is an element and
child.tag_name().namespace() != Some(XMLDSIG_NS) return an error (reject foreign
elements), and if a child is not an element but contains non-whitespace text or
non-trivial nodes (comments/PIs as disallowed per spec) return an error (reject
mixed content); apply the same stricter checks in the corresponding later block
referenced at lines ~1050-1072 so any unexpected non-XMLDSIG element or
non-whitespace text under <Signature> causes verification to fail.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/xmldsig/parse.rs`:
- Around line 381-384: The DEREncodedKeyValue handler currently calls
collect_text_content(child) which clones the entire text payload before
decode_base64_xml_text enforces MAX_DER_ENCODED_KEY_VALUE_TEXT_LEN /
MAX_DER_ENCODED_KEY_VALUE_LEN; change this to stream/validate the text nodes
first by replacing collect_text_content+decode_base64_xml_text with a new
decode_base64_xml_children(child, ...) helper that iterates
child.children().filter(|n| n.is_text()), enforces the existing
length/whitespace caps (MAX_DER_ENCODED_KEY_VALUE_TEXT_LEN and
MAX_DER_ENCODED_KEY_VALUE_LEN) as it appends, and returns the decoded bytes so
you can still push KeyInfoSource::DerEncodedKeyValue(der); keep
ensure_no_element_children(child, "DEREncodedKeyValue") and reuse the same
validation logic elsewhere (e.g., the ranges noted at lines ~550-594).

In `@src/xmldsig/verify.rs`:
- Around line 2510-2529: The test pipeline_reports_keyinfo_parse_error currently
corrupts KeyInfo but leaves <SignedInfo/> invalid, making the failure dependent
on parser order; update the test fixture so <SignedInfo> is a minimally valid
SignedInfo (e.g., include CanonicalizationMethod, SignatureMethod and at least
one Reference with DigestMethod and a DigestValue) and keep only the
dsig11:DEREncodedKeyValue corrupted, then call verify_signature_with_pem_key and
assert the returned error is SignatureVerificationPipelineError::ParseKeyInfo(_)
to make the regression independent of element parse order.

---

Outside diff comments:
In `@src/xmldsig/verify.rs`:
- Around line 976-984: The loop over signature_node children in verify.rs
currently ignores non-XMLDSIG elements and mixed content; change it to
fail-closed: inside the iteration over signature_node.children() (the block
around the for ... in signature_node.children().filter(|node|
node.is_element()).enumerate()), remove the filter that hides non-element nodes
and instead inspect every child node; if a child is an element and
child.tag_name().namespace() != Some(XMLDSIG_NS) return an error (reject foreign
elements), and if a child is not an element but contains non-whitespace text or
non-trivial nodes (comments/PIs as disallowed per spec) return an error (reject
mixed content); apply the same stricter checks in the corresponding later block
referenced at lines ~1050-1072 so any unexpected non-XMLDSIG element or
non-whitespace text under <Signature> causes verification to fail.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 20e18548-9962-42e0-aaa9-80cd3a6c2bc8

📥 Commits

Reviewing files that changed from the base of the PR and between dad900e and 5238c92.

📒 Files selected for processing (3)
  • src/xmldsig/mod.rs
  • src/xmldsig/parse.rs
  • src/xmldsig/verify.rs

@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 4, 2026

@coderabbitai Outside-diff item (verify.rs 976-984 / 1050-1072) fixed in eec05eb: parse_signature_children now iterates all child nodes and fails closed on foreign element children, non-whitespace text mixed content, and non-element children under .

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

Tip

For best results, initiate chat on the files or code changes.

@polaz: Confirmed — commit eec05eb completes the hardening of parse_signature_children. The three additional failure-closed paths (foreign-namespace element children, non-whitespace text mixed content, and non-element node types such as PIs or CDATA under <Signature>) close the remaining gaps beyond the XMLDSIG-namespace-only guard from 5238c92. The outside-diff concern is fully resolved.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T15:50:07.840Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), `push_normalized_signature_text` strips only the four XML whitespace characters (space 0x20, tab 0x09, CR 0x0D, LF 0x0A) from `SignatureValue` text before base64 decoding, and explicitly rejects any other ASCII whitespace (e.g., form feed 0x0C, vertical tab 0x0B) as `SignatureVerificationPipelineError::SignatureValueBase64(base64::DecodeError::InvalidByte(...))`. The regression test `push_normalized_signature_text_rejects_form_feed` covers the form-feed boundary case.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 41
File: tests/donor_full_verification_suite.rs:165-178
Timestamp: 2026-04-04T06:14:34.735Z
Learning: In `tests/donor_full_verification_suite.rs` (xml-sec Rust project), the `Expectation::Skip` arm must exercise skipped vectors through `VerifyContext::new().verify(xml)` (no injected PEM key) so that the real key-resolution path runs, and must assert a concrete blocker outcome per case: `Invalid(KeyNotFound)` for vectors with unresolved KeyValue/X.509 key material, and `ParseSignedInfo(UnsupportedAlgorithm)` for DSA-signature vectors. Accepting any `Err` or non-Valid result as a valid skip is insufficient and can mask stale skip reasons or unrelated failures.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 32
File: src/xmldsig/verify.rs:34-51
Timestamp: 2026-03-28T22:00:29.444Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), the `VerifyingKey` and `KeyResolver` public traits intentionally omit `Send + Sync` supertraits to preserve lightweight single-threaded implementations. The single-threaded-only constraint is documented on both trait definitions. Do not flag the absence of `Send + Sync` on these traits as a review issue.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T18:51:49.919Z
Learning: In `src/xmldsig/parse.rs` (xml-sec Rust project), the private function `n` (formerly `base64_decode_digest`) strips only the four XML whitespace characters (space 0x20, tab 0x09, CR 0x0D, LF 0x0A) from `DigestValue` base64 text before decoding, and returns `ParseError::Base64(format!("invalid XML whitespace U+{:04X} ...", ...))` for any other `is_ascii_whitespace()` character (e.g. form feed 0x0C). Regression tests `n_accepts_xml_whitespace_chars` and `n_rejects_non_xml_ascii_whitespace` cover these cases.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T20:47:05.798Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), the ASCII-whitespace branch in `push_normalized_signature_text` uses `u8::try_from(u32::from(ch)).expect("ASCII whitespace always fits into u8")` for the explicit narrowing conversion, producing `base64::DecodeError::InvalidByte(normalized.len(), invalid_byte)` as the error payload. The conversion is provably infallible because `ch.is_ascii_whitespace()` guarantees the character is in the 0x00–0x7F range.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 12
File: src/c14n/serialize.rs:382-398
Timestamp: 2026-03-16T17:41:41.053Z
Learning: Repo: structured-world/xml-sec (Rust)
Context: src/c14n/serialize.rs::collect_inherited_xml_attrs
Learning: For inclusive C14N subsets, collect_inherited_xml_attrs must stop at the nearest included ancestor (A in set → B out → C in) to avoid inheriting xml:* through an included ancestor. A unit test named `no_inheritance_past_included_ancestor` covers this scenario. Exclusive C14N should skip this inheritance step entirely.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 12
File: src/c14n/xml_base.rs:61-93
Timestamp: 2026-03-16T17:23:26.715Z
Learning: In the xml-sec Rust project, the xml:base URI resolver in `src/c14n/xml_base.rs` correctly uses RFC 3986 §5 for IRI resolution in C14N 1.1 mode, as C14N 1.1 §2.4 explicitly delegates to RFC 3986 §5. When the base URI has no scheme, `parse_base()` returns `None` and the reference is returned as-is — this is the correct fallback behavior. The W3C C14N 1.1 interop test suite notes "modifications to RFC 3986 dot segment processing for C14N11" which may represent edge cases worth verifying against the `remove_dot_segments()` implementation.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 6
File: src/c14n/ns_exclusive.rs:81-107
Timestamp: 2026-03-15T19:26:11.246Z
Learning: In the xml-sec project (Rust), roxmltree 0.21 does not expose lexical prefixes from parsed QNames — `ExpandedName` only provides `namespace()` and `name()`. As a result, `visibly_utilized_prefixes()` in `src/c14n/ns_exclusive.rs` and `write_qualified_name()` in `src/c14n/serialize.rs` must use `lookup_prefix(namespace_uri)` for prefix resolution. This is a known limitation: when multiple prefixes bind the same namespace URI, the reverse-mapping is ambiguous. The ambiguity is documented in code comments and tracked as a ROADMAP gap; the edge case is considered rare in SAML/XMLDSig use cases.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 12
File: src/c14n/serialize.rs:218-222
Timestamp: 2026-03-16T18:07:55.027Z
Learning: In the xml-sec project (Rust), RFC 3741 §1.2 (Exclusive XML Canonicalization) explicitly states that xml:* namespace attributes (xml:lang, xml:space, etc.) are NOT imported into orphan nodes of a document subset. This is one of the two explicit differences from Inclusive C14N. The `collect_inherited_xml_attrs` function in `src/c14n/serialize.rs` must be skipped entirely for Exclusive mode. RFC 3741 §4.4 places the burden on callers to explicitly include required xml:* attributes in the subset — it does NOT restore the inheritance rule.

Learnt from: CR
Repo: structured-world/xml-sec PR: 0
File: .github/instructions/rust.instructions.md:0-0
Timestamp: 2026-03-17T12:12:28.146Z
Learning: Applies to **/*.rs : Tier 1 - Flag logic bugs: checksum/hash mismatches (computing over wrong byte range, verifying against stale value)

Learnt from: CR
Repo: structured-world/xml-sec PR: 0
File: .github/instructions/code-review.instructions.md:0-0
Timestamp: 2026-03-17T12:12:07.790Z
Learning: Applies to **/*.rs : Corruption/validation tests: tamper the relevant field (e.g., digest value, signature bytes, base64 encoding) and assert the error

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 `@src/xmldsig/parse.rs`:
- Around line 625-628: The helper is_xml_whitespace_only is duplicated; extract
it into a shared internal module (e.g., xmldsig::utils or
xmldsig::internal::whitespace) as a single pub(crate) function, replace the
duplicate definitions in the parse and verify modules to call the new shared
function, and remove the redundant implementation so both parse's and verify's
code reference the same is_xml_whitespace_only symbol.

In `@src/xmldsig/verify.rs`:
- Around line 1123-1126: The function is_xml_whitespace_only is duplicated (one
in verify.rs and one in parse.rs); extract it into a single shared internal
utility module (e.g., xml_utils or utils) as a pub(crate) function named
is_xml_whitespace_only, remove the duplicate definitions, and update both
modules (verify.rs and parse.rs) to import and call the shared function; ensure
module visibility and imports are corrected and run tests to verify nothing
breaks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: bf4a51e6-3bdd-45dc-a152-5b985c143e1a

📥 Commits

Reviewing files that changed from the base of the PR and between 5238c92 and eec05eb.

📒 Files selected for processing (2)
  • src/xmldsig/parse.rs
  • src/xmldsig/verify.rs

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 5, 2026

@coderabbitai re-review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 5, 2026

@polaz: Kicking off a fresh full review now. I'll examine all current changes including the parse_key_info dispatcher, the KeyInfo/KeyInfoSource/KeyValueInfo/X509DataInfo model, DEREncodedKeyValue base64 path, pipeline wiring in verify.rs, whitespace.rs, and the fully hardened parse_signature_children from commits 2004cf8/5238c92/eec05eb.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/xml-sec PR: 41
File: tests/donor_full_verification_suite.rs:165-178
Timestamp: 2026-04-04T06:14:34.735Z
Learning: In `tests/donor_full_verification_suite.rs` (xml-sec Rust project), the `Expectation::Skip` arm must exercise skipped vectors through `VerifyContext::new().verify(xml)` (no injected PEM key) so that the real key-resolution path runs, and must assert a concrete blocker outcome per case: `Invalid(KeyNotFound)` for vectors with unresolved KeyValue/X.509 key material, and `ParseSignedInfo(UnsupportedAlgorithm)` for DSA-signature vectors. Accepting any `Err` or non-Valid result as a valid skip is insufficient and can mask stale skip reasons or unrelated failures.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T15:50:07.840Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), `push_normalized_signature_text` strips only the four XML whitespace characters (space 0x20, tab 0x09, CR 0x0D, LF 0x0A) from `SignatureValue` text before base64 decoding, and explicitly rejects any other ASCII whitespace (e.g., form feed 0x0C, vertical tab 0x0B) as `SignatureVerificationPipelineError::SignatureValueBase64(base64::DecodeError::InvalidByte(...))`. The regression test `push_normalized_signature_text_rejects_form_feed` covers the form-feed boundary case.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 32
File: src/xmldsig/verify.rs:34-51
Timestamp: 2026-03-28T22:00:29.444Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), the `VerifyingKey` and `KeyResolver` public traits intentionally omit `Send + Sync` supertraits to preserve lightweight single-threaded implementations. The single-threaded-only constraint is documented on both trait definitions. Do not flag the absence of `Send + Sync` on these traits as a review issue.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T18:51:49.919Z
Learning: In `src/xmldsig/parse.rs` (xml-sec Rust project), the private function `n` (formerly `base64_decode_digest`) strips only the four XML whitespace characters (space 0x20, tab 0x09, CR 0x0D, LF 0x0A) from `DigestValue` base64 text before decoding, and returns `ParseError::Base64(format!("invalid XML whitespace U+{:04X} ...", ...))` for any other `is_ascii_whitespace()` character (e.g. form feed 0x0C). Regression tests `n_accepts_xml_whitespace_chars` and `n_rejects_non_xml_ascii_whitespace` cover these cases.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T20:47:05.798Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), the ASCII-whitespace branch in `push_normalized_signature_text` uses `u8::try_from(u32::from(ch)).expect("ASCII whitespace always fits into u8")` for the explicit narrowing conversion, producing `base64::DecodeError::InvalidByte(normalized.len(), invalid_byte)` as the error payload. The conversion is provably infallible because `ch.is_ascii_whitespace()` guarantees the character is in the 0x00–0x7F range.

Learnt from: CR
Repo: structured-world/xml-sec PR: 0
File: .github/instructions/rust.instructions.md:0-0
Timestamp: 2026-03-17T12:12:28.146Z
Learning: Applies to **/*.rs : Tier 1 - Flag logic bugs: checksum/hash mismatches (computing over wrong byte range, verifying against stale value)
✅ Actions performed

Full review triggered.

@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 5, 2026

@coderabbitai re-review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 5, 2026

@polaz: Kicking off a fresh full review of PR #47 now, covering all current changes including parse_key_info, the KeyInfo/KeyInfoSource/KeyValueInfo/X509DataInfo data model, the DEREncodedKeyValue base64 path, pipeline wiring in verify.rs, the whitespace.rs helper, and the fully hardened parse_signature_children from commits 2004cf8/5238c92/eec05eb.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/xml-sec PR: 41
File: tests/donor_full_verification_suite.rs:165-178
Timestamp: 2026-04-04T06:14:34.735Z
Learning: In `tests/donor_full_verification_suite.rs` (xml-sec Rust project), the `Expectation::Skip` arm must exercise skipped vectors through `VerifyContext::new().verify(xml)` (no injected PEM key) so that the real key-resolution path runs, and must assert a concrete blocker outcome per case: `Invalid(KeyNotFound)` for vectors with unresolved KeyValue/X.509 key material, and `ParseSignedInfo(UnsupportedAlgorithm)` for DSA-signature vectors. Accepting any `Err` or non-Valid result as a valid skip is insufficient and can mask stale skip reasons or unrelated failures.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T15:50:07.840Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), `push_normalized_signature_text` strips only the four XML whitespace characters (space 0x20, tab 0x09, CR 0x0D, LF 0x0A) from `SignatureValue` text before base64 decoding, and explicitly rejects any other ASCII whitespace (e.g., form feed 0x0C, vertical tab 0x0B) as `SignatureVerificationPipelineError::SignatureValueBase64(base64::DecodeError::InvalidByte(...))`. The regression test `push_normalized_signature_text_rejects_form_feed` covers the form-feed boundary case.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 32
File: src/xmldsig/verify.rs:34-51
Timestamp: 2026-03-28T22:00:29.444Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), the `VerifyingKey` and `KeyResolver` public traits intentionally omit `Send + Sync` supertraits to preserve lightweight single-threaded implementations. The single-threaded-only constraint is documented on both trait definitions. Do not flag the absence of `Send + Sync` on these traits as a review issue.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T18:51:49.919Z
Learning: In `src/xmldsig/parse.rs` (xml-sec Rust project), the private function `n` (formerly `base64_decode_digest`) strips only the four XML whitespace characters (space 0x20, tab 0x09, CR 0x0D, LF 0x0A) from `DigestValue` base64 text before decoding, and returns `ParseError::Base64(format!("invalid XML whitespace U+{:04X} ...", ...))` for any other `is_ascii_whitespace()` character (e.g. form feed 0x0C). Regression tests `n_accepts_xml_whitespace_chars` and `n_rejects_non_xml_ascii_whitespace` cover these cases.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T20:47:05.798Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), the ASCII-whitespace branch in `push_normalized_signature_text` uses `u8::try_from(u32::from(ch)).expect("ASCII whitespace always fits into u8")` for the explicit narrowing conversion, producing `base64::DecodeError::InvalidByte(normalized.len(), invalid_byte)` as the error payload. The conversion is provably infallible because `ch.is_ascii_whitespace()` guarantees the character is in the 0x00–0x7F range.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 12
File: src/c14n/serialize.rs:382-398
Timestamp: 2026-03-16T17:41:41.053Z
Learning: Repo: structured-world/xml-sec (Rust)
Context: src/c14n/serialize.rs::collect_inherited_xml_attrs
Learning: For inclusive C14N subsets, collect_inherited_xml_attrs must stop at the nearest included ancestor (A in set → B out → C in) to avoid inheriting xml:* through an included ancestor. A unit test named `no_inheritance_past_included_ancestor` covers this scenario. Exclusive C14N should skip this inheritance step entirely.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 16
File: tests/fixtures_smoke.rs:104-104
Timestamp: 2026-03-17T15:17:20.125Z
Learning: In structured-world/xml-sec (Rust), `expect()` on `fs::read_to_string()` and other I/O calls is acceptable and idiomatic inside `#[test]` functions. The Tier 2 rule (use `Result` propagation instead of `expect()` on I/O paths) applies only to production code paths, not test code. Repo instructions (line 51) state: "Test code style: Tests prioritize readability and explicitness over DRY."

Learnt from: polaz
Repo: structured-world/xml-sec PR: 6
File: src/c14n/ns_exclusive.rs:81-107
Timestamp: 2026-03-15T19:26:11.246Z
Learning: In the xml-sec project (Rust), roxmltree 0.21 does not expose lexical prefixes from parsed QNames — `ExpandedName` only provides `namespace()` and `name()`. As a result, `visibly_utilized_prefixes()` in `src/c14n/ns_exclusive.rs` and `write_qualified_name()` in `src/c14n/serialize.rs` must use `lookup_prefix(namespace_uri)` for prefix resolution. This is a known limitation: when multiple prefixes bind the same namespace URI, the reverse-mapping is ambiguous. The ambiguity is documented in code comments and tracked as a ROADMAP gap; the edge case is considered rare in SAML/XMLDSig use cases.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 12
File: src/c14n/xml_base.rs:61-93
Timestamp: 2026-03-16T17:23:26.715Z
Learning: In the xml-sec Rust project, the xml:base URI resolver in `src/c14n/xml_base.rs` correctly uses RFC 3986 §5 for IRI resolution in C14N 1.1 mode, as C14N 1.1 §2.4 explicitly delegates to RFC 3986 §5. When the base URI has no scheme, `parse_base()` returns `None` and the reference is returned as-is — this is the correct fallback behavior. The W3C C14N 1.1 interop test suite notes "modifications to RFC 3986 dot segment processing for C14N11" which may represent edge cases worth verifying against the `remove_dot_segments()` implementation.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 41
File: tests/donor_full_verification_suite.rs:164-168
Timestamp: 2026-04-03T14:47:20.446Z
Learning: In `tests/donor_full_verification_suite.rs` (xml-sec, Rust), using `let _ = read_fixture(...)` to intentionally discard a `String` return value triggers `clippy::let_underscore_drop` under `-D warnings`, because `String` has a non-trivial `Drop` impl. The correct idiom is a plain statement call (e.g., `read_fixture(&root.join(case.xml_path));`) with an inline comment to document intent. Do not suggest `let _ = expr` for types with meaningful `Drop` implementations in this codebase.

Learnt from: CR
Repo: structured-world/xml-sec PR: 0
File: .github/instructions/rust.instructions.md:0-0
Timestamp: 2026-03-17T12:12:28.146Z
Learning: Applies to **/*.rs : Tier 1 - Flag logic bugs: checksum/hash mismatches (computing over wrong byte range, verifying against stale value)

Learnt from: CR
Repo: structured-world/xml-sec PR: 0
File: .github/instructions/rust.instructions.md:0-0
Timestamp: 2026-03-17T12:12:28.146Z
Learning: Applies to **/*.rs : Tier 2 - Flag safety issues: constant-time comparison must be used for cryptographic MACs/checksums

Learnt from: CR
Repo: structured-world/xml-sec PR: 0
File: .github/instructions/rust.instructions.md:0-0
Timestamp: 2026-03-17T12:12:28.146Z
Learning: Applies to **/*.rs : Tier 1 - Flag logic bugs: missing validation (unchecked index, unvalidated input from network/disk/external source)

Learnt from: CR
Repo: structured-world/xml-sec PR: 0
File: .github/instructions/rust.instructions.md:0-0
Timestamp: 2026-03-17T12:12:28.146Z
Learning: Applies to **/*.rs : Tier 1 - Flag logic bugs: integer overflow/truncation on sizes, offsets, lengths, or security-critical values
✅ Actions performed

Full review triggered.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 `@src/xmldsig/verify.rs`:
- Around line 629-631: The call to parse_key_info(key_info_node) validates
KeyInfo but the returned parsed key material is intentionally not consumed yet;
update the block around parse_key_info (and the surrounding
signature_children.key_info_node handling) to include a short comment stating
this is deliberate for the current P2-001 dispatch-only phase and that future
work will wire the parsed KeyInfo into the KeyResolver for actual key resolution
(preserve the existing error mapping to
SignatureVerificationPipelineError::ParseKeyInfo).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a173c093-c3c4-41ed-870c-3ceff37a47f9

📥 Commits

Reviewing files that changed from the base of the PR and between dad900e and 1931cbd.

📒 Files selected for processing (4)
  • src/xmldsig/mod.rs
  • src/xmldsig/parse.rs
  • src/xmldsig/verify.rs
  • src/xmldsig/whitespace.rs

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 `@src/xmldsig/verify.rs`:
- Around line 2559-2617: These three tests mutate Signature but leave SignedInfo
malformed and call verify_signature_with_pem_key, so sibling checks run before
SignedInfo/crypto; instead, build each test XML by starting from
signature_with_target_reference() so SignedInfo is valid, then run the
verification using VerifyContext::new().key(&RejectingKey) (or equivalent
VerifyContext invocation) to isolate the child-order/content check; replace the
verify_signature_with_pem_key(...) calls in
pipeline_rejects_foreign_element_children_under_signature,
pipeline_rejects_non_whitespace_mixed_content_under_signature, and
pipeline_rejects_keyinfo_out_of_order with code that uses
signature_with_target_reference() to create the base signature and
VerifyContext::new().key(&RejectingKey) to assert the specific Signature child
validation error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 04985649-7531-4319-b827-ac0a6f141f01

📥 Commits

Reviewing files that changed from the base of the PR and between 1931cbd and 9f9917e.

📒 Files selected for processing (1)
  • src/xmldsig/verify.rs

@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 5, 2026

@coderabbitai re-review

@polaz polaz requested a review from Copilot April 5, 2026 08:02
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 5, 2026

@polaz: Kicking off another full review of PR #47 now. I'll cover all current changes across src/xmldsig/parse.rs (parse_key_info, the KeyInfo/KeyInfoSource/KeyValueInfo/X509DataInfo model, DEREncodedKeyValue base64 path), src/xmldsig/verify.rs (pipeline wiring, ParseKeyInfo error variant, and the fully hardened parse_signature_children from commits 2004cf8/5238c92/eec05eb), src/xmldsig/whitespace.rs, and src/xmldsig/mod.rs.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/xml-sec PR: 41
File: tests/donor_full_verification_suite.rs:165-178
Timestamp: 2026-04-04T06:14:34.735Z
Learning: In `tests/donor_full_verification_suite.rs` (xml-sec Rust project), the `Expectation::Skip` arm must exercise skipped vectors through `VerifyContext::new().verify(xml)` (no injected PEM key) so that the real key-resolution path runs, and must assert a concrete blocker outcome per case: `Invalid(KeyNotFound)` for vectors with unresolved KeyValue/X.509 key material, and `ParseSignedInfo(UnsupportedAlgorithm)` for DSA-signature vectors. Accepting any `Err` or non-Valid result as a valid skip is insufficient and can mask stale skip reasons or unrelated failures.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T15:50:07.840Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), `push_normalized_signature_text` strips only the four XML whitespace characters (space 0x20, tab 0x09, CR 0x0D, LF 0x0A) from `SignatureValue` text before base64 decoding, and explicitly rejects any other ASCII whitespace (e.g., form feed 0x0C, vertical tab 0x0B) as `SignatureVerificationPipelineError::SignatureValueBase64(base64::DecodeError::InvalidByte(...))`. The regression test `push_normalized_signature_text_rejects_form_feed` covers the form-feed boundary case.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 32
File: src/xmldsig/verify.rs:34-51
Timestamp: 2026-03-28T22:00:29.444Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), the `VerifyingKey` and `KeyResolver` public traits intentionally omit `Send + Sync` supertraits to preserve lightweight single-threaded implementations. The single-threaded-only constraint is documented on both trait definitions. Do not flag the absence of `Send + Sync` on these traits as a review issue.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T18:51:49.919Z
Learning: In `src/xmldsig/parse.rs` (xml-sec Rust project), the private function `n` (formerly `base64_decode_digest`) strips only the four XML whitespace characters (space 0x20, tab 0x09, CR 0x0D, LF 0x0A) from `DigestValue` base64 text before decoding, and returns `ParseError::Base64(format!("invalid XML whitespace U+{:04X} ...", ...))` for any other `is_ascii_whitespace()` character (e.g. form feed 0x0C). Regression tests `n_accepts_xml_whitespace_chars` and `n_rejects_non_xml_ascii_whitespace` cover these cases.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T20:47:05.798Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), the ASCII-whitespace branch in `push_normalized_signature_text` uses `u8::try_from(u32::from(ch)).expect("ASCII whitespace always fits into u8")` for the explicit narrowing conversion, producing `base64::DecodeError::InvalidByte(normalized.len(), invalid_byte)` as the error payload. The conversion is provably infallible because `ch.is_ascii_whitespace()` guarantees the character is in the 0x00–0x7F range.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 12
File: src/c14n/serialize.rs:382-398
Timestamp: 2026-03-16T17:41:41.053Z
Learning: Repo: structured-world/xml-sec (Rust)
Context: src/c14n/serialize.rs::collect_inherited_xml_attrs
Learning: For inclusive C14N subsets, collect_inherited_xml_attrs must stop at the nearest included ancestor (A in set → B out → C in) to avoid inheriting xml:* through an included ancestor. A unit test named `no_inheritance_past_included_ancestor` covers this scenario. Exclusive C14N should skip this inheritance step entirely.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 16
File: tests/fixtures_smoke.rs:104-104
Timestamp: 2026-03-17T15:17:20.125Z
Learning: In structured-world/xml-sec (Rust), `expect()` on `fs::read_to_string()` and other I/O calls is acceptable and idiomatic inside `#[test]` functions. The Tier 2 rule (use `Result` propagation instead of `expect()` on I/O paths) applies only to production code paths, not test code. Repo instructions (line 51) state: "Test code style: Tests prioritize readability and explicitness over DRY."

Learnt from: polaz
Repo: structured-world/xml-sec PR: 6
File: src/c14n/ns_exclusive.rs:81-107
Timestamp: 2026-03-15T19:26:11.246Z
Learning: In the xml-sec project (Rust), roxmltree 0.21 does not expose lexical prefixes from parsed QNames — `ExpandedName` only provides `namespace()` and `name()`. As a result, `visibly_utilized_prefixes()` in `src/c14n/ns_exclusive.rs` and `write_qualified_name()` in `src/c14n/serialize.rs` must use `lookup_prefix(namespace_uri)` for prefix resolution. This is a known limitation: when multiple prefixes bind the same namespace URI, the reverse-mapping is ambiguous. The ambiguity is documented in code comments and tracked as a ROADMAP gap; the edge case is considered rare in SAML/XMLDSig use cases.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 12
File: src/c14n/xml_base.rs:61-93
Timestamp: 2026-03-16T17:23:26.715Z
Learning: In the xml-sec Rust project, the xml:base URI resolver in `src/c14n/xml_base.rs` correctly uses RFC 3986 §5 for IRI resolution in C14N 1.1 mode, as C14N 1.1 §2.4 explicitly delegates to RFC 3986 §5. When the base URI has no scheme, `parse_base()` returns `None` and the reference is returned as-is — this is the correct fallback behavior. The W3C C14N 1.1 interop test suite notes "modifications to RFC 3986 dot segment processing for C14N11" which may represent edge cases worth verifying against the `remove_dot_segments()` implementation.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 41
File: tests/donor_full_verification_suite.rs:164-168
Timestamp: 2026-04-03T14:47:20.446Z
Learning: In `tests/donor_full_verification_suite.rs` (xml-sec, Rust), using `let _ = read_fixture(...)` to intentionally discard a `String` return value triggers `clippy::let_underscore_drop` under `-D warnings`, because `String` has a non-trivial `Drop` impl. The correct idiom is a plain statement call (e.g., `read_fixture(&root.join(case.xml_path));`) with an inline comment to document intent. Do not suggest `let _ = expr` for types with meaningful `Drop` implementations in this codebase.

Learnt from: CR
Repo: structured-world/xml-sec PR: 0
File: .github/instructions/rust.instructions.md:0-0
Timestamp: 2026-03-17T12:12:28.146Z
Learning: Applies to **/*.rs : Tier 2 - Flag safety issues: constant-time comparison must be used for cryptographic MACs/checksums

Learnt from: CR
Repo: structured-world/xml-sec PR: 0
File: .github/instructions/rust.instructions.md:0-0
Timestamp: 2026-03-17T12:12:28.146Z
Learning: Applies to **/*.rs : Tier 1 - Flag logic bugs: checksum/hash mismatches (computing over wrong byte range, verifying against stale value)

Learnt from: CR
Repo: structured-world/xml-sec PR: 0
File: .github/instructions/rust.instructions.md:0-0
Timestamp: 2026-03-17T12:12:28.146Z
Learning: Applies to **/*.rs : Tier 1 - Flag logic bugs: integer overflow/truncation on sizes, offsets, lengths, or security-critical values

Learnt from: CR
Repo: structured-world/xml-sec PR: 0
File: .github/instructions/rust.instructions.md:0-0
Timestamp: 2026-03-17T12:12:28.146Z
Learning: Applies to **/*.rs : Tier 1 - Flag logic bugs: missing validation (unchecked index, unvalidated input from network/disk/external source)
✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 5, 2026

@coderabbitai re-review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 5, 2026

@polaz: Kicking off another full review of PR #47 now. I'll go through all current changes across src/xmldsig/parse.rs (parse_key_info, KeyInfo/KeyInfoSource/KeyValueInfo/X509DataInfo model, DEREncodedKeyValue base64 path, all internal helpers and tests), src/xmldsig/verify.rs (pipeline wiring, ParseKeyInfo error variant, fully hardened parse_signature_children from commits 2004cf8/5238c92/eec05eb), src/xmldsig/whitespace.rs, and src/xmldsig/mod.rs.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/xml-sec PR: 41
File: tests/donor_full_verification_suite.rs:165-178
Timestamp: 2026-04-04T06:14:34.735Z
Learning: In `tests/donor_full_verification_suite.rs` (xml-sec Rust project), the `Expectation::Skip` arm must exercise skipped vectors through `VerifyContext::new().verify(xml)` (no injected PEM key) so that the real key-resolution path runs, and must assert a concrete blocker outcome per case: `Invalid(KeyNotFound)` for vectors with unresolved KeyValue/X.509 key material, and `ParseSignedInfo(UnsupportedAlgorithm)` for DSA-signature vectors. Accepting any `Err` or non-Valid result as a valid skip is insufficient and can mask stale skip reasons or unrelated failures.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T15:50:07.840Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), `push_normalized_signature_text` strips only the four XML whitespace characters (space 0x20, tab 0x09, CR 0x0D, LF 0x0A) from `SignatureValue` text before base64 decoding, and explicitly rejects any other ASCII whitespace (e.g., form feed 0x0C, vertical tab 0x0B) as `SignatureVerificationPipelineError::SignatureValueBase64(base64::DecodeError::InvalidByte(...))`. The regression test `push_normalized_signature_text_rejects_form_feed` covers the form-feed boundary case.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 32
File: src/xmldsig/verify.rs:34-51
Timestamp: 2026-03-28T22:00:29.444Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), the `VerifyingKey` and `KeyResolver` public traits intentionally omit `Send + Sync` supertraits to preserve lightweight single-threaded implementations. The single-threaded-only constraint is documented on both trait definitions. Do not flag the absence of `Send + Sync` on these traits as a review issue.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T18:51:49.919Z
Learning: In `src/xmldsig/parse.rs` (xml-sec Rust project), the private function `n` (formerly `base64_decode_digest`) strips only the four XML whitespace characters (space 0x20, tab 0x09, CR 0x0D, LF 0x0A) from `DigestValue` base64 text before decoding, and returns `ParseError::Base64(format!("invalid XML whitespace U+{:04X} ...", ...))` for any other `is_ascii_whitespace()` character (e.g. form feed 0x0C). Regression tests `n_accepts_xml_whitespace_chars` and `n_rejects_non_xml_ascii_whitespace` cover these cases.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T20:47:05.798Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), the ASCII-whitespace branch in `push_normalized_signature_text` uses `u8::try_from(u32::from(ch)).expect("ASCII whitespace always fits into u8")` for the explicit narrowing conversion, producing `base64::DecodeError::InvalidByte(normalized.len(), invalid_byte)` as the error payload. The conversion is provably infallible because `ch.is_ascii_whitespace()` guarantees the character is in the 0x00–0x7F range.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 12
File: src/c14n/serialize.rs:382-398
Timestamp: 2026-03-16T17:41:41.053Z
Learning: Repo: structured-world/xml-sec (Rust)
Context: src/c14n/serialize.rs::collect_inherited_xml_attrs
Learning: For inclusive C14N subsets, collect_inherited_xml_attrs must stop at the nearest included ancestor (A in set → B out → C in) to avoid inheriting xml:* through an included ancestor. A unit test named `no_inheritance_past_included_ancestor` covers this scenario. Exclusive C14N should skip this inheritance step entirely.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 16
File: tests/fixtures_smoke.rs:104-104
Timestamp: 2026-03-17T15:17:20.125Z
Learning: In structured-world/xml-sec (Rust), `expect()` on `fs::read_to_string()` and other I/O calls is acceptable and idiomatic inside `#[test]` functions. The Tier 2 rule (use `Result` propagation instead of `expect()` on I/O paths) applies only to production code paths, not test code. Repo instructions (line 51) state: "Test code style: Tests prioritize readability and explicitness over DRY."

Learnt from: polaz
Repo: structured-world/xml-sec PR: 6
File: src/c14n/ns_exclusive.rs:81-107
Timestamp: 2026-03-15T19:26:11.246Z
Learning: In the xml-sec project (Rust), roxmltree 0.21 does not expose lexical prefixes from parsed QNames — `ExpandedName` only provides `namespace()` and `name()`. As a result, `visibly_utilized_prefixes()` in `src/c14n/ns_exclusive.rs` and `write_qualified_name()` in `src/c14n/serialize.rs` must use `lookup_prefix(namespace_uri)` for prefix resolution. This is a known limitation: when multiple prefixes bind the same namespace URI, the reverse-mapping is ambiguous. The ambiguity is documented in code comments and tracked as a ROADMAP gap; the edge case is considered rare in SAML/XMLDSig use cases.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 12
File: src/c14n/xml_base.rs:61-93
Timestamp: 2026-03-16T17:23:26.715Z
Learning: In the xml-sec Rust project, the xml:base URI resolver in `src/c14n/xml_base.rs` correctly uses RFC 3986 §5 for IRI resolution in C14N 1.1 mode, as C14N 1.1 §2.4 explicitly delegates to RFC 3986 §5. When the base URI has no scheme, `parse_base()` returns `None` and the reference is returned as-is — this is the correct fallback behavior. The W3C C14N 1.1 interop test suite notes "modifications to RFC 3986 dot segment processing for C14N11" which may represent edge cases worth verifying against the `remove_dot_segments()` implementation.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 41
File: tests/donor_full_verification_suite.rs:164-168
Timestamp: 2026-04-03T14:47:20.446Z
Learning: In `tests/donor_full_verification_suite.rs` (xml-sec, Rust), using `let _ = read_fixture(...)` to intentionally discard a `String` return value triggers `clippy::let_underscore_drop` under `-D warnings`, because `String` has a non-trivial `Drop` impl. The correct idiom is a plain statement call (e.g., `read_fixture(&root.join(case.xml_path));`) with an inline comment to document intent. Do not suggest `let _ = expr` for types with meaningful `Drop` implementations in this codebase.

Learnt from: CR
Repo: structured-world/xml-sec PR: 0
File: .github/instructions/rust.instructions.md:0-0
Timestamp: 2026-03-17T12:12:28.146Z
Learning: Applies to **/*.rs : Tier 2 - Flag safety issues: constant-time comparison must be used for cryptographic MACs/checksums

Learnt from: CR
Repo: structured-world/xml-sec PR: 0
File: .github/instructions/rust.instructions.md:0-0
Timestamp: 2026-03-17T12:12:28.146Z
Learning: Applies to **/*.rs : Tier 1 - Flag logic bugs: checksum/hash mismatches (computing over wrong byte range, verifying against stale value)
✅ Actions performed

Full review triggered.

@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 5, 2026

@coderabbitai re-review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 5, 2026

@polaz: Kicking off another full review of PR #47 now. I'll go through all current changes across src/xmldsig/parse.rs (parse_key_info, KeyInfo/KeyInfoSource/KeyValueInfo/X509DataInfo model, DEREncodedKeyValue base64 path, all internal helpers and tests), src/xmldsig/verify.rs (pipeline wiring, ParseKeyInfo error variant, fully hardened parse_signature_children from commits 2004cf8/5238c92/eec05eb), src/xmldsig/whitespace.rs, and src/xmldsig/mod.rs.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/xml-sec PR: 41
File: tests/donor_full_verification_suite.rs:165-178
Timestamp: 2026-04-04T06:14:34.735Z
Learning: In `tests/donor_full_verification_suite.rs` (xml-sec Rust project), the `Expectation::Skip` arm must exercise skipped vectors through `VerifyContext::new().verify(xml)` (no injected PEM key) so that the real key-resolution path runs, and must assert a concrete blocker outcome per case: `Invalid(KeyNotFound)` for vectors with unresolved KeyValue/X.509 key material, and `ParseSignedInfo(UnsupportedAlgorithm)` for DSA-signature vectors. Accepting any `Err` or non-Valid result as a valid skip is insufficient and can mask stale skip reasons or unrelated failures.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T15:50:07.840Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), `push_normalized_signature_text` strips only the four XML whitespace characters (space 0x20, tab 0x09, CR 0x0D, LF 0x0A) from `SignatureValue` text before base64 decoding, and explicitly rejects any other ASCII whitespace (e.g., form feed 0x0C, vertical tab 0x0B) as `SignatureVerificationPipelineError::SignatureValueBase64(base64::DecodeError::InvalidByte(...))`. The regression test `push_normalized_signature_text_rejects_form_feed` covers the form-feed boundary case.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 32
File: src/xmldsig/verify.rs:34-51
Timestamp: 2026-03-28T22:00:29.444Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), the `VerifyingKey` and `KeyResolver` public traits intentionally omit `Send + Sync` supertraits to preserve lightweight single-threaded implementations. The single-threaded-only constraint is documented on both trait definitions. Do not flag the absence of `Send + Sync` on these traits as a review issue.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T18:51:49.919Z
Learning: In `src/xmldsig/parse.rs` (xml-sec Rust project), the private function `n` (formerly `base64_decode_digest`) strips only the four XML whitespace characters (space 0x20, tab 0x09, CR 0x0D, LF 0x0A) from `DigestValue` base64 text before decoding, and returns `ParseError::Base64(format!("invalid XML whitespace U+{:04X} ...", ...))` for any other `is_ascii_whitespace()` character (e.g. form feed 0x0C). Regression tests `n_accepts_xml_whitespace_chars` and `n_rejects_non_xml_ascii_whitespace` cover these cases.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T20:47:05.798Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), the ASCII-whitespace branch in `push_normalized_signature_text` uses `u8::try_from(u32::from(ch)).expect("ASCII whitespace always fits into u8")` for the explicit narrowing conversion, producing `base64::DecodeError::InvalidByte(normalized.len(), invalid_byte)` as the error payload. The conversion is provably infallible because `ch.is_ascii_whitespace()` guarantees the character is in the 0x00–0x7F range.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 12
File: src/c14n/serialize.rs:382-398
Timestamp: 2026-03-16T17:41:41.053Z
Learning: Repo: structured-world/xml-sec (Rust)
Context: src/c14n/serialize.rs::collect_inherited_xml_attrs
Learning: For inclusive C14N subsets, collect_inherited_xml_attrs must stop at the nearest included ancestor (A in set → B out → C in) to avoid inheriting xml:* through an included ancestor. A unit test named `no_inheritance_past_included_ancestor` covers this scenario. Exclusive C14N should skip this inheritance step entirely.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 16
File: tests/fixtures_smoke.rs:104-104
Timestamp: 2026-03-17T15:17:20.125Z
Learning: In structured-world/xml-sec (Rust), `expect()` on `fs::read_to_string()` and other I/O calls is acceptable and idiomatic inside `#[test]` functions. The Tier 2 rule (use `Result` propagation instead of `expect()` on I/O paths) applies only to production code paths, not test code. Repo instructions (line 51) state: "Test code style: Tests prioritize readability and explicitness over DRY."

Learnt from: polaz
Repo: structured-world/xml-sec PR: 6
File: src/c14n/ns_exclusive.rs:81-107
Timestamp: 2026-03-15T19:26:11.246Z
Learning: In the xml-sec project (Rust), roxmltree 0.21 does not expose lexical prefixes from parsed QNames — `ExpandedName` only provides `namespace()` and `name()`. As a result, `visibly_utilized_prefixes()` in `src/c14n/ns_exclusive.rs` and `write_qualified_name()` in `src/c14n/serialize.rs` must use `lookup_prefix(namespace_uri)` for prefix resolution. This is a known limitation: when multiple prefixes bind the same namespace URI, the reverse-mapping is ambiguous. The ambiguity is documented in code comments and tracked as a ROADMAP gap; the edge case is considered rare in SAML/XMLDSig use cases.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 12
File: src/c14n/xml_base.rs:61-93
Timestamp: 2026-03-16T17:23:26.715Z
Learning: In the xml-sec Rust project, the xml:base URI resolver in `src/c14n/xml_base.rs` correctly uses RFC 3986 §5 for IRI resolution in C14N 1.1 mode, as C14N 1.1 §2.4 explicitly delegates to RFC 3986 §5. When the base URI has no scheme, `parse_base()` returns `None` and the reference is returned as-is — this is the correct fallback behavior. The W3C C14N 1.1 interop test suite notes "modifications to RFC 3986 dot segment processing for C14N11" which may represent edge cases worth verifying against the `remove_dot_segments()` implementation.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 41
File: tests/donor_full_verification_suite.rs:164-168
Timestamp: 2026-04-03T14:47:20.446Z
Learning: In `tests/donor_full_verification_suite.rs` (xml-sec, Rust), using `let _ = read_fixture(...)` to intentionally discard a `String` return value triggers `clippy::let_underscore_drop` under `-D warnings`, because `String` has a non-trivial `Drop` impl. The correct idiom is a plain statement call (e.g., `read_fixture(&root.join(case.xml_path));`) with an inline comment to document intent. Do not suggest `let _ = expr` for types with meaningful `Drop` implementations in this codebase.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 12
File: src/c14n/serialize.rs:218-222
Timestamp: 2026-03-16T18:07:55.027Z
Learning: In the xml-sec project (Rust), RFC 3741 §1.2 (Exclusive XML Canonicalization) explicitly states that xml:* namespace attributes (xml:lang, xml:space, etc.) are NOT imported into orphan nodes of a document subset. This is one of the two explicit differences from Inclusive C14N. The `collect_inherited_xml_attrs` function in `src/c14n/serialize.rs` must be skipped entirely for Exclusive mode. RFC 3741 §4.4 places the burden on callers to explicitly include required xml:* attributes in the subset — it does NOT restore the inheritance rule.

Learnt from: CR
Repo: structured-world/xml-sec PR: 0
File: .github/instructions/rust.instructions.md:0-0
Timestamp: 2026-03-17T12:12:28.146Z
Learning: Applies to **/*.rs : Tier 2 - Flag safety issues: constant-time comparison must be used for cryptographic MACs/checksums
✅ Actions performed

Full review triggered.

@polaz
Copy link
Copy Markdown
Member Author

polaz commented Apr 5, 2026

@coderabbitai re-review

@polaz polaz requested a review from Copilot April 5, 2026 09:03
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 5, 2026

@polaz: Kicking off another full review of PR #47 now. I'll examine all current changes across src/xmldsig/parse.rs (parse_key_info, KeyInfo/KeyInfoSource/KeyValueInfo/X509DataInfo model, DEREncodedKeyValue base64 path, all internal helpers and tests), src/xmldsig/verify.rs (pipeline wiring, ParseKeyInfo error variant, fully hardened parse_signature_children from commits 2004cf8/5238c92/eec05eb), src/xmldsig/whitespace.rs, and src/xmldsig/mod.rs.


🧠 Learnings used
Learnt from: polaz
Repo: structured-world/xml-sec PR: 41
File: tests/donor_full_verification_suite.rs:165-178
Timestamp: 2026-04-04T06:14:34.735Z
Learning: In `tests/donor_full_verification_suite.rs` (xml-sec Rust project), the `Expectation::Skip` arm must exercise skipped vectors through `VerifyContext::new().verify(xml)` (no injected PEM key) so that the real key-resolution path runs, and must assert a concrete blocker outcome per case: `Invalid(KeyNotFound)` for vectors with unresolved KeyValue/X.509 key material, and `ParseSignedInfo(UnsupportedAlgorithm)` for DSA-signature vectors. Accepting any `Err` or non-Valid result as a valid skip is insufficient and can mask stale skip reasons or unrelated failures.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T15:50:07.840Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), `push_normalized_signature_text` strips only the four XML whitespace characters (space 0x20, tab 0x09, CR 0x0D, LF 0x0A) from `SignatureValue` text before base64 decoding, and explicitly rejects any other ASCII whitespace (e.g., form feed 0x0C, vertical tab 0x0B) as `SignatureVerificationPipelineError::SignatureValueBase64(base64::DecodeError::InvalidByte(...))`. The regression test `push_normalized_signature_text_rejects_form_feed` covers the form-feed boundary case.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 32
File: src/xmldsig/verify.rs:34-51
Timestamp: 2026-03-28T22:00:29.444Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), the `VerifyingKey` and `KeyResolver` public traits intentionally omit `Send + Sync` supertraits to preserve lightweight single-threaded implementations. The single-threaded-only constraint is documented on both trait definitions. Do not flag the absence of `Send + Sync` on these traits as a review issue.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T18:51:49.919Z
Learning: In `src/xmldsig/parse.rs` (xml-sec Rust project), the private function `n` (formerly `base64_decode_digest`) strips only the four XML whitespace characters (space 0x20, tab 0x09, CR 0x0D, LF 0x0A) from `DigestValue` base64 text before decoding, and returns `ParseError::Base64(format!("invalid XML whitespace U+{:04X} ...", ...))` for any other `is_ascii_whitespace()` character (e.g. form feed 0x0C). Regression tests `n_accepts_xml_whitespace_chars` and `n_rejects_non_xml_ascii_whitespace` cover these cases.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 0
File: :0-0
Timestamp: 2026-03-28T20:47:05.798Z
Learning: In `src/xmldsig/verify.rs` (xml-sec Rust project), the ASCII-whitespace branch in `push_normalized_signature_text` uses `u8::try_from(u32::from(ch)).expect("ASCII whitespace always fits into u8")` for the explicit narrowing conversion, producing `base64::DecodeError::InvalidByte(normalized.len(), invalid_byte)` as the error payload. The conversion is provably infallible because `ch.is_ascii_whitespace()` guarantees the character is in the 0x00–0x7F range.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 12
File: src/c14n/serialize.rs:382-398
Timestamp: 2026-03-16T17:41:41.053Z
Learning: Repo: structured-world/xml-sec (Rust)
Context: src/c14n/serialize.rs::collect_inherited_xml_attrs
Learning: For inclusive C14N subsets, collect_inherited_xml_attrs must stop at the nearest included ancestor (A in set → B out → C in) to avoid inheriting xml:* through an included ancestor. A unit test named `no_inheritance_past_included_ancestor` covers this scenario. Exclusive C14N should skip this inheritance step entirely.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 16
File: tests/fixtures_smoke.rs:104-104
Timestamp: 2026-03-17T15:17:20.125Z
Learning: In structured-world/xml-sec (Rust), `expect()` on `fs::read_to_string()` and other I/O calls is acceptable and idiomatic inside `#[test]` functions. The Tier 2 rule (use `Result` propagation instead of `expect()` on I/O paths) applies only to production code paths, not test code. Repo instructions (line 51) state: "Test code style: Tests prioritize readability and explicitness over DRY."

Learnt from: polaz
Repo: structured-world/xml-sec PR: 6
File: src/c14n/ns_exclusive.rs:81-107
Timestamp: 2026-03-15T19:26:11.246Z
Learning: In the xml-sec project (Rust), roxmltree 0.21 does not expose lexical prefixes from parsed QNames — `ExpandedName` only provides `namespace()` and `name()`. As a result, `visibly_utilized_prefixes()` in `src/c14n/ns_exclusive.rs` and `write_qualified_name()` in `src/c14n/serialize.rs` must use `lookup_prefix(namespace_uri)` for prefix resolution. This is a known limitation: when multiple prefixes bind the same namespace URI, the reverse-mapping is ambiguous. The ambiguity is documented in code comments and tracked as a ROADMAP gap; the edge case is considered rare in SAML/XMLDSig use cases.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 12
File: src/c14n/xml_base.rs:61-93
Timestamp: 2026-03-16T17:23:26.715Z
Learning: In the xml-sec Rust project, the xml:base URI resolver in `src/c14n/xml_base.rs` correctly uses RFC 3986 §5 for IRI resolution in C14N 1.1 mode, as C14N 1.1 §2.4 explicitly delegates to RFC 3986 §5. When the base URI has no scheme, `parse_base()` returns `None` and the reference is returned as-is — this is the correct fallback behavior. The W3C C14N 1.1 interop test suite notes "modifications to RFC 3986 dot segment processing for C14N11" which may represent edge cases worth verifying against the `remove_dot_segments()` implementation.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 41
File: tests/donor_full_verification_suite.rs:164-168
Timestamp: 2026-04-03T14:47:20.446Z
Learning: In `tests/donor_full_verification_suite.rs` (xml-sec, Rust), using `let _ = read_fixture(...)` to intentionally discard a `String` return value triggers `clippy::let_underscore_drop` under `-D warnings`, because `String` has a non-trivial `Drop` impl. The correct idiom is a plain statement call (e.g., `read_fixture(&root.join(case.xml_path));`) with an inline comment to document intent. Do not suggest `let _ = expr` for types with meaningful `Drop` implementations in this codebase.

Learnt from: polaz
Repo: structured-world/xml-sec PR: 12
File: src/c14n/serialize.rs:218-222
Timestamp: 2026-03-16T18:07:55.027Z
Learning: In the xml-sec project (Rust), RFC 3741 §1.2 (Exclusive XML Canonicalization) explicitly states that xml:* namespace attributes (xml:lang, xml:space, etc.) are NOT imported into orphan nodes of a document subset. This is one of the two explicit differences from Inclusive C14N. The `collect_inherited_xml_attrs` function in `src/c14n/serialize.rs` must be skipped entirely for Exclusive mode. RFC 3741 §4.4 places the burden on callers to explicitly include required xml:* attributes in the subset — it does NOT restore the inheritance rule.

Learnt from: CR
Repo: structured-world/xml-sec PR: 0
File: .github/instructions/rust.instructions.md:0-0
Timestamp: 2026-03-17T12:12:28.146Z
Learning: Applies to **/*.rs : Tier 2 - Flag safety issues: constant-time comparison must be used for cryptographic MACs/checksums
✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

P2-001: KeyInfo parsing dispatch for XMLDSig verify

2 participants