moxml 0.1.24 → 0.1.26
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/.github/workflows/docs.yml +1 -1
- data/.github/workflows/opal.yml +18 -1
- data/.github/workflows/rake.yml +32 -3
- data/.github/workflows/release.yml +9 -0
- data/.github/workflows/round-trip.yml +72 -9
- data/.gitmodules +6 -0
- data/.rubocop_todo.yml +2 -0
- data/Gemfile +9 -1
- data/README.adoc +188 -0
- data/Rakefile +116 -4
- data/docs/signature/algorithms.md +108 -0
- data/docs/signature/architecture.md +148 -0
- data/docs/signature/c14n.md +110 -0
- data/docs/signature/examples.md +109 -0
- data/docs/signature/flows.md +132 -0
- data/docs/signature/quick-reference.md +115 -0
- data/docs/signature/security.md +143 -0
- data/examples/signature/auto_key_extraction.rb +24 -0
- data/examples/signature/enveloped_rsa.rb +45 -0
- data/examples/signature/hmac.rb +33 -0
- data/lib/compat/opal/moxml_boot.rb +4 -0
- data/lib/moxml/adapter.rb +5 -3
- data/lib/moxml/c14n/attribute_handler.rb +71 -0
- data/lib/moxml/c14n/character_encoder.rb +29 -0
- data/lib/moxml/c14n/data_model.rb +145 -0
- data/lib/moxml/c14n/exclusive.rb +188 -0
- data/lib/moxml/c14n/inclusive_10.rb +42 -0
- data/lib/moxml/c14n/inclusive_11.rb +21 -0
- data/lib/moxml/c14n/namespace_context.rb +68 -0
- data/lib/moxml/c14n/namespace_handler.rb +71 -0
- data/lib/moxml/c14n/node.rb +42 -0
- data/lib/moxml/c14n/nodes/attribute_node.rb +50 -0
- data/lib/moxml/c14n/nodes/comment_node.rb +29 -0
- data/lib/moxml/c14n/nodes/element_node.rb +62 -0
- data/lib/moxml/c14n/nodes/namespace_node.rb +41 -0
- data/lib/moxml/c14n/nodes/processing_instruction_node.rb +30 -0
- data/lib/moxml/c14n/nodes/root_node.rb +22 -0
- data/lib/moxml/c14n/nodes/text_node.rb +29 -0
- data/lib/moxml/c14n/nodes.rb +17 -0
- data/lib/moxml/c14n/processor.rb +118 -0
- data/lib/moxml/c14n/writer.rb +121 -0
- data/lib/moxml/c14n/xml_base_handler.rb +140 -0
- data/lib/moxml/c14n.rb +96 -0
- data/lib/moxml/config.rb +1 -1
- data/lib/moxml/signature/algorithms/base64_transform.rb +40 -0
- data/lib/moxml/signature/algorithms/canonicalization_base.rb +87 -0
- data/lib/moxml/signature/algorithms/digest_base.rb +51 -0
- data/lib/moxml/signature/algorithms/dsa_sha.rb +107 -0
- data/lib/moxml/signature/algorithms/ecdsa_sha.rb +125 -0
- data/lib/moxml/signature/algorithms/enveloped_signature_transform.rb +79 -0
- data/lib/moxml/signature/algorithms/exc_c14n_10.rb +23 -0
- data/lib/moxml/signature/algorithms/hmac_sha.rb +117 -0
- data/lib/moxml/signature/algorithms/inclusive_c14n_10.rb +21 -0
- data/lib/moxml/signature/algorithms/inclusive_c14n_11.rb +23 -0
- data/lib/moxml/signature/algorithms/rsa_pkcs1_sha.rb +56 -0
- data/lib/moxml/signature/algorithms/sha1.rb +13 -0
- data/lib/moxml/signature/algorithms/sha224.rb +13 -0
- data/lib/moxml/signature/algorithms/sha256.rb +13 -0
- data/lib/moxml/signature/algorithms/sha384.rb +13 -0
- data/lib/moxml/signature/algorithms/sha512.rb +13 -0
- data/lib/moxml/signature/algorithms/signature_method_base.rb +58 -0
- data/lib/moxml/signature/algorithms/transform_base.rb +40 -0
- data/lib/moxml/signature/algorithms.rb +113 -0
- data/lib/moxml/signature/errors.rb +99 -0
- data/lib/moxml/signature/key_extractor.rb +172 -0
- data/lib/moxml/signature/model/algorithm_method.rb +18 -0
- data/lib/moxml/signature/model/digest_method.rb +15 -0
- data/lib/moxml/signature/model/key/dsa_key_value.rb +29 -0
- data/lib/moxml/signature/model/key/ec_key_value.rb +22 -0
- data/lib/moxml/signature/model/key/rsa_key_value.rb +19 -0
- data/lib/moxml/signature/model/key/x509_data.rb +32 -0
- data/lib/moxml/signature/model/key/x509_digest.rb +19 -0
- data/lib/moxml/signature/model/key/x509_issuer_serial.rb +19 -0
- data/lib/moxml/signature/model/key_info.rb +21 -0
- data/lib/moxml/signature/model/key_value.rb +20 -0
- data/lib/moxml/signature/model/object_element.rb +18 -0
- data/lib/moxml/signature/model/reference.rb +22 -0
- data/lib/moxml/signature/model/signature.rb +21 -0
- data/lib/moxml/signature/model/signature_value.rb +16 -0
- data/lib/moxml/signature/model/signed_info.rb +20 -0
- data/lib/moxml/signature/model/transform.rb +16 -0
- data/lib/moxml/signature/model/transforms.rb +36 -0
- data/lib/moxml/signature/model.rb +35 -0
- data/lib/moxml/signature/parser.rb +270 -0
- data/lib/moxml/signature/reference_resolver.rb +79 -0
- data/lib/moxml/signature/reference_result.rb +22 -0
- data/lib/moxml/signature/serializer.rb +163 -0
- data/lib/moxml/signature/signer.rb +73 -0
- data/lib/moxml/signature/single_verification_result.rb +31 -0
- data/lib/moxml/signature/transform_pipeline.rb +106 -0
- data/lib/moxml/signature/verification_result.rb +27 -0
- data/lib/moxml/signature/verifier.rb +129 -0
- data/lib/moxml/signature.rb +94 -0
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml.rb +2 -0
- data/moxml.gemspec +1 -1
- data/reference-docs/w3c-xmldsig-bestpractices.md +216 -0
- data/reference-docs/w3c-xmldsig-core.md +400 -0
- data/spec/fixtures/xmldsig/keys/rsa_private.pem +28 -0
- data/spec/fixtures/xmldsig/keys/rsa_public.pem +9 -0
- data/spec/fixtures/xmldsig/keys/rsa_ref.pem +15 -0
- data/spec/fixtures/xmldsig/keys/rsa_ref.pub +6 -0
- data/spec/fixtures/xmldsig/sign2-doc.xml +6 -0
- data/spec/fixtures/xmldsig/sign2-result.xml +25 -0
- data/spec/fixtures/xmldsig/sign3-result.xml +39 -0
- data/spec/moxml/adapter/platform_spec.rb +64 -46
- data/spec/moxml/c14n/api_spec.rb +117 -0
- data/spec/moxml/c14n/c14n_spec.rb +88 -0
- data/spec/moxml/c14n/comments_pis_spec.rb +65 -0
- data/spec/moxml/c14n/inclusive10_spec.rb +60 -0
- data/spec/moxml/c14n/namespace_edge_cases_spec.rb +88 -0
- data/spec/moxml/c14n/xml_attributes_spec.rb +54 -0
- data/spec/moxml/opal_oga_adapter_spec.rb +14 -0
- data/spec/moxml/opal_oga_features_spec.rb +212 -0
- data/spec/moxml/opal_oga_smoke_spec.rb +35 -0
- data/spec/moxml/signature/algorithms/base64_transform_spec.rb +26 -0
- data/spec/moxml/signature/algorithms/digest_base_spec.rb +72 -0
- data/spec/moxml/signature/algorithms/dsa_sha_spec.rb +46 -0
- data/spec/moxml/signature/algorithms/ecdsa_sha_spec.rb +72 -0
- data/spec/moxml/signature/algorithms/enveloped_signature_transform_spec.rb +40 -0
- data/spec/moxml/signature/algorithms/hmac_sha_spec.rb +71 -0
- data/spec/moxml/signature/algorithms/rsa_pkcs1_sha_spec.rb +55 -0
- data/spec/moxml/signature/algorithms_spec.rb +76 -0
- data/spec/moxml/signature/cross_verify_spec.rb +64 -0
- data/spec/moxml/signature/edge_cases_spec.rb +284 -0
- data/spec/moxml/signature/fixtures_spec.rb +80 -0
- data/spec/moxml/signature/key_extractor_spec.rb +96 -0
- data/spec/moxml/signature/model_spec.rb +54 -0
- data/spec/moxml/signature/round_trip_spec.rb +113 -0
- data/spec/moxml/xpath/functions/node_functions_spec.rb +1 -1
- data/spec/spec_helper.rb +1 -1
- metadata +115 -2
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
module Model
|
|
6
|
+
class Signature
|
|
7
|
+
attr_accessor :id, :signed_info, :signature_value,
|
|
8
|
+
:key_info, :objects
|
|
9
|
+
|
|
10
|
+
def initialize(id: nil, signed_info: nil, signature_value: nil,
|
|
11
|
+
key_info: nil, objects: [])
|
|
12
|
+
@id = id
|
|
13
|
+
@signed_info = signed_info
|
|
14
|
+
@signature_value = signature_value
|
|
15
|
+
@key_info = key_info
|
|
16
|
+
@objects = objects
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
module Model
|
|
6
|
+
class SignedInfo
|
|
7
|
+
attr_accessor :id, :canonicalization_method, :signature_method,
|
|
8
|
+
:references
|
|
9
|
+
|
|
10
|
+
def initialize(id: nil, canonicalization_method: nil,
|
|
11
|
+
signature_method: nil, references: [])
|
|
12
|
+
@id = id
|
|
13
|
+
@canonicalization_method = canonicalization_method
|
|
14
|
+
@signature_method = signature_method
|
|
15
|
+
@references = references
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
module Model
|
|
6
|
+
class Transform
|
|
7
|
+
attr_accessor :algorithm, :parameters
|
|
8
|
+
|
|
9
|
+
def initialize(algorithm:, parameters: {})
|
|
10
|
+
@algorithm = algorithm
|
|
11
|
+
@parameters = parameters
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
module Model
|
|
6
|
+
class Transforms
|
|
7
|
+
attr_accessor :transforms
|
|
8
|
+
|
|
9
|
+
def initialize(transforms: [])
|
|
10
|
+
@transforms = Array(transforms)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def <<(transform)
|
|
14
|
+
@transforms << transform
|
|
15
|
+
self
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def each(&block)
|
|
19
|
+
@transforms.each(&block)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def empty?
|
|
23
|
+
@transforms.empty?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def size
|
|
27
|
+
@transforms.size
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def length
|
|
31
|
+
@transforms.size
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
# Plain-Ruby-Object model of the W3C ds:Signature schema.
|
|
6
|
+
#
|
|
7
|
+
# Models carry data; they do not (de)serialize themselves. The Serializer
|
|
8
|
+
# walks a model and emits a Moxml document; the Parser walks a Moxml
|
|
9
|
+
# document and constructs a model. This separation keeps the data shape
|
|
10
|
+
# and the wire shape independent.
|
|
11
|
+
module Model
|
|
12
|
+
autoload :Signature, "moxml/signature/model/signature"
|
|
13
|
+
autoload :SignedInfo, "moxml/signature/model/signed_info"
|
|
14
|
+
autoload :Reference, "moxml/signature/model/reference"
|
|
15
|
+
autoload :Transforms, "moxml/signature/model/transforms"
|
|
16
|
+
autoload :Transform, "moxml/signature/model/transform"
|
|
17
|
+
autoload :DigestMethod, "moxml/signature/model/digest_method"
|
|
18
|
+
autoload :AlgorithmMethod, "moxml/signature/model/algorithm_method"
|
|
19
|
+
autoload :SignatureValue, "moxml/signature/model/signature_value"
|
|
20
|
+
autoload :KeyInfo, "moxml/signature/model/key_info"
|
|
21
|
+
autoload :KeyValue, "moxml/signature/model/key_value"
|
|
22
|
+
autoload :ObjectElement, "moxml/signature/model/object_element"
|
|
23
|
+
|
|
24
|
+
module Key
|
|
25
|
+
autoload :X509Data, "moxml/signature/model/key/x509_data"
|
|
26
|
+
autoload :X509IssuerSerial,
|
|
27
|
+
"moxml/signature/model/key/x509_issuer_serial"
|
|
28
|
+
autoload :X509Digest, "moxml/signature/model/key/x509_digest"
|
|
29
|
+
autoload :RSAKeyValue, "moxml/signature/model/key/rsa_key_value"
|
|
30
|
+
autoload :DSAKeyValue, "moxml/signature/model/key/dsa_key_value"
|
|
31
|
+
autoload :ECKeyValue, "moxml/signature/model/key/ec_key_value"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
|
|
5
|
+
module Moxml
|
|
6
|
+
module Signature
|
|
7
|
+
# Translates a Moxml::Document containing ds:Signature into a Model::Signature.
|
|
8
|
+
#
|
|
9
|
+
# Robust against namespace prefix variations (ds:, dsig:, default ns).
|
|
10
|
+
class Parser
|
|
11
|
+
DS = { "ds" => DSIG_NS }.freeze
|
|
12
|
+
|
|
13
|
+
attr_reader :context
|
|
14
|
+
|
|
15
|
+
def initialize(context:)
|
|
16
|
+
@context = context
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# `signature_element`: a Moxml::Element whose name is Signature in the
|
|
20
|
+
# xmldsig# namespace.
|
|
21
|
+
def parse(signature_element)
|
|
22
|
+
Model::Signature.new(
|
|
23
|
+
id: signature_element["Id"],
|
|
24
|
+
signed_info: parse_signed_info(at_child(signature_element, "SignedInfo")),
|
|
25
|
+
signature_value: parse_signature_value(at_child(signature_element, "SignatureValue")),
|
|
26
|
+
key_info: parse_key_info(at_child(signature_element, "KeyInfo")),
|
|
27
|
+
objects: [],
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def parse_signed_info(elem)
|
|
34
|
+
return nil unless elem
|
|
35
|
+
|
|
36
|
+
Model::SignedInfo.new(
|
|
37
|
+
id: elem["Id"],
|
|
38
|
+
canonicalization_method: parse_algorithm_method(at_child(elem, "CanonicalizationMethod")),
|
|
39
|
+
signature_method: parse_algorithm_method(at_child(elem, "SignatureMethod")),
|
|
40
|
+
references: at_children(elem, "Reference").map { |r| parse_reference(r) },
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def parse_algorithm_method(elem)
|
|
45
|
+
return nil unless elem
|
|
46
|
+
|
|
47
|
+
parameters = {}
|
|
48
|
+
hmac_len = at_child(elem, "HMACOutputLength")
|
|
49
|
+
if hmac_len
|
|
50
|
+
length = begin
|
|
51
|
+
Integer(hmac_len.text.strip)
|
|
52
|
+
rescue StandardError
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
parameters[:hmac_output_length] = length if length
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
Model::AlgorithmMethod.new(
|
|
59
|
+
algorithm: elem["Algorithm"],
|
|
60
|
+
parameters: parameters,
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def parse_reference(elem)
|
|
65
|
+
return nil unless elem
|
|
66
|
+
|
|
67
|
+
transforms_elem = at_child(elem, "Transforms")
|
|
68
|
+
Model::Reference.new(
|
|
69
|
+
id: elem["Id"],
|
|
70
|
+
uri: elem["URI"],
|
|
71
|
+
type: elem["Type"],
|
|
72
|
+
transforms: parse_transforms(transforms_elem),
|
|
73
|
+
digest_method: parse_digest_method(at_child(elem, "DigestMethod")),
|
|
74
|
+
digest_value: at_child(elem, "DigestValue")&.text || "",
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def parse_transforms(elem)
|
|
79
|
+
return nil unless elem
|
|
80
|
+
|
|
81
|
+
transforms = at_children(elem, "Transform").map do |t|
|
|
82
|
+
xpath_children = at_children(t, "XPath").map(&:text)
|
|
83
|
+
Model::Transform.new(
|
|
84
|
+
algorithm: t["Algorithm"],
|
|
85
|
+
parameters: xpath_children.empty? ? {} : { xpaths: xpath_children },
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
Model::Transforms.new(transforms: transforms)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def parse_digest_method(elem)
|
|
92
|
+
return nil unless elem
|
|
93
|
+
|
|
94
|
+
Model::DigestMethod.new(algorithm: elem["Algorithm"])
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def parse_signature_value(elem)
|
|
98
|
+
return nil unless elem
|
|
99
|
+
|
|
100
|
+
text = (elem.text || "").gsub(/\s+/, "")
|
|
101
|
+
value = text.empty? ? nil : Base64.strict_decode64(text)
|
|
102
|
+
Model::SignatureValue.new(id: elem["Id"], value: value)
|
|
103
|
+
rescue ArgumentError => e
|
|
104
|
+
raise MalformedSignatureError.new(
|
|
105
|
+
"SignatureValue is not valid base64: #{e.message}",
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def parse_key_info(elem)
|
|
110
|
+
return nil unless elem
|
|
111
|
+
|
|
112
|
+
key_name_el = at_child(elem, "KeyName")
|
|
113
|
+
x509_data_el = at_child(elem, "X509Data")
|
|
114
|
+
key_value_el = at_child(elem, "KeyValue")
|
|
115
|
+
|
|
116
|
+
Model::KeyInfo.new(
|
|
117
|
+
id: elem["Id"],
|
|
118
|
+
key_name: key_name_el&.text,
|
|
119
|
+
x509_data: parse_x509_data(x509_data_el),
|
|
120
|
+
key_value: parse_key_value(key_value_el),
|
|
121
|
+
raw_elements: raw_key_info_children(elem),
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def parse_x509_data(elem)
|
|
126
|
+
return nil unless elem
|
|
127
|
+
|
|
128
|
+
certificates = at_children(elem, "X509Certificate").map do |cert_el|
|
|
129
|
+
strip_base64(cert_el.text)
|
|
130
|
+
end
|
|
131
|
+
issuer_serial_el = at_child(elem, "X509IssuerSerial")
|
|
132
|
+
subject_name_el = at_child(elem, "X509SubjectName")
|
|
133
|
+
ski_el = at_child(elem, "X509SKI")
|
|
134
|
+
crl_els = at_children(elem, "X509CRL")
|
|
135
|
+
# dsig11:X509Digest is in a different namespace; look it up loosely.
|
|
136
|
+
digest_els = elem.children.select do |c|
|
|
137
|
+
c.is_a?(::Moxml::Element) &&
|
|
138
|
+
c.name == "X509Digest" &&
|
|
139
|
+
c.namespace_uri == DSIG11_NS
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
Model::Key::X509Data.new(
|
|
143
|
+
issuer_serial: parse_x509_issuer_serial(issuer_serial_el),
|
|
144
|
+
subject_name: subject_name_el&.text,
|
|
145
|
+
subject_key_id: strip_base64(ski_el&.text),
|
|
146
|
+
certificates: certificates,
|
|
147
|
+
crls: crl_els.map { |e| strip_base64(e.text) },
|
|
148
|
+
digests: digest_els.map { |e| parse_x509_digest(e) },
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def parse_x509_issuer_serial(elem)
|
|
153
|
+
return nil unless elem
|
|
154
|
+
|
|
155
|
+
issuer = at_child(elem, "X509IssuerName")
|
|
156
|
+
serial = at_child(elem, "X509SerialNumber")
|
|
157
|
+
return nil unless issuer && serial
|
|
158
|
+
|
|
159
|
+
Model::Key::X509IssuerSerial.new(
|
|
160
|
+
issuer_name: issuer.text,
|
|
161
|
+
serial_number: serial.text,
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def parse_x509_digest(elem)
|
|
166
|
+
return nil unless elem
|
|
167
|
+
|
|
168
|
+
Model::Key::X509Digest.new(
|
|
169
|
+
algorithm: elem["Algorithm"],
|
|
170
|
+
digest: strip_base64(elem.text),
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def parse_key_value(elem)
|
|
175
|
+
return nil unless elem
|
|
176
|
+
|
|
177
|
+
rsa_el = at_child(elem, "RSAKeyValue")
|
|
178
|
+
dsa_el = at_child(elem, "DSAKeyValue")
|
|
179
|
+
ec_el = elem.children.find do |c|
|
|
180
|
+
c.is_a?(::Moxml::Element) &&
|
|
181
|
+
c.name == "ECKeyValue" &&
|
|
182
|
+
c.namespace_uri == DSIG11_NS
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
Model::KeyValue.new(
|
|
186
|
+
rsa_key_value: parse_rsa_key_value(rsa_el),
|
|
187
|
+
dsa_key_value: parse_dsa_key_value(dsa_el),
|
|
188
|
+
ec_key_value: parse_ec_key_value(ec_el),
|
|
189
|
+
)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def parse_rsa_key_value(elem)
|
|
193
|
+
return nil unless elem
|
|
194
|
+
|
|
195
|
+
Model::Key::RSAKeyValue.new(
|
|
196
|
+
modulus: text_of(elem, "Modulus"),
|
|
197
|
+
exponent: text_of(elem, "Exponent"),
|
|
198
|
+
)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def parse_dsa_key_value(elem)
|
|
202
|
+
return nil unless elem
|
|
203
|
+
|
|
204
|
+
Model::Key::DSAKeyValue.new(
|
|
205
|
+
p: text_of(elem, "P"),
|
|
206
|
+
q: text_of(elem, "Q"),
|
|
207
|
+
g: text_of(elem, "G"),
|
|
208
|
+
y: text_of(elem, "Y"),
|
|
209
|
+
j: text_of(elem, "J"),
|
|
210
|
+
seed: text_of(elem, "Seed"),
|
|
211
|
+
pgen_counter: text_of(elem, "PgenCounter"),
|
|
212
|
+
)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def parse_ec_key_value(elem)
|
|
216
|
+
return nil unless elem
|
|
217
|
+
|
|
218
|
+
named_curve_el = elem.children.find do |c|
|
|
219
|
+
c.is_a?(::Moxml::Element) &&
|
|
220
|
+
c.name == "NamedCurve" &&
|
|
221
|
+
c.namespace_uri == DSIG11_NS
|
|
222
|
+
end
|
|
223
|
+
public_key_el = elem.children.find do |c|
|
|
224
|
+
c.is_a?(::Moxml::Element) &&
|
|
225
|
+
c.name == "PublicKey" &&
|
|
226
|
+
c.namespace_uri == DSIG11_NS
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
Model::Key::ECKeyValue.new(
|
|
230
|
+
named_curve_uri: named_curve_el&.[]("URI"),
|
|
231
|
+
public_key: strip_base64(public_key_el&.text),
|
|
232
|
+
)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def text_of(parent, local_name)
|
|
236
|
+
elem = at_child(parent, local_name)
|
|
237
|
+
elem&.text
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def strip_base64(text)
|
|
241
|
+
return nil if text.nil?
|
|
242
|
+
|
|
243
|
+
text.to_s.gsub(/\s+/, "")
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def raw_key_info_children(elem)
|
|
247
|
+
# Children we don't model explicitly are preserved for round-trip
|
|
248
|
+
# fidelity (PGPData, SPKIData, MgmtData, dsig11:KeyInfoReference,
|
|
249
|
+
# dsig11:DEREncodedKeyValue, xenc:*).
|
|
250
|
+
elem.children.grep(::Moxml::Element).to_a
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def at_child(parent, local_name)
|
|
254
|
+
parent.children.find do |c|
|
|
255
|
+
c.is_a?(::Moxml::Element) &&
|
|
256
|
+
c.name == local_name &&
|
|
257
|
+
c.namespace_uri == DSIG_NS
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def at_children(parent, local_name)
|
|
262
|
+
parent.children.select do |c|
|
|
263
|
+
c.is_a?(::Moxml::Element) &&
|
|
264
|
+
c.name == local_name &&
|
|
265
|
+
c.namespace_uri == DSIG_NS
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
# Translates a Reference URI to a node (subtree) or octet string ready
|
|
6
|
+
# for the transform pipeline.
|
|
7
|
+
#
|
|
8
|
+
# Only same-document references and bare-octet inputs are handled in
|
|
9
|
+
# this tier. External URI dereferencing is documented in
|
|
10
|
+
# TODO.complete/11.
|
|
11
|
+
class ReferenceResolver
|
|
12
|
+
attr_reader :context, :document
|
|
13
|
+
|
|
14
|
+
def initialize(context:, document:)
|
|
15
|
+
@context = context
|
|
16
|
+
@document = document
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Returns:
|
|
20
|
+
# - Moxml::Node for same-document references (subtree apex)
|
|
21
|
+
# - String (octets) for octet inputs
|
|
22
|
+
def resolve(uri)
|
|
23
|
+
case uri.to_s
|
|
24
|
+
when ""
|
|
25
|
+
document.root
|
|
26
|
+
when /\A#/
|
|
27
|
+
resolve_fragment(uri[1..])
|
|
28
|
+
else
|
|
29
|
+
uri.to_s
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def resolve_fragment(fragment)
|
|
36
|
+
case fragment
|
|
37
|
+
when /\Axpointer\(\/\)\z/
|
|
38
|
+
document.root
|
|
39
|
+
when /\Axpointer\(id\(['"]([^'"]+)['"]\)\)\z/
|
|
40
|
+
find_by_id(::Regexp.last_match(1))
|
|
41
|
+
else
|
|
42
|
+
find_by_id(fragment)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def find_by_id(id)
|
|
47
|
+
candidate = search_by_xml_id(id) || search_by_attribute_id(id)
|
|
48
|
+
unless candidate
|
|
49
|
+
raise MalformedSignatureError.new(
|
|
50
|
+
"Reference URI ##{id.inspect} does not resolve to any element",
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
candidate
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def search_by_xml_id(id)
|
|
57
|
+
result = document.at_xpath("//*[@xml:id='#{xpath_escape(id)}']")
|
|
58
|
+
return nil unless result
|
|
59
|
+
return nil unless result.is_a?(::Moxml::Element)
|
|
60
|
+
|
|
61
|
+
result
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def search_by_attribute_id(id)
|
|
65
|
+
matches = document
|
|
66
|
+
.xpath("//*[@Id='#{xpath_escape(id)}'] | " \
|
|
67
|
+
"//*[@ID='#{xpath_escape(id)}']")
|
|
68
|
+
.grep(::Moxml::Element)
|
|
69
|
+
return nil if matches.empty?
|
|
70
|
+
|
|
71
|
+
matches.first
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def xpath_escape(value)
|
|
75
|
+
value.to_s.gsub("'", "''")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
# Per-reference digest comparison result. `valid?` is true iff the
|
|
6
|
+
# freshly-computed digest matches the expected DigestValue.
|
|
7
|
+
class ReferenceResult
|
|
8
|
+
attr_reader :uri, :digest_method, :expected, :computed
|
|
9
|
+
|
|
10
|
+
def initialize(uri:, digest_method:, expected:, computed:)
|
|
11
|
+
@uri = uri
|
|
12
|
+
@digest_method = digest_method
|
|
13
|
+
@expected = expected
|
|
14
|
+
@computed = computed
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def valid?
|
|
18
|
+
expected == computed
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
|
|
5
|
+
module Moxml
|
|
6
|
+
module Signature
|
|
7
|
+
# Translates a Model::Signature into a Moxml::Document.
|
|
8
|
+
#
|
|
9
|
+
# All XML construction goes through moxml primitives
|
|
10
|
+
# (create_element, []=, add_child, create_text). No string templates,
|
|
11
|
+
# no manual attribute escaping — moxml handles entity encoding
|
|
12
|
+
# correctly per the XML spec.
|
|
13
|
+
class Serializer
|
|
14
|
+
DS_PREFIX = "ds"
|
|
15
|
+
|
|
16
|
+
attr_reader :context
|
|
17
|
+
|
|
18
|
+
def initialize(context:)
|
|
19
|
+
@context = context
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def serialize(signature)
|
|
23
|
+
doc = context.create_document
|
|
24
|
+
root = create_namespaced_element("Signature", doc)
|
|
25
|
+
root["Id"] = signature.id if signature.id
|
|
26
|
+
doc.root = root
|
|
27
|
+
|
|
28
|
+
if signature.signed_info
|
|
29
|
+
root.add_child(build_signed_info(signature.signed_info, doc))
|
|
30
|
+
end
|
|
31
|
+
if signature.signature_value
|
|
32
|
+
root.add_child(build_signature_value(signature.signature_value, doc))
|
|
33
|
+
end
|
|
34
|
+
if signature.key_info
|
|
35
|
+
root.add_child(build_key_info(signature.key_info, doc))
|
|
36
|
+
end
|
|
37
|
+
signature.objects.each do |obj|
|
|
38
|
+
root.add_child(build_object(obj, doc))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
doc
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Produces a standalone ds:SignedInfo document. Used by the Signer
|
|
45
|
+
# when canonicalizing SignedInfo in isolation.
|
|
46
|
+
def serialize_signed_info(signed_info)
|
|
47
|
+
doc = context.create_document
|
|
48
|
+
root = create_namespaced_element("SignedInfo", doc)
|
|
49
|
+
root["Id"] = signed_info.id if signed_info.id
|
|
50
|
+
doc.root = root
|
|
51
|
+
populate_signed_info(root, signed_info, doc)
|
|
52
|
+
doc
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def build_signed_info(signed_info, doc)
|
|
58
|
+
el = create_namespaced_element("SignedInfo", doc)
|
|
59
|
+
el["Id"] = signed_info.id if signed_info.id
|
|
60
|
+
populate_signed_info(el, signed_info, doc)
|
|
61
|
+
el
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def populate_signed_info(root, signed_info, doc)
|
|
65
|
+
if signed_info.canonicalization_method
|
|
66
|
+
root.add_child(
|
|
67
|
+
build_algorithm_method("CanonicalizationMethod",
|
|
68
|
+
signed_info.canonicalization_method, doc),
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
if signed_info.signature_method
|
|
72
|
+
root.add_child(
|
|
73
|
+
build_algorithm_method("SignatureMethod",
|
|
74
|
+
signed_info.signature_method, doc),
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
signed_info.references.each do |ref|
|
|
78
|
+
root.add_child(build_reference(ref, doc))
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def build_algorithm_method(name, method, doc)
|
|
83
|
+
el = create_namespaced_element(name, doc)
|
|
84
|
+
el["Algorithm"] = method.algorithm
|
|
85
|
+
if method.parameters.is_a?(Hash) && method.parameters[:hmac_output_length]
|
|
86
|
+
len = create_namespaced_element("HMACOutputLength", doc)
|
|
87
|
+
len.add_child(doc.create_text(method.parameters[:hmac_output_length].to_s))
|
|
88
|
+
el.add_child(len)
|
|
89
|
+
end
|
|
90
|
+
el
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def build_reference(reference, doc)
|
|
94
|
+
el = create_namespaced_element("Reference", doc)
|
|
95
|
+
el["Id"] = reference.id if reference.id
|
|
96
|
+
el["URI"] = reference.uri if reference.uri
|
|
97
|
+
el["Type"] = reference.type if reference.type
|
|
98
|
+
|
|
99
|
+
if reference.transforms && !reference.transforms.empty?
|
|
100
|
+
tf = create_namespaced_element("Transforms", doc)
|
|
101
|
+
reference.transforms.each do |transform|
|
|
102
|
+
tr = create_namespaced_element("Transform", doc)
|
|
103
|
+
tr["Algorithm"] = transform.algorithm
|
|
104
|
+
tf.add_child(tr)
|
|
105
|
+
end
|
|
106
|
+
el.add_child(tf)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
if reference.digest_method
|
|
110
|
+
dm = create_namespaced_element("DigestMethod", doc)
|
|
111
|
+
dm["Algorithm"] = reference.digest_method.algorithm
|
|
112
|
+
el.add_child(dm)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
dv = create_namespaced_element("DigestValue", doc)
|
|
116
|
+
dv.add_child(doc.create_text(reference.digest_value || ""))
|
|
117
|
+
el.add_child(dv)
|
|
118
|
+
el
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def build_signature_value(signature_value, doc)
|
|
122
|
+
el = create_namespaced_element("SignatureValue", doc)
|
|
123
|
+
el["Id"] = signature_value.id if signature_value.id
|
|
124
|
+
if signature_value.value
|
|
125
|
+
el.add_child(doc.create_text(Base64.strict_encode64(signature_value.value)))
|
|
126
|
+
end
|
|
127
|
+
el
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def build_key_info(key_info, doc)
|
|
131
|
+
el = create_namespaced_element("KeyInfo", doc)
|
|
132
|
+
el["Id"] = key_info.id if key_info.id
|
|
133
|
+
if key_info.key_name
|
|
134
|
+
kn = create_namespaced_element("KeyName", doc)
|
|
135
|
+
kn.add_child(doc.create_text(key_info.key_name))
|
|
136
|
+
el.add_child(kn)
|
|
137
|
+
end
|
|
138
|
+
key_info.raw_elements.each { |raw| el.add_child(raw.dup) }
|
|
139
|
+
el
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def build_object(obj, doc)
|
|
143
|
+
el = create_namespaced_element("Object", doc)
|
|
144
|
+
el["Id"] = obj.id if obj.id
|
|
145
|
+
el["MimeType"] = obj.mime_type if obj.mime_type
|
|
146
|
+
el["Encoding"] = obj.encoding if obj.encoding
|
|
147
|
+
# Payload content (Manifest, SignatureProperties, XAdES
|
|
148
|
+
# QualifyingProperties) comes in later tiers.
|
|
149
|
+
el
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Create an element in the xmldsig# namespace with the conventional
|
|
153
|
+
# `ds` prefix. The namespace declaration is added once on the
|
|
154
|
+
# element itself; descendants inherit it.
|
|
155
|
+
def create_namespaced_element(local_name, doc)
|
|
156
|
+
el = doc.create_element(local_name)
|
|
157
|
+
el.add_namespace(DS_PREFIX, DSIG_NS)
|
|
158
|
+
el.namespace = { DS_PREFIX => DSIG_NS }
|
|
159
|
+
el
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|