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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +44 -0
  3. data/Cargo.lock +401 -0
  4. data/Cargo.toml +7 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +222 -0
  7. data/THIRD_PARTY_LICENSES.md +392 -0
  8. data/dependencies.yml +87 -0
  9. data/ext/hanko_xml/Cargo.toml +20 -0
  10. data/ext/hanko_xml/build/vendor.rs +817 -0
  11. data/ext/hanko_xml/build.rs +298 -0
  12. data/ext/hanko_xml/extconf.rb +6 -0
  13. data/ext/hanko_xml/src/dsig.rs +1851 -0
  14. data/ext/hanko_xml/src/errors.rs +151 -0
  15. data/ext/hanko_xml/src/lib.rs +490 -0
  16. data/ext/hanko_xml/src/sys.rs +43 -0
  17. data/lib/hanko_xml/algorithms.rb +228 -0
  18. data/lib/hanko_xml/dom.rb +137 -0
  19. data/lib/hanko_xml/errors.rb +132 -0
  20. data/lib/hanko_xml/exc-c14n.xsd +26 -0
  21. data/lib/hanko_xml/input.rb +50 -0
  22. data/lib/hanko_xml/key.rb +213 -0
  23. data/lib/hanko_xml/key_info.rb +67 -0
  24. data/lib/hanko_xml/native_version.rb +75 -0
  25. data/lib/hanko_xml/nokogiri_ext.rb +34 -0
  26. data/lib/hanko_xml/placement.rb +127 -0
  27. data/lib/hanko_xml/policy.rb +502 -0
  28. data/lib/hanko_xml/reference.rb +49 -0
  29. data/lib/hanko_xml/signature_info.rb +134 -0
  30. data/lib/hanko_xml/signed_document.rb +155 -0
  31. data/lib/hanko_xml/signer.rb +572 -0
  32. data/lib/hanko_xml/template.rb +150 -0
  33. data/lib/hanko_xml/transform.rb +87 -0
  34. data/lib/hanko_xml/verification.rb +377 -0
  35. data/lib/hanko_xml/verifier.rb +694 -0
  36. data/lib/hanko_xml/version.rb +5 -0
  37. data/lib/hanko_xml/xades/verifier.rb +319 -0
  38. data/lib/hanko_xml/xades.rb +287 -0
  39. data/lib/hanko_xml/xml.rb +93 -0
  40. data/lib/hanko_xml/xmldsig-core-schema.xsd +332 -0
  41. data/lib/hanko_xml.rb +122 -0
  42. data/sig/hanko_xml/key.rbs +48 -0
  43. data/sig/hanko_xml/native.rbs +12 -0
  44. data/sig/hanko_xml/native_version.rbs +13 -0
  45. data/sig/hanko_xml/nokogiri_ext.rbs +23 -0
  46. data/sig/hanko_xml/placement.rbs +27 -0
  47. data/sig/hanko_xml/policy.rbs +83 -0
  48. data/sig/hanko_xml/reference.rbs +36 -0
  49. data/sig/hanko_xml/signed_document.rbs +66 -0
  50. data/sig/hanko_xml/signer.rbs +50 -0
  51. data/sig/hanko_xml/template.rbs +41 -0
  52. data/sig/hanko_xml/verification.rbs +100 -0
  53. data/sig/hanko_xml/verifier.rbs +45 -0
  54. data/sig/hanko_xml/xades.rbs +70 -0
  55. data/sig/hanko_xml.rbs +62 -0
  56. metadata +105 -0
