hanko_xml 0.1.0-aarch64-linux
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.so +0 -0
- data/lib/hanko_xml/3.2/hanko_xml.so +0 -0
- data/lib/hanko_xml/3.3/hanko_xml.so +0 -0
- data/lib/hanko_xml/3.4/hanko_xml.so +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,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HankoXml
|
|
4
|
+
# Builds the unsigned <ds:Signature> Nokogiri tree for a Signer's
|
|
5
|
+
# configuration -- DigestValue/SignatureValue/certificate leaves are left
|
|
6
|
+
# empty for the native layer (Phase 2+) to fill in. See API_CONTRACT.md
|
|
7
|
+
# §5.2, §6.4.
|
|
8
|
+
#
|
|
9
|
+
# Needs a DOM: this is the one thing that cannot be built DOM-free,
|
|
10
|
+
# because doing it by string concatenation would put attacker-influenced
|
|
11
|
+
# values into hand-assembled markup (§6.1).
|
|
12
|
+
class Template
|
|
13
|
+
def self.build(doc, config)
|
|
14
|
+
new(doc, config).build
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(doc, config)
|
|
18
|
+
Dom.require!("HankoXml::Template")
|
|
19
|
+
|
|
20
|
+
@doc = doc
|
|
21
|
+
@config = config
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def build
|
|
25
|
+
signature = element("Signature")
|
|
26
|
+
signature.default_namespace = HankoXml::NS["ds"]
|
|
27
|
+
signature["Id"] = @config.signature_id.to_s if @config.signature_id
|
|
28
|
+
|
|
29
|
+
signature.add_child(build_signed_info)
|
|
30
|
+
signature.add_child(element("SignatureValue"))
|
|
31
|
+
|
|
32
|
+
key_info = build_key_info
|
|
33
|
+
signature.add_child(key_info) if key_info
|
|
34
|
+
|
|
35
|
+
signature
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def build_signed_info
|
|
41
|
+
signed_info = element("SignedInfo")
|
|
42
|
+
|
|
43
|
+
c14n = element("CanonicalizationMethod")
|
|
44
|
+
c14n["Algorithm"] = Algorithms.uri_for(:canonicalization, @config.canonicalization_method)
|
|
45
|
+
signed_info.add_child(c14n)
|
|
46
|
+
|
|
47
|
+
signature_method = element("SignatureMethod")
|
|
48
|
+
signature_method["Algorithm"] = Algorithms.uri_for(:signature, @config.signature_method)
|
|
49
|
+
signed_info.add_child(signature_method)
|
|
50
|
+
|
|
51
|
+
effective_references.each { |ref| signed_info.add_child(build_reference(ref)) }
|
|
52
|
+
|
|
53
|
+
signed_info
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def effective_references
|
|
57
|
+
refs = @config.references
|
|
58
|
+
refs = [Reference.new(uri: "", digest_method: @config.digest_method)] if refs.empty?
|
|
59
|
+
refs
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def build_reference(ref)
|
|
63
|
+
reference = element("Reference")
|
|
64
|
+
reference["URI"] = ref.uri
|
|
65
|
+
reference["Id"] = ref.id.to_s if ref.id
|
|
66
|
+
reference["Type"] = ref.type.to_s if ref.type
|
|
67
|
+
|
|
68
|
+
unless ref.transforms.empty?
|
|
69
|
+
transforms = element("Transforms")
|
|
70
|
+
ref.transforms.each { |t| transforms.add_child(build_transform(t)) }
|
|
71
|
+
reference.add_child(transforms)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
digest_method = element("DigestMethod")
|
|
75
|
+
digest_method["Algorithm"] = ref.digest_method_uri
|
|
76
|
+
reference.add_child(digest_method)
|
|
77
|
+
|
|
78
|
+
reference.add_child(element("DigestValue"))
|
|
79
|
+
|
|
80
|
+
reference
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def build_transform(transform)
|
|
84
|
+
node = element("Transform")
|
|
85
|
+
node["Algorithm"] = transform.uri
|
|
86
|
+
|
|
87
|
+
if transform.xpath
|
|
88
|
+
xpath_node = element("XPath")
|
|
89
|
+
xpath_node.content = transform.xpath
|
|
90
|
+
node.add_child(xpath_node)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
prefixes = transform.inclusive_prefixes
|
|
94
|
+
node.add_child(build_inclusive_namespaces(prefixes)) unless prefixes.empty?
|
|
95
|
+
|
|
96
|
+
node
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def build_inclusive_namespaces(prefixes)
|
|
100
|
+
node = element("InclusiveNamespaces")
|
|
101
|
+
node.namespace = node.add_namespace_definition("ec", Transform::EXC_C14N_NS)
|
|
102
|
+
node["PrefixList"] = prefixes.join(" ")
|
|
103
|
+
node
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
X509_CHILDREN = {
|
|
107
|
+
x509_certificate: "X509Certificate",
|
|
108
|
+
x509_subject_name: "X509SubjectName",
|
|
109
|
+
x509_issuer_serial: "X509IssuerSerial",
|
|
110
|
+
x509_ski: "X509SKI"
|
|
111
|
+
}.freeze
|
|
112
|
+
private_constant :X509_CHILDREN
|
|
113
|
+
|
|
114
|
+
def build_key_info
|
|
115
|
+
clauses = @config.key_info
|
|
116
|
+
return nil if clauses.empty?
|
|
117
|
+
|
|
118
|
+
key_info = element("KeyInfo")
|
|
119
|
+
|
|
120
|
+
x509_children = clauses.filter_map { |c| X509_CHILDREN[c.name] }.uniq
|
|
121
|
+
unless x509_children.empty?
|
|
122
|
+
# xmlsec writes only the X509Data children the template names (an
|
|
123
|
+
# empty <X509Data/> means "certificate"), so each clause needs its
|
|
124
|
+
# own placeholder (#52 E8).
|
|
125
|
+
x509_data = element("X509Data")
|
|
126
|
+
x509_children.each { |name| x509_data.add_child(element(name)) }
|
|
127
|
+
key_info.add_child(x509_data)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
clauses.each do |c|
|
|
131
|
+
case c.name
|
|
132
|
+
when :key_name
|
|
133
|
+
# xmlsec writes the xmlSecKey's own name, and a key loaded from
|
|
134
|
+
# PEM has none, so the name comes from the clause (#67).
|
|
135
|
+
key_name = element("KeyName")
|
|
136
|
+
key_name.content = c.value if c.value
|
|
137
|
+
key_info.add_child(key_name)
|
|
138
|
+
when :key_value
|
|
139
|
+
key_info.add_child(element("KeyValue"))
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
key_info
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def element(name)
|
|
147
|
+
@doc.create_element(name)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HankoXml
|
|
4
|
+
# A single <ds:Transform>, resolved to a URI at construction. Frozen value
|
|
5
|
+
# object. See API_CONTRACT.md §5.2, §5.7.
|
|
6
|
+
class Transform
|
|
7
|
+
EXC_C14N_NS = "http://www.w3.org/2001/10/xml-exc-c14n#"
|
|
8
|
+
EXCLUSIVE = %i[exc_c14n exc_c14n_with_comments].freeze
|
|
9
|
+
# An NCName, or the "#default" token for the default namespace
|
|
10
|
+
# (Exclusive XML Canonicalization §3).
|
|
11
|
+
PREFIX = /\A(?:#default|[A-Za-z_][A-Za-z0-9_.-]*)\z/
|
|
12
|
+
|
|
13
|
+
attr_reader :name, :uri, :xpath, :inclusive_prefixes
|
|
14
|
+
|
|
15
|
+
# +algorithm+ is a Symbol looked up in Algorithms.transform, or a String
|
|
16
|
+
# URI used verbatim. +xpath+ is required for :xpath / :xpath_filter2
|
|
17
|
+
# transforms and carried through as the transform's textual content.
|
|
18
|
+
# +inclusive_prefixes+ (exclusive c14n only) becomes an
|
|
19
|
+
# <ec:InclusiveNamespaces PrefixList="..."/> child: namespace prefixes
|
|
20
|
+
# canonicalized as in inclusive c14n, e.g. %w[xs] for SAML xsi:type values.
|
|
21
|
+
def initialize(algorithm, xpath: nil, inclusive_prefixes: [])
|
|
22
|
+
@name = algorithm.is_a?(Symbol) ? algorithm : Algorithms.name_for(:transform, algorithm)
|
|
23
|
+
@uri = Algorithms.uri_for(:transform, algorithm)
|
|
24
|
+
|
|
25
|
+
raise HankoXml::ConfigurationError, "unknown transform #{algorithm.inspect}" if @uri.nil?
|
|
26
|
+
|
|
27
|
+
@xpath = xpath
|
|
28
|
+
@inclusive_prefixes = coerce_inclusive_prefixes(inclusive_prefixes)
|
|
29
|
+
freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Exclusive c14n, optionally with an InclusiveNamespaces PrefixList.
|
|
33
|
+
def self.exc_c14n(inclusive_prefixes: [], with_comments: false)
|
|
34
|
+
new(with_comments ? :exc_c14n_with_comments : :exc_c14n, inclusive_prefixes: inclusive_prefixes)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.c14n
|
|
38
|
+
new(:c14n)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.enveloped_signature
|
|
42
|
+
new(:enveloped_signature)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def enveloped_signature?
|
|
46
|
+
name == :enveloped_signature
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def secure?
|
|
50
|
+
Algorithms.secure?(:transform, uri)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.coerce(value)
|
|
54
|
+
value.is_a?(Transform) ? value : new(value)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def ==(other)
|
|
58
|
+
other.is_a?(Transform) && other.uri == uri && other.xpath == xpath &&
|
|
59
|
+
other.inclusive_prefixes == inclusive_prefixes
|
|
60
|
+
end
|
|
61
|
+
alias eql? ==
|
|
62
|
+
|
|
63
|
+
def hash
|
|
64
|
+
[uri, xpath, inclusive_prefixes].hash
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def coerce_inclusive_prefixes(prefixes)
|
|
70
|
+
prefixes = Array(prefixes).map(&:to_s)
|
|
71
|
+
return prefixes.freeze if prefixes.empty?
|
|
72
|
+
|
|
73
|
+
unless EXCLUSIVE.include?(name)
|
|
74
|
+
raise HankoXml::ConfigurationError,
|
|
75
|
+
"inclusive_prefixes: applies only to exclusive c14n transforms, not #{uri}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
invalid = prefixes.grep_v(PREFIX)
|
|
79
|
+
unless invalid.empty?
|
|
80
|
+
raise HankoXml::ConfigurationError,
|
|
81
|
+
"inclusive_prefixes: #{invalid.inspect} are not namespace prefixes (NCName or \"#default\")"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
prefixes.uniq.freeze
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HankoXml
|
|
4
|
+
# The result of a signature verification attempt.
|
|
5
|
+
# Provides methods to inspect which nodes were actually signed, defending against
|
|
6
|
+
# XML Signature Wrapping (XSW) attacks.
|
|
7
|
+
# See API_CONTRACT.md §5.4.
|
|
8
|
+
class Verification # rubocop:disable Metrics/ClassLength
|
|
9
|
+
# Details about a specific `<Reference>` inside the verified signature.
|
|
10
|
+
Reference = Struct.new(:uri, :node, :digest_method, :transforms, :valid?, :errors, :signature_exclusion,
|
|
11
|
+
keyword_init: true) do
|
|
12
|
+
# @return [Boolean] True if the reference uses the enveloped-signature transform.
|
|
13
|
+
def enveloped?
|
|
14
|
+
transforms.include?(:enveloped_signature)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Which <ds:Signature> subtrees this Reference leaves out: :this (the
|
|
18
|
+
# enveloped-signature transform), and for the accepted XPath Filter 2.0
|
|
19
|
+
# form (#85) :all (every signature) or :root_children (signatures that
|
|
20
|
+
# are children of the root).
|
|
21
|
+
def signature_exclusions
|
|
22
|
+
[(:this if enveloped?), signature_exclusion].compact
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
attr_reader :valid, :errors, :signature_info, :signature_node, :document, :certificate, :id_attributes, :resolver,
|
|
27
|
+
:references, :reference_element_indexes, :reference_valid, :namespaces,
|
|
28
|
+
:signature_element_index
|
|
29
|
+
|
|
30
|
+
# +document+ is nil when the input was bytes and Nokogiri is not loaded
|
|
31
|
+
# (#102). Everything that needs no DOM still works then; #signed?,
|
|
32
|
+
# #signed_nodes, #at_signed and #signed raise NokogiriRequired.
|
|
33
|
+
def initialize(valid:, errors:, signature_info:, signature_node:, document:, certificate: nil,
|
|
34
|
+
id_attributes: %w[Id ID id], resolver: nil, reference_element_indexes: nil,
|
|
35
|
+
reference_valid: nil, namespaces: {}, signature_element_index: nil, certificate_chain: nil,
|
|
36
|
+
trusted: true, root_signature: nil)
|
|
37
|
+
@valid = valid
|
|
38
|
+
@errors = errors.dup
|
|
39
|
+
@signature_info = signature_info
|
|
40
|
+
@signature_node = signature_node
|
|
41
|
+
@document = document
|
|
42
|
+
@certificate = certificate
|
|
43
|
+
# The chain store: validated, leaf first; nil for the pinned trust sources.
|
|
44
|
+
@certificate_chain = certificate_chain&.dup&.freeze
|
|
45
|
+
# False only for Verifier.without_trust_verification (#103).
|
|
46
|
+
@trusted = trusted
|
|
47
|
+
# Whether the root element is a <ds:Signature>, for #covers_root?
|
|
48
|
+
# without a DOM; nil falls back to this signature being the root.
|
|
49
|
+
@root_signature = root_signature
|
|
50
|
+
@id_attributes = (id_attributes || []).dup.freeze
|
|
51
|
+
@resolver = resolver
|
|
52
|
+
@namespaces = (namespaces || {}).transform_keys(&:to_s).freeze
|
|
53
|
+
# Native.verify returns document-order element indexes from libxml2's ID
|
|
54
|
+
# table (the same table xmlsec used for `#id`). When present, those are
|
|
55
|
+
# authoritative for #references[].node / #signed? — Ruby does not
|
|
56
|
+
# re-resolve IDs (issues #28 / #34 / S-03).
|
|
57
|
+
@reference_element_indexes = reference_element_indexes&.dup&.freeze
|
|
58
|
+
# Native.verify's per-Reference digest status (S-09) -- authoritative
|
|
59
|
+
# when present. See #reference_valid_for.
|
|
60
|
+
@reference_valid = reference_valid&.dup&.freeze
|
|
61
|
+
# The <Signature>'s own document-order element index, in the same
|
|
62
|
+
# numbering, so #covers_root? can apply the enveloped exclusion without
|
|
63
|
+
# a DOM.
|
|
64
|
+
@signature_element_index = signature_element_index
|
|
65
|
+
# Resolution must happen, and #errors/#valid must be able to react to it
|
|
66
|
+
# (see detect_resolution_mismatches!), before we freeze below -- the object
|
|
67
|
+
# is immutable from here on, so #references can't be computed lazily on
|
|
68
|
+
# first access the way it used to be.
|
|
69
|
+
@references = build_references
|
|
70
|
+
detect_resolution_mismatches!
|
|
71
|
+
@errors.freeze
|
|
72
|
+
freeze
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @return [Boolean] True if the signature is completely valid.
|
|
76
|
+
def valid?
|
|
77
|
+
@valid
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# --- Authenticated metadata ---
|
|
81
|
+
# Registry names (:rsa_sha256), as API_CONTRACT.md §5.4 shows, or the URI
|
|
82
|
+
# String for an algorithm the registry does not know (#96).
|
|
83
|
+
def signature_method
|
|
84
|
+
signature_info && (signature_info.signature_method_name || signature_info.signature_method)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def canonicalization_method
|
|
88
|
+
signature_info && (signature_info.canonicalization_method_name || signature_info.canonicalization_method)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def digest_methods
|
|
92
|
+
# nil signature_info: a verification that failed before inspection (#94).
|
|
93
|
+
return [] unless signature_info
|
|
94
|
+
|
|
95
|
+
signature_info.references.map { |ref| ref.digest_method_name || ref.digest_method }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def signed_nodes
|
|
99
|
+
require_document!("Verification#signed_nodes")
|
|
100
|
+
# Same rule as #signed?: nothing is covered unless the signature held (#91).
|
|
101
|
+
return [] unless valid?
|
|
102
|
+
|
|
103
|
+
# For each reference, get the nodes it covers
|
|
104
|
+
nodes = []
|
|
105
|
+
references.each do |ref|
|
|
106
|
+
next unless ref.node
|
|
107
|
+
|
|
108
|
+
# A reference covers the node and all its descendants.
|
|
109
|
+
# But if it's enveloped, it excludes the signature itself.
|
|
110
|
+
nodes << ref.node
|
|
111
|
+
end
|
|
112
|
+
nodes
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def signed?(node)
|
|
116
|
+
require_document!("Verification#signed?")
|
|
117
|
+
return false unless valid?
|
|
118
|
+
|
|
119
|
+
# True if the node is equal to or a descendant of any reference's target
|
|
120
|
+
# node, EXCEPT inside a <ds:Signature> the reference leaves out: this
|
|
121
|
+
# one (enveloped-signature), or the ones an XPath Filter 2.0 subtract
|
|
122
|
+
# removes (#85).
|
|
123
|
+
references.any? do |ref|
|
|
124
|
+
next false unless ref.node
|
|
125
|
+
next false unless node.ancestors.include?(ref.node) || node == ref.node
|
|
126
|
+
|
|
127
|
+
ref.signature_exclusions.none? { |exclusion| excluded_by?(node, exclusion) }
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Whether the signature held AND the key came from trust material the
|
|
132
|
+
# caller supplied. False for every result of
|
|
133
|
+
# Verifier.without_trust_verification, which checks integrity only: its
|
|
134
|
+
# #valid? says the document was not changed, never who signed it (#103).
|
|
135
|
+
def trusted?
|
|
136
|
+
valid? && @trusted
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# The <SignedInfo> that was verified: #signature_info once the signature
|
|
140
|
+
# held, nil otherwise, so it never stands for unauthenticated claims
|
|
141
|
+
# (#104). #signature_info is the same data before verification.
|
|
142
|
+
def signed_info
|
|
143
|
+
valid? ? signature_info : nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# The certificates that established trust, leaf first, or [] unless
|
|
147
|
+
# trusted?. For store: it is the chain OpenSSL validated, up to the trust
|
|
148
|
+
# anchor; for certificate: / certificates: it is the pinned certificate;
|
|
149
|
+
# key: and keys: with a bare public key have none (#104).
|
|
150
|
+
def certificate_chain
|
|
151
|
+
return [] unless trusted?
|
|
152
|
+
return @certificate_chain if @certificate_chain
|
|
153
|
+
|
|
154
|
+
certificate ? [certificate] : []
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# What the document's <KeyInfo> claimed, read out but never trusted: the
|
|
158
|
+
# Verifier pins its key from trust material and ignores <KeyInfo> (S-01).
|
|
159
|
+
# Available whether or not the signature held; nil when <SignedInfo> was
|
|
160
|
+
# never read (#104).
|
|
161
|
+
def key_info
|
|
162
|
+
return nil unless signature_info
|
|
163
|
+
|
|
164
|
+
KeyInfo::Parsed.new(key_names: signature_info.key_names, certificates: signature_info.certificates)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Whether a Reference covers the root element. With a DOM this is
|
|
168
|
+
# #signed?(document.root); without one, the same rule over the native
|
|
169
|
+
# element indexes, where the root is index 0 (#102).
|
|
170
|
+
def covers_root?
|
|
171
|
+
return signed?(document.root) if document
|
|
172
|
+
return false unless valid? && reference_element_indexes
|
|
173
|
+
|
|
174
|
+
references.each_with_index.any? do |ref, index|
|
|
175
|
+
next false unless reference_element_indexes[index]&.zero?
|
|
176
|
+
|
|
177
|
+
# Only a <Signature> that is itself the root can take the root out:
|
|
178
|
+
# :this when it is this one, :all when it is any (#85).
|
|
179
|
+
exclusions = ref.signature_exclusions
|
|
180
|
+
!((exclusions.include?(:this) && signature_element_index&.zero?) ||
|
|
181
|
+
(exclusions.include?(:all) && root_signature))
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def root_signature
|
|
186
|
+
@root_signature.nil? ? signature_element_index&.zero? : @root_signature
|
|
187
|
+
end
|
|
188
|
+
private :root_signature
|
|
189
|
+
|
|
190
|
+
def partial?
|
|
191
|
+
!covers_root?
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# +ns+ is merged over the Verifier's +namespaces:+, which is merged over
|
|
195
|
+
# HankoXml::NS, so +ds:+ is always bound (#96).
|
|
196
|
+
def at_signed(xpath, ns = {})
|
|
197
|
+
require_document!("Verification#at_signed")
|
|
198
|
+
node = document.at_xpath(xpath, xpath_namespaces(ns))
|
|
199
|
+
raise HankoXml::NotSignedError, "No node found" unless node
|
|
200
|
+
raise HankoXml::NotSignedError, "Node is not signed" unless signed?(node)
|
|
201
|
+
|
|
202
|
+
node
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def signed(xpath, ns = {})
|
|
206
|
+
require_document!("Verification#signed")
|
|
207
|
+
nodes = document.xpath(xpath, xpath_namespaces(ns)).to_a
|
|
208
|
+
raise HankoXml::NotSignedError, "Some nodes are not signed" unless nodes.all? { |n| signed?(n) }
|
|
209
|
+
|
|
210
|
+
nodes
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
private
|
|
214
|
+
|
|
215
|
+
def excluded_by?(node, exclusion)
|
|
216
|
+
chain = [node, *node.ancestors]
|
|
217
|
+
case exclusion
|
|
218
|
+
when :this then chain.include?(signature_node)
|
|
219
|
+
when :all then chain.any? { |n| dsig_signature?(n) }
|
|
220
|
+
when :root_children then chain.any? { |n| dsig_signature?(n) && n.parent == document.root }
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def dsig_signature?(node)
|
|
225
|
+
node.respond_to?(:namespace) && node.name == "Signature" && node.namespace&.href == HankoXml::NS["ds"]
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Coverage questions are answered on the caller's nodes, so they need
|
|
229
|
+
# the DOM. Without it they raise rather than guess (§6.1).
|
|
230
|
+
def require_document!(capability)
|
|
231
|
+
return if document
|
|
232
|
+
|
|
233
|
+
raise HankoXml::NokogiriRequired,
|
|
234
|
+
"#{capability} needs a DOM: verify a Nokogiri document, or load Nokogiri before verifying, " \
|
|
235
|
+
"to use it (#102)."
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def xpath_namespaces(ns)
|
|
239
|
+
HankoXml::NS.merge(namespaces).merge((ns || {}).transform_keys(&:to_s))
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def build_references
|
|
243
|
+
return [] unless signature_info
|
|
244
|
+
|
|
245
|
+
signature_info.references.map.with_index do |ref, index|
|
|
246
|
+
target_node = resolve_reference(ref, index)
|
|
247
|
+
|
|
248
|
+
Reference.new(
|
|
249
|
+
uri: ref.uri,
|
|
250
|
+
node: target_node,
|
|
251
|
+
digest_method: ref.digest_method_name,
|
|
252
|
+
transforms: ref.transform_names,
|
|
253
|
+
valid?: reference_valid_for(index),
|
|
254
|
+
errors: reference_errors_for(ref, index),
|
|
255
|
+
signature_exclusion: ref.signature_exclusion
|
|
256
|
+
).freeze
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Native.verify reports each SignedInfo Reference's own digest status,
|
|
261
|
+
# independent of the overall signature/key outcome (S-09): a
|
|
262
|
+
# harmless-fragment Reference and the payload Reference can genuinely
|
|
263
|
+
# disagree, so "the whole signature is valid" cannot stand in for "this
|
|
264
|
+
# Reference's digest matched". Falls back to the overall result only when
|
|
265
|
+
# there is no real per-reference answer -- a Verification built directly
|
|
266
|
+
# (specs, error paths that never reached crypto) or a reference xmlsec
|
|
267
|
+
# never got to because verification aborted earlier.
|
|
268
|
+
def reference_valid_for(index)
|
|
269
|
+
return valid? unless @reference_valid
|
|
270
|
+
|
|
271
|
+
status = @reference_valid[index]
|
|
272
|
+
status.nil? ? valid? : status
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# A DigestMismatch only when xmlsec itself reported this Reference's
|
|
276
|
+
# digest as wrong; the fallback in #reference_valid_for proves nothing
|
|
277
|
+
# about one Reference (#96). The URI is document text, so it goes in
|
|
278
|
+
# #reference_uri, not the message.
|
|
279
|
+
def reference_errors_for(ref, index)
|
|
280
|
+
return [] unless @reference_valid && @reference_valid[index] == false
|
|
281
|
+
|
|
282
|
+
[HankoXml::DigestMismatch.new("a <Reference>'s digest does not match", reference_uri: ref.uri).freeze]
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# When Native.verify supplied element indexes, those are the authority.
|
|
286
|
+
# A caller-supplied resolver: may only *agree* with them — disagreement is
|
|
287
|
+
# ResolutionMismatch (API_CONTRACT §5.9). Without native indexes (unit tests
|
|
288
|
+
# that construct Verification directly), fall back to resolver / id_attributes.
|
|
289
|
+
def detect_resolution_mismatches!
|
|
290
|
+
return unless resolver
|
|
291
|
+
return unless signature_info
|
|
292
|
+
|
|
293
|
+
# A resolver: is a check the caller asked for; without the DOM it
|
|
294
|
+
# cannot run, and it must never be skipped.
|
|
295
|
+
require_document!("resolver:")
|
|
296
|
+
|
|
297
|
+
signature_info.references.each_with_index do |ref, index|
|
|
298
|
+
next unless ref.uri&.start_with?("#")
|
|
299
|
+
|
|
300
|
+
id = ref.uri[1..]
|
|
301
|
+
resolver_node = resolver.call(document, id)
|
|
302
|
+
expected = if reference_element_indexes
|
|
303
|
+
node_at_element_index(reference_element_indexes[index])
|
|
304
|
+
elsif !id_attributes.empty?
|
|
305
|
+
resolve_by_id_attributes(id)
|
|
306
|
+
else
|
|
307
|
+
next
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
record_resolution_mismatch(id) unless same_node?(resolver_node, expected)
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def record_resolution_mismatch(id)
|
|
315
|
+
@errors << HankoXml::ResolutionMismatch.new(
|
|
316
|
+
"resolver and xmlsec ID resolution disagree on a reference's target",
|
|
317
|
+
xmlsec_errors: ["reference: ##{id}"]
|
|
318
|
+
)
|
|
319
|
+
@valid = false
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def same_node?(left, right)
|
|
323
|
+
return true if left.nil? && right.nil?
|
|
324
|
+
return false if left.nil? || right.nil?
|
|
325
|
+
|
|
326
|
+
left == right
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def resolve_reference(ref, index)
|
|
330
|
+
return node_at_element_index(reference_element_indexes[index]) if reference_element_indexes
|
|
331
|
+
|
|
332
|
+
# Fallback for Verification constructed without a Native.verify result
|
|
333
|
+
# (specs / error paths that never reached crypto).
|
|
334
|
+
return nil unless document
|
|
335
|
+
|
|
336
|
+
if ref.uri.nil? || SignatureInfo::WHOLE_DOCUMENT_URIS.include?(ref.uri)
|
|
337
|
+
document.root
|
|
338
|
+
elsif ref.uri.start_with?("#")
|
|
339
|
+
resolve_fragment_reference(ref.uri[1..])
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def node_at_element_index(index)
|
|
344
|
+
return nil if index.nil? || document.nil?
|
|
345
|
+
|
|
346
|
+
document.xpath("//*")[index]
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def resolve_fragment_reference(id)
|
|
350
|
+
return resolver.call(document, id) if resolver
|
|
351
|
+
|
|
352
|
+
resolve_by_id_attributes(id)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def resolve_by_id_attributes(id)
|
|
356
|
+
predicates = []
|
|
357
|
+
bindings = { id: id }
|
|
358
|
+
|
|
359
|
+
id_attributes.each_with_index do |attr, index|
|
|
360
|
+
name_key = :"attr_name_#{index}"
|
|
361
|
+
if attr.is_a?(Array) && attr.size == 2
|
|
362
|
+
ns_key = :"attr_ns_#{index}"
|
|
363
|
+
bindings[name_key] = attr[1].to_s
|
|
364
|
+
bindings[ns_key] = attr[0].to_s
|
|
365
|
+
predicates << "@*[local-name()=$#{name_key} and namespace-uri()=$#{ns_key} and .=$id]"
|
|
366
|
+
else
|
|
367
|
+
bindings[name_key] = attr.to_s
|
|
368
|
+
predicates << "@*[local-name()=$#{name_key} and namespace-uri()='' and .=$id]"
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
return nil if predicates.empty?
|
|
373
|
+
|
|
374
|
+
document.at_xpath("//*[(#{predicates.join(" or ")})]", nil, bindings)
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
end
|