hanko_xml 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c179727024902245d166e6754fbc184637057182cc1c8e44eab643b0c69bb344
4
- data.tar.gz: 50a2ec96ba76f3bb2888639ea18f730c95e1e8c0b21b83d8223716ec7054693b
3
+ metadata.gz: 241e74065f571d268cad6511824328bc09678c65b016fef414a7ac6998007624
4
+ data.tar.gz: be22bc7e10619a4dbfc10df005f9d9b42dde710037482237f8b0306c837b8722
5
5
  SHA512:
6
- metadata.gz: f6f5636282fbce016d2409cf7f2327259c16ec5ff3c9b2b6983656fdab9d6984828be47cc07e61001f2f941a44e58bc142d585e608e9cf0b1ec1d07f59b3bc92
7
- data.tar.gz: d4737956455e23fc4edf6ff830d1765402e926b3976247f1c58089a91f0019a447ad57f0337c633e2994522efbddd623201fe238a8bb95a663e66a621b96ef31
6
+ metadata.gz: aaffd58bef9c74f6ae5f7c08a9f48cb1bbf0799b3e4ed16566875fd89056c4852a655bf2a71d95b38803d6f145d45219c530875a790d07aa4b1e7aee03a097fd
7
+ data.tar.gz: e3121dfe4e80797e154cee91b18cedb4dbbdced16a5ea6abdddd2d36ac593718b838c596186b4e73b20954b270b4a2bea9f0b5c6367b4c554059f3e79562b8ca
data/CHANGELOG.md CHANGED
@@ -8,6 +8,72 @@ tracked separately, and is reported by `HankoXml::NativeVersion.build_versions`.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.2.0] - 2026-10-19
12
+
13
+ ### Added
14
+
15
+ - **#162:** XAdES-T signing. `Xades::Signer.new(timestamper: ->(request_der) { response_der })`
16
+ adds a `<xades:SignatureTimeStamp>`: an RFC 3161 token over the
17
+ `<ds:SignatureValue>`, canonicalized by xmlsec (#161). The callable does the
18
+ TSA exchange, so the library never opens a connection. The response must
19
+ grant the timestamp and carry the request's imprint and nonce, otherwise
20
+ signing raises `SigningError`. `timestamp_digest_method:` (default: the
21
+ Signer's `digest_method:`) and `timestamp_canonicalization_method:` (default
22
+ `:exc_c14n`) go through the Signer's policy.
23
+ - **#163:** XAdES-T verification. `Xades::Verifier` checks every
24
+ `<xades:SignatureTimeStamp>` of the signature against the TSA trust in
25
+ `tsa_store:` or `tsa_certificates:`, which is kept separate from the signer's.
26
+ OpenSSL verifies the token, and its imprint must cover the `<ds:SignatureValue>`
27
+ as xmlsec canonicalizes it. `Xades::Verification#signature_timestamps` lists
28
+ them (time, serial, TSA certificate) and `#timestamp_time` gives the earliest.
29
+ `timestamps: :require` also fails a signature without one (S-22).
30
+ - **#167:** XAdES-LT validation data. `Xades::Augmenter` adds unsigned
31
+ properties to an existing, signed XAdES signature: a `SignatureTimeStamp`
32
+ (`timestamper:`, B to T) and `CertificateValues` / `RevocationValues`
33
+ (`validation_data:`, T to LT), leaving everything signed byte-identical.
34
+ `Xades::Signer` takes `validation_data:` too, with `timestamper:`.
35
+ `Xades::ValidationData` holds caller-supplied certificates, CRLs and OCSP
36
+ responses, type-checked only; the library never fetches them, and nothing
37
+ embedded is trusted yet (#168).
38
+ - **#168:** XAdES-LT verification. When a signature embeds `CertificateValues`
39
+ or `RevocationValues`, `Xades::Verifier` validates the signing certificate
40
+ at the time of its earliest verified SignatureTimeStamp: XML-DSig trust at
41
+ that time (an expired certificate still verifies; embedded certificates may
42
+ complete a `store:` chain), then embedded OCSP responses or CRLs for every
43
+ certificate below the anchor, verified by OpenSSL against the same anchor
44
+ and issued no earlier than that time (`revocation_grace:` widens it).
45
+ `long_term: :verify | :require | :ignore`; the result is
46
+ `Xades::Verification#long_term_validation`. Fail closed (S-23).
47
+ - **#169:** XAdES-LTA. `Xades::Augmenter.new(timestamper:, archive_timestamp: true)`
48
+ appends a `<xades141:ArchiveTimeStamp>` over the EN 319 132-1 input (the
49
+ References' data as xmlsec digested it, SignedInfo, SignatureValue, KeyInfo,
50
+ every unsigned property before it and the other `ds:Object`s); running it
51
+ again renews it. `Xades::Verifier` verifies every ArchiveTimeStamp against
52
+ the TSA trust, failing closed, and lists them in
53
+ `Xades::Verification#archive_timestamps`. `Native.reference_data` returns
54
+ each Reference's digested octets from xmlsec. SignXML's DSS-produced
55
+ HU_POL-3 ArchiveTimeStamp verifies.
56
+
57
+ ### Changed
58
+
59
+ - **#163:** `Xades::Verifier` no longer ignores unsigned SignatureTimeStamps.
60
+ A signature that carries one now fails verification unless the timestamp
61
+ verifies, which needs `tsa_store:` or `tsa_certificates:`. Pass
62
+ `timestamps: :ignore` to keep the XAdES-B-only behavior.
63
+ - **#168:** `Xades::Verifier` no longer ignores embedded `CertificateValues` /
64
+ `RevocationValues`. A signature that carries them is validated at its
65
+ timestamp time and fails when that evidence does not hold, which can also
66
+ make a signature fail that verified before (and one whose certificate has
67
+ expired pass). Pass `long_term: :ignore` to keep the XAdES-T behavior.
68
+ - **#169:** `Xades::Verifier` no longer ignores `xades141:ArchiveTimeStamp`s:
69
+ one that does not verify, or one present without TSA trust, fails the
70
+ signature. `timestamps: :ignore` leaves them unread.
71
+
72
+ ### Fixed
73
+
74
+ - **#158:** Without Nokogiri, `Signer#sign!` and `Signer#template` given a String
75
+ now raise `NokogiriRequired` naming themselves instead of `HankoXml.parse`.
76
+
11
77
  ## [0.1.0] - 2026-10-15
12
78
 
13
79
  First release.
data/Cargo.lock CHANGED
@@ -138,7 +138,7 @@ checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
138
138
 
139
139
  [[package]]
140
140
  name = "hanko_xml"
141
- version = "0.1.0"
141
+ version = "0.2.0"
142
142
  dependencies = [
143
143
  "bindgen 0.73.1",
144
144
  "magnus",
data/README.md CHANGED
@@ -18,9 +18,10 @@ It works nicely with Nokogiri, signing and verifying the documents you already h
18
18
  - **Full XML-DSig Support:** Sign and verify Enveloped, Enveloping, and Detached signatures.
19
19
  - **Nokogiri Native, Nokogiri Optional:** Signs and verifies the Nokogiri documents you already hold, with opt-in `doc.sign!` / `doc.signature_valid?` shorthands (`require "hanko_xml/nokogiri_ext"`), and verifies Strings, files and IOs without Nokogiri.
20
20
  - **XSW Defense:** Provides a robust `Policy` engine and structural `signed?(node)` checks to defeat XML Signature Wrapping (XSW) attacks.
21
+ - **XAdES, B to LTA:** `HankoXml::Xades::Signer`, `Augmenter` and `Verifier` sign, timestamp (RFC 3161, through a TSA exchange you supply), embed validation data and add renewable archive timestamps, and verify all of it fail closed. See the [XAdES guide](docs/src/content/guide/xades.md).
21
22
  - **Rust-Backed:** Memory-safe FFI boundaries, zero shell-outs to CLI binaries, and massive performance gains over pure-Ruby cryptography.
22
23
 
23
- A feature and security comparison with Python [SignXML](https://pypi.org/project/signxml/), including a plan to close the remaining gaps (XAdES first), lives in [`docs/src/content/guide/comparison-signxml.md`](docs/src/content/guide/comparison-signxml.md).
24
+ A feature and security comparison with Python [SignXML](https://pypi.org/project/signxml/), including the plan that closed the XAdES gap, lives in [`docs/src/content/guide/comparison-signxml.md`](docs/src/content/guide/comparison-signxml.md).
24
25
 
25
26
  ## Requirements
26
27
 
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "hanko_xml"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  edition = "2021"
5
5
  authors = ["Danimar Ribeiro <danimaribeiro@gmail.com>"]
6
6
  license = "MIT"
@@ -647,6 +647,18 @@ impl<'mngr> DsigCtx<'mngr> {
647
647
  }
648
648
  }
649
649
 
650
+ /// Keeps each SignedInfo Reference's transform output, the octets
651
+ /// xmlsec digested, for `reference_octets` (XAdES ArchiveTimeStamp,
652
+ /// #169).
653
+ fn store_references(&mut self) {
654
+ // SAFETY: self.ptr is a live xmlSecDSigCtx we exclusively own; flags
655
+ // is application-settable and read when each Reference context is
656
+ // initialized. Same ABI caveat as the signKey write.
657
+ unsafe {
658
+ (*self.ptr).flags |= sys::XMLSEC_DSIG_FLAGS_STORE_SIGNEDINFO_REFERENCES;
659
+ }
660
+ }
661
+
650
662
  /// By default xmlsec also resolves and digests the references inside
651
663
  /// every `<Object><Manifest>`, whose URIs, transforms and digest methods
652
664
  /// never reach Ruby Policy, which reads only `<SignedInfo>` (#75). XML-DSig
@@ -933,6 +945,45 @@ impl<'mngr> DsigCtx<'mngr> {
933
945
  }
934
946
  }
935
947
 
948
+ impl DsigCtx<'_> {
949
+ /// Per SignedInfo `<Reference>`, in document order, the octets xmlsec
950
+ /// digested (its pre-digest buffer), after `verify()` ran with
951
+ /// `store_references()`. `None` for a reference it did not process.
952
+ fn reference_octets(&self) -> Vec<Option<Vec<u8>>> {
953
+ // SAFETY: as in reference_statuses: signedInfoReferences is an
954
+ // embedded xmlSecPtrList that verify() populated.
955
+ let list = unsafe { ptr::addr_of!((*self.ptr).signedInfoReferences) as sys::xmlSecPtrListPtr };
956
+ // SAFETY: list is a valid, initialized xmlSecPtrList for self's lifetime.
957
+ let size = unsafe { sys::xmlSecPtrListGetSize(list) };
958
+ (0..size)
959
+ .map(|pos| {
960
+ // SAFETY: pos is in [0, size).
961
+ let item = unsafe { sys::xmlSecPtrListGetItem(list, pos) };
962
+ if item.is_null() {
963
+ return None;
964
+ }
965
+ // SAFETY: item is a live xmlSecDSigReferenceCtx owned by the
966
+ // list; the buffer it returns (or null) lives as long as it.
967
+ let buffer = unsafe {
968
+ sys::xmlSecDSigReferenceCtxGetPreDigestBuffer(item as sys::xmlSecDSigReferenceCtxPtr)
969
+ };
970
+ if buffer.is_null() {
971
+ return None;
972
+ }
973
+ // SAFETY: buffer is a valid xmlSecBuffer; data points at size
974
+ // initialized bytes (or is null when size is 0).
975
+ let (data, len) = unsafe { (sys::xmlSecBufferGetData(buffer), sys::xmlSecBufferGetSize(buffer)) };
976
+ let len = usize::try_from(len).ok()?;
977
+ if data.is_null() || len == 0 {
978
+ return Some(Vec::new());
979
+ }
980
+ // SAFETY: data is non-null and points at len bytes owned by buffer.
981
+ Some(unsafe { std::slice::from_raw_parts(data, len) }.to_vec())
982
+ })
983
+ .collect()
984
+ }
985
+ }
986
+
936
987
  impl Drop for DsigCtx<'_> {
937
988
  fn drop(&mut self) {
938
989
  // SAFETY: we're the exclusive owner of this context.
@@ -1027,6 +1078,9 @@ pub struct VerifyResult {
1027
1078
  /// (`Verification#certificate`), which `certificates:` otherwise leaves
1028
1079
  /// ambiguous.
1029
1080
  pub trusted_index: Option<usize>,
1081
+ /// Per SignedInfo `<Reference>`, the octets xmlsec digested, when
1082
+ /// `verify_document` was asked to store them; empty otherwise (#169).
1083
+ pub reference_octets: Vec<Option<Vec<u8>>>,
1030
1084
  }
1031
1085
 
1032
1086
  const DSIG_NS: &str = "http://www.w3.org/2000/09/xmldsig#";
@@ -1372,6 +1426,158 @@ pub fn validate_signature_schema(
1372
1426
  result
1373
1427
  }
1374
1428
 
1429
+ // ---------------------------------------------------------------------------
1430
+ // Element canonicalization (#161)
1431
+ // ---------------------------------------------------------------------------
1432
+
1433
+ /// One step from an element to one of its child elements: namespace href,
1434
+ /// local name, and which of the children with that name. `None` means there
1435
+ /// must be exactly one.
1436
+ pub type ChildStep = (String, String, Option<usize>);
1437
+
1438
+ /// The only transforms a [`TransformCtx`] reads: the six canonicalization
1439
+ /// methods `restrict_transforms` also allows.
1440
+ const C14N_TRANSFORMS: &[unsafe extern "C" fn() -> sys::xmlSecTransformId] = &[
1441
+ sys::xmlSecTransformInclC14NGetKlass,
1442
+ sys::xmlSecTransformInclC14NWithCommentsGetKlass,
1443
+ sys::xmlSecTransformInclC14N11GetKlass,
1444
+ sys::xmlSecTransformInclC14N11WithCommentsGetKlass,
1445
+ sys::xmlSecTransformExclC14NGetKlass,
1446
+ sys::xmlSecTransformExclC14NWithCommentsGetKlass,
1447
+ ];
1448
+
1449
+ /// An owned xmlsec transforms chain that accepts only the C14N transforms.
1450
+ struct TransformCtx(sys::xmlSecTransformCtxPtr);
1451
+
1452
+ impl TransformCtx {
1453
+ fn c14n_only() -> Result<Self, &'static str> {
1454
+ // SAFETY: creates and initializes a context we own; freed on drop.
1455
+ let ptr = unsafe { sys::xmlSecTransformCtxCreate() };
1456
+ if ptr.is_null() {
1457
+ return Err("failed to create xmlsec transform context");
1458
+ }
1459
+ let ctx = Self(ptr);
1460
+ for get_klass in C14N_TRANSFORMS {
1461
+ // SAFETY: ctx.0 is live and its enabledTransforms list was
1462
+ // initialized by xmlSecTransformCtxCreate. A non-empty list makes
1463
+ // xmlSecTransformNodeRead refuse any transform not in it, which is
1464
+ // how xmlSecDSigCtxEnableReferenceTransform restricts a DSig context.
1465
+ let rc = unsafe {
1466
+ sys::xmlSecPtrListAdd(&mut (*ctx.0).enabledTransforms, get_klass() as *mut c_void)
1467
+ };
1468
+ if rc < 0 {
1469
+ return Err("failed to enable a canonicalization transform");
1470
+ }
1471
+ }
1472
+ Ok(ctx)
1473
+ }
1474
+ }
1475
+
1476
+ impl Drop for TransformCtx {
1477
+ fn drop(&mut self) {
1478
+ // SAFETY: we're the exclusive owner of this context.
1479
+ unsafe { sys::xmlSecTransformCtxDestroy(self.0) }
1480
+ }
1481
+ }
1482
+
1483
+ /// An xmlsec node set, destroyed on drop. xmlSecTransformCtxXmlExecute
1484
+ /// leaves the set to its caller.
1485
+ struct NodeSet(sys::xmlSecNodeSetPtr);
1486
+
1487
+ impl Drop for NodeSet {
1488
+ fn drop(&mut self) {
1489
+ // SAFETY: we're the exclusive owner of this node set.
1490
+ unsafe { sys::xmlSecNodeSetDestroy(self.0) }
1491
+ }
1492
+ }
1493
+
1494
+ /// Follows `path` from `start`, one child element per step. A step with no
1495
+ /// index fails unless exactly one child matches, so an ambiguous document is
1496
+ /// refused instead of resolved to its first match.
1497
+ fn follow_path(start: sys::xmlNodePtr, path: &[ChildStep]) -> Result<sys::xmlNodePtr, DsigError> {
1498
+ let mut node = start;
1499
+ for (ns_href, name, index) in path {
1500
+ // SAFETY: node is a valid element of the document being walked.
1501
+ let matches = unsafe { find_children(node, name, ns_href) };
1502
+ node = match index {
1503
+ None if matches.len() == 1 => matches[0],
1504
+ None => return Err(DsigError::native("a canonicalize path step does not match exactly one element")),
1505
+ Some(i) => *matches
1506
+ .get(*i)
1507
+ .ok_or(DsigError::native("a canonicalize path step index is out of range"))?,
1508
+ };
1509
+ }
1510
+ Ok(node)
1511
+ }
1512
+
1513
+ /// Canonicalizes one element of the nth `<ds:Signature>` with xmlsec's own
1514
+ /// C14N transforms (#161): the bytes a digest over that element covers.
1515
+ ///
1516
+ /// `target` and `method` are paths from the `<ds:Signature>`. `method` must
1517
+ /// lead to a `CanonicalizationMethod`-shaped element: xmlsec reads its
1518
+ /// `Algorithm` and, for exclusive c14n, its `InclusiveNamespaces`, exactly as
1519
+ /// it reads a `<SignedInfo>`'s. Only the six c14n algorithms are accepted.
1520
+ /// Comments in the element are kept or dropped as the algorithm says.
1521
+ pub fn canonicalize_element(
1522
+ xml: &[u8],
1523
+ signature_index: usize,
1524
+ target: &[ChildStep],
1525
+ method: &[ChildStep],
1526
+ ) -> Result<Vec<u8>, DsigError> {
1527
+ errors::clear_errors();
1528
+
1529
+ let doc = OwnedXmlDoc::parse(xml).map_err(DsigError::parse)?;
1530
+ let root = doc
1531
+ .root_element()
1532
+ .ok_or(DsigError::parse("XML document has no root element"))?;
1533
+ let sig_node = doc
1534
+ .find_nth_signature(root, signature_index)
1535
+ .ok_or(DsigError::native("signature index out of range"))?;
1536
+ let target_node = follow_path(sig_node, target)?;
1537
+ let method_node = follow_path(sig_node, method)?;
1538
+
1539
+ errors::clear_errors();
1540
+ let ctx = TransformCtx::c14n_only().map_err(DsigError::native)?;
1541
+ // SAFETY: ctx.0 is a fresh context; method_node is valid in doc, which
1542
+ // outlives ctx. The transform read is appended to ctx and freed with it.
1543
+ let transform = unsafe {
1544
+ sys::xmlSecTransformCtxNodeRead(ctx.0, method_node, sys::xmlSecTransformUsageC14NMethod as _)
1545
+ };
1546
+ if transform.is_null() {
1547
+ return Err(DsigError::native("the canonicalization method is not a supported c14n algorithm"));
1548
+ }
1549
+
1550
+ // SAFETY: doc.0 and target_node are valid; withComments = 1 puts comments
1551
+ // in the set so the transform decides whether they are rendered.
1552
+ let nodes = unsafe { sys::xmlSecNodeSetGetChildren(doc.0, target_node, 1, 0) };
1553
+ if nodes.is_null() {
1554
+ return Err(DsigError::native("failed to select the element to canonicalize"));
1555
+ }
1556
+ let nodes = NodeSet(nodes);
1557
+
1558
+ // SAFETY: ctx.0 has its one c14n transform and no URI; nodes belongs to
1559
+ // doc. The result buffer is owned by ctx.
1560
+ let rc = unsafe { sys::xmlSecTransformCtxXmlExecute(ctx.0, nodes.0) };
1561
+ // SAFETY: ctx.0 is live; result is set by a successful execute.
1562
+ let result = unsafe { (*ctx.0).result };
1563
+ if rc < 0 || result.is_null() {
1564
+ return Err(DsigError::native("canonicalization failed"));
1565
+ }
1566
+
1567
+ // SAFETY: result is a live xmlSecBuffer owned by ctx; its data is size
1568
+ // bytes long. The bytes are copied out before ctx is dropped.
1569
+ let (data, size) = unsafe { (sys::xmlSecBufferGetData(result), sys::xmlSecBufferGetSize(result)) };
1570
+ let size = usize::try_from(size).map_err(|_| DsigError::native("canonicalization failed"))?;
1571
+ if size == 0 {
1572
+ return Ok(Vec::new());
1573
+ }
1574
+ if data.is_null() {
1575
+ return Err(DsigError::native("canonicalization failed"));
1576
+ }
1577
+ // SAFETY: see above.
1578
+ Ok(unsafe { std::slice::from_raw_parts(data as *const u8, size) }.to_vec())
1579
+ }
1580
+
1375
1581
  /// Signs the document's nth `<ds:Signature>` template.
1376
1582
  ///
1377
1583
  /// The XML must already contain a `<ds:Signature>` skeleton with empty
@@ -1428,6 +1634,10 @@ pub fn sign_document(
1428
1634
  /// Returns a `VerifyResult` with status and captured error messages.
1429
1635
  /// Only raises (returns `Err`) for conditions that are not a verification
1430
1636
  /// verdict (unparseable input, out-of-range index, unreadable key).
1637
+ #[allow(
1638
+ clippy::too_many_arguments,
1639
+ reason = "one positional argument per Native.verify parameter, plus store_references (#169)"
1640
+ )]
1431
1641
  pub fn verify_document(
1432
1642
  xml: &[u8],
1433
1643
  trusted_pems: &[Vec<u8>],
@@ -1436,6 +1646,7 @@ pub fn verify_document(
1436
1646
  signature_index: usize,
1437
1647
  referenced_documents: HashMap<Vec<u8>, Vec<u8>>,
1438
1648
  allow_xpath_filter2: bool,
1649
+ store_references: bool,
1439
1650
  ) -> Result<VerifyResult, DsigError> {
1440
1651
  errors::clear_errors();
1441
1652
  let serves_external = !referenced_documents.is_empty();
@@ -1489,6 +1700,7 @@ pub fn verify_document(
1489
1700
  reference_valid: vec![None; reference_element_indexes.len()],
1490
1701
  reference_element_indexes,
1491
1702
  trusted_index: None,
1703
+ reference_octets: Vec::new(),
1492
1704
  });
1493
1705
  }
1494
1706
 
@@ -1496,6 +1708,7 @@ pub fn verify_document(
1496
1708
  let mut captured_errors = Vec::new();
1497
1709
  let mut reference_valid = Vec::new();
1498
1710
  let mut trusted_index = None;
1711
+ let mut reference_octets = Vec::new();
1499
1712
  for (key, index) in keys_to_try.into_iter().zip(tried_indexes) {
1500
1713
  errors::clear_errors();
1501
1714
  let mut ctx = DsigCtx::for_signing(&mngr, key).map_err(DsigError::native)?;
@@ -1505,11 +1718,17 @@ pub fn verify_document(
1505
1718
  if allow_xpath_filter2 {
1506
1719
  ctx.enable_xpath_filter2().map_err(DsigError::native)?;
1507
1720
  }
1721
+ if store_references {
1722
+ ctx.store_references();
1723
+ }
1508
1724
  status = ctx.verify(sig_node);
1509
1725
  captured_errors = errors::drain_errors();
1510
1726
  reference_valid = ctx.reference_statuses();
1511
1727
  if status == VerifyStatus::Succeeded {
1512
1728
  trusted_index = Some(index);
1729
+ if store_references {
1730
+ reference_octets = ctx.reference_octets();
1731
+ }
1513
1732
  break;
1514
1733
  }
1515
1734
  }
@@ -1520,6 +1739,7 @@ pub fn verify_document(
1520
1739
  reference_element_indexes,
1521
1740
  reference_valid,
1522
1741
  trusted_index,
1742
+ reference_octets,
1523
1743
  })
1524
1744
  }
1525
1745
 
@@ -1,4 +1,4 @@
1
- //! Magnus glue: 5 functions on `HankoXml::Native`, matching the §7 contract.
1
+ //! Magnus glue: the functions on `HankoXml::Native`, matching the §7 contract.
2
2
  //!
3
3
  //! Every argument from Ruby is copied into a Rust-owned `Vec<u8>` or `String`
4
4
  //! **before** any native call, so there is no `unsafe` in this file — no
@@ -124,6 +124,18 @@ fn rarray_to_strings(ruby: &Ruby, arr: RArray) -> Result<Vec<String>, Error> {
124
124
  Ok(result)
125
125
  }
126
126
 
127
+ /// Extracts an RArray of `[ns_href, local_name, index]` steps (`index` nil
128
+ /// or an Integer) for [`dsig::canonicalize_element`].
129
+ fn rarray_to_steps(ruby: &Ruby, arr: RArray) -> Result<Vec<dsig::ChildStep>, Error> {
130
+ let mut result = Vec::new();
131
+ for item in arr.into_iter() {
132
+ let step: (String, String, Option<usize>) = TryConvert::try_convert(item)
133
+ .map_err(|_| native_error(ruby, "path step is not a [ns_href, name, index] triple".into()))?;
134
+ result.push(step);
135
+ }
136
+ Ok(result)
137
+ }
138
+
127
139
  // ---------------------------------------------------------------------------
128
140
  // Native functions — the §7 contract
129
141
  // ---------------------------------------------------------------------------
@@ -201,6 +213,7 @@ fn native_verify(
201
213
  signature_index,
202
214
  referenced,
203
215
  allow_xpath_filter2,
216
+ false,
204
217
  ) {
205
218
  Ok(result) => {
206
219
  let hash = ruby.hash_new();
@@ -308,6 +321,80 @@ fn native_validate_schema(
308
321
  string_slice_to_rarray(ruby, &messages)
309
322
  }
310
323
 
324
+ /// `HankoXml::Native.canonicalize(xml, signature_index, target_path, method_path)`
325
+ ///
326
+ /// Returns the element at `target_path` (from the nth `<ds:Signature>`)
327
+ /// canonicalized by xmlsec with the `CanonicalizationMethod` at
328
+ /// `method_path`, as a binary String (#161).
329
+ fn native_canonicalize(
330
+ ruby: &Ruby,
331
+ xml: RString,
332
+ signature_index: usize,
333
+ target_path: RArray,
334
+ method_path: RArray,
335
+ ) -> Result<RString, Error> {
336
+ ensure_initialized().map_err(|msg| native_error(ruby, msg.into()))?;
337
+
338
+ let xml_bytes = rstring_to_bytes(xml);
339
+ let target = rarray_to_steps(ruby, target_path)?;
340
+ let method = rarray_to_steps(ruby, method_path)?;
341
+ let bytes = dsig::canonicalize_element(&xml_bytes, signature_index, &target, &method)
342
+ .map_err(|err| dsig_error(ruby, err))?;
343
+ Ok(ruby.str_from_slice(&bytes))
344
+ }
345
+
346
+ /// `HankoXml::Native.reference_data(xml, trusted_pems, id_attributes, signature_index, referenced_documents,
347
+ /// allow_xpath_filter2)`
348
+ ///
349
+ /// Verifies the nth `<ds:Signature>` as `verify` does and returns, per
350
+ /// SignedInfo `<Reference>` in document order, the octets xmlsec digested
351
+ /// (String, or nil when it did not process that reference). Raises
352
+ /// NativeError when the signature does not verify: those octets are only
353
+ /// worth anything for a signature that holds (#169).
354
+ fn native_reference_data(
355
+ ruby: &Ruby,
356
+ xml: RString,
357
+ trusted_pems: RArray,
358
+ id_attributes: RArray,
359
+ signature_index: usize,
360
+ referenced_documents: RArray,
361
+ allow_xpath_filter2: bool,
362
+ ) -> Result<RArray, Error> {
363
+ ensure_initialized().map_err(|msg| native_error(ruby, msg.into()))?;
364
+
365
+ let xml_bytes = rstring_to_bytes(xml);
366
+ let trusted = rarray_to_byte_vecs(ruby, trusted_pems)?;
367
+ let id_attrs = rarray_to_strings(ruby, id_attributes)?;
368
+ let referenced = rarray_to_byte_pairs(ruby, referenced_documents)?;
369
+
370
+ let result = dsig::verify_document(
371
+ &xml_bytes,
372
+ &trusted,
373
+ &[],
374
+ &id_attrs,
375
+ signature_index,
376
+ referenced,
377
+ allow_xpath_filter2,
378
+ true,
379
+ )
380
+ .map_err(|err| dsig_error(ruby, err))?;
381
+ if result.status != dsig::VerifyStatus::Succeeded {
382
+ return Err(native_error(ruby, "the signature does not verify, so its reference data is not read".into()));
383
+ }
384
+
385
+ let octets = ruby.ary_new();
386
+ for entry in &result.reference_octets {
387
+ let value = match entry {
388
+ Some(bytes) => ruby.str_from_slice(bytes).as_value(),
389
+ None => ruby.qnil().as_value(),
390
+ };
391
+ octets
392
+ .push(value)
393
+ .map_err(|e| native_error(ruby, format!("array error: {e}")))?;
394
+ }
395
+ Ok(octets)
396
+ }
397
+
311
398
  /// `HankoXml::Native.inspect_sig(xml, signature_index)`
312
399
  ///
313
400
  /// Returns metadata about the nth `<ds:Signature>` without performing
@@ -485,6 +572,8 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
485
572
  native.define_singleton_method("inspect_sig", function!(native_inspect, 2))?;
486
573
  native.define_singleton_method("scan", function!(native_scan, 3))?;
487
574
  native.define_singleton_method("validate_schema", function!(native_validate_schema, 3))?;
575
+ native.define_singleton_method("canonicalize", function!(native_canonicalize, 4))?;
576
+ native.define_singleton_method("reference_data", function!(native_reference_data, 6))?;
488
577
 
489
578
  Ok(())
490
579
  }
@@ -134,6 +134,7 @@ module HankoXml
134
134
  # it into +doc_or_xml+ and performs no cryptography -- for inspection
135
135
  # and testing (§5.2).
136
136
  def template(doc_or_xml)
137
+ Dom.require!("HankoXml::Signer#template")
137
138
  doc = HankoXml.coerce_document(doc_or_xml)
138
139
  HankoXml::Template.build(doc, self)
139
140
  end
@@ -185,8 +186,10 @@ module HankoXml
185
186
  node_signer = signer_for_node(doc_or_xml)
186
187
  return node_signer.sign!(doc_or_xml.document, force: force, signature: signature) if node_signer
187
188
 
188
- doc = HankoXml.coerce_document(doc_or_xml, policy: policy)
189
+ # Before coercing: a String would otherwise fail inside HankoXml.parse
190
+ # and name that instead of the method the caller used.
189
191
  Dom.require!("HankoXml::Signer#sign!")
192
+ doc = HankoXml.coerce_document(doc_or_xml, policy: policy)
190
193
 
191
194
  signatures, adopted = find_or_build_signatures(doc)
192
195
  all_sigs_in_order = doc.xpath(".//ds:Signature", ds: NS["ds"])
@@ -510,7 +510,7 @@ module HankoXml
510
510
  HankoXml::UntrustedCertificate.new("No X509Certificate found in KeyInfo for store verification")]
511
511
  end
512
512
 
513
- chain, error = verified_chain_from_store(embedded)
513
+ chain, error = verified_chain_from_store(embedded, chain_intermediates)
514
514
  leaf = chain&.first
515
515
  error ||= signing_certificate_violation(leaf)
516
516
  return [nil, nil, error] if error
@@ -549,9 +549,16 @@ module HankoXml
549
549
  sig_info.certificates.map { |pem| OpenSSL::X509::Certificate.new(pem) }
550
550
  end
551
551
 
552
+ # Untrusted certificates that may complete a store: chain besides those in
553
+ # <KeyInfo>; never candidates for the signing certificate. Xades::Verifier
554
+ # supplies a signature's embedded CertificateValues (#168).
555
+ def chain_intermediates
556
+ []
557
+ end
558
+
552
559
  # The chain OpenSSL built and verified for the one end-entity certificate
553
560
  # that chains to the store, leaf first, or an error.
554
- def verified_chain_from_store(certificates)
561
+ def verified_chain_from_store(certificates, intermediates = [])
555
562
  candidates = end_entity_certificates(certificates)
556
563
  candidates = certificates if candidates.empty?
557
564
 
@@ -559,7 +566,7 @@ module HankoXml
559
566
  last_error = "Certificate chain validation failed"
560
567
 
561
568
  candidates.each do |leaf|
562
- chain = certificates.reject { |cert| cert.equal?(leaf) }
569
+ chain = certificates.reject { |cert| cert.equal?(leaf) } + intermediates
563
570
  ctx = OpenSSL::X509::StoreContext.new(@store, leaf, chain)
564
571
  if @policy.check_certificate_validity
565
572
  ctx.time = @verification_time if @verification_time
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HankoXml
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end