hanko_xml 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +44 -0
- data/Cargo.lock +401 -0
- data/Cargo.toml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +222 -0
- data/THIRD_PARTY_LICENSES.md +392 -0
- data/dependencies.yml +87 -0
- data/ext/hanko_xml/Cargo.toml +20 -0
- data/ext/hanko_xml/build/vendor.rs +817 -0
- data/ext/hanko_xml/build.rs +298 -0
- data/ext/hanko_xml/extconf.rb +6 -0
- data/ext/hanko_xml/src/dsig.rs +1851 -0
- data/ext/hanko_xml/src/errors.rs +151 -0
- data/ext/hanko_xml/src/lib.rs +490 -0
- data/ext/hanko_xml/src/sys.rs +43 -0
- data/lib/hanko_xml/algorithms.rb +228 -0
- data/lib/hanko_xml/dom.rb +137 -0
- data/lib/hanko_xml/errors.rb +132 -0
- data/lib/hanko_xml/exc-c14n.xsd +26 -0
- data/lib/hanko_xml/input.rb +50 -0
- data/lib/hanko_xml/key.rb +213 -0
- data/lib/hanko_xml/key_info.rb +67 -0
- data/lib/hanko_xml/native_version.rb +75 -0
- data/lib/hanko_xml/nokogiri_ext.rb +34 -0
- data/lib/hanko_xml/placement.rb +127 -0
- data/lib/hanko_xml/policy.rb +502 -0
- data/lib/hanko_xml/reference.rb +49 -0
- data/lib/hanko_xml/signature_info.rb +134 -0
- data/lib/hanko_xml/signed_document.rb +155 -0
- data/lib/hanko_xml/signer.rb +572 -0
- data/lib/hanko_xml/template.rb +150 -0
- data/lib/hanko_xml/transform.rb +87 -0
- data/lib/hanko_xml/verification.rb +377 -0
- data/lib/hanko_xml/verifier.rb +694 -0
- data/lib/hanko_xml/version.rb +5 -0
- data/lib/hanko_xml/xades/verifier.rb +319 -0
- data/lib/hanko_xml/xades.rb +287 -0
- data/lib/hanko_xml/xml.rb +93 -0
- data/lib/hanko_xml/xmldsig-core-schema.xsd +332 -0
- data/lib/hanko_xml.rb +122 -0
- data/sig/hanko_xml/key.rbs +48 -0
- data/sig/hanko_xml/native.rbs +12 -0
- data/sig/hanko_xml/native_version.rbs +13 -0
- data/sig/hanko_xml/nokogiri_ext.rbs +23 -0
- data/sig/hanko_xml/placement.rbs +27 -0
- data/sig/hanko_xml/policy.rbs +83 -0
- data/sig/hanko_xml/reference.rbs +36 -0
- data/sig/hanko_xml/signed_document.rbs +66 -0
- data/sig/hanko_xml/signer.rbs +50 -0
- data/sig/hanko_xml/template.rbs +41 -0
- data/sig/hanko_xml/verification.rbs +100 -0
- data/sig/hanko_xml/verifier.rbs +45 -0
- data/sig/hanko_xml/xades.rbs +70 -0
- data/sig/hanko_xml.rbs +62 -0
- metadata +105 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
//! Error callback capture for libxmlsec1.
|
|
2
|
+
//!
|
|
3
|
+
//! libxmlsec1's default error handler prints to stderr, which is
|
|
4
|
+
//! unacceptable in a library. This module installs a custom callback
|
|
5
|
+
//! via `xmlSecErrorsSetCallback` that captures error messages into a
|
|
6
|
+
//! thread-local buffer. Each native call should bracket with
|
|
7
|
+
//! [`clear_errors`] / [`drain_errors`] to collect diagnostics.
|
|
8
|
+
//!
|
|
9
|
+
//! Thread-safety: the GVL is held for the entire duration of every
|
|
10
|
+
//! native call (§7.1), so a thread-local `RefCell<Vec<String>>` is
|
|
11
|
+
//! sufficient — no `Mutex` needed.
|
|
12
|
+
|
|
13
|
+
use std::cell::{Cell, RefCell};
|
|
14
|
+
use std::ffi::CStr;
|
|
15
|
+
use std::os::raw::{c_char, c_int, c_void};
|
|
16
|
+
|
|
17
|
+
thread_local! {
|
|
18
|
+
static ERROR_BUF: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
|
|
19
|
+
/// How many error-level libxml2 diagnostics the structured handler has
|
|
20
|
+
/// seen on this thread. Callers compare it before and after a parse.
|
|
21
|
+
static LIBXML2_ERRORS: Cell<usize> = const { Cell::new(0) };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Clears the thread-local error buffer. Call at the start of each
|
|
25
|
+
/// native entry point.
|
|
26
|
+
pub fn clear_errors() {
|
|
27
|
+
ERROR_BUF.with(|buf| buf.borrow_mut().clear());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Drains and returns all captured error messages since the last
|
|
31
|
+
/// [`clear_errors`]. Call at the end of each native entry point.
|
|
32
|
+
pub fn drain_errors() -> Vec<String> {
|
|
33
|
+
ERROR_BUF.with(|buf| {
|
|
34
|
+
let mut b = buf.borrow_mut();
|
|
35
|
+
let drained = b.clone();
|
|
36
|
+
b.clear();
|
|
37
|
+
drained
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// The `extern "C"` callback installed via `xmlSecErrorsSetCallback`.
|
|
42
|
+
///
|
|
43
|
+
/// Signature matches `xmlSecErrorsCallback`:
|
|
44
|
+
/// ```c
|
|
45
|
+
/// typedef void (*xmlSecErrorsCallback)(
|
|
46
|
+
/// const char* file, int line, const char* func,
|
|
47
|
+
/// const char* errorObject, const char* errorSubject,
|
|
48
|
+
/// int reason, const char* msg);
|
|
49
|
+
/// ```
|
|
50
|
+
///
|
|
51
|
+
/// # Safety
|
|
52
|
+
///
|
|
53
|
+
/// Called by libxmlsec1 internally. All pointer arguments are either
|
|
54
|
+
/// valid C strings or null. We treat null as empty and never retain
|
|
55
|
+
/// any pointer past this function's return.
|
|
56
|
+
unsafe extern "C" fn error_callback(
|
|
57
|
+
file: *const c_char,
|
|
58
|
+
line: c_int,
|
|
59
|
+
func: *const c_char,
|
|
60
|
+
error_object: *const c_char,
|
|
61
|
+
error_subject: *const c_char,
|
|
62
|
+
reason: c_int,
|
|
63
|
+
msg: *const c_char,
|
|
64
|
+
) {
|
|
65
|
+
let safe_str = |ptr: *const c_char| -> &str {
|
|
66
|
+
if ptr.is_null() {
|
|
67
|
+
""
|
|
68
|
+
} else {
|
|
69
|
+
// SAFETY: libxmlsec1 passes NUL-terminated C strings or null.
|
|
70
|
+
unsafe { CStr::from_ptr(ptr) }.to_str().unwrap_or("")
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
let file_s = safe_str(file);
|
|
75
|
+
let func_s = safe_str(func);
|
|
76
|
+
let obj_s = safe_str(error_object);
|
|
77
|
+
let subj_s = safe_str(error_subject);
|
|
78
|
+
let msg_s = safe_str(msg).trim_end();
|
|
79
|
+
|
|
80
|
+
let entry = format!(
|
|
81
|
+
"func={func_s}:file={file_s}:line={line}:obj={obj_s}:subj={subj_s}:error={reason}:{msg_s}"
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
ERROR_BUF.with(|buf| {
|
|
85
|
+
buf.borrow_mut().push(entry);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Installs our error callback. Must be called once during
|
|
90
|
+
/// `init_xmlsec()`, after `xmlSecInit()`.
|
|
91
|
+
///
|
|
92
|
+
/// # Safety
|
|
93
|
+
///
|
|
94
|
+
/// `xmlSecErrorsSetCallback` is safe to call after `xmlSecInit()`.
|
|
95
|
+
pub unsafe fn install_error_callback() {
|
|
96
|
+
unsafe {
|
|
97
|
+
crate::sys::xmlSecErrorsSetCallback(Some(error_callback));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// libxml2's structured error handler, installed around each native parse
|
|
102
|
+
/// and schema validation by `dsig::Libxml2Capture`. Keeps libxml2's
|
|
103
|
+
/// diagnostics off stderr and in the same buffer as xmlsec's, so they reach
|
|
104
|
+
/// `#xmlsec_errors` and never `#message`: they can quote the document.
|
|
105
|
+
///
|
|
106
|
+
/// Only error and fatal levels are kept. Warnings are dropped, matching
|
|
107
|
+
/// what a strict Nokogiri parse rejects (§6.3).
|
|
108
|
+
///
|
|
109
|
+
/// # Safety
|
|
110
|
+
///
|
|
111
|
+
/// Called by libxml2 with a valid `xmlError` or null, and never retained.
|
|
112
|
+
/// Declared with `*const` (libxml2 2.12+); 2.9 passes the same pointer as
|
|
113
|
+
/// `*mut`, which has the same ABI.
|
|
114
|
+
unsafe extern "C" fn libxml2_error_callback(_ctx: *mut c_void, error: *const crate::sys::xmlError) {
|
|
115
|
+
if error.is_null() {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
// SAFETY: libxml2 passes a valid xmlError for the duration of the call.
|
|
119
|
+
let err = unsafe { &*error };
|
|
120
|
+
if (err.level as i64) < (crate::sys::xmlErrorLevel_XML_ERR_ERROR as i64) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
let message = if err.message.is_null() {
|
|
124
|
+
""
|
|
125
|
+
} else {
|
|
126
|
+
// SAFETY: a non-null message is a NUL-terminated C string.
|
|
127
|
+
unsafe { CStr::from_ptr(err.message) }.to_str().unwrap_or("")
|
|
128
|
+
};
|
|
129
|
+
let entry = format!("{}:{}: ERROR: {}", err.line, err.int2, message.trim_end());
|
|
130
|
+
|
|
131
|
+
LIBXML2_ERRORS.with(|count| count.set(count.get() + 1));
|
|
132
|
+
ERROR_BUF.with(|buf| buf.borrow_mut().push(entry));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// The handler above, typed as libxml2's `xmlStructuredErrorFunc`, whose
|
|
136
|
+
/// pointer argument is `*mut` before libxml2 2.12 and `*const` after.
|
|
137
|
+
#[allow(
|
|
138
|
+
clippy::useless_transmute,
|
|
139
|
+
reason = "a no-op on libxml2 2.12+, where the types already match; needed on 2.9"
|
|
140
|
+
)]
|
|
141
|
+
pub fn libxml2_error_handler() -> crate::sys::xmlStructuredErrorFunc {
|
|
142
|
+
let handler: unsafe extern "C" fn(*mut c_void, *const crate::sys::xmlError) = libxml2_error_callback;
|
|
143
|
+
// SAFETY: both spellings of the callback type have the same ABI; only
|
|
144
|
+
// the constness of the xmlError pointer differs between libxml2 versions.
|
|
145
|
+
unsafe { std::mem::transmute(Some(handler)) }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// Error-level libxml2 diagnostics seen on this thread so far.
|
|
149
|
+
pub fn libxml2_error_count() -> usize {
|
|
150
|
+
LIBXML2_ERRORS.with(Cell::get)
|
|
151
|
+
}
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
//! Magnus glue: 5 functions on `HankoXml::Native`, matching the §7 contract.
|
|
2
|
+
//!
|
|
3
|
+
//! Every argument from Ruby is copied into a Rust-owned `Vec<u8>` or `String`
|
|
4
|
+
//! **before** any native call, so there is no `unsafe` in this file — no
|
|
5
|
+
//! `RString::as_slice()` across a call that might release the GVL.
|
|
6
|
+
|
|
7
|
+
mod dsig;
|
|
8
|
+
mod errors;
|
|
9
|
+
mod sys;
|
|
10
|
+
|
|
11
|
+
use magnus::{
|
|
12
|
+
function, prelude::*, Error, ExceptionClass, IntoValue, RArray, RHash, RModule, RString,
|
|
13
|
+
Ruby, TryConvert, Value,
|
|
14
|
+
};
|
|
15
|
+
use std::collections::HashMap;
|
|
16
|
+
use std::sync::Once;
|
|
17
|
+
|
|
18
|
+
static INIT: Once = Once::new();
|
|
19
|
+
|
|
20
|
+
/// Runs [`dsig::init_xmlsec`] exactly once per process.
|
|
21
|
+
fn ensure_initialized() -> Result<(), &'static str> {
|
|
22
|
+
let mut result = Ok(());
|
|
23
|
+
INIT.call_once(|| result = dsig::init_xmlsec());
|
|
24
|
+
result
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Error construction helpers
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
fn error_class(ruby: &Ruby, class_name: &str) -> ExceptionClass {
|
|
32
|
+
ruby.eval::<ExceptionClass>(class_name)
|
|
33
|
+
.unwrap_or_else(|_| ruby.exception_runtime_error())
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fn native_error(ruby: &Ruby, message: String) -> Error {
|
|
37
|
+
Error::new(error_class(ruby, "HankoXml::NativeError"), message)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Converts a [`dsig::DsigError`] into the matching `HankoXml::Error`
|
|
41
|
+
/// subclass (S-13). The class comes from `err.kind`, and the message is the
|
|
42
|
+
/// fixed string from `dsig.rs`. The xmlsec log captured during the call is
|
|
43
|
+
/// drained here and set as `#xmlsec_errors`, never appended to `#message`:
|
|
44
|
+
/// it can contain document text (hrefs, URIs, node names) that applications
|
|
45
|
+
/// commonly log or display.
|
|
46
|
+
fn dsig_error(ruby: &Ruby, err: dsig::DsigError) -> Error {
|
|
47
|
+
let class_name = match err.kind {
|
|
48
|
+
dsig::ErrorKind::Parse => "HankoXml::ParseError",
|
|
49
|
+
dsig::ErrorKind::KeyLoad => "HankoXml::KeyLoadError",
|
|
50
|
+
dsig::ErrorKind::Signing => "HankoXml::SigningError",
|
|
51
|
+
dsig::ErrorKind::Native => "HankoXml::NativeError",
|
|
52
|
+
};
|
|
53
|
+
let cls = error_class(ruby, class_name);
|
|
54
|
+
let xmlsec_errors = errors::drain_errors();
|
|
55
|
+
|
|
56
|
+
let exception = match cls.new_instance((err.message,)) {
|
|
57
|
+
Ok(exception) => exception,
|
|
58
|
+
Err(e) => return e,
|
|
59
|
+
};
|
|
60
|
+
if let Ok(log) = string_slice_to_rarray(ruby, &xmlsec_errors) {
|
|
61
|
+
// Best effort: without the attribute the typed error is still right.
|
|
62
|
+
let _: Result<Value, Error> = exception.funcall("xmlsec_errors=", (log,));
|
|
63
|
+
}
|
|
64
|
+
Error::from(exception)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Argument extraction and Ruby array helpers
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/// Extracts an RString to an owned Vec<u8>.
|
|
72
|
+
fn rstring_to_bytes(s: RString) -> Vec<u8> {
|
|
73
|
+
// SAFETY: s is a live Ruby String argument on the call stack.
|
|
74
|
+
// We immediately copy to a Vec<u8> so the slice is not retained.
|
|
75
|
+
unsafe { s.as_slice() }.to_vec()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Converts a slice of Strings to a Ruby Array of RStrings.
|
|
79
|
+
fn string_slice_to_rarray(ruby: &Ruby, strings: &[String]) -> Result<RArray, Error> {
|
|
80
|
+
let arr = ruby.ary_new();
|
|
81
|
+
for s in strings {
|
|
82
|
+
arr.push(ruby.str_new(s))
|
|
83
|
+
.map_err(|e| native_error(ruby, format!("array push error: {e}")))?;
|
|
84
|
+
}
|
|
85
|
+
Ok(arr)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Extracts an RArray of RString into a Vec<Vec<u8>>.
|
|
89
|
+
fn rarray_to_byte_vecs(ruby: &Ruby, arr: RArray) -> Result<Vec<Vec<u8>>, Error> {
|
|
90
|
+
let mut result = Vec::new();
|
|
91
|
+
for item in arr.into_iter() {
|
|
92
|
+
let s: RString = TryConvert::try_convert(item)
|
|
93
|
+
.map_err(|_| native_error(ruby, "array element is not a String".into()))?;
|
|
94
|
+
result.push(rstring_to_bytes(s));
|
|
95
|
+
}
|
|
96
|
+
Ok(result)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// Extracts an RArray of `[String, String]` pairs into a map.
|
|
100
|
+
fn rarray_to_byte_pairs(ruby: &Ruby, arr: RArray) -> Result<HashMap<Vec<u8>, Vec<u8>>, Error> {
|
|
101
|
+
let mut result = HashMap::new();
|
|
102
|
+
for item in arr.into_iter() {
|
|
103
|
+
let pair: RArray = TryConvert::try_convert(item)
|
|
104
|
+
.map_err(|_| native_error(ruby, "referenced document is not a [uri, bytes] pair".into()))?;
|
|
105
|
+
let [uri, bytes] = rarray_to_byte_vecs(ruby, pair)?
|
|
106
|
+
.try_into()
|
|
107
|
+
.map_err(|_| native_error(ruby, "referenced document is not a [uri, bytes] pair".into()))?;
|
|
108
|
+
result.insert(uri, bytes);
|
|
109
|
+
}
|
|
110
|
+
Ok(result)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Extracts an RArray of RString into a Vec<String>.
|
|
114
|
+
fn rarray_to_strings(ruby: &Ruby, arr: RArray) -> Result<Vec<String>, Error> {
|
|
115
|
+
let mut result = Vec::new();
|
|
116
|
+
for item in arr.into_iter() {
|
|
117
|
+
let s: RString = TryConvert::try_convert(item)
|
|
118
|
+
.map_err(|_| native_error(ruby, "array element is not a String".into()))?;
|
|
119
|
+
result.push(
|
|
120
|
+
String::from_utf8(rstring_to_bytes(s))
|
|
121
|
+
.map_err(|_| native_error(ruby, "id_attribute is not valid UTF-8".into()))?,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
Ok(result)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Native functions — the §7 contract
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
/// `HankoXml::Native.sign(xml, key_pem, cert_pems, id_attributes, signature_index)`
|
|
132
|
+
///
|
|
133
|
+
/// Signs the document's nth `<ds:Signature>` template and returns the
|
|
134
|
+
/// complete signed XML as a binary String.
|
|
135
|
+
fn native_sign(
|
|
136
|
+
ruby: &Ruby,
|
|
137
|
+
xml: RString,
|
|
138
|
+
key_pem: RString,
|
|
139
|
+
cert_pems: RArray,
|
|
140
|
+
id_attributes: RArray,
|
|
141
|
+
signature_index: usize,
|
|
142
|
+
) -> Result<RString, Error> {
|
|
143
|
+
ensure_initialized().map_err(|msg| native_error(ruby, msg.into()))?;
|
|
144
|
+
|
|
145
|
+
let xml_bytes = rstring_to_bytes(xml);
|
|
146
|
+
let key_bytes = rstring_to_bytes(key_pem);
|
|
147
|
+
let cert_bytes = rarray_to_byte_vecs(ruby, cert_pems)?;
|
|
148
|
+
let id_attrs = rarray_to_strings(ruby, id_attributes)?;
|
|
149
|
+
|
|
150
|
+
match dsig::sign_document(&xml_bytes, &key_bytes, &cert_bytes, &id_attrs, signature_index) {
|
|
151
|
+
Ok(signed_bytes) => Ok(ruby.str_from_slice(&signed_bytes)),
|
|
152
|
+
Err(err) => Err(dsig_error(ruby, err)),
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// `HankoXml::Native.verify(xml, trusted_pems, untrusted_pems, id_attributes, signature_index, referenced_documents)`
|
|
157
|
+
///
|
|
158
|
+
/// Verifies the nth `<ds:Signature>` and returns a Hash:
|
|
159
|
+
/// `{ status:, errors:, reference_element_indexes: Array<Integer|nil>,
|
|
160
|
+
/// reference_valid: Array<true|false|nil> }`
|
|
161
|
+
///
|
|
162
|
+
/// `reference_element_indexes` is the document-order element index of each
|
|
163
|
+
/// SignedInfo Reference's target as resolved by libxml2's ID table (the same
|
|
164
|
+
/// table xmlsec used for `#id` verification). Empty-URI references report the
|
|
165
|
+
/// root. Unresolved / non-fragment URIs are `nil`.
|
|
166
|
+
///
|
|
167
|
+
/// `reference_valid` is each SignedInfo Reference's own digest verification
|
|
168
|
+
/// status, same order, independent of the overall signature/key outcome
|
|
169
|
+
/// (S-09): a harmless-fragment Reference and the payload Reference can
|
|
170
|
+
/// genuinely disagree. `nil` means the reference was never processed
|
|
171
|
+
/// (verification aborted before reaching it).
|
|
172
|
+
///
|
|
173
|
+
/// Only raises for conditions that are not a verification verdict.
|
|
174
|
+
#[allow(
|
|
175
|
+
clippy::too_many_arguments,
|
|
176
|
+
reason = "one positional argument per Native.verify parameter (API_CONTRACT §7)"
|
|
177
|
+
)]
|
|
178
|
+
fn native_verify(
|
|
179
|
+
ruby: &Ruby,
|
|
180
|
+
xml: RString,
|
|
181
|
+
trusted_pems: RArray,
|
|
182
|
+
untrusted_pems: RArray,
|
|
183
|
+
id_attributes: RArray,
|
|
184
|
+
signature_index: usize,
|
|
185
|
+
referenced_documents: RArray,
|
|
186
|
+
allow_xpath_filter2: bool,
|
|
187
|
+
) -> Result<RHash, Error> {
|
|
188
|
+
ensure_initialized().map_err(|msg| native_error(ruby, msg.into()))?;
|
|
189
|
+
|
|
190
|
+
let xml_bytes = rstring_to_bytes(xml);
|
|
191
|
+
let trusted = rarray_to_byte_vecs(ruby, trusted_pems)?;
|
|
192
|
+
let untrusted = rarray_to_byte_vecs(ruby, untrusted_pems)?;
|
|
193
|
+
let id_attrs = rarray_to_strings(ruby, id_attributes)?;
|
|
194
|
+
let referenced = rarray_to_byte_pairs(ruby, referenced_documents)?;
|
|
195
|
+
|
|
196
|
+
match dsig::verify_document(
|
|
197
|
+
&xml_bytes,
|
|
198
|
+
&trusted,
|
|
199
|
+
&untrusted,
|
|
200
|
+
&id_attrs,
|
|
201
|
+
signature_index,
|
|
202
|
+
referenced,
|
|
203
|
+
allow_xpath_filter2,
|
|
204
|
+
) {
|
|
205
|
+
Ok(result) => {
|
|
206
|
+
let hash = ruby.hash_new();
|
|
207
|
+
let status_sym = ruby.to_symbol(result.status.as_str());
|
|
208
|
+
hash.aset(ruby.to_symbol("status"), status_sym)
|
|
209
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))?;
|
|
210
|
+
|
|
211
|
+
let errors_array = string_slice_to_rarray(ruby, &result.errors)?;
|
|
212
|
+
hash.aset(ruby.to_symbol("errors"), errors_array)
|
|
213
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))?;
|
|
214
|
+
|
|
215
|
+
let indexes = ruby.ary_new();
|
|
216
|
+
for entry in &result.reference_element_indexes {
|
|
217
|
+
let value = match entry {
|
|
218
|
+
Some(i) => ruby.integer_from_i64(*i as i64).as_value(),
|
|
219
|
+
None => ruby.qnil().as_value(),
|
|
220
|
+
};
|
|
221
|
+
indexes
|
|
222
|
+
.push(value)
|
|
223
|
+
.map_err(|e| native_error(ruby, format!("array error: {e}")))?;
|
|
224
|
+
}
|
|
225
|
+
hash.aset(ruby.to_symbol("reference_element_indexes"), indexes)
|
|
226
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))?;
|
|
227
|
+
|
|
228
|
+
let reference_valid = ruby.ary_new();
|
|
229
|
+
for entry in &result.reference_valid {
|
|
230
|
+
let value = match entry {
|
|
231
|
+
Some(valid) => valid.into_value_with(ruby),
|
|
232
|
+
None => ruby.qnil().as_value(),
|
|
233
|
+
};
|
|
234
|
+
reference_valid
|
|
235
|
+
.push(value)
|
|
236
|
+
.map_err(|e| native_error(ruby, format!("array error: {e}")))?;
|
|
237
|
+
}
|
|
238
|
+
hash.aset(ruby.to_symbol("reference_valid"), reference_valid)
|
|
239
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))?;
|
|
240
|
+
|
|
241
|
+
let trusted_index = match result.trusted_index {
|
|
242
|
+
Some(i) => ruby.integer_from_i64(i as i64).as_value(),
|
|
243
|
+
None => ruby.qnil().as_value(),
|
|
244
|
+
};
|
|
245
|
+
hash.aset(ruby.to_symbol("trusted_index"), trusted_index)
|
|
246
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))?;
|
|
247
|
+
|
|
248
|
+
Ok(hash)
|
|
249
|
+
}
|
|
250
|
+
Err(err) => Err(dsig_error(ruby, err)),
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/// `HankoXml::Native.scan(xml, id_attributes, strict)`
|
|
255
|
+
///
|
|
256
|
+
/// Returns the document's structure as a Hash, without cryptography:
|
|
257
|
+
/// `{ signature_count:, signature_values: Array<bool>,
|
|
258
|
+
/// signature_element_indexes: Array<Integer|nil>, duplicate_ids: bool,
|
|
259
|
+
/// root_ids: Array<String> }`. `strict` rejects any libxml2 error with a
|
|
260
|
+
/// `ParseError`, as a strict Nokogiri parse would (#102).
|
|
261
|
+
fn native_scan(ruby: &Ruby, xml: RString, id_attributes: RArray, strict: bool) -> Result<RHash, Error> {
|
|
262
|
+
ensure_initialized().map_err(|msg| native_error(ruby, msg.into()))?;
|
|
263
|
+
|
|
264
|
+
let xml_bytes = rstring_to_bytes(xml);
|
|
265
|
+
let id_attrs = rarray_to_strings(ruby, id_attributes)?;
|
|
266
|
+
|
|
267
|
+
let result = dsig::scan_document(&xml_bytes, &id_attrs, strict).map_err(|err| dsig_error(ruby, err))?;
|
|
268
|
+
let hash = ruby.hash_new();
|
|
269
|
+
let set = |key: &str, value: Value| {
|
|
270
|
+
hash.aset(ruby.to_symbol(key), value)
|
|
271
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))
|
|
272
|
+
};
|
|
273
|
+
set("signature_count", ruby.integer_from_i64(result.signature_count as i64).as_value())?;
|
|
274
|
+
let values = ruby.ary_new();
|
|
275
|
+
for present in result.signature_values {
|
|
276
|
+
values.push(present).map_err(|e| native_error(ruby, format!("array error: {e}")))?;
|
|
277
|
+
}
|
|
278
|
+
set("signature_values", values.as_value())?;
|
|
279
|
+
let indexes = ruby.ary_new();
|
|
280
|
+
for entry in result.signature_element_indexes {
|
|
281
|
+
let value = match entry {
|
|
282
|
+
Some(i) => ruby.integer_from_i64(i as i64).as_value(),
|
|
283
|
+
None => ruby.qnil().as_value(),
|
|
284
|
+
};
|
|
285
|
+
indexes.push(value).map_err(|e| native_error(ruby, format!("array error: {e}")))?;
|
|
286
|
+
}
|
|
287
|
+
set("signature_element_indexes", indexes.as_value())?;
|
|
288
|
+
set("duplicate_ids", result.duplicate_ids.into_value_with(ruby))?;
|
|
289
|
+
set("root_ids", string_slice_to_rarray(ruby, &result.root_ids)?.as_value())?;
|
|
290
|
+
Ok(hash)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/// `HankoXml::Native.validate_schema(xml, signature_index, xsd_path)`
|
|
294
|
+
///
|
|
295
|
+
/// Validates a copy of the nth `<ds:Signature>` against the XSD at
|
|
296
|
+
/// `xsd_path`. Returns libxml2's validation errors; empty means valid.
|
|
297
|
+
fn native_validate_schema(
|
|
298
|
+
ruby: &Ruby,
|
|
299
|
+
xml: RString,
|
|
300
|
+
signature_index: usize,
|
|
301
|
+
xsd_path: String,
|
|
302
|
+
) -> Result<RArray, Error> {
|
|
303
|
+
ensure_initialized().map_err(|msg| native_error(ruby, msg.into()))?;
|
|
304
|
+
|
|
305
|
+
let xml_bytes = rstring_to_bytes(xml);
|
|
306
|
+
let messages = dsig::validate_signature_schema(&xml_bytes, signature_index, &xsd_path)
|
|
307
|
+
.map_err(|err| dsig_error(ruby, err))?;
|
|
308
|
+
string_slice_to_rarray(ruby, &messages)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/// `HankoXml::Native.inspect_sig(xml, signature_index)`
|
|
312
|
+
///
|
|
313
|
+
/// Returns metadata about the nth `<ds:Signature>` without performing
|
|
314
|
+
/// any cryptography. Used by Policy enforcement before verify/sign.
|
|
315
|
+
fn native_inspect(
|
|
316
|
+
ruby: &Ruby,
|
|
317
|
+
xml: RString,
|
|
318
|
+
signature_index: usize,
|
|
319
|
+
) -> Result<RHash, Error> {
|
|
320
|
+
ensure_initialized().map_err(|msg| native_error(ruby, msg.into()))?;
|
|
321
|
+
|
|
322
|
+
let xml_bytes = rstring_to_bytes(xml);
|
|
323
|
+
|
|
324
|
+
errors::clear_errors();
|
|
325
|
+
match dsig::inspect_document(&xml_bytes, signature_index) {
|
|
326
|
+
Ok(result) => {
|
|
327
|
+
let hash = ruby.hash_new();
|
|
328
|
+
|
|
329
|
+
hash.aset(
|
|
330
|
+
ruby.to_symbol("signature_method"),
|
|
331
|
+
ruby.str_new(&result.signature_method),
|
|
332
|
+
)
|
|
333
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))?;
|
|
334
|
+
|
|
335
|
+
hash.aset(
|
|
336
|
+
ruby.to_symbol("canonicalization_method"),
|
|
337
|
+
ruby.str_new(&result.canonicalization_method),
|
|
338
|
+
)
|
|
339
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))?;
|
|
340
|
+
|
|
341
|
+
hash.aset(
|
|
342
|
+
ruby.to_symbol("signature_count"),
|
|
343
|
+
result.signature_count,
|
|
344
|
+
)
|
|
345
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))?;
|
|
346
|
+
|
|
347
|
+
// key_names array
|
|
348
|
+
let key_names = string_slice_to_rarray(ruby, &result.key_names)?;
|
|
349
|
+
hash.aset(ruby.to_symbol("key_names"), key_names)
|
|
350
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))?;
|
|
351
|
+
|
|
352
|
+
// certificates array
|
|
353
|
+
let certs = string_slice_to_rarray(ruby, &result.certificates)?;
|
|
354
|
+
hash.aset(ruby.to_symbol("certificates"), certs)
|
|
355
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))?;
|
|
356
|
+
|
|
357
|
+
// references array of hashes
|
|
358
|
+
let refs = ruby.ary_new();
|
|
359
|
+
for r in &result.references {
|
|
360
|
+
let ref_hash = ruby.hash_new();
|
|
361
|
+
ref_hash
|
|
362
|
+
.aset(ruby.to_symbol("uri"), ruby.str_new(&r.uri))
|
|
363
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))?;
|
|
364
|
+
ref_hash
|
|
365
|
+
.aset(ruby.to_symbol("digest_method"), ruby.str_new(&r.digest_method))
|
|
366
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))?;
|
|
367
|
+
|
|
368
|
+
let transforms = string_slice_to_rarray(ruby, &r.transforms)?;
|
|
369
|
+
ref_hash
|
|
370
|
+
.aset(ruby.to_symbol("transforms"), transforms)
|
|
371
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))?;
|
|
372
|
+
|
|
373
|
+
ref_hash
|
|
374
|
+
.aset(
|
|
375
|
+
ruby.to_symbol("id"),
|
|
376
|
+
r.id.as_ref()
|
|
377
|
+
.map(|s| ruby.str_new(s).as_value())
|
|
378
|
+
.unwrap_or_else(|| ruby.qnil().as_value()),
|
|
379
|
+
)
|
|
380
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))?;
|
|
381
|
+
|
|
382
|
+
ref_hash
|
|
383
|
+
.aset(
|
|
384
|
+
ruby.to_symbol("type"),
|
|
385
|
+
r.r#type
|
|
386
|
+
.as_ref()
|
|
387
|
+
.map(|s| ruby.str_new(s).as_value())
|
|
388
|
+
.unwrap_or_else(|| ruby.qnil().as_value()),
|
|
389
|
+
)
|
|
390
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))?;
|
|
391
|
+
|
|
392
|
+
let filters = ruby.ary_new();
|
|
393
|
+
for f in &r.xpath_filters {
|
|
394
|
+
let filter = ruby.hash_new();
|
|
395
|
+
let namespaces = ruby.hash_new();
|
|
396
|
+
for (prefix, href) in &f.namespaces {
|
|
397
|
+
namespaces
|
|
398
|
+
.aset(ruby.str_new(prefix), ruby.str_new(href))
|
|
399
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))?;
|
|
400
|
+
}
|
|
401
|
+
for (key, value) in [
|
|
402
|
+
("filter", ruby.str_new(&f.filter).as_value()),
|
|
403
|
+
("expression", ruby.str_new(&f.expression).as_value()),
|
|
404
|
+
("namespaces", namespaces.as_value()),
|
|
405
|
+
] {
|
|
406
|
+
filter
|
|
407
|
+
.aset(ruby.to_symbol(key), value)
|
|
408
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))?;
|
|
409
|
+
}
|
|
410
|
+
filters.push(filter).map_err(|e| native_error(ruby, format!("array error: {e}")))?;
|
|
411
|
+
}
|
|
412
|
+
ref_hash
|
|
413
|
+
.aset(ruby.to_symbol("xpath_filters"), filters)
|
|
414
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))?;
|
|
415
|
+
|
|
416
|
+
refs.push(ref_hash)
|
|
417
|
+
.map_err(|e| native_error(ruby, format!("array error: {e}")))?;
|
|
418
|
+
}
|
|
419
|
+
hash.aset(ruby.to_symbol("references"), refs)
|
|
420
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))?;
|
|
421
|
+
|
|
422
|
+
hash.aset(
|
|
423
|
+
ruby.to_symbol("has_retrieval_method"),
|
|
424
|
+
result.has_retrieval_method.into_value_with(ruby),
|
|
425
|
+
)
|
|
426
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))?;
|
|
427
|
+
for (key, value) in [
|
|
428
|
+
("empty_key_names", result.empty_key_names),
|
|
429
|
+
("key_derived_values", result.key_derived_values),
|
|
430
|
+
] {
|
|
431
|
+
hash.aset(ruby.to_symbol(key), value.into_value_with(ruby))
|
|
432
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))?;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
Ok(hash)
|
|
436
|
+
}
|
|
437
|
+
Err(err) => Err(dsig_error(ruby, err)),
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/// `HankoXml::Native.versions`
|
|
442
|
+
///
|
|
443
|
+
/// Returns `{ xmlsec: "1.3.12", libxml2: "2.15.3", crypto: "openssl" }`.
|
|
444
|
+
fn native_versions(ruby: &Ruby) -> Result<RHash, Error> {
|
|
445
|
+
ensure_initialized().map_err(|msg| native_error(ruby, msg.into()))?;
|
|
446
|
+
|
|
447
|
+
let (xmlsec, libxml2, crypto) = dsig::versions();
|
|
448
|
+
let hash = ruby.hash_new();
|
|
449
|
+
hash.aset(ruby.to_symbol("xmlsec"), ruby.str_new(&xmlsec))
|
|
450
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))?;
|
|
451
|
+
hash.aset(ruby.to_symbol("libxml2"), ruby.str_new(&libxml2))
|
|
452
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))?;
|
|
453
|
+
hash.aset(ruby.to_symbol("crypto"), ruby.str_new(&crypto))
|
|
454
|
+
.map_err(|e| native_error(ruby, format!("hash error: {e}")))?;
|
|
455
|
+
Ok(hash)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/// `HankoXml::Native.check_version` — kept from the POC.
|
|
459
|
+
///
|
|
460
|
+
/// Exact-match check: true only when the loaded libxmlsec1 is the same
|
|
461
|
+
/// major.minor.patch these bindings were generated against.
|
|
462
|
+
fn native_check_version() -> bool {
|
|
463
|
+
dsig::check_version()
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/// `HankoXml::Native.abi_compatible?`
|
|
467
|
+
///
|
|
468
|
+
/// True when the loaded libxmlsec1 is ABI compatible with the headers this
|
|
469
|
+
/// extension was compiled against. `HankoXml::NativeVersion` calls this at
|
|
470
|
+
/// require time; see [`dsig::abi_compatible`] for why it has to be fatal.
|
|
471
|
+
fn native_abi_compatible() -> bool {
|
|
472
|
+
dsig::abi_compatible()
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
#[magnus::init]
|
|
476
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
477
|
+
let module = ruby.define_module("HankoXml")?;
|
|
478
|
+
let native: RModule = module.define_module("Native")?;
|
|
479
|
+
|
|
480
|
+
native.define_singleton_method("check_version", function!(native_check_version, 0))?;
|
|
481
|
+
native.define_singleton_method("abi_compatible?", function!(native_abi_compatible, 0))?;
|
|
482
|
+
native.define_singleton_method("versions", function!(native_versions, 0))?;
|
|
483
|
+
native.define_singleton_method("sign", function!(native_sign, 5))?;
|
|
484
|
+
native.define_singleton_method("verify", function!(native_verify, 7))?;
|
|
485
|
+
native.define_singleton_method("inspect_sig", function!(native_inspect, 2))?;
|
|
486
|
+
native.define_singleton_method("scan", function!(native_scan, 3))?;
|
|
487
|
+
native.define_singleton_method("validate_schema", function!(native_validate_schema, 3))?;
|
|
488
|
+
|
|
489
|
+
Ok(())
|
|
490
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//! Raw FFI bindings generated by bindgen from the xmlsec1/libxml2 headers,
|
|
2
|
+
//! plus thin aliases for the enum constants whose generated names are
|
|
3
|
+
//! unreadable (§7.2).
|
|
4
|
+
//!
|
|
5
|
+
//! Nothing in here is safe to call directly — every call site must uphold
|
|
6
|
+
//! libxmlsec1's own invariants (init-before-use, non-null docs/nodes,
|
|
7
|
+
//! matching create/destroy pairs, etc). [`crate::dsig`] is the only module
|
|
8
|
+
//! that calls into this one; everything *it* exposes is safe.
|
|
9
|
+
|
|
10
|
+
#![allow(
|
|
11
|
+
non_upper_case_globals,
|
|
12
|
+
non_camel_case_types,
|
|
13
|
+
non_snake_case,
|
|
14
|
+
dead_code,
|
|
15
|
+
clippy::upper_case_acronyms
|
|
16
|
+
)]
|
|
17
|
+
|
|
18
|
+
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
|
|
19
|
+
|
|
20
|
+
// --- Readable aliases for bindgen's unreadable enum constant names ------
|
|
21
|
+
|
|
22
|
+
/// PEM key data format (`xmlSecKeyDataFormatPem`).
|
|
23
|
+
pub const KEY_FORMAT_PEM: xmlSecKeyDataFormat =
|
|
24
|
+
xmlSecKeyDataFormat_xmlSecKeyDataFormatPem;
|
|
25
|
+
|
|
26
|
+
/// PEM certificate format (`xmlSecKeyDataFormatCertPem`).
|
|
27
|
+
pub const KEY_FORMAT_CERT_PEM: xmlSecKeyDataFormat =
|
|
28
|
+
xmlSecKeyDataFormat_xmlSecKeyDataFormatCertPem;
|
|
29
|
+
|
|
30
|
+
/// Trusted key data type (`xmlSecKeyDataTypeTrusted`).
|
|
31
|
+
pub const KEY_DATA_TYPE_TRUSTED: xmlSecKeyDataType = xmlSecKeyDataTypeTrusted;
|
|
32
|
+
|
|
33
|
+
/// DSig verification succeeded.
|
|
34
|
+
pub const DSIG_STATUS_SUCCEEDED: xmlSecDSigStatus =
|
|
35
|
+
xmlSecDSigStatus_xmlSecDSigStatusSucceeded;
|
|
36
|
+
|
|
37
|
+
/// DSig verification found an invalid signature.
|
|
38
|
+
pub const DSIG_STATUS_INVALID: xmlSecDSigStatus =
|
|
39
|
+
xmlSecDSigStatus_xmlSecDSigStatusInvalid;
|
|
40
|
+
|
|
41
|
+
/// Same-document Reference URIs: empty `URI=""` and `URI="#id"`.
|
|
42
|
+
pub const URI_TYPE_EMPTY: xmlSecTransformUriType = xmlSecTransformUriTypeEmpty;
|
|
43
|
+
pub const URI_TYPE_SAME_DOCUMENT: xmlSecTransformUriType = xmlSecTransformUriTypeSameDocument;
|