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
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ jobs:
env:
PKCS11_MODULE: /usr/lib/softhsm/libsofthsm2.so
run: |
for profile in dom-xmloxide dom-roxmltree c14n-xmloxide c14n-roxmltree dsig-xmloxide dsig-roxmltree cms-xmloxide dsig-sign-xmloxide xmlenc-xmloxide xmlenc-roxmltree xslt legacy experimental fips-dsig pkcs11 no-backend; do
for profile in dom-xmloxide dom-roxmltree c14n-xmloxide c14n-roxmltree dsig-xmloxide dsig-roxmltree cms-xmloxide dsig-sign-xmloxide xmlenc-xmloxide xmlenc-roxmltree xslt legacy experimental dstu4145 fips-dsig pkcs11 no-backend; do
echo "::group::$profile"
cargo run --quiet -p xml-sec-tools -- check-features "$profile"
echo '::endgroup::'
Expand Down Expand Up @@ -287,6 +287,14 @@ jobs:
cargo clippy -p xml-sec --lib --no-default-features
--features cms,xml-backend-xmloxide
--target thumbv7em-none-eabihf -- -D warnings
- run: >-
cargo clippy -p xml-sec --lib --no-default-features
--features dstu4145,cms,xml-backend-xmloxide
--target thumbv7em-none-eabihf -- -D warnings
- run: >-
cargo check -p xml-sec --lib --no-default-features
--features dstu4145,cms,xml-backend-xmloxide
--target wasm32-unknown-unknown
- run: >-
cargo clippy -p xml-sec-xades --lib --no-default-features
--features xml-backend-roxmltree
Expand Down
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,9 @@ winnow ={ version = "1.0.4", default-features = false, features = ["parser"], op
snafu = { version = "0.9", default-features = false, features = ["rust_1_81"], optional = true }

# Crypto
structured-dstu4145 = { version = "0.20.1", default-features = false, features = ["alloc"], optional = true }
# Holds the prepared GOST substitution tables that digest states borrow.
once_cell = { version = "1", default-features = false, features = ["alloc", "race"], optional = true }
cryptoki = { version = "0.12.1", optional = true }
aws-lc-rs = { version = "1.19", default-features = false, features = ["fips"], optional = true }
rsa = { package = "sad-rsa", version = "0.10.2", default-features = false, features = ["sha1", "sha2", "hazmat"], optional = true }
Expand Down Expand Up @@ -522,6 +525,8 @@ c14n = [] # XML Canonicalization (inclusive + exclusive)
xslt = ["xmldsig", "std", "dep:icu_collator", "dep:icu_locale", "dep:roxmltree", "dep:time", "dep:url"]
# Experimental libxmlsec1 algorithm identifiers; build capability is not policy permission.
experimental-pq = ["xmldsig", "der/derive", "dep:slh-dsa", "dep:ml-kem", "dep:common", "dep:ctutils", "dep:hybrid-array", "dep:module-lattice", "dep:shake", "dep:const-oid", "signature/digest", "signature/alloc", "signature/rand_core"]
# DSTU 4145 verification with GOST 34.311 and DSTU 7564; build capability is not policy permission.
dstu4145 = ["xmldsig", "dep:structured-dstu4145", "dep:once_cell"]
no-unsafe = []
embedded = []

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ Just `cargo add xml-sec`.**

- **End-to-end XML security**: sign, verify, encrypt, and decrypt through public Rust APIs.
- **Modern signature algorithms**: curve-independent ECDSA with SHA-2/SHA-3, Ed25519/Ed448
with context/prehash variants, and opt-in experimental ML-DSA/SLH-DSA through `CryptoProvider`.
with context/prehash variants, opt-in experimental ML-DSA/SLH-DSA through `CryptoProvider`,
and opt-in verification of Ukrainian DSTU 4145 signatures.
- **Experimental post-quantum key establishment**: ML-KEM-512/768/1024 through RustCrypto,
RFC 9935 key encodings, and libxmlsec1-compatible HMAC/AES encapsulation workflows.
Recipient encapsulation is not sender authentication; explicit policy permission is required.
Expand Down
1 change: 1 addition & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ See [provider boundaries](crypto-providers.md) and [policy profiles](policy-prof
| `xslt` plus one XML backend | XMLDSig XSLT support; implies `xmldsig` and `std` |
| `experimental-pq` plus one XML backend | Experimental signature verification and key establishment; implies `xmldsig`, not `xmlenc`; PQ signing keys also need `xmldsig-sign` |
| `xmldsig,legacy-algorithms` plus one XML backend | Optional compatibility algorithms, still denied by secure policy defaults |
| `dstu4145` plus one XML backend | Verifies DSTU 4145 signatures with GOST 34.311 and DSTU 7564-256 digests, in XML and in X.509 chains; implies `xmldsig`; denied unless policy names the methods; no signing |
| `aws-lc-fips` plus one XML backend | Adds the official FIPS provider; implies `xmldsig` and `std`, not encryption |
| `pkcs11` plus one XML backend | Adds explicit external-key operations; implies `xmlenc` and `std` |
| `--all-features` | All available providers and mechanisms coexist; runtime selection is still explicit |
Expand Down
12 changes: 12 additions & 0 deletions docs/xmldsig.md
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,18 @@ operation policy. PKCS#8 and SPKI use RFC 9881/RFC 9909 encodings. ML-DSA import
seed-only, expanded-only, and consistent seed-plus-expanded keys; expanded imports are checked
before signing. See [the maintained `RustCrypto` patch](ml-dsa-patch.md) for provenance,
validation, and reproduction. These key primitives are dispatched through `CryptoProvider`.

The optional `dstu4145` feature verifies DSTU 4145 signatures with the GOST 34.311 and
DSTU 7564-256 digests, in XML, in X.509 paths and in CMS `SignedData`. No registry assigns XML
identifiers to them, so the library follows the ones in the test signatures published by the
Central Certification Authority of Ukraine; the 384 and 512-bit DSTU 7564 methods have no
published example and stay unknown. These URIs have no parameter for a substitution table, so
GOST 34.311 means table number one and a key stating another is refused. Custom curves need
primality evidence that a certificate does not carry, so only named domains and explicit copies
of them are accepted. The feature grants no permission: digests, signature methods and, for
certificates and CMS, `certificate_signature_algorithms` must each be allowed by policy. These
methods only verify.

`HMACOutputLength` uses XML Schema integer syntax (including XML whitespace and
an optional leading plus), not Unicode whitespace. `XMLDSig` 1.1 requires at least 80 bits and
half the hash width; the default policy tightens that floor to at least 128 bits. Arbitrary
Expand Down
10 changes: 6 additions & 4 deletions src/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1286,10 +1286,12 @@ impl KeyTrustPolicy {
Some(CertificateSignatureAlgorithms::Allowlist(allowed)) => {
allowed.contains(&algorithm)
}
None => !matches!(
algorithm,
crate::provider::X509SignatureAlgorithm::PostQuantum(_)
),
None => match algorithm {
crate::provider::X509SignatureAlgorithm::PostQuantum(_) => false,
#[cfg(feature = "dstu4145")]
crate::provider::X509SignatureAlgorithm::Dstu4145 { .. } => false,
_ => true,
},
};
if !permitted {
return Err(PolicyViolation::Algorithm {
Expand Down
46 changes: 46 additions & 0 deletions src/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,14 @@ pub enum X509SignatureAlgorithm {
Ed448,
/// Pure ML-DSA or SLH-DSA with the PKIX-mandated empty context.
PostQuantum(crate::xmldsig::PqAlgorithm),
/// DSTU 4145 with the selected digest; SPKI selects the curve.
#[cfg(feature = "dstu4145")]
Dstu4145 {
/// Message digest applied to the signed certificate data.
digest: DigestAlgorithm,
/// Whether the signature octets follow the big-endian profile.
big_endian: bool,
},
}

#[cfg(feature = "xmldsig")]
Expand All @@ -507,6 +515,12 @@ impl X509SignatureAlgorithm {
Self::Dsa(DigestAlgorithm::Md5 | DigestAlgorithm::Ripemd160)
| Self::RsaPkcs1v15(DigestAlgorithm::Md5 | DigestAlgorithm::Ripemd160)
| Self::Ecdsa(DigestAlgorithm::Md5 | DigestAlgorithm::Ripemd160) => return None,
#[cfg(feature = "dstu4145")]
Self::Dsa(DigestAlgorithm::Gost34311 | DigestAlgorithm::Dstu7564_256)
| Self::RsaPkcs1v15(DigestAlgorithm::Gost34311 | DigestAlgorithm::Dstu7564_256)
| Self::Ecdsa(DigestAlgorithm::Gost34311 | DigestAlgorithm::Dstu7564_256) => {
return None;
}
Self::Dsa(DigestAlgorithm::Sha1) => "1.2.840.10040.4.3",
Self::Dsa(DigestAlgorithm::Sha224) => "2.16.840.1.101.3.4.3.1",
Self::Dsa(DigestAlgorithm::Sha256) => "2.16.840.1.101.3.4.3.2",
Expand Down Expand Up @@ -538,6 +552,9 @@ impl X509SignatureAlgorithm {
Self::Ed25519 => "1.3.101.112",
Self::Ed448 => "1.3.101.113",
Self::PostQuantum(algorithm) => algorithm.oid(),
// The identifier also carries the field basis, which only SPKI fixes.
#[cfg(feature = "dstu4145")]
Self::Dstu4145 { .. } => return None,
})
}
}
Expand Down Expand Up @@ -1489,6 +1506,10 @@ macro_rules! rust_crypto_digest {
use sha2::Digest as _;
match algorithm {
$($(#[$attr])* DigestAlgorithm::$variant => Self::$variant(<$hasher>::new()),)+
#[cfg(feature = "dstu4145")]
DigestAlgorithm::Gost34311 | DigestAlgorithm::Dstu7564_256 => {
unreachable!("begin_digest sends these to the DSTU 4145 module")
}
}
}
}
Expand Down Expand Up @@ -2054,6 +2075,10 @@ impl CryptoProvider for RustCryptoProvider {
&self,
algorithm: DigestAlgorithm,
) -> Result<Box<dyn DigestState + '_>, ProviderError> {
#[cfg(feature = "dstu4145")]
if crate::xmldsig::dstu4145::is_ukrainian_digest(algorithm) {
return crate::xmldsig::dstu4145::begin_digest(algorithm);
}
Ok(Box::new(RustCryptoDigest::new(algorithm)))
}

Expand Down Expand Up @@ -2120,6 +2145,21 @@ impl CryptoProvider for RustCryptoProvider {
signature: &[u8],
issuer_spki_der: &[u8],
) -> Result<bool, ProviderError> {
// A refused key or framing is a non-matching signature.
#[cfg(feature = "dstu4145")]
if let X509SignatureAlgorithm::Dstu4145 { digest, big_endian } = algorithm
&& crate::xmldsig::dstu4145::is_ukrainian_digest(digest)
{
return Ok(crate::xmldsig::dstu4145::verify_identified(
digest,
big_endian,
issuer_spki_der,
signed_data,
signature,
&crate::xmldsig::pkix::CertificateSession::standalone(),
)
.unwrap_or(false));
}
rustcrypto_x509::verify_signature(algorithm, signed_data, signature, issuer_spki_der)
}

Expand Down Expand Up @@ -2224,6 +2264,10 @@ fn is_supported_x509_signature(algorithm: X509SignatureAlgorithm) -> bool {
rustcrypto_x509::xml_signature_algorithm(algorithm).is_some()
}
X509SignatureAlgorithm::PostQuantum(_) => cfg!(feature = "experimental-pq"),
#[cfg(feature = "dstu4145")]
X509SignatureAlgorithm::Dstu4145 { digest, .. } => {
crate::xmldsig::dstu4145::is_ukrainian_digest(digest)
}
X509SignatureAlgorithm::RsaPss {
digest, mgf_digest, ..
} => {
Expand Down Expand Up @@ -2470,6 +2514,8 @@ pub(crate) mod rustcrypto_x509 {
X509SignatureAlgorithm::Ecdsa(digest) => match digest {
#[cfg(feature = "legacy-algorithms")]
DigestAlgorithm::Md5 | DigestAlgorithm::Ripemd160 => None,
#[cfg(feature = "dstu4145")]
DigestAlgorithm::Gost34311 | DigestAlgorithm::Dstu7564_256 => None,
DigestAlgorithm::Sha1 => Some(SignatureAlgorithm::EcdsaSha1),
DigestAlgorithm::Sha224 => Some(SignatureAlgorithm::EcdsaSha224),
DigestAlgorithm::Sha256 => Some(SignatureAlgorithm::EcdsaSha256),
Expand Down
2 changes: 2 additions & 0 deletions src/provider/aws_lc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ fn digest_algorithm(algorithm: DigestAlgorithm) -> Option<&'static digest::Algor
DigestAlgorithm::Sha3_384 => &digest::SHA3_384,
DigestAlgorithm::Sha3_512 => &digest::SHA3_512,
DigestAlgorithm::Sha3_224 => return None,
#[cfg(feature = "dstu4145")]
DigestAlgorithm::Gost34311 | DigestAlgorithm::Dstu7564_256 => return None,
})
}

Expand Down
5 changes: 5 additions & 0 deletions src/provider/key_establishment/bit_hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ pub(super) fn hash(
total_bits,
algorithm.output_len(),
),
// The KDF names its digest in the document; these have no bit framing.
#[cfg(feature = "dstu4145")]
DigestAlgorithm::Gost34311 | DigestAlgorithm::Dstu7564_256 => Err(
ProviderError::InvalidInput(ProviderInputError::ConcatKdfParameters),
),
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/provider/rsa_pss.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ fn hash(algorithm: DigestAlgorithm, parts: &[&[u8]], output: &mut [u8; 64]) {
DigestAlgorithm::Md5 => compute!(md5::Md5),
#[cfg(feature = "legacy-algorithms")]
DigestAlgorithm::Ripemd160 => compute!(ripemd::Ripemd160),
// Parameterized PSS names its hashes in the document.
#[cfg(feature = "dstu4145")]
DigestAlgorithm::Gost34311 | DigestAlgorithm::Dstu7564_256 => {
let result = crate::xmldsig::dstu4145::digest_parts(algorithm, parts);
output[..result.len()].copy_from_slice(&result);
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/xmldsig/cms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,10 @@ impl CmsProvider<'_> {
// https://www.rfc-editor.org/rfc/rfc8419#section-3
X509SignatureAlgorithm::Ed25519 => digest == DigestAlgorithm::Sha512,
X509SignatureAlgorithm::Ed448 => false,
// The identifier names the hash that the signature covers, so
// digestAlgorithm can only repeat it, as for ECDSA.
#[cfg(feature = "dstu4145")]
X509SignatureAlgorithm::Dstu4145 { digest: hash, .. } => hash == digest,
_ => true,
};
Ok(if consistent {
Expand Down
38 changes: 33 additions & 5 deletions src/xmldsig/digest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,24 @@ pub enum DigestAlgorithm {
Sha3_384,
/// SHA3-512 (512-bit).
Sha3_512,
/// GOST 34.311-95 with the Ukrainian substitution table number one, the
/// only one its URI can name; operation policy must explicitly permit it.
#[cfg(feature = "dstu4145")]
Gost34311,
/// DSTU 7564 (Kupyna) with 256-bit output; operation policy must explicitly
/// permit it.
#[cfg(feature = "dstu4145")]
Dstu7564_256,
}

impl DigestAlgorithm {
/// Every compiled digest capability; this registry grants no policy permission.
pub const ALL: [Self; 9 + if cfg!(feature = "legacy-algorithms") {
2
} else {
0
}] = [
pub const ALL: [Self;
9 + if cfg!(feature = "legacy-algorithms") {
2
} else {
0
} + if cfg!(feature = "dstu4145") { 2 } else { 0 }] = [
#[cfg(feature = "legacy-algorithms")]
Self::Md5,
#[cfg(feature = "legacy-algorithms")]
Expand All @@ -61,6 +70,10 @@ impl DigestAlgorithm {
Self::Sha3_256,
Self::Sha3_384,
Self::Sha3_512,
#[cfg(feature = "dstu4145")]
Self::Gost34311,
#[cfg(feature = "dstu4145")]
Self::Dstu7564_256,
];
/// Parse a digest algorithm from its XML namespace URI.
///
Expand Down Expand Up @@ -97,6 +110,13 @@ impl DigestAlgorithm {
"http://www.w3.org/2007/05/xmldsig-more#sha3-256" => Some(Self::Sha3_256),
"http://www.w3.org/2007/05/xmldsig-more#sha3-384" => Some(Self::Sha3_384),
"http://www.w3.org/2007/05/xmldsig-more#sha3-512" => Some(Self::Sha3_512),
// No registry assigns these. They are the identifiers of the test
// signatures the Central Certification Authority of Ukraine publishes:
// https://czo.gov.ua/testexamples
#[cfg(feature = "dstu4145")]
"http://www.w3.org/2001/04/xmlenc#gost34311" => Some(Self::Gost34311),
#[cfg(feature = "dstu4145")]
"http://www.w3.org/2001/04/xmlenc#dstu7564-256" => Some(Self::Dstu7564_256),
_ => None,
}
}
Expand All @@ -118,6 +138,10 @@ impl DigestAlgorithm {
Self::Sha3_256 => "http://www.w3.org/2007/05/xmldsig-more#sha3-256",
Self::Sha3_384 => "http://www.w3.org/2007/05/xmldsig-more#sha3-384",
Self::Sha3_512 => "http://www.w3.org/2007/05/xmldsig-more#sha3-512",
#[cfg(feature = "dstu4145")]
Self::Gost34311 => "http://www.w3.org/2001/04/xmlenc#gost34311",
#[cfg(feature = "dstu4145")]
Self::Dstu7564_256 => "http://www.w3.org/2001/04/xmlenc#dstu7564-256",
}
}

Expand All @@ -136,6 +160,8 @@ impl DigestAlgorithm {
match self {
#[cfg(feature = "legacy-algorithms")]
Self::Md5 | Self::Ripemd160 => true,
#[cfg(feature = "dstu4145")]
Self::Gost34311 | Self::Dstu7564_256 => true,
_ => false,
}
}
Expand All @@ -153,6 +179,8 @@ impl DigestAlgorithm {
Self::Sha256 | Self::Sha3_256 => 32,
Self::Sha384 | Self::Sha3_384 => 48,
Self::Sha512 | Self::Sha3_512 => 64,
#[cfg(feature = "dstu4145")]
Self::Gost34311 | Self::Dstu7564_256 => 32,
}
}
}
Expand Down
Loading
Loading