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,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
# Reference generation + SignatureValue computation per spec §3.1.
|
|
6
|
+
#
|
|
7
|
+
# Flow:
|
|
8
|
+
# 1. For each Reference in signed_info.references:
|
|
9
|
+
# a. Resolve URI (same-document or octet).
|
|
10
|
+
# b. Apply each Transform in order.
|
|
11
|
+
# c. Compute digest via DigestMethod.
|
|
12
|
+
# d. Store base64 digest on Reference#digest_value.
|
|
13
|
+
# 2. Serialize SignedInfo.
|
|
14
|
+
# 3. Canonicalize per CanonicalizationMethod.
|
|
15
|
+
# 4. Sign via SignatureMethod#sign(canonical_octets, key).
|
|
16
|
+
# 5. Set signature.signature_value.
|
|
17
|
+
class Signer
|
|
18
|
+
attr_reader :context, :signature, :document, :key
|
|
19
|
+
|
|
20
|
+
def initialize(context:, signature:, document:, key:)
|
|
21
|
+
@context = context
|
|
22
|
+
@signature = signature
|
|
23
|
+
@document = document
|
|
24
|
+
@key = key
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def sign
|
|
28
|
+
Algorithms.load_builtins!
|
|
29
|
+
|
|
30
|
+
signature.signed_info.references.each do |ref|
|
|
31
|
+
digest = compute_reference_digest(ref)
|
|
32
|
+
ref.digest_value = digest
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
signature.signature_value = Model::SignatureValue.new(
|
|
36
|
+
value: compute_signature_value,
|
|
37
|
+
)
|
|
38
|
+
signature
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def compute_reference_digest(reference)
|
|
44
|
+
resolver = ReferenceResolver.new(context: context, document: document)
|
|
45
|
+
input = resolver.resolve(reference.uri)
|
|
46
|
+
pipeline = TransformPipeline.new(context: context, signature_element: nil)
|
|
47
|
+
transformed = pipeline.apply(input, reference.transforms)
|
|
48
|
+
canonical = pipeline.to_octets(transformed, reference)
|
|
49
|
+
digest_algo = Algorithms.lookup(:digest, reference.digest_method.algorithm).new
|
|
50
|
+
digest_algo.digest_base64(canonical)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def compute_signature_value
|
|
54
|
+
signed_info = signature.signed_info
|
|
55
|
+
serializer = Serializer.new(context: context)
|
|
56
|
+
signed_info_doc = serializer.serialize_signed_info(signed_info)
|
|
57
|
+
|
|
58
|
+
c14n_uri = signed_info.canonicalization_method.algorithm
|
|
59
|
+
c14n_klass = Algorithms.lookup(:canonicalization, c14n_uri)
|
|
60
|
+
c14n = c14n_klass.new(identifier_uri: c14n_uri)
|
|
61
|
+
canonical_octets = c14n.canonicalize(signed_info_doc.root)
|
|
62
|
+
|
|
63
|
+
sm_uri = signed_info.signature_method.algorithm
|
|
64
|
+
sm_klass = Algorithms.lookup(:signature_method, sm_uri)
|
|
65
|
+
sm = sm_klass.new(
|
|
66
|
+
identifier_uri: sm_uri,
|
|
67
|
+
parameters: signed_info.signature_method.parameters,
|
|
68
|
+
)
|
|
69
|
+
sm.sign(canonical_octets, key)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
# Verification outcome for a single Signature element. Carries the
|
|
6
|
+
# cryptographic-result boolean, per-reference results, and (when
|
|
7
|
+
# applicable) the error that caused failure.
|
|
8
|
+
class SingleVerificationResult
|
|
9
|
+
attr_reader :signature_id, :references, :error
|
|
10
|
+
|
|
11
|
+
def initialize(signature_id:, signature_valid:, references:, error: nil)
|
|
12
|
+
@signature_id = signature_id
|
|
13
|
+
@signature_valid = signature_valid
|
|
14
|
+
@references = references
|
|
15
|
+
@error = error
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def signature_valid?
|
|
19
|
+
@signature_valid
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def valid?
|
|
23
|
+
@signature_valid && references.all?(&:valid?)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def failing_references
|
|
27
|
+
references.reject(&:valid?)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
# Shared transform-pipeline logic used by both Signer (signing flow,
|
|
6
|
+
# signature_element: nil) and Verifier (verification flow, with the
|
|
7
|
+
# containing Signature element so the Enveloped Signature transform
|
|
8
|
+
# can remove it from the digest input).
|
|
9
|
+
#
|
|
10
|
+
# Each Transform in the chain is looked up via Algorithms.lookup(:transform, uri),
|
|
11
|
+
# with a fallback to Algorithms.lookup(:canonicalization, uri) per
|
|
12
|
+
# spec §6.6.1 (any canonicalization algorithm can be used as a transform).
|
|
13
|
+
#
|
|
14
|
+
# Type coercion matches spec §4.4.3.2 reference processing model:
|
|
15
|
+
# - octets → nodeset: parse as XML
|
|
16
|
+
# - nodeset → octets: apply inclusive C14N 1.0
|
|
17
|
+
class TransformPipeline
|
|
18
|
+
attr_reader :context, :signature_element
|
|
19
|
+
|
|
20
|
+
def initialize(context:, signature_element: nil)
|
|
21
|
+
@context = context
|
|
22
|
+
@signature_element = signature_element
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def apply(input, transforms_model)
|
|
26
|
+
current = input
|
|
27
|
+
current_type = type_of(current)
|
|
28
|
+
transforms = transforms_from(transforms_model)
|
|
29
|
+
|
|
30
|
+
transforms.each do |transform_model|
|
|
31
|
+
algo_class = lookup(transform_model.algorithm)
|
|
32
|
+
transform = algo_class.new(
|
|
33
|
+
parameters: transform_model.parameters,
|
|
34
|
+
context: context,
|
|
35
|
+
signature_element: signature_element,
|
|
36
|
+
)
|
|
37
|
+
current_type, current = coerce(current, current_type,
|
|
38
|
+
algo_class.input_type)
|
|
39
|
+
current = transform.transform(current)
|
|
40
|
+
current_type = algo_class.output_type
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
current
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Final step: convert the pipeline output (which may still be a node)
|
|
47
|
+
# to octets suitable for digesting. Uses inclusive C14N 1.0 as the
|
|
48
|
+
# default mapping per spec §4.4.3.2.
|
|
49
|
+
def to_octets(value, reference = nil)
|
|
50
|
+
return value if value.is_a?(String)
|
|
51
|
+
|
|
52
|
+
c14n_uri = canonicalization_from(reference) || DEFAULT_OCTET_C14N
|
|
53
|
+
klass = Algorithms.lookup(:canonicalization, c14n_uri)
|
|
54
|
+
klass.new(identifier_uri: c14n_uri).canonicalize(value)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
DEFAULT_OCTET_C14N = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
|
|
58
|
+
|
|
59
|
+
private_constant :DEFAULT_OCTET_C14N
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def transforms_from(transforms_model)
|
|
64
|
+
return [] unless transforms_model
|
|
65
|
+
|
|
66
|
+
transforms_model.transforms
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def lookup(uri)
|
|
70
|
+
Algorithms.lookup(:transform, uri)
|
|
71
|
+
rescue UnknownAlgorithm
|
|
72
|
+
Algorithms.lookup(:canonicalization, uri)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def type_of(value)
|
|
76
|
+
case value
|
|
77
|
+
when ::Moxml::Node, Array then :nodeset
|
|
78
|
+
else :octets
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def coerce(input, from, to)
|
|
83
|
+
return [to, input] if from == to
|
|
84
|
+
|
|
85
|
+
case [from, to]
|
|
86
|
+
when %i[octets nodeset]
|
|
87
|
+
parsed = context.parse(input.to_s)
|
|
88
|
+
[:nodeset, parsed.root]
|
|
89
|
+
when %i[nodeset octets]
|
|
90
|
+
octets = C14n::Inclusive10.new.canonicalize(input)
|
|
91
|
+
[:octets, octets]
|
|
92
|
+
else
|
|
93
|
+
raise TransformError, "cannot coerce #{from} to #{to}"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def canonicalization_from(reference)
|
|
98
|
+
return nil unless reference&.transforms
|
|
99
|
+
|
|
100
|
+
reference.transforms.transforms.reverse_each.find do |t|
|
|
101
|
+
Algorithms.registered?(:canonicalization, t.algorithm)
|
|
102
|
+
end&.algorithm
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
# Aggregated verification result for a document. A document may
|
|
6
|
+
# contain multiple Signature elements; this result wraps them all.
|
|
7
|
+
class VerificationResult
|
|
8
|
+
attr_reader :results
|
|
9
|
+
|
|
10
|
+
def initialize(results:)
|
|
11
|
+
@results = results
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def valid?
|
|
15
|
+
results.all?(&:valid?)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def signature_count
|
|
19
|
+
results.size
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def failing
|
|
23
|
+
results.reject(&:valid?)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
# Core Validation per spec §3.2, with the safe ordering prescribed by
|
|
6
|
+
# Best Practice 1: verify SignatureValue BEFORE running any reference
|
|
7
|
+
# transforms (which could be hostile).
|
|
8
|
+
class Verifier
|
|
9
|
+
attr_reader :context, :document, :key, :key_map
|
|
10
|
+
|
|
11
|
+
def initialize(context:, document:, key: nil, key_map: {}, **_options)
|
|
12
|
+
@context = context
|
|
13
|
+
@document = document
|
|
14
|
+
@key = key
|
|
15
|
+
@key_map = key_map || {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def verify
|
|
19
|
+
Algorithms.load_builtins!
|
|
20
|
+
signatures = find_signatures
|
|
21
|
+
|
|
22
|
+
results = signatures.map { |sig_elem| verify_one(sig_elem) }
|
|
23
|
+
VerificationResult.new(results: results)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def find_signatures
|
|
29
|
+
document.xpath("//ds:Signature", "ds" => DSIG_NS)
|
|
30
|
+
.grep(::Moxml::Element)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def verify_one(signature_element)
|
|
34
|
+
signature = Parser.new(context: context).parse(signature_element)
|
|
35
|
+
signature_value_ok, error = verify_signature_value(signature, signature_element)
|
|
36
|
+
reference_results = if signature_value_ok
|
|
37
|
+
verify_references(signature, signature_element)
|
|
38
|
+
else
|
|
39
|
+
[]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
SingleVerificationResult.new(
|
|
43
|
+
signature_id: signature.id,
|
|
44
|
+
signature_valid: signature_value_ok,
|
|
45
|
+
references: reference_results,
|
|
46
|
+
error: error,
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns [Boolean, ErrorOrNil] so the caller can attach the error
|
|
51
|
+
# to the result for debugging. The error is never raised — Best
|
|
52
|
+
# Practice 1 says authenticate first; surfacing why auth failed is
|
|
53
|
+
# an application concern, not a panic.
|
|
54
|
+
def verify_signature_value(signature, signature_element)
|
|
55
|
+
return [false, nil] if signature.signature_value.nil?
|
|
56
|
+
return [false, nil] if signature.signature_value.value.nil?
|
|
57
|
+
|
|
58
|
+
signed_info_elem = signature_element.at_xpath(
|
|
59
|
+
"./ds:SignedInfo", "ds" => DSIG_NS
|
|
60
|
+
)
|
|
61
|
+
return [false, nil] if signed_info_elem.nil?
|
|
62
|
+
|
|
63
|
+
c14n_uri = signature.signed_info.canonicalization_method&.algorithm
|
|
64
|
+
return [false, nil] if c14n_uri.nil?
|
|
65
|
+
|
|
66
|
+
canonical = canonicalize_signed_info(c14n_uri, signed_info_elem)
|
|
67
|
+
sm_uri = signature.signed_info.signature_method.algorithm
|
|
68
|
+
sm = Algorithms.lookup(:signature_method, sm_uri).new(
|
|
69
|
+
identifier_uri: sm_uri,
|
|
70
|
+
parameters: signature.signed_info.signature_method.parameters,
|
|
71
|
+
)
|
|
72
|
+
verification_key = resolve_key(signature)
|
|
73
|
+
return [false, nil] if verification_key.nil?
|
|
74
|
+
|
|
75
|
+
ok = sm.verify(canonical, verification_key, signature.signature_value.value)
|
|
76
|
+
[ok, nil]
|
|
77
|
+
rescue VerificationError, UnknownAlgorithm => e
|
|
78
|
+
[false, e]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Canonicalize the original SignedInfo element as it appears in the
|
|
82
|
+
# document. Re-serializing the parsed model would change namespace
|
|
83
|
+
# prefixes (e.g. SignedInfo → ds:SignedInfo) and break byte-exact
|
|
84
|
+
# verification.
|
|
85
|
+
def canonicalize_signed_info(c14n_uri, signed_info_elem)
|
|
86
|
+
Algorithms.lookup(:canonicalization, c14n_uri)
|
|
87
|
+
.new(identifier_uri: c14n_uri)
|
|
88
|
+
.canonicalize(signed_info_elem)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def resolve_key(signature)
|
|
92
|
+
return key if key
|
|
93
|
+
return nil unless signature.key_info
|
|
94
|
+
|
|
95
|
+
KeyExtractor.new(key_map: key_map).extract(signature.key_info)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def verify_references(signature, signature_element)
|
|
99
|
+
signature.signed_info.references.map do |ref|
|
|
100
|
+
verify_reference(ref, signature_element)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def verify_reference(reference, signature_element)
|
|
105
|
+
resolver = ReferenceResolver.new(context: context, document: document)
|
|
106
|
+
input = resolver.resolve(reference.uri)
|
|
107
|
+
pipeline = TransformPipeline.new(
|
|
108
|
+
context: context,
|
|
109
|
+
signature_element: signature_element,
|
|
110
|
+
)
|
|
111
|
+
transformed = pipeline.apply(input, reference.transforms)
|
|
112
|
+
canonical = pipeline.to_octets(transformed, reference)
|
|
113
|
+
|
|
114
|
+
digest_algo = Algorithms.lookup(
|
|
115
|
+
:digest, reference.digest_method.algorithm
|
|
116
|
+
).new
|
|
117
|
+
computed = digest_algo.digest_base64(canonical)
|
|
118
|
+
expected = (reference.digest_value || "").strip
|
|
119
|
+
|
|
120
|
+
ReferenceResult.new(
|
|
121
|
+
uri: reference.uri,
|
|
122
|
+
digest_method: reference.digest_method.algorithm,
|
|
123
|
+
expected: expected,
|
|
124
|
+
computed: computed,
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "base64"
|
|
5
|
+
|
|
6
|
+
module Moxml
|
|
7
|
+
# W3C XML Signature (xmldsig-core-1.1) processing for any moxml adapter.
|
|
8
|
+
#
|
|
9
|
+
# All XML operations flow through Moxml::Document / Moxml::Element; the
|
|
10
|
+
# signature module never touches the underlying adapter (Nokogiri, Oga,
|
|
11
|
+
# REXML, Ox, LibXML) directly.
|
|
12
|
+
module Signature
|
|
13
|
+
DSIG_NS = "http://www.w3.org/2000/09/xmldsig#"
|
|
14
|
+
DSIG11_NS = "http://www.w3.org/2009/xmldsig11#"
|
|
15
|
+
DSIG_MORE_NS = "http://www.w3.org/2001/04/xmldsig-more#"
|
|
16
|
+
|
|
17
|
+
autoload :Error, "moxml/signature/errors"
|
|
18
|
+
autoload :SignatureError, "moxml/signature/errors"
|
|
19
|
+
autoload :UnknownAlgorithm, "moxml/signature/errors"
|
|
20
|
+
autoload :DuplicateAlgorithm, "moxml/signature/errors"
|
|
21
|
+
autoload :SigningError, "moxml/signature/errors"
|
|
22
|
+
autoload :VerificationError, "moxml/signature/errors"
|
|
23
|
+
autoload :ReferenceDigestMismatch, "moxml/signature/errors"
|
|
24
|
+
autoload :SignatureValueMismatch, "moxml/signature/errors"
|
|
25
|
+
autoload :TransformError, "moxml/signature/errors"
|
|
26
|
+
autoload :CanonicalizationError, "moxml/signature/errors"
|
|
27
|
+
autoload :MalformedSignatureError, "moxml/signature/errors"
|
|
28
|
+
autoload :SignatureKeyError, "moxml/signature/errors"
|
|
29
|
+
|
|
30
|
+
autoload :Algorithms, "moxml/signature/algorithms"
|
|
31
|
+
autoload :C14n, "moxml/c14n"
|
|
32
|
+
autoload :Model, "moxml/signature/model"
|
|
33
|
+
autoload :Serializer, "moxml/signature/serializer"
|
|
34
|
+
autoload :Parser, "moxml/signature/parser"
|
|
35
|
+
autoload :ReferenceResolver, "moxml/signature/reference_resolver"
|
|
36
|
+
autoload :Signer, "moxml/signature/signer"
|
|
37
|
+
autoload :Verifier, "moxml/signature/verifier"
|
|
38
|
+
autoload :KeyExtractor, "moxml/signature/key_extractor"
|
|
39
|
+
autoload :TransformPipeline, "moxml/signature/transform_pipeline"
|
|
40
|
+
autoload :VerificationResult, "moxml/signature/verification_result"
|
|
41
|
+
autoload :SingleVerificationResult,
|
|
42
|
+
"moxml/signature/single_verification_result"
|
|
43
|
+
autoload :ReferenceResult, "moxml/signature/reference_result"
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
def sign(context:, document:, key:, **options)
|
|
47
|
+
signature = build_signature(context: context, document: document, **options)
|
|
48
|
+
Signer.new(
|
|
49
|
+
context: context,
|
|
50
|
+
signature: signature,
|
|
51
|
+
document: document,
|
|
52
|
+
key: key,
|
|
53
|
+
).sign
|
|
54
|
+
signature
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def verify(context:, document:, key: nil, **options)
|
|
58
|
+
Verifier.new(context: context, document: document, key: key, **options).verify
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def build_signature(**opts)
|
|
64
|
+
_context = opts[:context]
|
|
65
|
+
_document = opts[:document]
|
|
66
|
+
signed_info = Model::SignedInfo.new(
|
|
67
|
+
canonicalization_method: Model::AlgorithmMethod.new(
|
|
68
|
+
algorithm: opts[:canonicalization_method],
|
|
69
|
+
),
|
|
70
|
+
signature_method: Model::AlgorithmMethod.new(
|
|
71
|
+
algorithm: opts[:signature_method],
|
|
72
|
+
),
|
|
73
|
+
references: [
|
|
74
|
+
Model::Reference.new(
|
|
75
|
+
uri: opts[:reference_uri],
|
|
76
|
+
transforms: Model::Transforms.new(
|
|
77
|
+
transforms: opts[:transforms].map do |transform|
|
|
78
|
+
Model::Transform.new(algorithm: transform)
|
|
79
|
+
end,
|
|
80
|
+
),
|
|
81
|
+
digest_method: Model::DigestMethod.new(algorithm: opts[:digest_method]),
|
|
82
|
+
),
|
|
83
|
+
],
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
Model::Signature.new(
|
|
87
|
+
id: opts[:signature_id],
|
|
88
|
+
signed_info: signed_info,
|
|
89
|
+
key_info: opts[:key_info],
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
data/lib/moxml/version.rb
CHANGED
data/lib/moxml.rb
CHANGED
|
@@ -89,6 +89,8 @@ module Moxml
|
|
|
89
89
|
autoload :Adapter, "moxml/adapter"
|
|
90
90
|
autoload :XPath, "moxml/xpath"
|
|
91
91
|
autoload :SAX, "moxml/sax"
|
|
92
|
+
autoload :Signature, "moxml/signature"
|
|
93
|
+
autoload :C14n, "moxml/c14n"
|
|
92
94
|
|
|
93
95
|
# Error hierarchy — each subclass autoloads from the same file
|
|
94
96
|
autoload :Error, "moxml/error"
|
data/moxml.gemspec
CHANGED
|
@@ -27,7 +27,7 @@ Gem::Specification.new do |spec|
|
|
|
27
27
|
# RubyGem that have been added into git.
|
|
28
28
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
29
29
|
`git ls-files -z`.split("\x0").reject do |f|
|
|
30
|
-
f.match(%r{^(test|features)
|
|
30
|
+
f.match(%r{^(test|features|vendor/)})
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|