@@ -0,0 +1,298 @@
1
+ use std::env;
2
+ use std::path::PathBuf;
3
+ use std::process::Command;
4
+
5
+ #[path = "build/vendor.rs"]
6
+ mod vendor;
7
+
8
+ /// Everything bindgen needs to parse the xmlsec/libxml2 headers, produced by
9
+ /// whichever of the two paths below ran. The defines matter as much as the
10
+ /// include paths: xmlsec's headers refuse to compile without a crypto backend
11
+ /// selected (`XMLSEC_CRYPTO_OPENSSL`), and the rest of the `-D` set records
12
+ /// which algorithms the library was actually built with.
13
+ pub struct Headers {
14
+ pub include_paths: Vec<PathBuf>,
15
+ pub defines: Vec<(String, Option<String>)>,
16
+ }
17
+
18
+ /// Homebrew installs several of our dependencies keg-only (not symlinked
19
+ /// into the default prefix), so their .pc files aren't on the default
20
+ /// pkg-config search path. Add every keg-only formula we depend on
21
+ /// ourselves, rather than requiring every contributor to export
22
+ /// PKG_CONFIG_PATH by hand.
23
+ ///
24
+ /// This matters for more than convenience: `xmlsec1-openssl.pc`'s own
25
+ /// `Libs:`/`Cflags:` lines reference libxml2 with a bare `-lxml2` and no
26
+ /// `-I`/`-L`, because pkg-config couldn't resolve libxml2's keg-only .pc
27
+ /// file either. Left alone, that makes our linker and bindgen fall back to
28
+ /// macOS's own ancient system libxml2 (`/usr/lib/libxml2.2.dylib`) instead
29
+ /// of the Homebrew libxml2 that `libxmlsec1-openssl.dylib` itself actually
30
+ /// links against — two different libxml2 builds in the same process,
31
+ /// silently handing structs allocated by one ABI to code compiled against
32
+ /// the other. So every formula below is probed explicitly, not left to
33
+ /// `Requires:` chasing.
34
+ fn add_homebrew_pkgconfig_paths(formulas: &[&str]) {
35
+ let mut paths = match env::var_os("PKG_CONFIG_PATH") {
36
+ Some(existing) => env::split_paths(&existing).collect::<Vec<_>>(),
37
+ None => Vec::new(),
38
+ };
39
+
40
+ for formula in formulas {
41
+ let Ok(output) = Command::new("brew").args(["--prefix", formula]).output() else {
42
+ continue;
43
+ };
44
+ if !output.status.success() {
45
+ continue;
46
+ }
47
+ let Ok(prefix) = String::from_utf8(output.stdout) else {
48
+ continue;
49
+ };
50
+ let pkgconfig_dir = PathBuf::from(prefix.trim()).join("lib/pkgconfig");
51
+ if pkgconfig_dir.is_dir() {
52
+ paths.push(pkgconfig_dir);
53
+ }
54
+ }
55
+
56
+ if let Ok(joined) = env::join_paths(paths) {
57
+ env::set_var("PKG_CONFIG_PATH", joined);
58
+ }
59
+ }
60
+
61
+ /// Links libxml2 by its exact, absolute dylib path instead of the ordinary
62
+ /// `-lxml2` + `-L<dir>` name search.
63
+ ///
64
+ /// Why: rustc always emits every `-l<name>` flag *before* any `-L<dir>`
65
+ /// search path in the linker invocation it generates (verified empirically
66
+ /// with a linker-trace wrapper: `-lxml2` lands at argument position ~11,
67
+ /// our own `-L` flags land at ~40+, no matter what order we print
68
+ /// `cargo:rustc-link-lib`/`cargo:rustc-link-search` from build.rs). ld64
69
+ /// resolves `-lxml2` using only the search paths it has seen *so far* at
70
+ /// that position — none of ours — so it silently falls back to its default
71
+ /// search path and picks up macOS's own ancient system libxml2
72
+ /// (`/usr/lib/libxml2.2.dylib`) instead of the Homebrew build that
73
+ /// `libxmlsec1-openssl.dylib` itself is linked against. That leaves our
74
+ /// direct calls to `xmlReadMemory`/`xmlFreeDoc`/`xmlDocGetRootElement`
75
+ /// bound to a *different* libxml2 ABI than the one xmlsec's own internal
76
+ /// calls (`xmlSecFindNode`, ID-attribute lookups, etc.) use on the exact
77
+ /// same `xmlDocPtr` — two different struct layouts for the same pointer.
78
+ /// A direct path reference isn't resolved by name search at all, so the
79
+ /// ordering problem above doesn't apply to it.
80
+ fn link_homebrew_libxml2_by_absolute_path(libxml2: &pkg_config::Library) {
81
+ let dylib_path = libxml2
82
+ .link_paths
83
+ .first()
84
+ .map(|dir| dir.join("libxml2.dylib"))
85
+ .filter(|path| path.exists())
86
+ .expect("could not locate libxml2.dylib in the Homebrew libxml2 keg");
87
+ println!("cargo:rustc-link-arg={}", dylib_path.display());
88
+ }
89
+
90
+ fn main() {
91
+ println!("cargo:rerun-if-env-changed=HANKO_XML_VENDOR");
92
+ println!("cargo:rerun-if-env-changed=RUBY_TARGET");
93
+ println!("cargo:rerun-if-env-changed=HANKO_XML_OPENSSL_PREFIX");
94
+ println!("cargo:rerun-if-changed=../../dependencies.yml");
95
+
96
+ // Two mutually exclusive ways to find libxmlsec1 and libxml2:
97
+ //
98
+ // vendored (HANKO_XML_VENDOR=1) -- build both from the pinned source in
99
+ // dependencies.yml and link them statically. Used by CI to produce the
100
+ // precompiled platform gems. Headers and archives then come from one
101
+ // source and cannot disagree.
102
+ //
103
+ // system (default) -- probe pkg-config, as below. This is what an
104
+ // ordinary `gem install` does, so the vendored path never runs on a
105
+ // user's machine unless they opt in.
106
+ //
107
+ // Everything downstream (the bindgen allowlist, the generated bindings) is
108
+ // shared; only the include and link paths differ.
109
+ let headers = if should_vendor() {
110
+ vendor::build()
111
+ } else {
112
+ system_libraries()
113
+ };
114
+
115
+ generate_bindings(&headers);
116
+ }
117
+
118
+ /// Whether to build libxml2 and xmlsec1 from the pinned source rather than
119
+ /// probing the system for them.
120
+ ///
121
+ /// `HANKO_XML_VENDOR` decides it explicitly when set (`0` or empty forces the
122
+ /// system path). Otherwise it follows `RUBY_TARGET`, which rake-compiler and
123
+ /// rb-sys set when building a precompiled platform gem.
124
+ ///
125
+ /// Keying off `RUBY_TARGET` rather than requiring the variable to be threaded
126
+ /// in is both more robust and more correct. More correct because a precompiled
127
+ /// gem ships to machines we know nothing about, so it has to carry its own
128
+ /// libxml2 and xmlsec1 — there is no system copy to rely on, and the build
129
+ /// host's copy is usually the wrong architecture anyway. More robust because
130
+ /// passing an environment variable into the rb-sys-dock container depends on
131
+ /// the `pre-script` input, which older releases of
132
+ /// `oxidize-rb/actions/cross-gem` do not have; when it is missing the build
133
+ /// silently falls back to the system path instead of failing loudly.
134
+ fn should_vendor() -> bool {
135
+ if let Ok(value) = env::var("HANKO_XML_VENDOR") {
136
+ return !(value.is_empty() || value == "0");
137
+ }
138
+ env::var_os("RUBY_TARGET").is_some()
139
+ }
140
+
141
+ /// Probes the system libxmlsec1/libxml2 and emits their link metadata.
142
+ fn system_libraries() -> Headers {
143
+ add_homebrew_pkgconfig_paths(&["libxmlsec1", "libxml2"]);
144
+
145
+ // We emit link metadata ourselves below instead of letting pkg-config
146
+ // auto-emit it, so that we can single out libxml2 for absolute-path
147
+ // linking (macOS only — see link_homebrew_libxml2_by_absolute_path).
148
+ let mut xmlsec_cfg = pkg_config::Config::new();
149
+ xmlsec_cfg.cargo_metadata(cfg!(not(target_os = "macos")));
150
+ let xmlsec = xmlsec_cfg.probe("xmlsec1-openssl").expect(
151
+ "xmlsec1-openssl not found via pkg-config. Install the xmlsec1 \
152
+ development package first (macOS: `brew install xmlsec1`, \
153
+ Debian/Ubuntu: `apt install libxmlsec1-dev`).",
154
+ );
155
+ // Probed for its own sake: makes sure our libxml2 linkage and headers
156
+ // resolve to the exact same build that xmlsec1-openssl.dylib links
157
+ // against, instead of silently falling back to the OS's libxml2. See
158
+ // the comment on add_homebrew_pkgconfig_paths above.
159
+ let mut libxml2_cfg = pkg_config::Config::new();
160
+ libxml2_cfg.cargo_metadata(cfg!(not(target_os = "macos")));
161
+ let libxml2 = libxml2_cfg.probe("libxml-2.0").expect(
162
+ "libxml2 not found via pkg-config. Install its development package \
163
+ first (macOS: `brew install libxml2`, Debian/Ubuntu: `apt install libxml2-dev`).",
164
+ );
165
+
166
+ if cfg!(target_os = "macos") {
167
+ for path in &xmlsec.link_paths {
168
+ println!("cargo:rustc-link-search=native={}", path.display());
169
+ }
170
+ for lib in &xmlsec.libs {
171
+ if lib != "xml2" {
172
+ println!("cargo:rustc-link-lib=dylib={lib}");
173
+ }
174
+ }
175
+ link_homebrew_libxml2_by_absolute_path(&libxml2);
176
+ }
177
+
178
+ // libxml2's include path goes first so its headers win over whatever
179
+ // libxml2 copy the system's default clang search path would otherwise
180
+ // find, keeping bindgen's struct layout in lockstep with the dylib we
181
+ // actually link.
182
+ Headers {
183
+ include_paths: libxml2
184
+ .include_paths
185
+ .iter()
186
+ .chain(&xmlsec.include_paths)
187
+ .cloned()
188
+ .collect(),
189
+ defines: libxml2
190
+ .defines
191
+ .iter()
192
+ .chain(&xmlsec.defines)
193
+ .map(|(name, value)| (name.clone(), value.clone()))
194
+ .collect(),
195
+ }
196
+ }
197
+
198
+ /// Runs bindgen over the xmlsec/libxml2 headers found at `include_paths` and
199
+ /// writes the result to OUT_DIR. Shared by both the vendored and system paths:
200
+ /// the allowlist and the generated surface must not depend on which one ran.
201
+ fn generate_bindings(headers: &Headers) {
202
+ let mut builder = bindgen::Builder::default()
203
+ .header_contents(
204
+ "hanko_xml_wrapper.h",
205
+ "#include <xmlsec/xmlsec.h>\n\
206
+ #include <xmlsec/crypto.h>\n\
207
+ #include <xmlsec/xmldsig.h>\n\
208
+ #include <xmlsec/xmltree.h>\n\
209
+ #include <xmlsec/errors.h>\n\
210
+ #include <xmlsec/keys.h>\n\
211
+ #include <xmlsec/keyinfo.h>\n\
212
+ #include <xmlsec/app.h>\n\
213
+ #include <xmlsec/io.h>\n\
214
+ #include <xmlsec/openssl/app.h>\n\
215
+ #include <xmlsec/openssl/crypto.h>\n\
216
+ #include <libxml/tree.h>\n\
217
+ #include <libxml/parser.h>\n\
218
+ #include <libxml/xmlerror.h>\n\
219
+ #include <libxml/xmlschemas.h>\n\
220
+ #include <libxml/valid.h>\n\
221
+ #include <libxml/xmlmemory.h>\n\
222
+ #include <libxml/xmlversion.h>\n",
223
+ )
224
+ .allowlist_function("xmlSec.*")
225
+ .allowlist_type("xmlSec.*")
226
+ .allowlist_var("xmlSec.*")
227
+ .allowlist_var("XMLSEC_.*")
228
+ .allowlist_var("LIBXML_.*")
229
+ // libxml2 parsing and document management
230
+ .allowlist_function("xmlReadMemory")
231
+ .allowlist_function("xmlFreeDoc")
232
+ .allowlist_function("xmlDocGetRootElement")
233
+ // libxml2 ID table lookup (same table xmlsec uses for URI="#id")
234
+ .allowlist_function("xmlGetID")
235
+ // libxml2 serialization (for sign → return signed bytes)
236
+ .allowlist_function("xmlDocDumpMemory")
237
+ .allowlist_function("xmlDocDumpFormatMemory")
238
+ // libxml2 node content and attribute extraction (for inspect)
239
+ .allowlist_function("xmlNodeGetContent")
240
+ .allowlist_function("xmlGetProp")
241
+ // In-scope namespaces of an XPath Filter 2.0 <XPath>, so Policy can
242
+ // check what its prefixes mean (#85)
243
+ .allowlist_function("xmlGetNsList")
244
+ .allowlist_function("__xmlFree")
245
+ .allowlist_function("xmlMemFree")
246
+ // libxml2 node/child traversal
247
+ .allowlist_function("xmlNodeListGetString")
248
+ .allowlist_type("xmlNode")
249
+ .allowlist_type("xmlDoc")
250
+ .allowlist_type("xmlAttr")
251
+ // Named directly: libxml2 2.15's prototypes (xmlGetID) stopped using
252
+ // the typedef, so no allowlisted function pulls it in there (#102)
253
+ .allowlist_type("xmlAttrPtr")
254
+ .allowlist_type("xmlNs")
255
+ .allowlist_type("xmlID")
256
+ // libxml2 error capture: keeps diagnostics off stderr, and lets the
257
+ // bytes path reject what a strict Nokogiri parse rejects (#102)
258
+ .allowlist_function("xmlSetStructuredErrorFunc")
259
+ .allowlist_type("xmlError")
260
+ .allowlist_type("xmlErrorLevel")
261
+ .allowlist_type("xmlParserOption")
262
+ // XSD validation of a <Signature>, for schema: :default without a
263
+ // DOM (#102). A copy of the element is validated, as Nokogiri did.
264
+ .allowlist_function("xmlNewDoc")
265
+ .allowlist_function("xmlDocCopyNode")
266
+ .allowlist_function("xmlDocSetRootElement")
267
+ .allowlist_function("xmlSchemaNewParserCtxt")
268
+ .allowlist_function("xmlSchemaSetParserStructuredErrors")
269
+ .allowlist_function("xmlSchemaParse")
270
+ .allowlist_function("xmlSchemaFreeParserCtxt")
271
+ .allowlist_function("xmlSchemaFree")
272
+ .allowlist_function("xmlSchemaNewValidCtxt")
273
+ .allowlist_function("xmlSchemaSetValidStructuredErrors")
274
+ .allowlist_function("xmlSchemaValidateDoc")
275
+ .allowlist_function("xmlSchemaFreeValidCtxt")
276
+ .derive_default(true)
277
+ .generate_comments(false)
278
+ .parse_callbacks(Box::new(bindgen::CargoCallbacks::new()));
279
+
280
+ for include_path in &headers.include_paths {
281
+ builder = builder.clang_arg(format!("-I{}", include_path.display()));
282
+ }
283
+ for (name, value) in &headers.defines {
284
+ builder = builder.clang_arg(match value {
285
+ Some(v) => format!("-D{name}={v}"),
286
+ None => format!("-D{name}"),
287
+ });
288
+ }
289
+
290
+ let bindings = builder
291
+ .generate()
292
+ .expect("failed to generate xmlsec bindgen bindings");
293
+
294
+ let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
295
+ bindings
296
+ .write_to_file(out_path.join("bindings.rs"))
297
+ .expect("failed to write xmlsec bindgen bindings");
298
+ }
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("hanko_xml/hanko_xml")