Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,51 @@ env:
RUSTFLAGS: -Dwarnings

jobs:
build:
build-matrix:
name: build-matrix (${{ matrix.rust }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
rust: [stable, "1.92.0"]
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ matrix.rust }}
Comment thread
polaz marked this conversation as resolved.
- uses: Swatinem/rust-cache@v2
- run: cargo build --all-features

test:
build:
runs-on: ubuntu-latest
needs: build-matrix
if: ${{ always() }}
steps:
- run: test "${{ needs.build-matrix.result }}" = "success"

test-matrix:
name: test-matrix (${{ matrix.rust }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
rust: [stable, "1.92.0"]
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ matrix.rust }}
Comment thread
polaz marked this conversation as resolved.
- uses: taiki-e/install-action@nextest
- uses: Swatinem/rust-cache@v2
- run: cargo test --all-features
- run: cargo nextest run --all-features
Comment thread
polaz marked this conversation as resolved.
- run: cargo test --doc --all-features

test:
runs-on: ubuntu-latest
needs: test-matrix
if: ${{ always() }}
steps:
- run: test "${{ needs.test-matrix.result }}" = "success"

clippy:
runs-on: ubuntu-latest
Expand Down
9 changes: 4 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "xml-sec"
version = "0.1.0"
edition = "2021"
edition = "2024"
rust-version = "1.92"
license = "Apache-2.0"
description = "Pure Rust XML Security: XMLDSig, XMLEnc, C14N. Drop-in replacement for libxmlsec1."
Expand All @@ -23,13 +23,12 @@ ring = "0.17"
x509-parser = "0.18"
der = "0.8"

# Base64 encoding/decoding
base64 = "0.22"

# Error handling
thiserror = "2"

[dev-dependencies]
# W3C test vectors
base64 = "0.22"

[features]
default = ["xmldsig", "c14n"]
xmldsig = [] # XML Digital Signatures (sign + verify)
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ Every SAML, SOAP, and WS-Security implementation depends on libxmlsec1 — a C l

**Pre-release.** API is unstable. Not ready for production use.

Current toolchain target: latest stable Rust.
Current MSRV: Rust 1.92.

## Specifications

| Spec | Status |
Expand Down
2 changes: 2 additions & 0 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[toolchain]
channel = "stable"
2 changes: 1 addition & 1 deletion src/c14n/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ use roxmltree::{Document, Node};

use ns_exclusive::ExclusiveNsRenderer;
use ns_inclusive::InclusiveNsRenderer;
use serialize::{serialize_canonical, C14nConfig};
use serialize::{C14nConfig, serialize_canonical};

/// C14N algorithm mode (without the comments flag).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down
2 changes: 1 addition & 1 deletion src/c14n/ns_exclusive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ fn visibly_utilized_prefixes<'a>(node: Node<'a, '_>) -> HashSet<&'a str> {
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::super::serialize::{serialize_canonical, C14nConfig};
use super::super::serialize::{C14nConfig, serialize_canonical};
use super::*;
use roxmltree::Document;
use std::collections::HashSet;
Expand Down
2 changes: 1 addition & 1 deletion src/c14n/ns_inclusive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ impl NsRenderer for InclusiveNsRenderer {
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::super::serialize::{serialize_canonical, C14nConfig};
use super::super::serialize::{C14nConfig, serialize_canonical};
use super::*;
use roxmltree::Document;

Expand Down
41 changes: 19 additions & 22 deletions src/c14n/serialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ use std::collections::{HashMap, HashSet};

use roxmltree::{Document, Node, NodeType};

use super::C14nError;
use super::escape::{escape_attr, escape_cr, escape_text};
use super::prefix::{attribute_prefix, element_prefix};
use super::xml_base::{compute_effective_xml_base, resolve_uri};
use super::C14nError;

/// The XML namespace URI.
///
Expand Down Expand Up @@ -143,10 +143,8 @@ fn serialize_children(
if in_set {
// Document-level text nodes are ignored by C14N.
// Only text inside elements is serialized.
if !is_doc_root {
if let Some(text) = child.text() {
escape_text(text, output);
}
if !is_doc_root && let Some(text) = child.text() {
escape_text(text, output);
}
}
}
Expand All @@ -164,20 +162,18 @@ fn serialize_children(
}
}
NodeType::PI => {
if in_set {
if let Some(pi) = child.pi() {
if is_doc_root {
write_doc_level_separator(&child, output);
}
output.extend_from_slice(b"<?");
output.extend_from_slice(pi.target.as_bytes());
if let Some(value) = pi.value {
output.push(b' ');
// C14N spec: \r in PI content must be escaped to &#xD;
escape_cr(value, output);
}
output.extend_from_slice(b"?>");
if in_set && let Some(pi) = child.pi() {
if is_doc_root {
write_doc_level_separator(&child, output);
}
output.extend_from_slice(b"<?");
output.extend_from_slice(pi.target.as_bytes());
if let Some(value) = pi.value {
output.push(b' ');
// C14N spec: \r in PI content must be escaped to &#xD;
escape_cr(value, output);
}
output.extend_from_slice(b"?>");
}
}
NodeType::Root => {
Expand Down Expand Up @@ -389,10 +385,11 @@ fn collect_inherited_xml_attrs<'a>(

// If parent element is in the node set, no inheritance needed — the parent
// will render its own xml:* attributes, and the element inherits normally.
if let Some(parent) = node.parent() {
if parent.is_element() && pred(parent) {
return Vec::new();
}
if let Some(parent) = node.parent()
&& parent.is_element()
&& pred(parent)
{
return Vec::new();
}

// Collect inheritable xml:* attr names already on this element (own attrs
Expand Down
28 changes: 14 additions & 14 deletions src/c14n/xml_base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ pub(crate) fn compute_effective_xml_base(
// xml:base in the canonical output. However, we still collect
// its xml:base value as the resolution seed, so that the
// omitted chain below resolves against an absolute base.
if let Some(pred) = node_set {
if pred(n) {
if let Some(base) = xml_base_value(n) {
bases.push(base);
}
break;
if let Some(pred) = node_set
&& pred(n)
{
if let Some(base) = xml_base_value(n) {
bases.push(base);
}
break;
}
if let Some(base) = xml_base_value(n) {
bases.push(base);
Expand Down Expand Up @@ -157,10 +157,10 @@ pub(crate) fn resolve_uri(base: &str, reference: &str) -> String {
if let Some(rest) = ref_path.strip_prefix("//") {
let mut auth_end = rest.len();
for ch in ['/', '?', '#'] {
if let Some(pos) = rest.find(ch) {
if pos < auth_end {
auth_end = pos;
}
if let Some(pos) = rest.find(ch)
&& pos < auth_end
{
auth_end = pos;
}
}
let new_authority = &rest[..auth_end];
Expand Down Expand Up @@ -235,10 +235,10 @@ fn parse_base(base: &str) -> Option<BaseParts<'_>> {
rest = &rest[2..];
let mut auth_end = rest.len();
for ch in ['/', '?', '#'] {
if let Some(pos) = rest.find(ch) {
if pos < auth_end {
auth_end = pos;
}
if let Some(pos) = rest.find(ch)
&& pos < auth_end
{
auth_end = pos;
}
}
authority = Some(&rest[..auth_end]);
Expand Down
Loading
Loading