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 +4 -4
- data/CHANGELOG.md +66 -0
- data/Cargo.lock +1 -1
- data/README.md +2 -1
- data/ext/hanko_xml/Cargo.toml +1 -1
- data/ext/hanko_xml/src/dsig.rs +220 -0
- data/ext/hanko_xml/src/lib.rs +90 -1
- data/lib/hanko_xml/signer.rb +4 -1
- data/lib/hanko_xml/verifier.rb +10 -3
- data/lib/hanko_xml/version.rb +1 -1
- data/lib/hanko_xml/xades/augmenter.rb +248 -0
- data/lib/hanko_xml/xades/locator.rb +67 -0
- data/lib/hanko_xml/xades/long_term.rb +252 -0
- data/lib/hanko_xml/xades/timestamp.rb +386 -0
- data/lib/hanko_xml/xades/validation_data.rb +115 -0
- data/lib/hanko_xml/xades/verifier.rb +311 -37
- data/lib/hanko_xml/xades.rb +93 -6
- data/sig/hanko_xml/native.rbs +2 -0
- data/sig/hanko_xml/xades.rbs +91 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 241e74065f571d268cad6511824328bc09678c65b016fef414a7ac6998007624
|
|
4
|
+
data.tar.gz: be22bc7e10619a4dbfc10df005f9d9b42dde710037482237f8b0306c837b8722
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
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
|
|
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
|
|
data/ext/hanko_xml/Cargo.toml
CHANGED
data/ext/hanko_xml/src/dsig.rs
CHANGED
|
@@ -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
|
|
data/ext/hanko_xml/src/lib.rs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//! Magnus glue:
|
|
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
|
}
|
data/lib/hanko_xml/signer.rb
CHANGED
|
@@ -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
|
-
|
|
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"])
|
data/lib/hanko_xml/verifier.rb
CHANGED
|
@@ -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
|
data/lib/hanko_xml/version.rb
CHANGED