hanko_xml 0.1.0-arm64-darwin
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/LICENSE.txt +21 -0
- data/README.md +222 -0
- data/THIRD_PARTY_LICENSES.md +392 -0
- data/dependencies.yml +87 -0
- data/lib/hanko_xml/3.1/hanko_xml.bundle +0 -0
- data/lib/hanko_xml/3.2/hanko_xml.bundle +0 -0
- data/lib/hanko_xml/3.3/hanko_xml.bundle +0 -0
- data/lib/hanko_xml/3.4/hanko_xml.bundle +0 -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 +104 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HankoXml
|
|
4
|
+
# Registry of algorithm URIs, keyed by short Symbol names, per slot
|
|
5
|
+
# (:canonicalization, :digest, :signature, :transform). See
|
|
6
|
+
# API_CONTRACT.md §5.7.
|
|
7
|
+
#
|
|
8
|
+
# Every algorithm slot elsewhere in the library accepts either a Symbol
|
|
9
|
+
# (looked up here) or a String URI (used verbatim, subject to Policy).
|
|
10
|
+
#
|
|
11
|
+
# Registration is process-global and additive. It does NOT grant use --
|
|
12
|
+
# a Policy allowlist must separately permit the algorithm. Built-in names
|
|
13
|
+
# and URIs cannot be re-pointed, even with replace: true (S-16).
|
|
14
|
+
module Algorithms # rubocop:disable Metrics/ModuleLength
|
|
15
|
+
Entry = Struct.new(:slot, :name, :uri, :secure, keyword_init: true) do
|
|
16
|
+
def secure?
|
|
17
|
+
!!secure
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Name/URI lookup for a single algorithm slot (e.g. :digest).
|
|
22
|
+
class Table
|
|
23
|
+
def initialize(slot)
|
|
24
|
+
@slot = slot
|
|
25
|
+
@by_name = {}
|
|
26
|
+
@by_uri = {}
|
|
27
|
+
@builtin_names = {}.freeze
|
|
28
|
+
@builtin_uris = {}.freeze
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def register(name, uri, secure:, replace: false)
|
|
32
|
+
name = name.to_sym
|
|
33
|
+
uri = uri.to_s
|
|
34
|
+
check_not_builtin!(name, uri)
|
|
35
|
+
|
|
36
|
+
if @by_name.key?(name) && !replace
|
|
37
|
+
raise ArgumentError,
|
|
38
|
+
"#{@slot} algorithm #{name.inspect} is already registered " \
|
|
39
|
+
"(pass replace: true to override)"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
previous = @by_name[name]
|
|
43
|
+
@by_uri.delete(previous.uri) if previous && @by_uri[previous.uri].equal?(previous)
|
|
44
|
+
|
|
45
|
+
entry = Entry.new(slot: @slot, name: name, uri: uri, secure: secure).freeze
|
|
46
|
+
@by_name[name] = entry
|
|
47
|
+
@by_uri[uri] = entry
|
|
48
|
+
entry
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Marks everything registered so far as built-in (S-16): those names
|
|
52
|
+
# and URIs are what Policy presets are written in terms of, so
|
|
53
|
+
# re-pointing one would silently change what, e.g., Policy.strict
|
|
54
|
+
# accepts for every caller in the process.
|
|
55
|
+
def seal_builtins!
|
|
56
|
+
@builtin_names = @by_name.keys.to_h { |name| [name, true] }.freeze
|
|
57
|
+
@builtin_uris = @by_uri.keys.to_h { |uri| [uri, true] }.freeze
|
|
58
|
+
self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def [](name_or_uri)
|
|
62
|
+
case name_or_uri
|
|
63
|
+
when Symbol
|
|
64
|
+
@by_name[name_or_uri]
|
|
65
|
+
when String
|
|
66
|
+
@by_uri[name_or_uri] || @by_name[name_or_uri.to_sym]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def uri_for(name_or_uri)
|
|
71
|
+
return name_or_uri if name_or_uri.is_a?(String) && !@by_name.key?(name_or_uri.to_sym)
|
|
72
|
+
|
|
73
|
+
entry = self[name_or_uri]
|
|
74
|
+
entry&.uri || (name_or_uri.is_a?(String) ? name_or_uri : nil)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def name_for(uri)
|
|
78
|
+
@by_uri[uri.to_s]&.name
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def secure?(name_or_uri)
|
|
82
|
+
entry = self[name_or_uri]
|
|
83
|
+
entry.nil? ? false : entry.secure?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def names
|
|
87
|
+
@by_name.keys
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def check_not_builtin!(name, uri)
|
|
93
|
+
if @builtin_names.key?(name)
|
|
94
|
+
raise ArgumentError, "#{@slot} algorithm #{name.inspect} is built in and cannot be replaced"
|
|
95
|
+
end
|
|
96
|
+
return unless @builtin_uris.key?(uri)
|
|
97
|
+
|
|
98
|
+
raise ArgumentError,
|
|
99
|
+
"#{@slot} URI #{uri} is built in as #{@by_uri[uri].name.inspect} and cannot be re-registered"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
class << self
|
|
104
|
+
def canonicalization
|
|
105
|
+
@canonicalization ||= build_canonicalization_table
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def digest
|
|
109
|
+
@digest ||= build_digest_table
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def signature
|
|
113
|
+
@signature ||= build_signature_table
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def transform
|
|
117
|
+
@transform ||= build_transform_table
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def register(slot, name, uri, secure:, replace: false)
|
|
121
|
+
table_for(slot).register(name, uri, secure: secure, replace: replace)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def uri_for(slot, name_or_uri)
|
|
125
|
+
table_for(slot).uri_for(name_or_uri)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def name_for(slot, uri)
|
|
129
|
+
table_for(slot).name_for(uri)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def secure?(slot, name_or_uri)
|
|
133
|
+
table_for(slot).secure?(name_or_uri)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def table_for(slot)
|
|
139
|
+
case slot.to_sym
|
|
140
|
+
when :canonicalization then canonicalization
|
|
141
|
+
when :digest then digest
|
|
142
|
+
when :signature then signature
|
|
143
|
+
when :transform then transform
|
|
144
|
+
else
|
|
145
|
+
raise ArgumentError, "unknown algorithm slot #{slot.inspect}"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def build_canonicalization_table
|
|
150
|
+
table = Table.new(:canonicalization)
|
|
151
|
+
table.register(:c14n, "http://www.w3.org/TR/2001/REC-xml-c14n-20010315", secure: true)
|
|
152
|
+
table.register(:c14n_with_comments,
|
|
153
|
+
"http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments", secure: true)
|
|
154
|
+
table.register(:exc_c14n, "http://www.w3.org/2001/10/xml-exc-c14n#", secure: true)
|
|
155
|
+
table.register(:exc_c14n_with_comments,
|
|
156
|
+
"http://www.w3.org/2001/10/xml-exc-c14n#WithComments", secure: true)
|
|
157
|
+
table.register(:c14n11, "http://www.w3.org/2006/12/xml-c14n11", secure: true)
|
|
158
|
+
table.register(:c14n11_with_comments,
|
|
159
|
+
"http://www.w3.org/2006/12/xml-c14n11#WithComments", secure: true)
|
|
160
|
+
table.seal_builtins!
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def build_digest_table
|
|
164
|
+
table = Table.new(:digest)
|
|
165
|
+
table.register(:sha1, "http://www.w3.org/2000/09/xmldsig#sha1", secure: false)
|
|
166
|
+
table.register(:sha224, "http://www.w3.org/2001/04/xmldsig-more#sha224", secure: true)
|
|
167
|
+
table.register(:sha256, "http://www.w3.org/2001/04/xmlenc#sha256", secure: true)
|
|
168
|
+
table.register(:sha384, "http://www.w3.org/2001/04/xmldsig-more#sha384", secure: true)
|
|
169
|
+
table.register(:sha512, "http://www.w3.org/2001/04/xmlenc#sha512", secure: true)
|
|
170
|
+
table.register(:ripemd160, "http://www.w3.org/2001/04/xmlenc#ripemd160", secure: false)
|
|
171
|
+
# RFC 6931 SHA-3. xmlsec 1.3 implements these; xmlsec 1.2 (Ubuntu CI)
|
|
172
|
+
# does not — Native.sign/verify then fails closed (#60).
|
|
173
|
+
table.register(:sha3_224, "http://www.w3.org/2007/05/xmldsig-more#sha3-224", secure: true)
|
|
174
|
+
table.register(:sha3_256, "http://www.w3.org/2007/05/xmldsig-more#sha3-256", secure: true)
|
|
175
|
+
table.register(:sha3_384, "http://www.w3.org/2007/05/xmldsig-more#sha3-384", secure: true)
|
|
176
|
+
table.register(:sha3_512, "http://www.w3.org/2007/05/xmldsig-more#sha3-512", secure: true)
|
|
177
|
+
table.seal_builtins!
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def build_signature_table
|
|
181
|
+
table = Table.new(:signature)
|
|
182
|
+
table.register(:rsa_sha1, "http://www.w3.org/2000/09/xmldsig#rsa-sha1", secure: false)
|
|
183
|
+
table.register(:rsa_sha224, "http://www.w3.org/2001/04/xmldsig-more#rsa-sha224", secure: true)
|
|
184
|
+
table.register(:rsa_sha256, "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", secure: true)
|
|
185
|
+
table.register(:rsa_sha384, "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384", secure: true)
|
|
186
|
+
table.register(:rsa_sha512, "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512", secure: true)
|
|
187
|
+
# RFC 6931 RSA-PSS (MGF1, default salt = hash length). xmlsec 1.3
|
|
188
|
+
# implements these without <RSAPSSParams>; xmlsec 1.2 does not (#60).
|
|
189
|
+
table.register(:rsa_pss_sha256, "http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1",
|
|
190
|
+
secure: true)
|
|
191
|
+
table.register(:rsa_pss_sha384, "http://www.w3.org/2007/05/xmldsig-more#sha384-rsa-MGF1",
|
|
192
|
+
secure: true)
|
|
193
|
+
table.register(:rsa_pss_sha512, "http://www.w3.org/2007/05/xmldsig-more#sha512-rsa-MGF1",
|
|
194
|
+
secure: true)
|
|
195
|
+
table.register(:ecdsa_sha256, "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256", secure: true)
|
|
196
|
+
table.register(:ecdsa_sha384, "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384", secure: true)
|
|
197
|
+
table.register(:ecdsa_sha512, "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512", secure: true)
|
|
198
|
+
# RFC 9231 put ECDSA-SHA3 under the 2021/04 namespace, not RFC 6931's
|
|
199
|
+
# 2007/05 one; xmlsec 1.3 uses the same URIs (#60).
|
|
200
|
+
table.register(:ecdsa_sha3_256, "http://www.w3.org/2021/04/xmldsig-more#ecdsa-sha3-256",
|
|
201
|
+
secure: true)
|
|
202
|
+
table.register(:ecdsa_sha3_384, "http://www.w3.org/2021/04/xmldsig-more#ecdsa-sha3-384",
|
|
203
|
+
secure: true)
|
|
204
|
+
table.register(:ecdsa_sha3_512, "http://www.w3.org/2021/04/xmldsig-more#ecdsa-sha3-512",
|
|
205
|
+
secure: true)
|
|
206
|
+
table.register(:dsa_sha1, "http://www.w3.org/2000/09/xmldsig#dsa-sha1", secure: false)
|
|
207
|
+
table.register(:hmac_sha256, "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256", secure: true)
|
|
208
|
+
table.seal_builtins!
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def build_transform_table
|
|
212
|
+
table = Table.new(:transform)
|
|
213
|
+
# Every canonicalization algorithm is also a valid transform.
|
|
214
|
+
canonicalization.names.each do |name|
|
|
215
|
+
entry = canonicalization[name]
|
|
216
|
+
table.register(entry.name, entry.uri, secure: entry.secure?)
|
|
217
|
+
end
|
|
218
|
+
table.register(:enveloped_signature, "http://www.w3.org/2000/09/xmldsig#enveloped-signature",
|
|
219
|
+
secure: true)
|
|
220
|
+
table.register(:base64, "http://www.w3.org/2000/09/xmldsig#base64", secure: true)
|
|
221
|
+
table.register(:xpath, "http://www.w3.org/TR/1999/REC-xpath-19991116", secure: false)
|
|
222
|
+
table.register(:xpath_filter2, "http://www.w3.org/2002/06/xmldsig-filter2", secure: false)
|
|
223
|
+
table.register(:xslt, "http://www.w3.org/TR/1999/REC-xslt-19991116", secure: false)
|
|
224
|
+
table.seal_builtins!
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HankoXml
|
|
4
|
+
# The single place that probes for, or calls into, Nokogiri. Nothing else
|
|
5
|
+
# in the library references the Nokogiri constant directly -- see
|
|
6
|
+
# API_CONTRACT.md §6.1. spec/hanko_xml/dom_spec.rb enforces this, and that
|
|
7
|
+
# nothing in lib/ ever requires Nokogiri (the caller decides load order,
|
|
8
|
+
# §3.1).
|
|
9
|
+
#
|
|
10
|
+
# The DOM operations below take no decision of their own: each is the one
|
|
11
|
+
# Nokogiri call its caller needs, kept here so a DOM-free path (#102) has a
|
|
12
|
+
# single seam to replace or guard.
|
|
13
|
+
#
|
|
14
|
+
# The rule: a missing Nokogiri may remove a capability, it may never weaken
|
|
15
|
+
# one. Every check that runs with Nokogiri present runs identically in its
|
|
16
|
+
# absence; the capabilities that disappear are the ones that cannot be
|
|
17
|
+
# expressed at all without a DOM, and each of them raises
|
|
18
|
+
# HankoXml::NokogiriRequired rather than quietly returning a less careful
|
|
19
|
+
# answer.
|
|
20
|
+
#
|
|
21
|
+
# @api private
|
|
22
|
+
module Dom
|
|
23
|
+
class << self
|
|
24
|
+
def available?
|
|
25
|
+
defined?(::Nokogiri::XML::Document) ? true : false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def document?(obj)
|
|
29
|
+
available? && obj.is_a?(::Nokogiri::XML::Document)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def node?(obj)
|
|
33
|
+
available? && obj.is_a?(::Nokogiri::XML::Node)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def certificate?(obj)
|
|
37
|
+
defined?(::OpenSSL::X509::Certificate) && obj.is_a?(::OpenSSL::X509::Certificate)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def pkey?(obj)
|
|
41
|
+
defined?(::OpenSSL::PKey::PKey) && obj.is_a?(::OpenSSL::PKey::PKey)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Raises HankoXml::NokogiriRequired, naming +capability+ and the fix,
|
|
45
|
+
# unless Nokogiri is loaded.
|
|
46
|
+
def require!(capability)
|
|
47
|
+
return if available?
|
|
48
|
+
|
|
49
|
+
raise HankoXml::NokogiriRequired,
|
|
50
|
+
"#{capability} needs a DOM, and Nokogiri is not loaded. Add `gem \"nokogiri\"` to your " \
|
|
51
|
+
"Gemfile and `require \"nokogiri\"` before using it. Verification, signature_present? and " \
|
|
52
|
+
"signing an existing <Signature> template work without it (API_CONTRACT.md §6.1)."
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Parses +source+ strict (no RECOVER) and nonet: the one way that does
|
|
56
|
+
# not break XML-DSig (API_CONTRACT.md §6.3). Returns the document and
|
|
57
|
+
# libxml2's errors, or nil and the SyntaxError strict mode raised.
|
|
58
|
+
def parse(source)
|
|
59
|
+
doc = ::Nokogiri::XML(source) { |config| config.strict.nonet }
|
|
60
|
+
[doc, doc.errors]
|
|
61
|
+
rescue ::Nokogiri::XML::SyntaxError => e
|
|
62
|
+
[nil, [e]]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Byte-faithful serialization: AS_XML only, never FORMAT, which reindents
|
|
66
|
+
# the tree and invalidates any signature over it (§6.3).
|
|
67
|
+
def to_xml(node, declaration: true)
|
|
68
|
+
options = ::Nokogiri::XML::Node::SaveOptions::AS_XML
|
|
69
|
+
options |= ::Nokogiri::XML::Node::SaveOptions::NO_DECLARATION unless declaration
|
|
70
|
+
node.to_xml(save_with: options)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Runs +expression+ against +node+. On an XPath syntax error, returns
|
|
74
|
+
# what the block returns (the block usually raises); without a block
|
|
75
|
+
# the error propagates.
|
|
76
|
+
def xpath(node, expression, namespaces)
|
|
77
|
+
node.xpath(expression, namespaces)
|
|
78
|
+
rescue ::Nokogiri::XML::XPath::SyntaxError
|
|
79
|
+
raise unless block_given?
|
|
80
|
+
|
|
81
|
+
yield
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Loads the XSD at +path+, with the path as the base URL so its
|
|
85
|
+
# relative imports resolve (#61).
|
|
86
|
+
def schema_from_file(path)
|
|
87
|
+
require!("XML Schema validation")
|
|
88
|
+
|
|
89
|
+
::Nokogiri::XML::Schema.from_document(::Nokogiri::XML(::File.read(path), path))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Validates a copy of +node+, as the root of a fresh document, against
|
|
93
|
+
# +schema+. Returns the validation errors.
|
|
94
|
+
def schema_errors(schema, node)
|
|
95
|
+
doc = ::Nokogiri::XML::Document.new
|
|
96
|
+
doc.add_child(node.dup)
|
|
97
|
+
schema.validate(doc)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Mixes +mod+ into Nokogiri's Document and Node. A no-op without
|
|
101
|
+
# Nokogiri. Returns whether it was installed.
|
|
102
|
+
def extend_nodes(mod)
|
|
103
|
+
return false unless available?
|
|
104
|
+
|
|
105
|
+
::Nokogiri::XML::Document.include(mod)
|
|
106
|
+
::Nokogiri::XML::Node.include(mod)
|
|
107
|
+
true
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# What works in this process (API_CONTRACT.md §6.1). Verification, and
|
|
111
|
+
# signing a template the document already holds, run on bytes through
|
|
112
|
+
# the native layer, so they never need Nokogiri; the rest need a DOM.
|
|
113
|
+
# #102.
|
|
114
|
+
#
|
|
115
|
+
# - verify: Verifier / HankoXml.valid? / verify! / signature_present?,
|
|
116
|
+
# including `schema: :default`.
|
|
117
|
+
# - sign: Signer, HankoXml.sign / sign!.
|
|
118
|
+
# - sign_existing_template: Signer#sign / HankoXml.sign of a document
|
|
119
|
+
# that already holds the <Signature> template, on bytes (#102).
|
|
120
|
+
# - coverage_checks: Verification#signed?, #signed_nodes, #at_signed.
|
|
121
|
+
# - schema: a caller's own Nokogiri::XML::Schema as `schema:`.
|
|
122
|
+
# - parse: HankoXml.parse / serialize.
|
|
123
|
+
# - template_build: Template, Signer#template.
|
|
124
|
+
def capabilities
|
|
125
|
+
{
|
|
126
|
+
verify: true,
|
|
127
|
+
sign: available?,
|
|
128
|
+
sign_existing_template: true,
|
|
129
|
+
coverage_checks: available?,
|
|
130
|
+
schema: available?,
|
|
131
|
+
parse: available?,
|
|
132
|
+
template_build: available?
|
|
133
|
+
}
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
module HankoXml
|
|
6
|
+
# Base class for everything this gem raises. Rescue this to catch anything
|
|
7
|
+
# hanko_xml-specific. See API_CONTRACT.md §5.10 for the full hierarchy and
|
|
8
|
+
# the reasoning behind it (in particular: never KeyError/ArgumentError/
|
|
9
|
+
# TypeError/IOError, which would shadow core Ruby constants inside this
|
|
10
|
+
# module).
|
|
11
|
+
class Error < StandardError
|
|
12
|
+
# A private key in PEM form, including a truncated one. Redacted from
|
|
13
|
+
# every message and log line (API_CONTRACT.md §5.10, #96).
|
|
14
|
+
PRIVATE_KEY_PEM = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----.*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|\z)/m
|
|
15
|
+
FILTERED = "[FILTERED]"
|
|
16
|
+
|
|
17
|
+
# The xmlsec1 error log lines captured for this failure, or libxml2's
|
|
18
|
+
# diagnostics for a ParseError / SchemaError. Untrusted: they can quote
|
|
19
|
+
# the document.
|
|
20
|
+
attr_reader :xmlsec_errors
|
|
21
|
+
|
|
22
|
+
# The <ds:Signature> Nokogiri::XML::Element this error relates to, when
|
|
23
|
+
# one was located.
|
|
24
|
+
attr_accessor :signature_node
|
|
25
|
+
|
|
26
|
+
# Never carries key material: a HankoXml::Key or OpenSSL::PKey passed as
|
|
27
|
+
# +message+ becomes [FILTERED], and private-key PEM text is redacted.
|
|
28
|
+
# (NativeVersionMismatch can be raised before key.rb is loaded.)
|
|
29
|
+
def self.filter(value)
|
|
30
|
+
return nil if value.nil?
|
|
31
|
+
return FILTERED if value.is_a?(OpenSSL::PKey::PKey) || (defined?(HankoXml::Key) && value.is_a?(HankoXml::Key))
|
|
32
|
+
|
|
33
|
+
value.to_s.gsub(PRIVATE_KEY_PEM, FILTERED)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def initialize(message = nil, xmlsec_errors: [], signature_node: nil)
|
|
37
|
+
super(Error.filter(message))
|
|
38
|
+
self.xmlsec_errors = xmlsec_errors
|
|
39
|
+
@signature_node = signature_node
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def xmlsec_errors=(lines)
|
|
43
|
+
@xmlsec_errors = Array(lines).map { |line| Error.filter(line) }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Invalid Signer/Verifier construction.
|
|
48
|
+
class ConfigurationError < Error; end
|
|
49
|
+
|
|
50
|
+
# A Policy allowlist was violated.
|
|
51
|
+
class PolicyError < ConfigurationError; end
|
|
52
|
+
|
|
53
|
+
# A Placement XPath matched zero or more than one node.
|
|
54
|
+
class PlacementError < ConfigurationError; end
|
|
55
|
+
|
|
56
|
+
# The requested capability needs a DOM, and Nokogiri is not loaded. See
|
|
57
|
+
# API_CONTRACT.md §6.1.
|
|
58
|
+
class NokogiriRequired < ConfigurationError; end
|
|
59
|
+
|
|
60
|
+
# <Signature> failed XSD validation (Verifier schema: option).
|
|
61
|
+
class SchemaError < ConfigurationError; end
|
|
62
|
+
|
|
63
|
+
# The XML would not parse.
|
|
64
|
+
class ParseError < Error; end
|
|
65
|
+
|
|
66
|
+
# A key or certificate was unreadable or in an unsupported format.
|
|
67
|
+
class KeyLoadError < Error; end
|
|
68
|
+
|
|
69
|
+
# A Reference target does not exist, is ambiguous, or has no ID attribute.
|
|
70
|
+
class ReferenceError < Error
|
|
71
|
+
attr_accessor :reference_uri
|
|
72
|
+
|
|
73
|
+
def initialize(message = nil, reference_uri: nil, **kwargs)
|
|
74
|
+
super(message, **kwargs)
|
|
75
|
+
@reference_uri = reference_uri
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# libxmlsec1 could not produce a signature.
|
|
80
|
+
class SigningError < Error; end
|
|
81
|
+
|
|
82
|
+
# Verification did not succeed (raised by #verify!).
|
|
83
|
+
class VerificationError < Error; end
|
|
84
|
+
|
|
85
|
+
# The signature verified as XML-DSig, but its XAdES signed properties do not
|
|
86
|
+
# hold: they are not the ones xmlsec digested, or they do not commit to the
|
|
87
|
+
# certificate that verified (#58).
|
|
88
|
+
class XadesError < VerificationError; end
|
|
89
|
+
|
|
90
|
+
# Cryptographic mismatch.
|
|
91
|
+
class SignatureInvalid < VerificationError; end
|
|
92
|
+
|
|
93
|
+
# No <ds:Signature> in the document.
|
|
94
|
+
class SignatureMissing < VerificationError; end
|
|
95
|
+
|
|
96
|
+
# Verifier#signature was :only and the document has more than one.
|
|
97
|
+
class MultipleSignatures < VerificationError; end
|
|
98
|
+
|
|
99
|
+
# Certificate/key trust resolution (chain or pinning) failed.
|
|
100
|
+
class UntrustedCertificate < VerificationError; end
|
|
101
|
+
|
|
102
|
+
# Certificate is outside notBefore/notAfter as of verification_time.
|
|
103
|
+
class CertificateExpired < VerificationError; end
|
|
104
|
+
|
|
105
|
+
# One <Reference>'s digest did not match.
|
|
106
|
+
class DigestMismatch < VerificationError
|
|
107
|
+
attr_accessor :reference_uri
|
|
108
|
+
|
|
109
|
+
def initialize(message = nil, reference_uri: nil, **kwargs)
|
|
110
|
+
super(message, **kwargs)
|
|
111
|
+
@reference_uri = reference_uri
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# A caller-supplied `resolver:` and xmlsec's ID table disagreed on what a
|
|
116
|
+
# Reference URI points to. See API_CONTRACT.md §5.9.
|
|
117
|
+
class ResolutionMismatch < VerificationError; end
|
|
118
|
+
|
|
119
|
+
# Verification#at_signed found a node the signature does not cover.
|
|
120
|
+
class NotSignedError < Error; end
|
|
121
|
+
|
|
122
|
+
# A mutator was called on a HankoXml::SignedDocument.
|
|
123
|
+
class FrozenSignatureError < Error; end
|
|
124
|
+
|
|
125
|
+
# libxmlsec1 failed in an unexpected way.
|
|
126
|
+
class NativeError < Error; end
|
|
127
|
+
|
|
128
|
+
# The loaded libxmlsec1 is not ABI compatible with the headers this
|
|
129
|
+
# extension was compiled against. Raised at require time by
|
|
130
|
+
# {HankoXml::NativeVersion}; see that file for why this cannot be a warning.
|
|
131
|
+
class NativeVersionMismatch < NativeError; end
|
|
132
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Schema for Exclusive XML Canonicalization InclusiveNamespaces
|
|
3
|
+
http://www.w3.org/2001/10/xml-exc-c14n#
|
|
4
|
+
As published in https://www.w3.org/TR/xml-exc-c14n/#sec-Schema
|
|
5
|
+
|
|
6
|
+
Copyright 2002 The Internet Society and W3C (Massachusetts Institute
|
|
7
|
+
of Technology, Institut National de Recherche en Informatique et en
|
|
8
|
+
Automatique, Keio University). All Rights Reserved.
|
|
9
|
+
http://www.w3.org/Consortium/Legal/
|
|
10
|
+
|
|
11
|
+
This document is governed by the W3C Software License [1] as described
|
|
12
|
+
in the FAQ [2].
|
|
13
|
+
|
|
14
|
+
[1] http://www.w3.org/Consortium/Legal/copyright-software-19980720
|
|
15
|
+
[2] http://www.w3.org/Consortium/Legal/IPR-FAQ-20000620.html#DTD
|
|
16
|
+
-->
|
|
17
|
+
<schema xmlns="http://www.w3.org/2001/XMLSchema"
|
|
18
|
+
xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#"
|
|
19
|
+
targetNamespace="http://www.w3.org/2001/10/xml-exc-c14n#"
|
|
20
|
+
version="0.1" elementFormDefault="qualified">
|
|
21
|
+
|
|
22
|
+
<element name="InclusiveNamespaces" type="ec:InclusiveNamespaces"/>
|
|
23
|
+
<complexType name="InclusiveNamespaces">
|
|
24
|
+
<attribute name="PrefixList" type="NMTOKENS"/>
|
|
25
|
+
</complexType>
|
|
26
|
+
</schema>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HankoXml
|
|
4
|
+
# A document on its way to the native layer: always its bytes, plus the
|
|
5
|
+
# caller's DOM when there is one (API_CONTRACT.md §6.1, #102).
|
|
6
|
+
#
|
|
7
|
+
# Every security check reads the bytes, through the native layer, so it
|
|
8
|
+
# runs the same with or without Nokogiri. The DOM only serves what cannot
|
|
9
|
+
# exist without one: Verification#signed? and friends, XPath signature
|
|
10
|
+
# selection, a caller's own Nokogiri schema, a resolver:.
|
|
11
|
+
#
|
|
12
|
+
# @api private
|
|
13
|
+
class Input
|
|
14
|
+
attr_reader :bytes, :document
|
|
15
|
+
|
|
16
|
+
# +source+ is anything §6.2 accepts. A Nokogiri Document or Node is
|
|
17
|
+
# serialized; anything else is read as bytes (+policy+'s
|
|
18
|
+
# +max_document_size+ applies first, S-12) and, when Nokogiri is loaded,
|
|
19
|
+
# also parsed, so the result carries a DOM either way.
|
|
20
|
+
def self.read(source, policy: nil)
|
|
21
|
+
if Dom.document?(source) || Dom.node?(source)
|
|
22
|
+
document = Dom.document?(source) ? source : source.document
|
|
23
|
+
return new(HankoXml.serialize(document).b, document, dom: true)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
raw = HankoXml.read_source(source, policy: policy)
|
|
27
|
+
document = Dom.available? ? HankoXml.parse(raw) : nil
|
|
28
|
+
new(raw.b, document, dom: false)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(bytes, document, dom:)
|
|
32
|
+
@bytes = bytes.frozen? ? bytes : bytes.dup.freeze
|
|
33
|
+
@document = document
|
|
34
|
+
@dom = dom
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Whether the caller handed over a DOM (rather than bytes).
|
|
38
|
+
def dom?
|
|
39
|
+
@dom
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Native.scan of the bytes, memoized per ID-attribute list. Input that
|
|
43
|
+
# arrived as bytes is parsed strictly, as HankoXml.parse does with
|
|
44
|
+
# Nokogiri; a DOM the caller already holds is taken as it is.
|
|
45
|
+
def scan(id_attributes)
|
|
46
|
+
@scans ||= {}
|
|
47
|
+
@scans[id_attributes] ||= Native.scan(bytes, id_attributes, !dom?)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|