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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
/.direnv
/result
**/.DS_Store
.idea
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "samael"
version = "0.0.19"
version = "0.0.20"
authors = ["Nathan Jaremko <nathan@jaremko.ca>"]
edition = "2021"
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ You'll need these dependencies for this example
```toml
[dependencies]
tokio = { version = "1.28.1", features = ["full"] }
samael = { version = "0.0.12", features = ["xmlsec"] }
samael = { version = "0.0.20", features = ["xmlsec"] }
warp = "0.3.5"
reqwest = "0.11.18"
openssl = "0.10.52"
Expand Down
1 change: 1 addition & 0 deletions bindings.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
#include <xmlsec/templates.h>
#include <xmlsec/transforms.h>
#include <xmlsec/xmldsig.h>
#include <xmlsec/list.h>
#include <xmlsec/xmlsec.h>
#include <xmlsec/xmltree.h>
29 changes: 14 additions & 15 deletions bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ fn main() {
// Tell the compiler about our custom cfg flags
println!("cargo:rustc-check-cfg=cfg(xmlsec_dynamic)");
println!("cargo:rustc-check-cfg=cfg(xmlsec_static)");
println!("cargo:rerun-if-changed=bindings.h");

if env::var_os("CARGO_FEATURE_XMLSEC").is_some() {
let path_out = PathBuf::from(env::var("OUT_DIR").unwrap());
Expand All @@ -41,24 +42,22 @@ fn main() {
println!("cargo:rustc-link-lib=ssl"); // -lssl
println!("cargo:rustc-link-lib=crypto"); // -lcrypto

if !path_bindings.exists() {
PkgConfig::new()
.probe("xmlsec1")
.expect("Could not find xmlsec1 using pkg-config");
PkgConfig::new()
.probe("xmlsec1")
.expect("Could not find xmlsec1 using pkg-config");

let bindbuild = BindgenBuilder::default()
.header("bindings.h")
.clang_args(cflags)
.clang_args(fetch_xmlsec_config_libs())
.layout_tests(true)
.generate_comments(true);
let bindbuild = BindgenBuilder::default()
.header("bindings.h")
.clang_args(cflags)
.clang_args(fetch_xmlsec_config_libs())
.layout_tests(true)
.generate_comments(true);

let bindings = bindbuild.generate().expect("Unable to generate bindings");
let bindings = bindbuild.generate().expect("Unable to generate bindings");

bindings
.write_to_file(path_bindings)
.expect("Couldn't write bindings!");
}
bindings
.write_to_file(path_bindings)
.expect("Couldn't write bindings!");
}
}

Expand Down
32 changes: 16 additions & 16 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
flake-utils.url = "github:numtide/flake-utils";
nix-filter.url = "github:numtide/nix-filter";
rust-overlay = {
Expand Down
6 changes: 4 additions & 2 deletions src/crypto/cert_encoding.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use base64::{engine::general_purpose, Engine as _};
use crate::crypto::CertificateDer;
use base64::{engine::general_purpose, Engine as _};

// strip out 76-width format and decode base64
pub fn decode_x509_cert(x509_cert: &str) -> Result<CertificateDer, base64::DecodeError> {
Expand All @@ -10,7 +10,9 @@ pub fn decode_x509_cert(x509_cert: &str) -> Result<CertificateDer, base64::Decod
.filter(|b| !b" \n\t\r\x0b\x0c".contains(b))
.collect::<Vec<u8>>();

general_purpose::STANDARD.decode(stripped).map(|data| data.into())
general_purpose::STANDARD
.decode(stripped)
.map(|data| data.into())
}

// 76-width base64 encoding (MIME)
Expand Down
24 changes: 20 additions & 4 deletions src/crypto/crypto_disabled.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! This module provides the behaviour if no crypto is available.

use crate::crypto::{CryptoError, CryptoProvider, CertificateDer};
use crate::crypto::{CertificateDer, CryptoError, CryptoProvider, ReduceMode};
use crate::schema::CipherValue;

pub struct NoCrypto;
Expand All @@ -16,9 +16,12 @@ impl CryptoProvider for NoCrypto {
Ok(())
}

fn reduce_xml_to_signed(_xml_str: &str, _certs: &[CertificateDer]) -> Result<String, CryptoError> {
// Since we cannot verify anything. Return empty.
Ok(String::new())
fn reduce_xml_to_signed(
_xml_str: &str,
_certs: &[CertificateDer],
_reduce_mode: ReduceMode,
) -> Result<String, CryptoError> {
Err(CryptoError::CryptoDisabled)
}

fn decrypt_assertion_key_info(
Expand All @@ -44,3 +47,16 @@ impl CryptoProvider for NoCrypto {
Err(CryptoError::CryptoDisabled)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn reduce_xml_to_signed_fails_closed_when_crypto_is_disabled() {
let error = NoCrypto::reduce_xml_to_signed("<Response/>", &[], ReduceMode::ValidateAndMark)
.expect_err("signature reduction should fail when crypto support is disabled");

assert!(matches!(error, CryptoError::CryptoDisabled));
}
}
37 changes: 33 additions & 4 deletions src/crypto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::schema::CipherValue;
pub use cert_encoding::*;
pub use ids::*;
use thiserror::Error;
pub use url_verification::{UrlVerifier, UrlVerifierError, sign_url};
pub use url_verification::{sign_url, UrlVerifier, UrlVerifierError};
#[cfg(feature = "xmlsec")]
pub use xmlsec::*;

Expand Down Expand Up @@ -61,21 +61,50 @@ impl From<Vec<u8>> for CertificateDer {
}
}

/// Defines which algorithm is used to reduce signed XML.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReduceMode {
/// Returns xmlsec's pre-digest content for exactly one verified reference across the document.
PreDigest,
/// Legacy mode that preserves the verified content and every element ancestor up to the
/// document root.
///
/// This is kept for compatibility with older callers. It is not the default because unsigned
/// ancestors can survive reduction in this mode.
ValidateAndMark,
/// Returns a rooted XML document containing only xmlsec-verified content.
///
/// If the verified reference is a full element, that element becomes the output root. If the
/// verified reference reduces to a child sequence, the referenced element is retained only as a
/// stripped shell so the verified descendants remain rooted.
ValidateAndMarkNoAncestors,
}

impl Default for ReduceMode {
fn default() -> Self {
Self::ValidateAndMarkNoAncestors
}
}

pub trait CryptoProvider {
type PrivateKey;

fn verify_signed_xml<Bytes: AsRef<[u8]>>(
xml: Bytes,
x509_cert_der: &CertificateDer,
id_attribute: Option<&str>,
) -> Result<(), CryptoError>;

/// Takes an XML document, parses it, verifies all XML digital signatures against the given
/// certificates, and returns a derived version of the document where all elements that are not
/// covered by a digital signature have been removed.
/// certificates, and returns output according to `reduce_mode`.
///
/// `ReduceMode::PreDigest` returns xmlsec's verified pre-digest payload for exactly one
/// reference. The validate modes return a rooted XML document derived from the original input
/// with all unsigned content removed.
fn reduce_xml_to_signed(
xml_str: &str,
certs_der: &[CertificateDer],
reduce_mode: ReduceMode,
) -> Result<String, CryptoError>;

fn decrypt_assertion_key_info(
Expand Down
19 changes: 9 additions & 10 deletions src/crypto/url_verification.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use crate::crypto::CertificateDer;
use crate::signature::SignatureAlgorithm;
use base64::{engine::general_purpose, Engine as _};
use std::collections::HashMap;
use std::str::FromStr;
use openssl::error::ErrorStack;
use openssl::pkey::{PKey, Private};
use std::collections::HashMap;
use std::str::FromStr;
use thiserror::Error;
use url::Url;
use crate::crypto::CertificateDer;

#[derive(Debug, Error, Clone)]
pub enum UrlVerifierError {
Expand All @@ -17,7 +17,7 @@ pub enum UrlVerifierError {
#[error("No query to sign")]
NoQueryToSign,
#[error("Signing error")]
SigningError(#[from] ErrorStack)
SigningError(#[from] ErrorStack),
}

pub struct UrlVerifier {
Expand Down Expand Up @@ -55,9 +55,7 @@ impl UrlVerifier {
Ok(Self { public_key })
}

pub fn from_x509(
public_cert: &CertificateDer,
) -> Result<Self, Box<dyn std::error::Error>> {
pub fn from_x509(public_cert: &CertificateDer) -> Result<Self, Box<dyn std::error::Error>> {
let public_cert = openssl::x509::X509::from_der(public_cert.der_data())?;
let public_key = public_cert.public_key()?;
Ok(Self { public_key })
Expand Down Expand Up @@ -195,8 +193,10 @@ impl UrlVerifier {
}
}


pub fn sign_url(mut unsigned_url: Url, private_key: &PKey<Private>) -> Result<Url, UrlVerifierError> {
pub fn sign_url(
mut unsigned_url: Url,
private_key: &PKey<Private>,
) -> Result<Url, UrlVerifierError> {
// Refer to section 3.4.4.1 (page 17) of
//
// https://docs.oasis-open.org/security/saml/v2.0/saml-bindings-2.0-os.pdf
Expand Down Expand Up @@ -230,7 +230,6 @@ pub fn sign_url(mut unsigned_url: Url, private_key: &PKey<Private>) -> Result<Ur
.ok_or(UrlVerifierError::NoQueryToSign)?
.to_string();


let mut signer =
openssl::sign::Signer::new(openssl::hash::MessageDigest::sha256(), private_key)?;

Expand Down
Loading
Loading