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,1851 @@
|
|
|
1
|
+
//! Safe wrappers around the raw [`crate::sys`] bindings, implementing
|
|
2
|
+
//! the §7 native contract: `sign_document`, `verify_document`,
|
|
3
|
+
//! `inspect_document`, `versions`, and `check_version`.
|
|
4
|
+
//!
|
|
5
|
+
//! Every FFI call in this module lives in its own minimal `unsafe` block
|
|
6
|
+
//! with a `// SAFETY:` comment justifying it. The public functions
|
|
7
|
+
//! (`sign_document`, `verify_document`, `inspect_document`) contain no
|
|
8
|
+
//! `unsafe` — the RAII types below each own exactly one C resource and
|
|
9
|
+
//! free it on `Drop`.
|
|
10
|
+
//!
|
|
11
|
+
//! Phase 2 changes from the POC:
|
|
12
|
+
//! - `XmlDoc<'a>` and `XmlNode<'a>` are deleted — bytes-only I/O means
|
|
13
|
+
//! every document is `OwnedXmlDoc`, never borrowed.
|
|
14
|
+
//! - `XmlSecKey` added (with `into_raw()`) for signing.
|
|
15
|
+
//! - `errors` module captures xmlsec diagnostics (no more stderr).
|
|
16
|
+
//! - All functions take `id_attributes` + `signature_index`.
|
|
17
|
+
|
|
18
|
+
use crate::errors;
|
|
19
|
+
use crate::sys;
|
|
20
|
+
use std::cell::RefCell;
|
|
21
|
+
use std::collections::HashMap;
|
|
22
|
+
use std::ffi::CStr;
|
|
23
|
+
use std::os::raw::{c_char, c_int, c_void};
|
|
24
|
+
use std::ptr;
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// OwnedXmlDoc — owned libxml2 document
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/// An owned libxml2 document parsed from an in-memory buffer. Frees itself
|
|
31
|
+
/// on drop.
|
|
32
|
+
pub struct OwnedXmlDoc(sys::xmlDocPtr);
|
|
33
|
+
|
|
34
|
+
/// Safely frees memory allocated by libxml2 functions (xmlGetProp, xmlNodeGetContent, xmlDocDumpMemory).
|
|
35
|
+
unsafe fn xml_free(ptr: *mut std::os::raw::c_void) {
|
|
36
|
+
if !ptr.is_null() {
|
|
37
|
+
extern "C" {
|
|
38
|
+
static xmlFree: Option<unsafe extern "C" fn(*mut std::os::raw::c_void)>;
|
|
39
|
+
}
|
|
40
|
+
if let Some(free_fn) = xmlFree {
|
|
41
|
+
free_fn(ptr);
|
|
42
|
+
} else {
|
|
43
|
+
extern "C" {
|
|
44
|
+
fn free(ptr: *mut std::os::raw::c_void);
|
|
45
|
+
}
|
|
46
|
+
free(ptr);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Which Ruby exception a native failure maps to (S-13).
|
|
52
|
+
///
|
|
53
|
+
/// Decided at the call site that failed, never by looking at the message:
|
|
54
|
+
/// the xmlsec log can contain document text (algorithm hrefs, URIs, node
|
|
55
|
+
/// names), so matching words in it let a document pick its own exception
|
|
56
|
+
/// class.
|
|
57
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
58
|
+
pub enum ErrorKind {
|
|
59
|
+
Parse,
|
|
60
|
+
KeyLoad,
|
|
61
|
+
Signing,
|
|
62
|
+
Native,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// A failure from one of the public entry points below. `message` is always
|
|
66
|
+
/// a fixed string from this file, so it never carries document content; the
|
|
67
|
+
/// xmlsec log for the call is drained separately by the caller.
|
|
68
|
+
#[derive(Debug)]
|
|
69
|
+
pub struct DsigError {
|
|
70
|
+
pub kind: ErrorKind,
|
|
71
|
+
pub message: &'static str,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
impl DsigError {
|
|
75
|
+
fn parse(message: &'static str) -> Self {
|
|
76
|
+
Self { kind: ErrorKind::Parse, message }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
fn key_load(message: &'static str) -> Self {
|
|
80
|
+
Self { kind: ErrorKind::KeyLoad, message }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fn signing(message: &'static str) -> Self {
|
|
84
|
+
Self { kind: ErrorKind::Signing, message }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn native(message: &'static str) -> Self {
|
|
88
|
+
Self { kind: ErrorKind::Native, message }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Converts a buffer length to the integer type an FFI function takes (S-12).
|
|
93
|
+
///
|
|
94
|
+
/// A plain `as` cast wraps silently: past `i32::MAX` an `int` length goes
|
|
95
|
+
/// negative, and past `u32::MAX` an unsigned one keeps only the low 32 bits,
|
|
96
|
+
/// so libxml2/xmlsec would read a different number of bytes than the caller
|
|
97
|
+
/// passed. The width of `xmlSecSize` also differs between xmlsec 1.2 and 1.3,
|
|
98
|
+
/// hence the generic target.
|
|
99
|
+
fn ffi_len<T: TryFrom<usize>>(len: usize) -> Option<T> {
|
|
100
|
+
T::try_from(len).ok()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
impl OwnedXmlDoc {
|
|
104
|
+
/// Parses `xml` bytes into a libxml2 document.
|
|
105
|
+
///
|
|
106
|
+
/// Documents longer than `c_int::MAX` bytes (about 2 GiB) are rejected
|
|
107
|
+
/// rather than truncated (S-12).
|
|
108
|
+
///
|
|
109
|
+
/// libxml2's diagnostics go to the error buffer, never stderr. A
|
|
110
|
+
/// recoverable error (an undefined namespace prefix, an invalid
|
|
111
|
+
/// `xml:id`) still yields a document; see [`Self::parse_strict`].
|
|
112
|
+
pub fn parse(xml: &[u8]) -> Result<Self, &'static str> {
|
|
113
|
+
let len: std::os::raw::c_int = ffi_len(xml.len())
|
|
114
|
+
.ok_or("failed to parse XML document: larger than the 2 GiB libxml2 limit")?;
|
|
115
|
+
let _capture = Libxml2Capture::start();
|
|
116
|
+
// Never NOENT, DTDLOAD or HUGE; NONET so nothing is fetched (§6.3).
|
|
117
|
+
let options = sys::xmlParserOption_XML_PARSE_NONET as std::os::raw::c_int;
|
|
118
|
+
// SAFETY: xmlReadMemory reads exactly len (== xml.len()) bytes
|
|
119
|
+
// starting at xml.as_ptr() and copies them into buffers it manages
|
|
120
|
+
// itself, so xml doesn't need to outlive this call.
|
|
121
|
+
let ptr = unsafe {
|
|
122
|
+
sys::xmlReadMemory(xml.as_ptr().cast(), len, ptr::null(), ptr::null(), options)
|
|
123
|
+
};
|
|
124
|
+
if ptr.is_null() {
|
|
125
|
+
return Err("failed to parse XML document");
|
|
126
|
+
}
|
|
127
|
+
Ok(Self(ptr))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Like [`Self::parse`], but any error-level libxml2 diagnostic fails
|
|
131
|
+
/// the parse: the rule `HankoXml.parse` applies with Nokogiri (strict,
|
|
132
|
+
/// `doc.errors` empty), so input given as bytes is held to the same
|
|
133
|
+
/// standard without a DOM (#102).
|
|
134
|
+
pub fn parse_strict(xml: &[u8]) -> Result<Self, &'static str> {
|
|
135
|
+
let before = errors::libxml2_error_count();
|
|
136
|
+
let doc = Self::parse(xml)?;
|
|
137
|
+
if errors::libxml2_error_count() != before {
|
|
138
|
+
return Err("the document is not well-formed XML");
|
|
139
|
+
}
|
|
140
|
+
Ok(doc)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Returns the root element, or None if the document is empty.
|
|
144
|
+
pub fn root_element(&self) -> Option<sys::xmlNodePtr> {
|
|
145
|
+
// SAFETY: self.0 is valid (parse only wraps non-null).
|
|
146
|
+
let root = unsafe { sys::xmlDocGetRootElement(self.0) };
|
|
147
|
+
(!root.is_null()).then_some(root)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// Registers ID-attribute names so `URI="#foo"` can resolve.
|
|
151
|
+
///
|
|
152
|
+
/// `id_attrs` is a list of unqualified attribute name strings
|
|
153
|
+
/// (e.g. `["Id", "ID", "id"]`).
|
|
154
|
+
pub fn register_id_attributes(&self, root: sys::xmlNodePtr, id_attrs: &[String]) {
|
|
155
|
+
if id_attrs.is_empty() {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// Build a NUL-terminated array of NUL-terminated C strings.
|
|
159
|
+
let c_strings: Vec<Vec<u8>> = id_attrs
|
|
160
|
+
.iter()
|
|
161
|
+
.map(|s| {
|
|
162
|
+
let mut v = s.as_bytes().to_vec();
|
|
163
|
+
v.push(0);
|
|
164
|
+
v
|
|
165
|
+
})
|
|
166
|
+
.collect();
|
|
167
|
+
let mut ptrs: Vec<*const sys::xmlChar> = c_strings
|
|
168
|
+
.iter()
|
|
169
|
+
.map(|v| v.as_ptr().cast())
|
|
170
|
+
.collect();
|
|
171
|
+
ptrs.push(ptr::null()); // NUL-terminated sentinel
|
|
172
|
+
|
|
173
|
+
// SAFETY: self.0 and root are valid; ptrs is a NUL-terminated
|
|
174
|
+
// array of NUL-terminated byte strings.
|
|
175
|
+
unsafe { sys::xmlSecAddIDs(self.0, root, ptrs.as_mut_ptr()) }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/// Finds the nth `<ds:Signature>` element in document order (0-based).
|
|
179
|
+
/// Returns the node pointer, or None if there are fewer than
|
|
180
|
+
/// `index + 1` signatures.
|
|
181
|
+
pub fn find_nth_signature(&self, root: sys::xmlNodePtr, index: usize) -> Option<sys::xmlNodePtr> {
|
|
182
|
+
let mut signatures = Vec::new();
|
|
183
|
+
collect_signature_nodes(root, &mut signatures);
|
|
184
|
+
signatures.get(index).copied()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/// Counts the total number of `<ds:Signature>` elements.
|
|
188
|
+
pub fn count_signatures(&self, root: sys::xmlNodePtr) -> usize {
|
|
189
|
+
let mut signatures = Vec::new();
|
|
190
|
+
collect_signature_nodes(root, &mut signatures);
|
|
191
|
+
signatures.len()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/// Serializes the document to an XML byte string.
|
|
195
|
+
pub fn serialize(&self) -> Result<Vec<u8>, &'static str> {
|
|
196
|
+
let mut buf: *mut sys::xmlChar = ptr::null_mut();
|
|
197
|
+
let mut size: i32 = 0;
|
|
198
|
+
|
|
199
|
+
// SAFETY: self.0 is valid; xmlDocDumpMemory writes into its own
|
|
200
|
+
// allocated buffer and returns the pointer + length.
|
|
201
|
+
unsafe { sys::xmlDocDumpMemory(self.0, &mut buf, &mut size) };
|
|
202
|
+
|
|
203
|
+
if buf.is_null() || size <= 0 {
|
|
204
|
+
return Err("failed to serialize XML document");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// SAFETY: buf is non-null, allocated by libxml2, and size is its
|
|
208
|
+
// length. We copy the bytes out, then free the libxml2 buffer.
|
|
209
|
+
let bytes = unsafe { std::slice::from_raw_parts(buf as *const u8, size as usize) }.to_vec();
|
|
210
|
+
unsafe { xml_free(buf as *mut _) };
|
|
211
|
+
|
|
212
|
+
Ok(bytes)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
impl Drop for OwnedXmlDoc {
|
|
217
|
+
fn drop(&mut self) {
|
|
218
|
+
// SAFETY: we're the exclusive owner of this doc.
|
|
219
|
+
unsafe { sys::xmlFreeDoc(self.0) }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Libxml2Capture — libxml2 diagnostics into the error buffer
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
/// Routes libxml2's structured errors on this thread to
|
|
228
|
+
/// [`errors::libxml2_error_handler`] until dropped, then removes the handler.
|
|
229
|
+
/// Nokogiri installs its own handler around each of its calls the same way,
|
|
230
|
+
/// so the two never see each other's errors even on one libxml2 (§3.1).
|
|
231
|
+
struct Libxml2Capture;
|
|
232
|
+
|
|
233
|
+
impl Libxml2Capture {
|
|
234
|
+
fn start() -> Self {
|
|
235
|
+
// SAFETY: sets a thread-local libxml2 global to a valid handler
|
|
236
|
+
// with a null context; the handler never dereferences the context.
|
|
237
|
+
unsafe { sys::xmlSetStructuredErrorFunc(ptr::null_mut(), errors::libxml2_error_handler()) };
|
|
238
|
+
Self
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
impl Drop for Libxml2Capture {
|
|
243
|
+
fn drop(&mut self) {
|
|
244
|
+
// SAFETY: restores libxml2's default (no structured handler).
|
|
245
|
+
unsafe { sys::xmlSetStructuredErrorFunc(ptr::null_mut(), None) };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Signature node collection
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
/// Recursively collects all `<ds:Signature>` nodes in document order.
|
|
254
|
+
fn collect_signature_nodes(node: sys::xmlNodePtr, result: &mut Vec<sys::xmlNodePtr>) {
|
|
255
|
+
if node.is_null() {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// SAFETY: node is non-null and valid within the document's lifetime.
|
|
260
|
+
let node_ref = unsafe { &*node };
|
|
261
|
+
|
|
262
|
+
// Check if this is a <Signature> in the dsig namespace
|
|
263
|
+
if node_ref.type_ == sys::xmlElementType_XML_ELEMENT_NODE {
|
|
264
|
+
let name = unsafe { node_name(node) };
|
|
265
|
+
let ns_href = unsafe { node_ns_href(node) };
|
|
266
|
+
|
|
267
|
+
if name == "Signature" && ns_href == "http://www.w3.org/2000/09/xmldsig#" {
|
|
268
|
+
result.push(node);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Recurse into children
|
|
273
|
+
let mut child = node_ref.children;
|
|
274
|
+
while !child.is_null() {
|
|
275
|
+
collect_signature_nodes(child, result);
|
|
276
|
+
child = unsafe { (*child).next };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/// Returns the local name of a node as a Rust string.
|
|
281
|
+
///
|
|
282
|
+
/// # Safety
|
|
283
|
+
/// `node` must be non-null and point to a valid xmlNode.
|
|
284
|
+
unsafe fn node_name(node: sys::xmlNodePtr) -> String {
|
|
285
|
+
let name_ptr = (*node).name;
|
|
286
|
+
if name_ptr.is_null() {
|
|
287
|
+
return String::new();
|
|
288
|
+
}
|
|
289
|
+
let cstr = std::ffi::CStr::from_ptr(name_ptr as *const _);
|
|
290
|
+
cstr.to_str().unwrap_or("").to_string()
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/// Returns the namespace href of a node, or "" if none.
|
|
294
|
+
///
|
|
295
|
+
/// # Safety
|
|
296
|
+
/// `node` must be non-null and point to a valid xmlNode.
|
|
297
|
+
unsafe fn node_ns_href(node: sys::xmlNodePtr) -> String {
|
|
298
|
+
let ns = (*node).ns;
|
|
299
|
+
if ns.is_null() {
|
|
300
|
+
return String::new();
|
|
301
|
+
}
|
|
302
|
+
let href = (*ns).href;
|
|
303
|
+
if href.is_null() {
|
|
304
|
+
return String::new();
|
|
305
|
+
}
|
|
306
|
+
let cstr = std::ffi::CStr::from_ptr(href as *const _);
|
|
307
|
+
cstr.to_str().unwrap_or("").to_string()
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/// Returns the text content of a node attribute, or None.
|
|
311
|
+
///
|
|
312
|
+
/// # Safety
|
|
313
|
+
/// `node` must be non-null and valid. `attr_name` is a NUL-terminated C string.
|
|
314
|
+
unsafe fn get_attribute(node: sys::xmlNodePtr, attr_name: &[u8]) -> Option<String> {
|
|
315
|
+
let val = sys::xmlGetProp(node, attr_name.as_ptr().cast());
|
|
316
|
+
if val.is_null() {
|
|
317
|
+
return None;
|
|
318
|
+
}
|
|
319
|
+
let cstr = std::ffi::CStr::from_ptr(val as *const _);
|
|
320
|
+
let result = cstr.to_str().unwrap_or("").to_string();
|
|
321
|
+
unsafe { xml_free(val as *mut _) };
|
|
322
|
+
Some(result)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/// The namespace bindings in scope on an element, as (prefix, href) pairs;
|
|
326
|
+
/// the default namespace has an empty prefix.
|
|
327
|
+
///
|
|
328
|
+
/// # Safety
|
|
329
|
+
/// `node` must be a non-null, valid element.
|
|
330
|
+
unsafe fn in_scope_namespaces(node: sys::xmlNodePtr) -> Vec<(String, String)> {
|
|
331
|
+
let list = sys::xmlGetNsList((*node).doc, node);
|
|
332
|
+
if list.is_null() {
|
|
333
|
+
return Vec::new();
|
|
334
|
+
}
|
|
335
|
+
let mut out = Vec::new();
|
|
336
|
+
let mut entry = list;
|
|
337
|
+
// The list is NULL-terminated and owned by us; its xmlNs entries belong
|
|
338
|
+
// to the document.
|
|
339
|
+
while !(*entry).is_null() {
|
|
340
|
+
let ns = &**entry;
|
|
341
|
+
let text = |ptr: *const sys::xmlChar| {
|
|
342
|
+
if ptr.is_null() {
|
|
343
|
+
String::new()
|
|
344
|
+
} else {
|
|
345
|
+
std::ffi::CStr::from_ptr(ptr.cast()).to_str().unwrap_or("").to_string()
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
out.push((text(ns.prefix), text(ns.href)));
|
|
349
|
+
entry = entry.add(1);
|
|
350
|
+
}
|
|
351
|
+
unsafe { xml_free(list.cast()) };
|
|
352
|
+
out
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/// Returns the text content of a node (all child text concatenated).
|
|
356
|
+
///
|
|
357
|
+
/// # Safety
|
|
358
|
+
/// `node` must be non-null and valid.
|
|
359
|
+
unsafe fn get_text_content(node: sys::xmlNodePtr) -> String {
|
|
360
|
+
let content = sys::xmlNodeGetContent(node);
|
|
361
|
+
if content.is_null() {
|
|
362
|
+
return String::new();
|
|
363
|
+
}
|
|
364
|
+
let cstr = std::ffi::CStr::from_ptr(content as *const _);
|
|
365
|
+
let result = cstr.to_str().unwrap_or("").to_string();
|
|
366
|
+
unsafe { xml_free(content as *mut _) };
|
|
367
|
+
result
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/// Finds a direct child element with the given local name and namespace href.
|
|
371
|
+
///
|
|
372
|
+
/// # Safety
|
|
373
|
+
/// `parent` must be non-null and valid.
|
|
374
|
+
unsafe fn find_child(parent: sys::xmlNodePtr, name: &str, ns_href: &str) -> Option<sys::xmlNodePtr> {
|
|
375
|
+
let mut child = (*parent).children;
|
|
376
|
+
while !child.is_null() {
|
|
377
|
+
let child_ref = &*child;
|
|
378
|
+
if child_ref.type_ == sys::xmlElementType_XML_ELEMENT_NODE
|
|
379
|
+
&& node_name(child) == name
|
|
380
|
+
&& node_ns_href(child) == ns_href
|
|
381
|
+
{
|
|
382
|
+
return Some(child);
|
|
383
|
+
}
|
|
384
|
+
child = child_ref.next;
|
|
385
|
+
}
|
|
386
|
+
None
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/// Collects all direct child elements with the given local name and namespace href.
|
|
390
|
+
///
|
|
391
|
+
/// # Safety
|
|
392
|
+
/// `parent` must be non-null and valid.
|
|
393
|
+
unsafe fn find_children(parent: sys::xmlNodePtr, name: &str, ns_href: &str) -> Vec<sys::xmlNodePtr> {
|
|
394
|
+
let mut result = Vec::new();
|
|
395
|
+
let mut child = (*parent).children;
|
|
396
|
+
while !child.is_null() {
|
|
397
|
+
let child_ref = &*child;
|
|
398
|
+
if child_ref.type_ == sys::xmlElementType_XML_ELEMENT_NODE
|
|
399
|
+
&& node_name(child) == name
|
|
400
|
+
&& node_ns_href(child) == ns_href
|
|
401
|
+
{
|
|
402
|
+
result.push(child);
|
|
403
|
+
}
|
|
404
|
+
child = child_ref.next;
|
|
405
|
+
}
|
|
406
|
+
result
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
410
|
+
// XmlSecKey — RAII wrapper for xmlsec key
|
|
411
|
+
// ---------------------------------------------------------------------------
|
|
412
|
+
|
|
413
|
+
/// An xmlsec key loaded from PEM. Frees itself on drop unless ownership
|
|
414
|
+
/// is transferred via `into_raw()`.
|
|
415
|
+
pub struct XmlSecKey(sys::xmlSecKeyPtr);
|
|
416
|
+
|
|
417
|
+
impl XmlSecKey {
|
|
418
|
+
/// Loads a private or public key from PEM bytes.
|
|
419
|
+
pub fn from_pem(pem: &[u8]) -> Result<Self, &'static str> {
|
|
420
|
+
let len = ffi_len(pem.len()).ok_or("failed to load PEM key: too large")?;
|
|
421
|
+
// SAFETY: pem is a valid slice; xmlSecOpenSSLAppKeyLoadMemory
|
|
422
|
+
// copies what it needs. xmlsec tries a private key first, then a
|
|
423
|
+
// SubjectPublicKeyInfo / RSA public key — the `key:` verify path
|
|
424
|
+
// depends on that fallback.
|
|
425
|
+
let ptr = unsafe {
|
|
426
|
+
sys::xmlSecOpenSSLAppKeyLoadMemory(
|
|
427
|
+
pem.as_ptr(),
|
|
428
|
+
len,
|
|
429
|
+
sys::KEY_FORMAT_PEM,
|
|
430
|
+
ptr::null(), // no password (already decrypted in Ruby)
|
|
431
|
+
ptr::null_mut(),
|
|
432
|
+
ptr::null_mut(),
|
|
433
|
+
)
|
|
434
|
+
};
|
|
435
|
+
if ptr.is_null() {
|
|
436
|
+
return Err("failed to load PEM key");
|
|
437
|
+
}
|
|
438
|
+
Ok(Self(ptr))
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/// Loads a public key from a PEM certificate.
|
|
442
|
+
///
|
|
443
|
+
/// `xmlSecKeyDataFormatCertPem` makes xmlsec read the X.509 cert and
|
|
444
|
+
/// extract its SPKI, so the cert can be pinned as `signKey` the same
|
|
445
|
+
/// way a bare public-key PEM is. Without this, a cert PEM is only a
|
|
446
|
+
/// KeysMngr trust anchor and xmlsec still takes `<KeyValue>` from the
|
|
447
|
+
/// document (S-01).
|
|
448
|
+
pub fn from_cert_pem(pem: &[u8]) -> Result<Self, &'static str> {
|
|
449
|
+
let len = ffi_len(pem.len()).ok_or("failed to load PEM certificate: too large")?;
|
|
450
|
+
// SAFETY: pem is a valid slice; xmlSecOpenSSLAppKeyLoadMemory
|
|
451
|
+
// copies what it needs. KEY_FORMAT_CERT_PEM dispatches to
|
|
452
|
+
// xmlSecOpenSSLAppKeyFromCertLoadBIO rather than treating the
|
|
453
|
+
// bytes as a PKCS#1/SPKI key.
|
|
454
|
+
let ptr = unsafe {
|
|
455
|
+
sys::xmlSecOpenSSLAppKeyLoadMemory(
|
|
456
|
+
pem.as_ptr(),
|
|
457
|
+
len,
|
|
458
|
+
sys::KEY_FORMAT_CERT_PEM,
|
|
459
|
+
ptr::null(),
|
|
460
|
+
ptr::null_mut(),
|
|
461
|
+
ptr::null_mut(),
|
|
462
|
+
)
|
|
463
|
+
};
|
|
464
|
+
if ptr.is_null() {
|
|
465
|
+
return Err("failed to load PEM certificate");
|
|
466
|
+
}
|
|
467
|
+
Ok(Self(ptr))
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/// Attaches a PEM certificate to this key (for KeyInfo population).
|
|
471
|
+
pub fn add_cert_pem(&self, cert_pem: &[u8]) -> Result<(), &'static str> {
|
|
472
|
+
let len = ffi_len(cert_pem.len()).ok_or("failed to attach certificate to key: too large")?;
|
|
473
|
+
// SAFETY: self.0 is valid; cert_pem is a valid slice.
|
|
474
|
+
let rc = unsafe {
|
|
475
|
+
sys::xmlSecOpenSSLAppKeyCertLoadMemory(
|
|
476
|
+
self.0,
|
|
477
|
+
cert_pem.as_ptr(),
|
|
478
|
+
len,
|
|
479
|
+
sys::KEY_FORMAT_PEM,
|
|
480
|
+
)
|
|
481
|
+
};
|
|
482
|
+
if rc < 0 {
|
|
483
|
+
return Err("failed to attach certificate to key");
|
|
484
|
+
}
|
|
485
|
+
Ok(())
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/// Transfers ownership of the raw pointer. The caller is responsible
|
|
489
|
+
/// for freeing it (typically by adopting it into a KeysMngr or DsigCtx).
|
|
490
|
+
/// This `XmlSecKey` must not be used or dropped after this call.
|
|
491
|
+
pub fn into_raw(self) -> sys::xmlSecKeyPtr {
|
|
492
|
+
let ptr = self.0;
|
|
493
|
+
std::mem::forget(self);
|
|
494
|
+
ptr
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
impl Drop for XmlSecKey {
|
|
499
|
+
fn drop(&mut self) {
|
|
500
|
+
// SAFETY: we're the exclusive owner.
|
|
501
|
+
unsafe { sys::xmlSecKeyDestroy(self.0) }
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
// KeysMngr — xmlsec keys manager
|
|
507
|
+
// ---------------------------------------------------------------------------
|
|
508
|
+
|
|
509
|
+
/// An xmlsec keys manager. Frees itself on drop.
|
|
510
|
+
struct KeysMngr(sys::xmlSecKeysMngrPtr);
|
|
511
|
+
|
|
512
|
+
impl KeysMngr {
|
|
513
|
+
/// Creates a new keys manager with trusted and untrusted certificates.
|
|
514
|
+
fn new(
|
|
515
|
+
trusted_pems: &[Vec<u8>],
|
|
516
|
+
untrusted_pems: &[Vec<u8>],
|
|
517
|
+
) -> Result<Self, &'static str> {
|
|
518
|
+
// SAFETY: no arguments; returns either null or a freshly
|
|
519
|
+
// allocated manager we exclusively own.
|
|
520
|
+
let ptr = unsafe { sys::xmlSecKeysMngrCreate() };
|
|
521
|
+
if ptr.is_null() {
|
|
522
|
+
return Err("failed to create xmlsec keys manager");
|
|
523
|
+
}
|
|
524
|
+
let mngr = Self(ptr);
|
|
525
|
+
|
|
526
|
+
// SAFETY: mngr.0 is non-null and exclusively ours.
|
|
527
|
+
if unsafe { sys::xmlSecOpenSSLAppDefaultKeysMngrInit(mngr.0) } < 0 {
|
|
528
|
+
return Err("failed to initialize xmlsec keys manager");
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
for cert_pem in trusted_pems {
|
|
532
|
+
let len = ffi_len(cert_pem.len()).ok_or("failed to load trusted certificate: too large")?;
|
|
533
|
+
// SAFETY: mngr.0 is valid and initialized; cert_pem is a valid slice.
|
|
534
|
+
let loaded = unsafe {
|
|
535
|
+
sys::xmlSecOpenSSLAppKeysMngrCertLoadMemory(
|
|
536
|
+
mngr.0,
|
|
537
|
+
cert_pem.as_ptr(),
|
|
538
|
+
len,
|
|
539
|
+
sys::xmlSecKeyDataFormat_xmlSecKeyDataFormatPem,
|
|
540
|
+
sys::xmlSecKeyDataTypeTrusted,
|
|
541
|
+
)
|
|
542
|
+
};
|
|
543
|
+
if loaded < 0 {
|
|
544
|
+
return Err("failed to load trusted certificate");
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
for cert_pem in untrusted_pems {
|
|
549
|
+
let len =
|
|
550
|
+
ffi_len(cert_pem.len()).ok_or("failed to load untrusted/intermediate certificate: too large")?;
|
|
551
|
+
// SAFETY: same as above, but with type 0 (not trusted — intermediate).
|
|
552
|
+
let loaded = unsafe {
|
|
553
|
+
sys::xmlSecOpenSSLAppKeysMngrCertLoadMemory(
|
|
554
|
+
mngr.0,
|
|
555
|
+
cert_pem.as_ptr(),
|
|
556
|
+
len,
|
|
557
|
+
sys::xmlSecKeyDataFormat_xmlSecKeyDataFormatPem,
|
|
558
|
+
0, // not trusted — intermediate for chain building
|
|
559
|
+
)
|
|
560
|
+
};
|
|
561
|
+
if loaded < 0 {
|
|
562
|
+
return Err("failed to load untrusted/intermediate certificate");
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
Ok(mngr)
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
impl Drop for KeysMngr {
|
|
571
|
+
fn drop(&mut self) {
|
|
572
|
+
// SAFETY: we're the exclusive owner; any DsigCtx borrowing us has
|
|
573
|
+
// already been dropped first — enforced by its lifetime parameter.
|
|
574
|
+
unsafe { sys::xmlSecKeysMngrDestroy(self.0) }
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// ---------------------------------------------------------------------------
|
|
579
|
+
// DsigCtx — xmlsec signature context
|
|
580
|
+
// ---------------------------------------------------------------------------
|
|
581
|
+
|
|
582
|
+
/// An xmlsec signature context bound to a [`KeysMngr`]. Frees itself on
|
|
583
|
+
/// drop.
|
|
584
|
+
struct DsigCtx<'mngr> {
|
|
585
|
+
ptr: sys::xmlSecDSigCtxPtr,
|
|
586
|
+
_mngr: std::marker::PhantomData<&'mngr KeysMngr>,
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
impl<'mngr> DsigCtx<'mngr> {
|
|
590
|
+
fn new(mngr: &'mngr KeysMngr) -> Result<Self, &'static str> {
|
|
591
|
+
// SAFETY: mngr.0 is valid; tying this context's lifetime to
|
|
592
|
+
// `'mngr` makes the borrow checker keep `mngr` alive at least
|
|
593
|
+
// as long as we are.
|
|
594
|
+
let ptr = unsafe { sys::xmlSecDSigCtxCreate(mngr.0) };
|
|
595
|
+
if ptr.is_null() {
|
|
596
|
+
return Err("failed to create xmlsec signature context");
|
|
597
|
+
}
|
|
598
|
+
Ok(Self {
|
|
599
|
+
ptr,
|
|
600
|
+
_mngr: std::marker::PhantomData,
|
|
601
|
+
})
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/// Creates a signature context with `signKey` already set.
|
|
605
|
+
///
|
|
606
|
+
/// Used for both signing and verification. On verify, a pre-set
|
|
607
|
+
/// `signKey` makes xmlsec skip KeyInfo — the document cannot pick
|
|
608
|
+
/// the verification key (S-01).
|
|
609
|
+
fn for_signing(mngr: &'mngr KeysMngr, key: XmlSecKey) -> Result<Self, &'static str> {
|
|
610
|
+
let mut ctx = Self::new(mngr)?;
|
|
611
|
+
// SAFETY: setting signKey transfers ownership of the key to the context.
|
|
612
|
+
// xmlSecDSigCtxDestroy will call xmlSecKeyDestroy on it.
|
|
613
|
+
//
|
|
614
|
+
// This is the most ABI-fragile line in the crate: a direct field write
|
|
615
|
+
// into xmlSecDSigCtx, at an offset bindgen computed from whichever
|
|
616
|
+
// xmlsec headers were present at build time. If xmlsec ever moves
|
|
617
|
+
// signKey, bindgen regenerates a different-but-still-valid Rust struct
|
|
618
|
+
// and nothing fails to compile — the write simply lands somewhere else.
|
|
619
|
+
// That is why the vendored builds pin xmlsec (dependencies.yml) rather
|
|
620
|
+
// than binding against whatever the OS ships: headers and archive then
|
|
621
|
+
// come from one source and cannot disagree. For system builds,
|
|
622
|
+
// HankoXml::NativeVersion is the runtime backstop.
|
|
623
|
+
unsafe {
|
|
624
|
+
(*ctx.ptr).signKey = key.into_raw();
|
|
625
|
+
}
|
|
626
|
+
ctx.restrict_reference_uris_to_same_document();
|
|
627
|
+
ctx.resolve_bare_fragments_by_id();
|
|
628
|
+
ctx.ignore_manifests();
|
|
629
|
+
ctx.restrict_transforms()?;
|
|
630
|
+
Ok(ctx)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/// By default xmlsec turns `URI="#foo"` into the XPointer
|
|
634
|
+
/// `xpointer(id('foo'))` by string formatting, so an Id containing an
|
|
635
|
+
/// apostrophe produced an invalid expression and could not be signed or
|
|
636
|
+
/// verified (#71). The flag (named for the Visa3D protocol that needed
|
|
637
|
+
/// it first) makes xmlsec look the bare name up with `xmlGetID` instead:
|
|
638
|
+
/// the same ID table and the same node set without comments, minus the
|
|
639
|
+
/// quoting. It is also stricter -- `id('a b')` selects two elements,
|
|
640
|
+
/// the lookup finds none. `#xpointer(...)` URIs are unaffected.
|
|
641
|
+
fn resolve_bare_fragments_by_id(&mut self) {
|
|
642
|
+
// SAFETY: self.ptr is a live xmlSecDSigCtx we exclusively own; flags
|
|
643
|
+
// is application-settable and read when each Reference context is
|
|
644
|
+
// initialized. Same ABI caveat as the signKey write.
|
|
645
|
+
unsafe {
|
|
646
|
+
(*self.ptr).flags |= sys::XMLSEC_DSIG_FLAGS_USE_VISA3D_HACK;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/// By default xmlsec also resolves and digests the references inside
|
|
651
|
+
/// every `<Object><Manifest>`, whose URIs, transforms and digest methods
|
|
652
|
+
/// never reach Ruby Policy, which reads only `<SignedInfo>` (#75). XML-DSig
|
|
653
|
+
/// core validation leaves Manifest references to the application, and
|
|
654
|
+
/// this library never treats their targets as signed: a `<Reference>` to
|
|
655
|
+
/// the `<Manifest>` still digests the Manifest element itself, and nothing
|
|
656
|
+
/// it points to.
|
|
657
|
+
fn ignore_manifests(&mut self) {
|
|
658
|
+
// SAFETY: self.ptr is a live xmlSecDSigCtx we exclusively own; flags
|
|
659
|
+
// is application-settable and read while processing the Signature.
|
|
660
|
+
// Same ABI caveat as the signKey write.
|
|
661
|
+
unsafe {
|
|
662
|
+
(*self.ptr).flags |= sys::XMLSEC_DSIG_FLAGS_IGNORE_MANIFESTS;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/// xmlSecDSigCtxCreate leaves `enabledReferenceUris` /
|
|
667
|
+
/// `transformCtx.enabledUris` as `xmlSecTransformUriTypeAny`, so a
|
|
668
|
+
/// `<Reference URI="file://…">` or `http://…` is fetched before Ruby
|
|
669
|
+
/// Policy can reject it (S-04). Limit both to empty + same-document;
|
|
670
|
+
/// `referenced_documents:` is the only supported non-fragment path
|
|
671
|
+
/// and it never hits libxml2 I/O.
|
|
672
|
+
fn restrict_reference_uris_to_same_document(&mut self) {
|
|
673
|
+
let allowed = sys::URI_TYPE_EMPTY | sys::URI_TYPE_SAME_DOCUMENT;
|
|
674
|
+
// SAFETY: self.ptr is a live xmlSecDSigCtx we exclusively own.
|
|
675
|
+
// enabledReferenceUris and transformCtx.enabledUris are
|
|
676
|
+
// application-settable; xmlsec reads them when resolving
|
|
677
|
+
// Reference hrefs. Same ABI caveat as the signKey write.
|
|
678
|
+
unsafe {
|
|
679
|
+
(*self.ptr).enabledReferenceUris = allowed;
|
|
680
|
+
(*self.ptr).transformCtx.enabledUris = allowed;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/// Lets References name URIs outside the document, for a verification
|
|
685
|
+
/// whose caller passed `referenced_documents:` (#74). Only the IO
|
|
686
|
+
/// callbacks installed by [`init_xmlsec`] can open such a URI, and they
|
|
687
|
+
/// serve nothing but that map: no filesystem, no network.
|
|
688
|
+
fn allow_referenced_documents(&mut self) {
|
|
689
|
+
let allowed = sys::URI_TYPE_EMPTY
|
|
690
|
+
| sys::URI_TYPE_SAME_DOCUMENT
|
|
691
|
+
| sys::xmlSecTransformUriTypeLocal
|
|
692
|
+
| sys::xmlSecTransformUriTypeRemote;
|
|
693
|
+
// SAFETY: same fields and caveat as
|
|
694
|
+
// restrict_reference_uris_to_same_document.
|
|
695
|
+
unsafe {
|
|
696
|
+
(*self.ptr).enabledReferenceUris = allowed;
|
|
697
|
+
(*self.ptr).transformCtx.enabledUris = allowed;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/// Restricts `enabledReferenceTransforms` to core DSig transforms only.
|
|
702
|
+
///
|
|
703
|
+
/// xmlSecDSigCtxCreate leaves `enabledReferenceTransforms` as NULL, so
|
|
704
|
+
/// all registered transforms are allowed, including XSLT when the host
|
|
705
|
+
/// xmlsec was built with libxslt (S-10). Initialize the list and add
|
|
706
|
+
/// only safe core DSig transforms — excluding XSLT, XPath, and XPointer.
|
|
707
|
+
/// SHA-3 digest klasses exist only in xmlsec 1.3+; they are looked up
|
|
708
|
+
/// by href so a 1.2 host still compiles (#60).
|
|
709
|
+
fn restrict_transforms(&mut self) -> Result<(), &'static str> {
|
|
710
|
+
// List of safe transform IDs to allow. These cover the standard
|
|
711
|
+
// DSig operations: canonicalization, base64, enveloped signature.
|
|
712
|
+
// Notably absent: XSLT, XPath, XPointer, and other potentially
|
|
713
|
+
// dangerous transforms.
|
|
714
|
+
let safe_transforms: &[unsafe extern "C" fn() -> sys::xmlSecTransformId] = &[
|
|
715
|
+
sys::xmlSecTransformEnvelopedGetKlass,
|
|
716
|
+
sys::xmlSecTransformBase64GetKlass,
|
|
717
|
+
sys::xmlSecTransformInclC14NGetKlass,
|
|
718
|
+
sys::xmlSecTransformInclC14NWithCommentsGetKlass,
|
|
719
|
+
sys::xmlSecTransformInclC14N11GetKlass,
|
|
720
|
+
sys::xmlSecTransformInclC14N11WithCommentsGetKlass,
|
|
721
|
+
sys::xmlSecTransformExclC14NGetKlass,
|
|
722
|
+
sys::xmlSecTransformExclC14NWithCommentsGetKlass,
|
|
723
|
+
// Digest methods
|
|
724
|
+
sys::xmlSecOpenSSLTransformSha1GetKlass,
|
|
725
|
+
sys::xmlSecOpenSSLTransformSha224GetKlass,
|
|
726
|
+
sys::xmlSecOpenSSLTransformSha256GetKlass,
|
|
727
|
+
sys::xmlSecOpenSSLTransformSha384GetKlass,
|
|
728
|
+
sys::xmlSecOpenSSLTransformSha512GetKlass,
|
|
729
|
+
];
|
|
730
|
+
|
|
731
|
+
// SAFETY: self.ptr is a live xmlSecDSigCtx we exclusively own.
|
|
732
|
+
// xmlSecDSigCtxEnableReferenceTransform initializes the
|
|
733
|
+
// enabledReferenceTransforms list on first call and adds the
|
|
734
|
+
// transform ID. After this, only transforms in the list are allowed.
|
|
735
|
+
for get_klass in safe_transforms {
|
|
736
|
+
let transform_id = unsafe { get_klass() };
|
|
737
|
+
let rc = unsafe {
|
|
738
|
+
sys::xmlSecDSigCtxEnableReferenceTransform(self.ptr, transform_id)
|
|
739
|
+
};
|
|
740
|
+
if rc < 0 {
|
|
741
|
+
return Err("failed to enable safe transform");
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
self.enable_sha3_digest_transforms()
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/// Enables xmlsec's XPath Filter 2.0 transform, which `restrict_transforms`
|
|
748
|
+
/// leaves out (S-10). Called only when the caller's Policy allows
|
|
749
|
+
/// `:xpath_filter2`, and Ruby Policy has already limited every such
|
|
750
|
+
/// transform to a single `subtract` of the document's signatures (#85).
|
|
751
|
+
/// xmlsec evaluates it; this crate never evaluates XPath itself.
|
|
752
|
+
fn enable_xpath_filter2(&mut self) -> Result<(), &'static str> {
|
|
753
|
+
// SAFETY: self.ptr is a live xmlSecDSigCtx we exclusively own, its
|
|
754
|
+
// enabled list already initialized by restrict_transforms.
|
|
755
|
+
let rc = unsafe {
|
|
756
|
+
sys::xmlSecDSigCtxEnableReferenceTransform(self.ptr, sys::xmlSecTransformXPath2GetKlass())
|
|
757
|
+
};
|
|
758
|
+
if rc < 0 {
|
|
759
|
+
return Err("failed to enable the XPath Filter 2.0 transform");
|
|
760
|
+
}
|
|
761
|
+
Ok(())
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/// Enables SHA-3 `DigestMethod` klasses when the linked xmlsec registered
|
|
765
|
+
/// them. Looked up by href rather than `xmlSecOpenSSLTransformSha3_*GetKlass`
|
|
766
|
+
/// so Ubuntu's xmlsec 1.2 (no SHA-3 symbols) still compiles (S-10, #60).
|
|
767
|
+
fn enable_sha3_digest_transforms(&mut self) -> Result<(), &'static str> {
|
|
768
|
+
const SHA3_DIGEST_HREFS: &[&[u8]] = &[
|
|
769
|
+
b"http://www.w3.org/2007/05/xmldsig-more#sha3-224",
|
|
770
|
+
b"http://www.w3.org/2007/05/xmldsig-more#sha3-256",
|
|
771
|
+
b"http://www.w3.org/2007/05/xmldsig-more#sha3-384",
|
|
772
|
+
b"http://www.w3.org/2007/05/xmldsig-more#sha3-512",
|
|
773
|
+
];
|
|
774
|
+
|
|
775
|
+
// SAFETY: xmlSecTransformIdsGet returns the process-global registry
|
|
776
|
+
// list, populated once by init_xmlsec and never mutated afterwards.
|
|
777
|
+
let registry = unsafe { sys::xmlSecTransformIdsGet() };
|
|
778
|
+
if registry.is_null() {
|
|
779
|
+
return Err("xmlsec transform registry is unavailable");
|
|
780
|
+
}
|
|
781
|
+
// SAFETY: registry is a valid list (checked above).
|
|
782
|
+
let size = unsafe { sys::xmlSecPtrListGetSize(registry) };
|
|
783
|
+
for pos in 0..size {
|
|
784
|
+
// SAFETY: pos < size; every item is a static xmlSecTransformKlass.
|
|
785
|
+
let id = unsafe { sys::xmlSecPtrListGetItem(registry, pos) } as sys::xmlSecTransformId;
|
|
786
|
+
if id.is_null() {
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
// SAFETY: id is a non-null static klass; href is a static
|
|
790
|
+
// NUL-terminated string or null.
|
|
791
|
+
let href = unsafe { (*id).href };
|
|
792
|
+
if href.is_null() {
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
// SAFETY: href is non-null and NUL-terminated (see above).
|
|
796
|
+
let href = unsafe { CStr::from_ptr(href.cast()) }.to_bytes();
|
|
797
|
+
if !SHA3_DIGEST_HREFS.contains(&href) {
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
// SAFETY: self.ptr is a live xmlSecDSigCtx we exclusively own;
|
|
801
|
+
// enabledReferenceTransforms was initialized by the SHA-2 loop
|
|
802
|
+
// above, and id is static for the life of the process.
|
|
803
|
+
let rc = unsafe { sys::xmlSecDSigCtxEnableReferenceTransform(self.ptr, id) };
|
|
804
|
+
if rc < 0 {
|
|
805
|
+
return Err("failed to enable SHA-3 digest transform");
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
Ok(())
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/// Lets the signing KeyInfo writer fill `<KeyName>`, `<KeyValue>` and
|
|
812
|
+
/// `<X509Data>`.
|
|
813
|
+
///
|
|
814
|
+
/// xmlsec 1.3 registers the KeyValue key data *disabled* globally (it
|
|
815
|
+
/// is unsafe to *read*), so with the default empty
|
|
816
|
+
/// `keyInfoWriteCtx.enabledKeyData` a `<KeyValue/>` template stayed
|
|
817
|
+
/// empty. A non-empty local list replaces the global one for this
|
|
818
|
+
/// context only, so it must name every KeyInfo child we write and the
|
|
819
|
+
/// key value classes KeyValue may serialize. Reading is untouched:
|
|
820
|
+
/// verification pins `signKey` and never consults KeyInfo (S-01).
|
|
821
|
+
///
|
|
822
|
+
/// Classes are picked from xmlsec's registry by name rather than by
|
|
823
|
+
/// `xmlSec*GetKlass` symbol because the EC class is `ec` in 1.3 but
|
|
824
|
+
/// `ecdsa` (a different symbol) in the 1.2 that Linux distros ship.
|
|
825
|
+
fn enable_key_info_writing(&mut self) -> Result<(), &'static str> {
|
|
826
|
+
const WRITABLE: &[&[u8]] = &[b"key-name", b"key-value", b"x509", b"rsa", b"ec", b"ecdsa", b"dsa"];
|
|
827
|
+
|
|
828
|
+
// SAFETY: xmlSecKeyDataIdsGet returns the process-global registry
|
|
829
|
+
// list, populated once by init_xmlsec and never mutated afterwards.
|
|
830
|
+
let registry = unsafe { sys::xmlSecKeyDataIdsGet() };
|
|
831
|
+
if registry.is_null() {
|
|
832
|
+
return Err("xmlsec key data registry is unavailable");
|
|
833
|
+
}
|
|
834
|
+
// SAFETY: registry is a valid list (checked above).
|
|
835
|
+
let size = unsafe { sys::xmlSecPtrListGetSize(registry) };
|
|
836
|
+
for pos in 0..size {
|
|
837
|
+
// SAFETY: pos < size; every item is a static xmlSecKeyDataKlass.
|
|
838
|
+
let id = unsafe { sys::xmlSecPtrListGetItem(registry, pos) } as sys::xmlSecKeyDataId;
|
|
839
|
+
if id.is_null() {
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
// SAFETY: id is a non-null static klass; name is a static
|
|
843
|
+
// NUL-terminated string or null.
|
|
844
|
+
let name = unsafe { (*id).name };
|
|
845
|
+
if name.is_null() {
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
// SAFETY: name is non-null and NUL-terminated (see above).
|
|
849
|
+
let name = unsafe { std::ffi::CStr::from_ptr(name.cast()) }.to_bytes();
|
|
850
|
+
if !WRITABLE.contains(&name) {
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
// SAFETY: self.ptr is a live xmlSecDSigCtx we exclusively own;
|
|
854
|
+
// enabledKeyData was initialized as an xmlSecKeyDataIdListId
|
|
855
|
+
// list by xmlSecDSigCtxCreate, and id is static for the life of
|
|
856
|
+
// the process. Same ABI caveat as the signKey write.
|
|
857
|
+
let rc = unsafe {
|
|
858
|
+
sys::xmlSecPtrListAdd(&mut (*self.ptr).keyInfoWriteCtx.enabledKeyData, id as sys::xmlSecPtr)
|
|
859
|
+
};
|
|
860
|
+
if rc < 0 {
|
|
861
|
+
return Err("failed to enable KeyInfo key data for writing");
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
Ok(())
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/// Signs the document at the given signature node.
|
|
868
|
+
fn sign(&mut self, sig_node: sys::xmlNodePtr) -> Result<(), &'static str> {
|
|
869
|
+
// SAFETY: self.ptr is valid and exclusively ours; sig_node is
|
|
870
|
+
// valid within the document's lifetime.
|
|
871
|
+
let rc = unsafe { sys::xmlSecDSigCtxSign(self.ptr, sig_node) };
|
|
872
|
+
if rc < 0 {
|
|
873
|
+
return Err("xmlSecDSigCtxSign failed");
|
|
874
|
+
}
|
|
875
|
+
Ok(())
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/// Verifies the signature at the given node. Returns the status.
|
|
879
|
+
fn verify(&mut self, sig_node: sys::xmlNodePtr) -> VerifyStatus {
|
|
880
|
+
// SAFETY: self.ptr is valid; sig_node is valid within the
|
|
881
|
+
// document's lifetime.
|
|
882
|
+
if unsafe { sys::xmlSecDSigCtxVerify(self.ptr, sig_node) } < 0 {
|
|
883
|
+
return VerifyStatus::Unknown;
|
|
884
|
+
}
|
|
885
|
+
// SAFETY: self.ptr is valid; status is a plain enum field.
|
|
886
|
+
let status = unsafe { (*self.ptr).status };
|
|
887
|
+
match status {
|
|
888
|
+
s if s == sys::DSIG_STATUS_SUCCEEDED => VerifyStatus::Succeeded,
|
|
889
|
+
s if s == sys::DSIG_STATUS_INVALID => VerifyStatus::Invalid,
|
|
890
|
+
_ => VerifyStatus::Unknown,
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/// Per-`<SignedInfo><Reference>` digest verification status, in document
|
|
895
|
+
/// order, after `verify()` has run (S-09). `xmlSecDSigCtxVerify`
|
|
896
|
+
/// processes and digest-checks every SignedInfo Reference before it ever
|
|
897
|
+
/// looks at the overall `SignatureValue` (see xmldsig.c), so this is
|
|
898
|
+
/// independent of the overall `signKey`/`SignatureValue` outcome: a
|
|
899
|
+
/// harmless-fragment Reference and the payload Reference in a
|
|
900
|
+
/// multi-Reference signature can genuinely disagree.
|
|
901
|
+
///
|
|
902
|
+
/// `Some(true)`/`Some(false)` is a completed status; `None` means the
|
|
903
|
+
/// reference was never processed (verification aborted earlier, e.g. a
|
|
904
|
+
/// malformed `SignedInfo`) and the caller has no real answer to give.
|
|
905
|
+
/// `<Manifest>` references are not read here -- this library does not
|
|
906
|
+
/// otherwise resolve or expose Manifest semantics.
|
|
907
|
+
fn reference_statuses(&self) -> Vec<Option<bool>> {
|
|
908
|
+
// SAFETY: self.ptr is a live xmlSecDSigCtx; signedInfoReferences is
|
|
909
|
+
// an embedded xmlSecPtrList (not a pointer) that verify() populated.
|
|
910
|
+
let list = unsafe { ptr::addr_of!((*self.ptr).signedInfoReferences) as sys::xmlSecPtrListPtr };
|
|
911
|
+
// SAFETY: list points at a valid, initialized xmlSecPtrList for the
|
|
912
|
+
// lifetime of self.
|
|
913
|
+
let size = unsafe { sys::xmlSecPtrListGetSize(list) };
|
|
914
|
+
(0..size)
|
|
915
|
+
.map(|pos| {
|
|
916
|
+
// SAFETY: pos is in [0, size), so GetItem returns one of the
|
|
917
|
+
// list's own xmlSecDSigReferenceCtxPtr entries.
|
|
918
|
+
let item = unsafe { sys::xmlSecPtrListGetItem(list, pos) };
|
|
919
|
+
if item.is_null() {
|
|
920
|
+
return None;
|
|
921
|
+
}
|
|
922
|
+
let reference_ctx = item as sys::xmlSecDSigReferenceCtxPtr;
|
|
923
|
+
// SAFETY: reference_ctx is a live xmlSecDSigReferenceCtx
|
|
924
|
+
// owned by the list; status is a plain enum field.
|
|
925
|
+
let status = unsafe { (*reference_ctx).status };
|
|
926
|
+
match status {
|
|
927
|
+
s if s == sys::DSIG_STATUS_SUCCEEDED => Some(true),
|
|
928
|
+
s if s == sys::DSIG_STATUS_INVALID => Some(false),
|
|
929
|
+
_ => None,
|
|
930
|
+
}
|
|
931
|
+
})
|
|
932
|
+
.collect()
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
impl Drop for DsigCtx<'_> {
|
|
937
|
+
fn drop(&mut self) {
|
|
938
|
+
// SAFETY: we're the exclusive owner of this context.
|
|
939
|
+
unsafe { sys::xmlSecDSigCtxDestroy(self.ptr) }
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
944
|
+
pub enum VerifyStatus {
|
|
945
|
+
Succeeded,
|
|
946
|
+
Invalid,
|
|
947
|
+
Unknown,
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
impl VerifyStatus {
|
|
951
|
+
pub fn as_str(&self) -> &'static str {
|
|
952
|
+
match self {
|
|
953
|
+
Self::Succeeded => "succeeded",
|
|
954
|
+
Self::Invalid => "invalid",
|
|
955
|
+
Self::Unknown => "unknown",
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// ---------------------------------------------------------------------------
|
|
961
|
+
// Public API — the §7 contract
|
|
962
|
+
// ---------------------------------------------------------------------------
|
|
963
|
+
|
|
964
|
+
/// Result of `inspect_document`.
|
|
965
|
+
#[derive(Debug)]
|
|
966
|
+
pub struct InspectResult {
|
|
967
|
+
pub signature_method: String,
|
|
968
|
+
pub canonicalization_method: String,
|
|
969
|
+
pub signature_count: usize,
|
|
970
|
+
pub key_names: Vec<String>,
|
|
971
|
+
pub certificates: Vec<String>,
|
|
972
|
+
pub references: Vec<InspectReference>,
|
|
973
|
+
/// Whether `<KeyInfo>` contains a `<RetrievalMethod>` (S-06,
|
|
974
|
+
/// `allow_retrieval_method`) -- checked structurally, before any
|
|
975
|
+
/// dereferencing, the same way S-04 checks Reference URIs.
|
|
976
|
+
pub has_retrieval_method: bool,
|
|
977
|
+
/// Whether `<KeyInfo>` has a `<KeyName>` with no text, which the Signer
|
|
978
|
+
/// must fill before xmlsec signs (#67).
|
|
979
|
+
pub empty_key_names: bool,
|
|
980
|
+
/// Whether a key-derived `<KeyInfo>` leaf (`X509Certificate`,
|
|
981
|
+
/// `X509SubjectName`, `X509IssuerSerial`, `X509SKI`, `KeyValue`) has any
|
|
982
|
+
/// content. xmlsec fills only empty ones, so an adopted template's stale
|
|
983
|
+
/// values would be signed (#72). Lets a Signer without a DOM refuse such
|
|
984
|
+
/// a template (#102).
|
|
985
|
+
pub key_derived_values: bool,
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
#[derive(Debug)]
|
|
989
|
+
pub struct InspectReference {
|
|
990
|
+
pub uri: String,
|
|
991
|
+
pub digest_method: String,
|
|
992
|
+
pub transforms: Vec<String>,
|
|
993
|
+
pub id: Option<String>,
|
|
994
|
+
pub r#type: Option<String>,
|
|
995
|
+
/// The `<XPath>` children of every XPath Filter 2.0 transform, in order,
|
|
996
|
+
/// for Policy's shape check (#85).
|
|
997
|
+
pub xpath_filters: Vec<XPathFilter>,
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/// One XPath Filter 2.0 `<XPath>`: its `Filter` type, its expression, and
|
|
1001
|
+
/// the namespace bindings in scope on it, which give the expression's
|
|
1002
|
+
/// prefixes their meaning.
|
|
1003
|
+
#[derive(Debug)]
|
|
1004
|
+
pub struct XPathFilter {
|
|
1005
|
+
pub filter: String,
|
|
1006
|
+
pub expression: String,
|
|
1007
|
+
pub namespaces: Vec<(String, String)>,
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/// Result of `verify_document`.
|
|
1011
|
+
#[derive(Debug)]
|
|
1012
|
+
pub struct VerifyResult {
|
|
1013
|
+
pub status: VerifyStatus,
|
|
1014
|
+
pub errors: Vec<String>,
|
|
1015
|
+
/// Per SignedInfo `<Reference>`, the document-order element index of the
|
|
1016
|
+
/// node xmlsec's ID table resolved (same table `#id` verification used).
|
|
1017
|
+
/// Empty URI → root element's index. Unresolved / non-fragment → `None`.
|
|
1018
|
+
pub reference_element_indexes: Vec<Option<usize>>,
|
|
1019
|
+
/// Per SignedInfo `<Reference>`, its own digest verification status --
|
|
1020
|
+
/// independent of the overall signature/key outcome (S-09). Same
|
|
1021
|
+
/// indexing/order as `reference_element_indexes`. `None` when the
|
|
1022
|
+
/// reference was never processed (verification aborted before reaching
|
|
1023
|
+
/// it), not when we simply do not know.
|
|
1024
|
+
pub reference_valid: Vec<Option<bool>>,
|
|
1025
|
+
/// Index into `trusted_pems` of the PEM whose key verified the signature,
|
|
1026
|
+
/// or `None` when none did. Lets Ruby name the certificate that verified
|
|
1027
|
+
/// (`Verification#certificate`), which `certificates:` otherwise leaves
|
|
1028
|
+
/// ambiguous.
|
|
1029
|
+
pub trusted_index: Option<usize>,
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
const DSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
|
|
1033
|
+
const XPATH_FILTER2_NS: &str = "http://www.w3.org/2002/06/xmldsig-filter2";
|
|
1034
|
+
|
|
1035
|
+
/// Inspects the nth `<ds:Signature>` without performing any cryptography.
|
|
1036
|
+
/// Returns metadata about the signature for policy enforcement.
|
|
1037
|
+
///
|
|
1038
|
+
/// No `unsafe` in this function body — every FFI call is behind a helper.
|
|
1039
|
+
pub fn inspect_document(
|
|
1040
|
+
xml: &[u8],
|
|
1041
|
+
signature_index: usize,
|
|
1042
|
+
) -> Result<InspectResult, DsigError> {
|
|
1043
|
+
let doc = OwnedXmlDoc::parse(xml).map_err(DsigError::parse)?;
|
|
1044
|
+
let root = doc
|
|
1045
|
+
.root_element()
|
|
1046
|
+
.ok_or(DsigError::parse("XML document has no root element"))?;
|
|
1047
|
+
|
|
1048
|
+
let sig_count = doc.count_signatures(root);
|
|
1049
|
+
let sig_node = doc
|
|
1050
|
+
.find_nth_signature(root, signature_index)
|
|
1051
|
+
.ok_or(DsigError::native("signature index out of range"))?;
|
|
1052
|
+
|
|
1053
|
+
// Extract <SignedInfo>
|
|
1054
|
+
let signed_info = unsafe { find_child(sig_node, "SignedInfo", DSIG_NS) }
|
|
1055
|
+
.ok_or(DsigError::native("no <SignedInfo> element in <Signature>"))?;
|
|
1056
|
+
|
|
1057
|
+
// CanonicalizationMethod/@Algorithm
|
|
1058
|
+
let c14n_method = unsafe { find_child(signed_info, "CanonicalizationMethod", DSIG_NS) }
|
|
1059
|
+
.and_then(|n| unsafe { get_attribute(n, b"Algorithm\0") })
|
|
1060
|
+
.unwrap_or_default();
|
|
1061
|
+
|
|
1062
|
+
// SignatureMethod/@Algorithm
|
|
1063
|
+
let sig_method = unsafe { find_child(signed_info, "SignatureMethod", DSIG_NS) }
|
|
1064
|
+
.and_then(|n| unsafe { get_attribute(n, b"Algorithm\0") })
|
|
1065
|
+
.unwrap_or_default();
|
|
1066
|
+
|
|
1067
|
+
// References
|
|
1068
|
+
let ref_nodes = unsafe { find_children(signed_info, "Reference", DSIG_NS) };
|
|
1069
|
+
let mut references = Vec::new();
|
|
1070
|
+
for ref_node in ref_nodes {
|
|
1071
|
+
let uri = unsafe { get_attribute(ref_node, b"URI\0") }.unwrap_or_default();
|
|
1072
|
+
let id = unsafe { get_attribute(ref_node, b"Id\0") };
|
|
1073
|
+
let type_attr = unsafe { get_attribute(ref_node, b"Type\0") };
|
|
1074
|
+
|
|
1075
|
+
// DigestMethod/@Algorithm
|
|
1076
|
+
let digest_method = unsafe { find_child(ref_node, "DigestMethod", DSIG_NS) }
|
|
1077
|
+
.and_then(|n| unsafe { get_attribute(n, b"Algorithm\0") })
|
|
1078
|
+
.unwrap_or_default();
|
|
1079
|
+
|
|
1080
|
+
// Transforms
|
|
1081
|
+
let mut transforms = Vec::new();
|
|
1082
|
+
let mut xpath_filters = Vec::new();
|
|
1083
|
+
if let Some(transforms_node) = unsafe { find_child(ref_node, "Transforms", DSIG_NS) } {
|
|
1084
|
+
let transform_nodes = unsafe { find_children(transforms_node, "Transform", DSIG_NS) };
|
|
1085
|
+
for t_node in transform_nodes {
|
|
1086
|
+
if let Some(alg) = unsafe { get_attribute(t_node, b"Algorithm\0") } {
|
|
1087
|
+
if alg == XPATH_FILTER2_NS {
|
|
1088
|
+
for xpath in unsafe { find_children(t_node, "XPath", XPATH_FILTER2_NS) } {
|
|
1089
|
+
xpath_filters.push(XPathFilter {
|
|
1090
|
+
filter: unsafe { get_attribute(xpath, b"Filter\0") }.unwrap_or_default(),
|
|
1091
|
+
expression: unsafe { get_text_content(xpath) },
|
|
1092
|
+
namespaces: unsafe { in_scope_namespaces(xpath) },
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
transforms.push(alg);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
references.push(InspectReference {
|
|
1102
|
+
uri,
|
|
1103
|
+
digest_method,
|
|
1104
|
+
transforms,
|
|
1105
|
+
id,
|
|
1106
|
+
r#type: type_attr,
|
|
1107
|
+
xpath_filters,
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// KeyInfo — KeyName, X509Certificate, and RetrievalMethod presence
|
|
1112
|
+
let mut key_names = Vec::new();
|
|
1113
|
+
let mut certificates = Vec::new();
|
|
1114
|
+
let mut has_retrieval_method = false;
|
|
1115
|
+
let mut empty_key_names = false;
|
|
1116
|
+
let mut key_derived_values = false;
|
|
1117
|
+
if let Some(key_info) = unsafe { find_child(sig_node, "KeyInfo", DSIG_NS) } {
|
|
1118
|
+
// KeyName elements
|
|
1119
|
+
let key_name_nodes = unsafe { find_children(key_info, "KeyName", DSIG_NS) };
|
|
1120
|
+
for kn in key_name_nodes {
|
|
1121
|
+
let text = unsafe { get_text_content(kn) };
|
|
1122
|
+
if text.trim().is_empty() {
|
|
1123
|
+
empty_key_names = true;
|
|
1124
|
+
}
|
|
1125
|
+
if !text.is_empty() {
|
|
1126
|
+
key_names.push(text);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Key-derived leaves with any child node, as the Ruby DOM path
|
|
1131
|
+
// checks them (Signer#empty_key_derived_leaves!).
|
|
1132
|
+
let mut leaves = unsafe { find_children(key_info, "KeyValue", DSIG_NS) };
|
|
1133
|
+
for x509_data in unsafe { find_children(key_info, "X509Data", DSIG_NS) } {
|
|
1134
|
+
for name in ["X509Certificate", "X509SubjectName", "X509IssuerSerial", "X509SKI"] {
|
|
1135
|
+
leaves.extend(unsafe { find_children(x509_data, name, DSIG_NS) });
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
// SAFETY: each leaf is a live element of this document.
|
|
1139
|
+
key_derived_values = leaves.iter().any(|&leaf| unsafe { !(*leaf).children.is_null() });
|
|
1140
|
+
|
|
1141
|
+
// X509Data/X509Certificate elements
|
|
1142
|
+
let x509_data_nodes = unsafe { find_children(key_info, "X509Data", DSIG_NS) };
|
|
1143
|
+
for x509_data in x509_data_nodes {
|
|
1144
|
+
let cert_nodes = unsafe { find_children(x509_data, "X509Certificate", DSIG_NS) };
|
|
1145
|
+
for cert_node in cert_nodes {
|
|
1146
|
+
let text = unsafe { get_text_content(cert_node) }.trim().to_string();
|
|
1147
|
+
if !text.is_empty() {
|
|
1148
|
+
// Wrap in PEM markers for consistency
|
|
1149
|
+
let pem = format!("-----BEGIN CERTIFICATE-----\n{text}\n-----END CERTIFICATE-----");
|
|
1150
|
+
certificates.push(pem);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
has_retrieval_method =
|
|
1156
|
+
unsafe { find_child(key_info, "RetrievalMethod", DSIG_NS) }.is_some();
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
Ok(InspectResult {
|
|
1160
|
+
signature_method: sig_method,
|
|
1161
|
+
canonicalization_method: c14n_method,
|
|
1162
|
+
signature_count: sig_count,
|
|
1163
|
+
key_names,
|
|
1164
|
+
certificates,
|
|
1165
|
+
references,
|
|
1166
|
+
has_retrieval_method,
|
|
1167
|
+
empty_key_names,
|
|
1168
|
+
key_derived_values,
|
|
1169
|
+
})
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
/// Result of `scan_document`: the structural facts the Ruby layer used to
|
|
1173
|
+
/// read from a Nokogiri DOM, taken from the same parse xmlsec works on.
|
|
1174
|
+
#[derive(Debug)]
|
|
1175
|
+
pub struct ScanResult {
|
|
1176
|
+
pub signature_count: usize,
|
|
1177
|
+
/// Per `<ds:Signature>` in document order, whether its
|
|
1178
|
+
/// `<SignatureValue>` has non-whitespace content.
|
|
1179
|
+
pub signature_values: Vec<bool>,
|
|
1180
|
+
/// Per `<ds:Signature>`, its document-order element index (root = 0),
|
|
1181
|
+
/// the numbering `Native.verify`'s `reference_element_indexes` use.
|
|
1182
|
+
pub signature_element_indexes: Vec<Option<usize>>,
|
|
1183
|
+
/// Whether two ID attributes (any of `id_attrs`, matched by local name
|
|
1184
|
+
/// the way `xmlSecAddIDs` matches them) carry the same value (S-06).
|
|
1185
|
+
pub duplicate_ids: bool,
|
|
1186
|
+
/// Values `#value` resolves to the root element through the ID table
|
|
1187
|
+
/// xmlsec uses, for `require_signed_root` (S-06).
|
|
1188
|
+
pub root_ids: Vec<String>,
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/// Scans the document's structure without cryptography: signature count
|
|
1192
|
+
/// and presence, duplicate IDs, and the root's IDs. `strict` rejects any
|
|
1193
|
+
/// libxml2 error, the way a strict Nokogiri parse does; it is set for input
|
|
1194
|
+
/// that arrived as bytes, and unset for a DOM the caller already holds.
|
|
1195
|
+
pub fn scan_document(xml: &[u8], id_attrs: &[String], strict: bool) -> Result<ScanResult, DsigError> {
|
|
1196
|
+
errors::clear_errors();
|
|
1197
|
+
|
|
1198
|
+
let doc = if strict { OwnedXmlDoc::parse_strict(xml) } else { OwnedXmlDoc::parse(xml) }
|
|
1199
|
+
.map_err(DsigError::parse)?;
|
|
1200
|
+
let root = doc
|
|
1201
|
+
.root_element()
|
|
1202
|
+
.ok_or(DsigError::parse("XML document has no root element"))?;
|
|
1203
|
+
|
|
1204
|
+
let mut signatures = Vec::new();
|
|
1205
|
+
collect_signature_nodes(root, &mut signatures);
|
|
1206
|
+
let signature_values = signatures
|
|
1207
|
+
.iter()
|
|
1208
|
+
.map(|&sig| {
|
|
1209
|
+
unsafe { find_child(sig, "SignatureValue", DSIG_NS) }
|
|
1210
|
+
.map(|value| !ruby_strip(&unsafe { get_text_content(value) }).is_empty())
|
|
1211
|
+
.unwrap_or(false)
|
|
1212
|
+
})
|
|
1213
|
+
.collect();
|
|
1214
|
+
let signature_element_indexes = signatures
|
|
1215
|
+
.iter()
|
|
1216
|
+
.map(|&sig| element_document_index(root, sig))
|
|
1217
|
+
.collect();
|
|
1218
|
+
|
|
1219
|
+
let mut id_values: Vec<(sys::xmlAttrPtr, String)> = Vec::new();
|
|
1220
|
+
collect_id_attributes(root, id_attrs, &mut id_values);
|
|
1221
|
+
let mut seen = std::collections::HashSet::new();
|
|
1222
|
+
let duplicate_ids = id_values.iter().any(|(_, value)| !seen.insert(value.as_str()));
|
|
1223
|
+
|
|
1224
|
+
doc.register_id_attributes(root, id_attrs);
|
|
1225
|
+
let root_ids = id_values
|
|
1226
|
+
.iter()
|
|
1227
|
+
.filter(|(attr, _)| unsafe { (**attr).parent } == root)
|
|
1228
|
+
.filter(|(_, value)| element_for_id(&doc, value) == Some(root))
|
|
1229
|
+
.map(|(_, value)| value.clone())
|
|
1230
|
+
.collect();
|
|
1231
|
+
|
|
1232
|
+
errors::clear_errors();
|
|
1233
|
+
Ok(ScanResult {
|
|
1234
|
+
signature_count: signatures.len(),
|
|
1235
|
+
signature_values,
|
|
1236
|
+
signature_element_indexes,
|
|
1237
|
+
duplicate_ids,
|
|
1238
|
+
root_ids,
|
|
1239
|
+
})
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/// Ruby's `String#strip`: ASCII whitespace and NUL, nothing else, so a
|
|
1243
|
+
/// `<SignatureValue>` holding only U+00A0 counts as present in both layers.
|
|
1244
|
+
fn ruby_strip(text: &str) -> &str {
|
|
1245
|
+
text.trim_matches(|c: char| c.is_ascii_whitespace() || c == '\x0b' || c == '\0')
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/// Every attribute, in document order, whose local name is one of
|
|
1249
|
+
/// `id_attrs`, whatever its namespace: the attributes `xmlSecAddIDs`
|
|
1250
|
+
/// registers.
|
|
1251
|
+
fn collect_id_attributes(node: sys::xmlNodePtr, id_attrs: &[String], out: &mut Vec<(sys::xmlAttrPtr, String)>) {
|
|
1252
|
+
if node.is_null() || id_attrs.is_empty() {
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
// SAFETY: node is non-null and valid within the document's lifetime.
|
|
1256
|
+
let node_ref = unsafe { &*node };
|
|
1257
|
+
if node_ref.type_ == sys::xmlElementType_XML_ELEMENT_NODE {
|
|
1258
|
+
let mut attr = node_ref.properties;
|
|
1259
|
+
while !attr.is_null() {
|
|
1260
|
+
// SAFETY: attr is a valid attribute of this element.
|
|
1261
|
+
let attr_ref = unsafe { &*attr };
|
|
1262
|
+
let name = if attr_ref.name.is_null() {
|
|
1263
|
+
""
|
|
1264
|
+
} else {
|
|
1265
|
+
// SAFETY: attribute names are NUL-terminated.
|
|
1266
|
+
unsafe { CStr::from_ptr(attr_ref.name.cast()) }.to_str().unwrap_or("")
|
|
1267
|
+
};
|
|
1268
|
+
if id_attrs.iter().any(|id| id == name) {
|
|
1269
|
+
// SAFETY: attr's children are its value's text nodes.
|
|
1270
|
+
let value = unsafe { sys::xmlNodeListGetString(node_ref.doc, attr_ref.children, 1) };
|
|
1271
|
+
let text = if value.is_null() {
|
|
1272
|
+
String::new()
|
|
1273
|
+
} else {
|
|
1274
|
+
// SAFETY: non-null, NUL-terminated, freed right after.
|
|
1275
|
+
let text = unsafe { CStr::from_ptr(value.cast()) }.to_str().unwrap_or("").to_string();
|
|
1276
|
+
unsafe { xml_free(value.cast()) };
|
|
1277
|
+
text
|
|
1278
|
+
};
|
|
1279
|
+
out.push((attr, text));
|
|
1280
|
+
}
|
|
1281
|
+
attr = attr_ref.next;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
let mut child = node_ref.children;
|
|
1285
|
+
while !child.is_null() {
|
|
1286
|
+
collect_id_attributes(child, id_attrs, out);
|
|
1287
|
+
// SAFETY: child is a valid sibling in this document.
|
|
1288
|
+
child = unsafe { (*child).next };
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
/// Validates a copy of the nth `<ds:Signature>`, as the root of a fresh
|
|
1293
|
+
/// document, against the XSD at `xsd_path` (relative imports resolve
|
|
1294
|
+
/// against it). Returns libxml2's validation errors; empty means valid.
|
|
1295
|
+
/// Nokogiri's `schema: :default` path validated the same copy (#102).
|
|
1296
|
+
pub fn validate_signature_schema(
|
|
1297
|
+
xml: &[u8],
|
|
1298
|
+
signature_index: usize,
|
|
1299
|
+
xsd_path: &str,
|
|
1300
|
+
) -> Result<Vec<String>, DsigError> {
|
|
1301
|
+
errors::clear_errors();
|
|
1302
|
+
|
|
1303
|
+
let doc = OwnedXmlDoc::parse(xml).map_err(DsigError::parse)?;
|
|
1304
|
+
let root = doc
|
|
1305
|
+
.root_element()
|
|
1306
|
+
.ok_or(DsigError::parse("XML document has no root element"))?;
|
|
1307
|
+
let sig_node = doc
|
|
1308
|
+
.find_nth_signature(root, signature_index)
|
|
1309
|
+
.ok_or(DsigError::native("signature index out of range"))?;
|
|
1310
|
+
|
|
1311
|
+
let mut path = xsd_path.as_bytes().to_vec();
|
|
1312
|
+
path.push(0);
|
|
1313
|
+
errors::clear_errors();
|
|
1314
|
+
let _capture = Libxml2Capture::start();
|
|
1315
|
+
|
|
1316
|
+
// SAFETY: path is NUL-terminated for the call; the parser context is
|
|
1317
|
+
// freed below on every path.
|
|
1318
|
+
let parser = unsafe { sys::xmlSchemaNewParserCtxt(path.as_ptr().cast()) };
|
|
1319
|
+
if parser.is_null() {
|
|
1320
|
+
return Err(DsigError::native("failed to load the XML-DSig schema"));
|
|
1321
|
+
}
|
|
1322
|
+
// SAFETY: parser is valid; the handler takes a null context.
|
|
1323
|
+
unsafe { sys::xmlSchemaSetParserStructuredErrors(parser, errors::libxml2_error_handler(), ptr::null_mut()) };
|
|
1324
|
+
// SAFETY: parser is valid. The parsed schema outlives its parser.
|
|
1325
|
+
let schema = unsafe { sys::xmlSchemaParse(parser) };
|
|
1326
|
+
// SAFETY: freed exactly once, after xmlSchemaParse.
|
|
1327
|
+
unsafe { sys::xmlSchemaFreeParserCtxt(parser) };
|
|
1328
|
+
if schema.is_null() {
|
|
1329
|
+
return Err(DsigError::native("failed to load the XML-DSig schema"));
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// SAFETY: "1.0" is NUL-terminated; the new document is owned below.
|
|
1333
|
+
let copy = unsafe { sys::xmlNewDoc(c"1.0".as_ptr().cast()) };
|
|
1334
|
+
if copy.is_null() {
|
|
1335
|
+
// SAFETY: schema is valid and freed once.
|
|
1336
|
+
unsafe { sys::xmlSchemaFree(schema) };
|
|
1337
|
+
return Err(DsigError::native("failed to copy <Signature> for schema validation"));
|
|
1338
|
+
}
|
|
1339
|
+
let copy = OwnedXmlDoc(copy);
|
|
1340
|
+
// SAFETY: sig_node is valid in doc; the recursive copy belongs to copy.0
|
|
1341
|
+
// once set as its root, and is freed with it.
|
|
1342
|
+
let copied = unsafe { sys::xmlDocCopyNode(sig_node, copy.0, 1) };
|
|
1343
|
+
if !copied.is_null() {
|
|
1344
|
+
// SAFETY: copy.0 has no root yet, so nothing is unlinked or leaked.
|
|
1345
|
+
unsafe { sys::xmlDocSetRootElement(copy.0, copied) };
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
errors::clear_errors();
|
|
1349
|
+
// SAFETY: schema is valid; the context is freed below.
|
|
1350
|
+
let valid_ctxt = unsafe { sys::xmlSchemaNewValidCtxt(schema) };
|
|
1351
|
+
let result = if valid_ctxt.is_null() || copied.is_null() {
|
|
1352
|
+
Err(DsigError::native("failed to set up schema validation"))
|
|
1353
|
+
} else {
|
|
1354
|
+
// SAFETY: valid_ctxt and copy.0 are valid for the call.
|
|
1355
|
+
let status = unsafe {
|
|
1356
|
+
sys::xmlSchemaSetValidStructuredErrors(valid_ctxt, errors::libxml2_error_handler(), ptr::null_mut());
|
|
1357
|
+
sys::xmlSchemaValidateDoc(valid_ctxt, copy.0)
|
|
1358
|
+
};
|
|
1359
|
+
let mut messages = errors::drain_errors();
|
|
1360
|
+
if status != 0 && messages.is_empty() {
|
|
1361
|
+
messages.push("<Signature> does not validate against the XML-DSig schema".into());
|
|
1362
|
+
}
|
|
1363
|
+
Ok(messages)
|
|
1364
|
+
};
|
|
1365
|
+
// SAFETY: each freed once; a null context is skipped.
|
|
1366
|
+
unsafe {
|
|
1367
|
+
if !valid_ctxt.is_null() {
|
|
1368
|
+
sys::xmlSchemaFreeValidCtxt(valid_ctxt);
|
|
1369
|
+
}
|
|
1370
|
+
sys::xmlSchemaFree(schema);
|
|
1371
|
+
}
|
|
1372
|
+
result
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/// Signs the document's nth `<ds:Signature>` template.
|
|
1376
|
+
///
|
|
1377
|
+
/// The XML must already contain a `<ds:Signature>` skeleton with empty
|
|
1378
|
+
/// `<DigestValue>`, `<SignatureValue>` etc. — built by Ruby's Template
|
|
1379
|
+
/// layer.
|
|
1380
|
+
pub fn sign_document(
|
|
1381
|
+
xml: &[u8],
|
|
1382
|
+
key_pem: &[u8],
|
|
1383
|
+
cert_pems: &[Vec<u8>],
|
|
1384
|
+
id_attrs: &[String],
|
|
1385
|
+
signature_index: usize,
|
|
1386
|
+
) -> Result<Vec<u8>, DsigError> {
|
|
1387
|
+
errors::clear_errors();
|
|
1388
|
+
|
|
1389
|
+
let doc = OwnedXmlDoc::parse(xml).map_err(DsigError::parse)?;
|
|
1390
|
+
let root = doc
|
|
1391
|
+
.root_element()
|
|
1392
|
+
.ok_or(DsigError::parse("XML document has no root element"))?;
|
|
1393
|
+
|
|
1394
|
+
doc.register_id_attributes(root, id_attrs);
|
|
1395
|
+
|
|
1396
|
+
let sig_node = doc
|
|
1397
|
+
.find_nth_signature(root, signature_index)
|
|
1398
|
+
.ok_or(DsigError::native("signature index out of range or no <Signature> found"))?;
|
|
1399
|
+
|
|
1400
|
+
let key = XmlSecKey::from_pem(key_pem).map_err(DsigError::key_load)?;
|
|
1401
|
+
|
|
1402
|
+
for cert_pem in cert_pems {
|
|
1403
|
+
key.add_cert_pem(cert_pem).map_err(DsigError::key_load)?;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
let mngr = KeysMngr::new(&[], &[]).map_err(DsigError::native)?;
|
|
1407
|
+
|
|
1408
|
+
let mut ctx = DsigCtx::for_signing(&mngr, key).map_err(DsigError::native)?;
|
|
1409
|
+
ctx.enable_key_info_writing().map_err(DsigError::native)?;
|
|
1410
|
+
|
|
1411
|
+
ctx.sign(sig_node).map_err(DsigError::signing)?;
|
|
1412
|
+
|
|
1413
|
+
doc.serialize().map_err(DsigError::native)
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
/// Verifies the nth `<ds:Signature>` against trusted/untrusted certs.
|
|
1417
|
+
///
|
|
1418
|
+
/// `trusted_pems` may contain certificate PEMs, key PEMs, or both.
|
|
1419
|
+
/// Every trusted PEM is converted to a public key and pinned on the
|
|
1420
|
+
/// dsig context as `signKey`, which makes xmlsec skip KeyInfo lookup.
|
|
1421
|
+
/// That is the `key:` / `certificate:` contract: the document cannot
|
|
1422
|
+
/// supply the verification key via `<KeyValue>`, `<DEREncodedKeyValue>`,
|
|
1423
|
+
/// or `<RetrievalMethod>` (S-01). Multiple certificate PEMs are tried
|
|
1424
|
+
/// in order until one verifies (`certificates:` — any one of these).
|
|
1425
|
+
/// An empty trust list fails closed (`:invalid`), never by reading
|
|
1426
|
+
/// KeyInfo.
|
|
1427
|
+
///
|
|
1428
|
+
/// Returns a `VerifyResult` with status and captured error messages.
|
|
1429
|
+
/// Only raises (returns `Err`) for conditions that are not a verification
|
|
1430
|
+
/// verdict (unparseable input, out-of-range index, unreadable key).
|
|
1431
|
+
pub fn verify_document(
|
|
1432
|
+
xml: &[u8],
|
|
1433
|
+
trusted_pems: &[Vec<u8>],
|
|
1434
|
+
untrusted_pems: &[Vec<u8>],
|
|
1435
|
+
id_attrs: &[String],
|
|
1436
|
+
signature_index: usize,
|
|
1437
|
+
referenced_documents: HashMap<Vec<u8>, Vec<u8>>,
|
|
1438
|
+
allow_xpath_filter2: bool,
|
|
1439
|
+
) -> Result<VerifyResult, DsigError> {
|
|
1440
|
+
errors::clear_errors();
|
|
1441
|
+
let serves_external = !referenced_documents.is_empty();
|
|
1442
|
+
let _referenced = ReferencedDocuments::activate(referenced_documents);
|
|
1443
|
+
|
|
1444
|
+
let doc = OwnedXmlDoc::parse(xml).map_err(DsigError::parse)?;
|
|
1445
|
+
let root = doc
|
|
1446
|
+
.root_element()
|
|
1447
|
+
.ok_or(DsigError::parse("XML document has no root element"))?;
|
|
1448
|
+
|
|
1449
|
+
doc.register_id_attributes(root, id_attrs);
|
|
1450
|
+
|
|
1451
|
+
let sig_node = doc
|
|
1452
|
+
.find_nth_signature(root, signature_index)
|
|
1453
|
+
.ok_or(DsigError::native("no <Signature> element found in document"))?;
|
|
1454
|
+
|
|
1455
|
+
let mut pinned_key_pems: Vec<&[u8]> = Vec::new();
|
|
1456
|
+
let mut cert_pems: Vec<&[u8]> = Vec::new();
|
|
1457
|
+
let mut key_indexes = Vec::new();
|
|
1458
|
+
let mut cert_indexes = Vec::new();
|
|
1459
|
+
for (index, pem) in trusted_pems.iter().enumerate() {
|
|
1460
|
+
if pem_is_certificate(pem) {
|
|
1461
|
+
cert_pems.push(pem);
|
|
1462
|
+
cert_indexes.push(index);
|
|
1463
|
+
} else {
|
|
1464
|
+
pinned_key_pems.push(pem);
|
|
1465
|
+
key_indexes.push(index);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
if pinned_key_pems.len() > 1 {
|
|
1470
|
+
return Err(DsigError::native("at most one trusted key PEM is supported"));
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// Chain building stays a Ruby `store:` concern. Native only ever pins
|
|
1474
|
+
// a verification key; untrusted PEMs are still accepted so the FFI
|
|
1475
|
+
// shape is unchanged, but they are not consulted once signKey is set.
|
|
1476
|
+
// Loads the untrusted PEMs, so a failure is about certificate input.
|
|
1477
|
+
let mngr = KeysMngr::new(&[], untrusted_pems).map_err(DsigError::key_load)?;
|
|
1478
|
+
|
|
1479
|
+
let keys_to_try = load_pinned_keys(&pinned_key_pems, &cert_pems)?;
|
|
1480
|
+
// load_pinned_keys tries the one key PEM alone, or else every certificate.
|
|
1481
|
+
let tried_indexes = if pinned_key_pems.is_empty() { cert_indexes } else { key_indexes };
|
|
1482
|
+
let reference_element_indexes =
|
|
1483
|
+
collect_reference_element_indexes(&doc, root, sig_node);
|
|
1484
|
+
|
|
1485
|
+
if keys_to_try.is_empty() {
|
|
1486
|
+
return Ok(VerifyResult {
|
|
1487
|
+
status: VerifyStatus::Invalid,
|
|
1488
|
+
errors: vec!["no trusted key or certificate provided".into()],
|
|
1489
|
+
reference_valid: vec![None; reference_element_indexes.len()],
|
|
1490
|
+
reference_element_indexes,
|
|
1491
|
+
trusted_index: None,
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
let mut status = VerifyStatus::Invalid;
|
|
1496
|
+
let mut captured_errors = Vec::new();
|
|
1497
|
+
let mut reference_valid = Vec::new();
|
|
1498
|
+
let mut trusted_index = None;
|
|
1499
|
+
for (key, index) in keys_to_try.into_iter().zip(tried_indexes) {
|
|
1500
|
+
errors::clear_errors();
|
|
1501
|
+
let mut ctx = DsigCtx::for_signing(&mngr, key).map_err(DsigError::native)?;
|
|
1502
|
+
if serves_external {
|
|
1503
|
+
ctx.allow_referenced_documents();
|
|
1504
|
+
}
|
|
1505
|
+
if allow_xpath_filter2 {
|
|
1506
|
+
ctx.enable_xpath_filter2().map_err(DsigError::native)?;
|
|
1507
|
+
}
|
|
1508
|
+
status = ctx.verify(sig_node);
|
|
1509
|
+
captured_errors = errors::drain_errors();
|
|
1510
|
+
reference_valid = ctx.reference_statuses();
|
|
1511
|
+
if status == VerifyStatus::Succeeded {
|
|
1512
|
+
trusted_index = Some(index);
|
|
1513
|
+
break;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
Ok(VerifyResult {
|
|
1518
|
+
status,
|
|
1519
|
+
errors: captured_errors,
|
|
1520
|
+
reference_element_indexes,
|
|
1521
|
+
reference_valid,
|
|
1522
|
+
trusted_index,
|
|
1523
|
+
})
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
/// One key PEM pins immediately. Otherwise each certificate PEM is loaded
|
|
1527
|
+
/// as a public key so `certificates:` can try them until one matches.
|
|
1528
|
+
fn load_pinned_keys(
|
|
1529
|
+
key_pems: &[&[u8]],
|
|
1530
|
+
cert_pems: &[&[u8]],
|
|
1531
|
+
) -> Result<Vec<XmlSecKey>, DsigError> {
|
|
1532
|
+
if let Some(pem) = key_pems.first() {
|
|
1533
|
+
return Ok(vec![XmlSecKey::from_pem(pem).map_err(DsigError::key_load)?]);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
cert_pems
|
|
1537
|
+
.iter()
|
|
1538
|
+
.map(|pem| XmlSecKey::from_cert_pem(pem).map_err(DsigError::key_load))
|
|
1539
|
+
.collect()
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/// For each SignedInfo `<Reference>`, report the document-order element index
|
|
1543
|
+
/// of the node the libxml2 ID table resolves — the same table xmlsec used for
|
|
1544
|
+
/// `#id` fragment verification. Empty URI → root index. Non-fragment / missing
|
|
1545
|
+
/// ID → `None`. `#xpointer(/)` is the whole document too, with comments (#85).
|
|
1546
|
+
fn collect_reference_element_indexes(
|
|
1547
|
+
doc: &OwnedXmlDoc,
|
|
1548
|
+
root: sys::xmlNodePtr,
|
|
1549
|
+
sig_node: sys::xmlNodePtr,
|
|
1550
|
+
) -> Vec<Option<usize>> {
|
|
1551
|
+
let signed_info = unsafe { find_child(sig_node, "SignedInfo", DSIG_NS) };
|
|
1552
|
+
let Some(signed_info) = signed_info else {
|
|
1553
|
+
return Vec::new();
|
|
1554
|
+
};
|
|
1555
|
+
|
|
1556
|
+
let ref_nodes = unsafe { find_children(signed_info, "Reference", DSIG_NS) };
|
|
1557
|
+
ref_nodes
|
|
1558
|
+
.into_iter()
|
|
1559
|
+
.map(|ref_node| {
|
|
1560
|
+
let uri = unsafe { get_attribute(ref_node, b"URI\0") }.unwrap_or_default();
|
|
1561
|
+
resolve_reference_element(doc, root, &uri)
|
|
1562
|
+
.and_then(|node| element_document_index(root, node))
|
|
1563
|
+
})
|
|
1564
|
+
.collect()
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
fn resolve_reference_element(
|
|
1568
|
+
doc: &OwnedXmlDoc,
|
|
1569
|
+
root: sys::xmlNodePtr,
|
|
1570
|
+
uri: &str,
|
|
1571
|
+
) -> Option<sys::xmlNodePtr> {
|
|
1572
|
+
if uri.is_empty() || uri == "#xpointer(/)" {
|
|
1573
|
+
return Some(root);
|
|
1574
|
+
}
|
|
1575
|
+
if let Some(id) = uri.strip_prefix('#') {
|
|
1576
|
+
// Bare fragment only — other xpointer(...) forms are left unresolved
|
|
1577
|
+
// here (Ruby already treats them as node: nil).
|
|
1578
|
+
if id.is_empty() || id.contains('(') {
|
|
1579
|
+
return None;
|
|
1580
|
+
}
|
|
1581
|
+
return element_for_id(doc, id);
|
|
1582
|
+
}
|
|
1583
|
+
None
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
fn element_for_id(doc: &OwnedXmlDoc, id: &str) -> Option<sys::xmlNodePtr> {
|
|
1587
|
+
let mut id_bytes = id.as_bytes().to_vec();
|
|
1588
|
+
id_bytes.push(0);
|
|
1589
|
+
|
|
1590
|
+
// SAFETY: doc.0 is a live OwnedXmlDoc; id_bytes is NUL-terminated for the
|
|
1591
|
+
// duration of the call. xmlGetID returns a pointer into the document's ID
|
|
1592
|
+
// table (not newly allocated).
|
|
1593
|
+
let attr = unsafe { sys::xmlGetID(doc.0, id_bytes.as_ptr().cast()) };
|
|
1594
|
+
if attr.is_null() {
|
|
1595
|
+
return None;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// SAFETY: attr is a valid xmlAttr from this document; its parent is the
|
|
1599
|
+
// element that owns the ID attribute.
|
|
1600
|
+
let parent = unsafe { (*attr).parent };
|
|
1601
|
+
if parent.is_null() {
|
|
1602
|
+
return None;
|
|
1603
|
+
}
|
|
1604
|
+
let parent_ref = unsafe { &*parent };
|
|
1605
|
+
if parent_ref.type_ != sys::xmlElementType_XML_ELEMENT_NODE {
|
|
1606
|
+
return None;
|
|
1607
|
+
}
|
|
1608
|
+
Some(parent)
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
/// Document-order index among element nodes only (root = 0), matching
|
|
1612
|
+
/// Nokogiri/`//*` order for the same serialized tree.
|
|
1613
|
+
fn element_document_index(root: sys::xmlNodePtr, target: sys::xmlNodePtr) -> Option<usize> {
|
|
1614
|
+
let mut index = 0usize;
|
|
1615
|
+
find_element_index(root, target, &mut index)
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
fn find_element_index(
|
|
1619
|
+
node: sys::xmlNodePtr,
|
|
1620
|
+
target: sys::xmlNodePtr,
|
|
1621
|
+
index: &mut usize,
|
|
1622
|
+
) -> Option<usize> {
|
|
1623
|
+
if node.is_null() {
|
|
1624
|
+
return None;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// SAFETY: node is non-null within the document lifetime.
|
|
1628
|
+
let node_ref = unsafe { &*node };
|
|
1629
|
+
if node_ref.type_ == sys::xmlElementType_XML_ELEMENT_NODE {
|
|
1630
|
+
if std::ptr::eq(node, target) {
|
|
1631
|
+
return Some(*index);
|
|
1632
|
+
}
|
|
1633
|
+
*index += 1;
|
|
1634
|
+
|
|
1635
|
+
let mut child = node_ref.children;
|
|
1636
|
+
while !child.is_null() {
|
|
1637
|
+
if let Some(found) = find_element_index(child, target, index) {
|
|
1638
|
+
return Some(found);
|
|
1639
|
+
}
|
|
1640
|
+
child = unsafe { (*child).next };
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
None
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
fn pem_is_certificate(pem: &[u8]) -> bool {
|
|
1647
|
+
const MARKER: &[u8] = b"BEGIN CERTIFICATE";
|
|
1648
|
+
pem.windows(MARKER.len()).any(|window| window == MARKER)
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
/// Returns version information for the linked libraries.
|
|
1652
|
+
pub fn versions() -> (String, String, String) {
|
|
1653
|
+
let xmlsec_version = std::str::from_utf8(sys::XMLSEC_VERSION)
|
|
1654
|
+
.unwrap_or("unknown\0")
|
|
1655
|
+
.trim_matches('\0')
|
|
1656
|
+
.to_string();
|
|
1657
|
+
|
|
1658
|
+
let libxml2_version = std::str::from_utf8(sys::LIBXML_DOTTED_VERSION)
|
|
1659
|
+
.unwrap_or("unknown\0")
|
|
1660
|
+
.trim_matches('\0')
|
|
1661
|
+
.to_string();
|
|
1662
|
+
|
|
1663
|
+
// The crypto backend is always openssl in our build
|
|
1664
|
+
let crypto = "openssl".to_string();
|
|
1665
|
+
|
|
1666
|
+
(xmlsec_version, libxml2_version, crypto)
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
/// Initializes xmlsec + its OpenSSL crypto backend + our error callback.
|
|
1670
|
+
/// `crate` wraps this in a `std::sync::Once` so it runs exactly once per
|
|
1671
|
+
/// process.
|
|
1672
|
+
pub fn init_xmlsec() -> Result<(), &'static str> {
|
|
1673
|
+
// SAFETY: each of these takes either no arguments or a null config
|
|
1674
|
+
// pointer (meaning "use defaults"). They must run in this exact order,
|
|
1675
|
+
// before any other xmlsec call — guaranteed by the caller's Once.
|
|
1676
|
+
unsafe {
|
|
1677
|
+
if sys::xmlSecInit() < 0 {
|
|
1678
|
+
return Err("xmlSecInit failed");
|
|
1679
|
+
}
|
|
1680
|
+
if sys::xmlSecOpenSSLAppInit(ptr::null()) < 0 {
|
|
1681
|
+
return Err("xmlSecOpenSSLAppInit failed");
|
|
1682
|
+
}
|
|
1683
|
+
if sys::xmlSecOpenSSLInit() < 0 {
|
|
1684
|
+
return Err("xmlSecOpenSSLInit failed");
|
|
1685
|
+
}
|
|
1686
|
+
errors::install_error_callback();
|
|
1687
|
+
|
|
1688
|
+
// xmlSecInit registers file, HTTP and FTP readers for Reference
|
|
1689
|
+
// URIs. Replace them with one that reads only the caller's
|
|
1690
|
+
// referenced_documents: map (#74), so no URI can ever reach the
|
|
1691
|
+
// filesystem or network, whatever enabledReferenceUris allows. This
|
|
1692
|
+
// is xmlsec's own input list; libxml2's parser I/O (Nokogiri's) is
|
|
1693
|
+
// untouched.
|
|
1694
|
+
sys::xmlSecIOCleanupCallbacks();
|
|
1695
|
+
if sys::xmlSecIORegisterCallbacks(
|
|
1696
|
+
Some(referenced_match),
|
|
1697
|
+
Some(referenced_open),
|
|
1698
|
+
Some(referenced_read),
|
|
1699
|
+
Some(referenced_close),
|
|
1700
|
+
) < 0
|
|
1701
|
+
{
|
|
1702
|
+
return Err("xmlSecIORegisterCallbacks failed");
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
Ok(())
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// ---------------------------------------------------------------------------
|
|
1709
|
+
// ReferencedDocuments — the only source xmlsec may read a URI from
|
|
1710
|
+
// ---------------------------------------------------------------------------
|
|
1711
|
+
|
|
1712
|
+
thread_local! {
|
|
1713
|
+
/// The `referenced_documents:` of the verification running on this
|
|
1714
|
+
/// thread, keyed by URI (the Reference URI up to any `#`). The GVL is held
|
|
1715
|
+
/// for the whole native call, and xmlsec calls the IO callbacks on the
|
|
1716
|
+
/// calling thread, so a thread-local is enough (§7.1).
|
|
1717
|
+
static REFERENCED: RefCell<HashMap<Vec<u8>, Vec<u8>>> = RefCell::new(HashMap::new());
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
/// Makes a map visible to the IO callbacks until dropped, so an early
|
|
1721
|
+
/// return cannot leave one verification's documents readable by the next.
|
|
1722
|
+
struct ReferencedDocuments;
|
|
1723
|
+
|
|
1724
|
+
impl ReferencedDocuments {
|
|
1725
|
+
fn activate(documents: HashMap<Vec<u8>, Vec<u8>>) -> Self {
|
|
1726
|
+
REFERENCED.with(|map| *map.borrow_mut() = documents);
|
|
1727
|
+
Self
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
impl Drop for ReferencedDocuments {
|
|
1732
|
+
fn drop(&mut self) {
|
|
1733
|
+
REFERENCED.with(|map| map.borrow_mut().clear());
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
/// An open referenced document: its bytes and the read position.
|
|
1738
|
+
struct ReferencedReader {
|
|
1739
|
+
data: Vec<u8>,
|
|
1740
|
+
pos: usize,
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
/// # Safety
|
|
1744
|
+
///
|
|
1745
|
+
/// `uri` is null or a NUL-terminated string, per `xmlInputMatchCallback`.
|
|
1746
|
+
unsafe extern "C" fn referenced_match(uri: *const c_char) -> c_int {
|
|
1747
|
+
if uri.is_null() {
|
|
1748
|
+
return 0;
|
|
1749
|
+
}
|
|
1750
|
+
// SAFETY: non-null and NUL-terminated (see above).
|
|
1751
|
+
let uri = unsafe { CStr::from_ptr(uri) }.to_bytes();
|
|
1752
|
+
REFERENCED.with(|map| map.try_borrow().map(|m| m.contains_key(uri)).unwrap_or(false)) as c_int
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/// # Safety
|
|
1756
|
+
///
|
|
1757
|
+
/// `uri` is null or a NUL-terminated string, per `xmlInputOpenCallback`.
|
|
1758
|
+
unsafe extern "C" fn referenced_open(uri: *const c_char) -> *mut c_void {
|
|
1759
|
+
if uri.is_null() {
|
|
1760
|
+
return ptr::null_mut();
|
|
1761
|
+
}
|
|
1762
|
+
// SAFETY: non-null and NUL-terminated (see above).
|
|
1763
|
+
let uri = unsafe { CStr::from_ptr(uri) }.to_bytes();
|
|
1764
|
+
let data = REFERENCED.with(|map| map.try_borrow().ok().and_then(|m| m.get(uri).cloned()));
|
|
1765
|
+
match data {
|
|
1766
|
+
Some(data) => Box::into_raw(Box::new(ReferencedReader { data, pos: 0 })).cast(),
|
|
1767
|
+
None => ptr::null_mut(),
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
/// # Safety
|
|
1772
|
+
///
|
|
1773
|
+
/// `context` came from [`referenced_open`] and has not been closed;
|
|
1774
|
+
/// `buffer` has room for `len` bytes.
|
|
1775
|
+
unsafe extern "C" fn referenced_read(context: *mut c_void, buffer: *mut c_char, len: c_int) -> c_int {
|
|
1776
|
+
if context.is_null() || buffer.is_null() || len < 0 {
|
|
1777
|
+
return -1;
|
|
1778
|
+
}
|
|
1779
|
+
// SAFETY: context is a live ReferencedReader (see above).
|
|
1780
|
+
let reader = unsafe { &mut *context.cast::<ReferencedReader>() };
|
|
1781
|
+
let count = (reader.data.len() - reader.pos).min(len as usize);
|
|
1782
|
+
// SAFETY: count <= len bytes fit in buffer, and count bytes remain in data.
|
|
1783
|
+
unsafe { ptr::copy_nonoverlapping(reader.data.as_ptr().add(reader.pos), buffer.cast::<u8>(), count) };
|
|
1784
|
+
reader.pos += count;
|
|
1785
|
+
count as c_int
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
/// # Safety
|
|
1789
|
+
///
|
|
1790
|
+
/// `context` came from [`referenced_open`] and is closed exactly once.
|
|
1791
|
+
unsafe extern "C" fn referenced_close(context: *mut c_void) -> c_int {
|
|
1792
|
+
if !context.is_null() {
|
|
1793
|
+
// SAFETY: context is the Box leaked by referenced_open.
|
|
1794
|
+
drop(unsafe { Box::from_raw(context.cast::<ReferencedReader>()) });
|
|
1795
|
+
}
|
|
1796
|
+
0
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
/// True when the linked libxmlsec1 matches the version these bindings were
|
|
1800
|
+
/// generated against, down to the patch level.
|
|
1801
|
+
///
|
|
1802
|
+
/// Stricter than [`abi_compatible`] and used only for reporting: an exact
|
|
1803
|
+
/// mismatch is worth telling the user about, but on its own it is not a
|
|
1804
|
+
/// reason to refuse to load.
|
|
1805
|
+
pub fn check_version() -> bool {
|
|
1806
|
+
check_version_mode(sys::xmlSecCheckVersionMode_xmlSecCheckVersionExactMatch)
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
/// True when the linked libxmlsec1 is ABI compatible with the headers these
|
|
1810
|
+
/// bindings were generated from — same major version, and a minor version no
|
|
1811
|
+
/// older than ours.
|
|
1812
|
+
///
|
|
1813
|
+
/// This is the check that actually matters. bindgen bakes xmlsec's and
|
|
1814
|
+
/// libxml2's struct layouts into this extension at build time, and nothing
|
|
1815
|
+
/// about a layout change is visible to the dynamic linker: every symbol still
|
|
1816
|
+
/// resolves, the library still loads, and then a field gets read at the offset
|
|
1817
|
+
/// our headers claimed rather than the one the loaded library used. Refusing
|
|
1818
|
+
/// to load is the only safe response in a library whose job is deciding
|
|
1819
|
+
/// whether a signature can be trusted.
|
|
1820
|
+
pub fn abi_compatible() -> bool {
|
|
1821
|
+
check_version_mode(sys::xmlSecCheckVersionMode_xmlSecCheckVersionABICompatible)
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
fn check_version_mode(mode: sys::xmlSecCheckVersionMode) -> bool {
|
|
1825
|
+
// SAFETY: takes only plain integers — no pointers, no preconditions
|
|
1826
|
+
// beyond the xmlsec headers/library being loaded.
|
|
1827
|
+
unsafe {
|
|
1828
|
+
sys::xmlSecCheckVersionExt(
|
|
1829
|
+
sys::XMLSEC_VERSION_MAJOR as i32,
|
|
1830
|
+
sys::XMLSEC_VERSION_MINOR as i32,
|
|
1831
|
+
sys::XMLSEC_VERSION_SUBMINOR as i32,
|
|
1832
|
+
mode,
|
|
1833
|
+
) == 1
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
|
|
1838
|
+
#[cfg(test)]
|
|
1839
|
+
mod tests {
|
|
1840
|
+
use super::ffi_len;
|
|
1841
|
+
use std::os::raw::c_int;
|
|
1842
|
+
|
|
1843
|
+
#[test]
|
|
1844
|
+
fn ffi_len_rejects_lengths_that_would_wrap() {
|
|
1845
|
+
assert_eq!(ffi_len::<c_int>(0), Some(0));
|
|
1846
|
+
assert_eq!(ffi_len::<c_int>(c_int::MAX as usize), Some(c_int::MAX));
|
|
1847
|
+
assert_eq!(ffi_len::<c_int>(c_int::MAX as usize + 1), None);
|
|
1848
|
+
assert_eq!(ffi_len::<u32>(u32::MAX as usize + 1), None);
|
|
1849
|
+
assert_eq!(ffi_len::<c_int>(usize::MAX), None);
|
|
1850
|
+
}
|
|
1851
|
+
}
|