moxml 0.1.25 → 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/release.yml +9 -0
- data/README.adoc +64 -0
- 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/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/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/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/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/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
- metadata +111 -2
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
module Moxml
|
|
6
|
+
module Signature
|
|
7
|
+
module Algorithms
|
|
8
|
+
# RSASSA-PKCS1-v1_5 per RFC 3447 §8.2, bound to a digest.
|
|
9
|
+
#
|
|
10
|
+
# One Ruby class registered under five URIs (sha1 / sha224 / sha256 /
|
|
11
|
+
# sha384 / sha512). When instantiated, the class is told which URI was
|
|
12
|
+
# resolved so it can pick the right digest.
|
|
13
|
+
class RsaPkcs1Sha < SignatureMethodBase
|
|
14
|
+
PAIRINGS = {
|
|
15
|
+
"http://www.w3.org/2000/09/xmldsig#rsa-sha1" => "SHA1",
|
|
16
|
+
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha224" => "SHA224",
|
|
17
|
+
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" => "SHA256",
|
|
18
|
+
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha384" => "SHA384",
|
|
19
|
+
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" => "SHA512",
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
PAIRINGS.each_key { |uri| identifier uri }
|
|
23
|
+
|
|
24
|
+
def initialize(identifier_uri:, parameters: nil)
|
|
25
|
+
super(nil)
|
|
26
|
+
@identifier_uri = identifier_uri
|
|
27
|
+
@parameters = parameters
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def compute_signature(data, key)
|
|
31
|
+
rsa_key = coerce_key(key)
|
|
32
|
+
rsa_key.sign(digest_name, data)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def verify_signature(data, key, signature)
|
|
36
|
+
rsa_key = coerce_key(key)
|
|
37
|
+
rsa_key.verify(digest_name, signature, data)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def digest_name
|
|
43
|
+
PAIRINGS.fetch(@identifier_uri)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def coerce_key(key)
|
|
47
|
+
return key if key.is_a?(OpenSSL::PKey::RSA)
|
|
48
|
+
|
|
49
|
+
raise SignatureKeyError,
|
|
50
|
+
"RSA signature method requires OpenSSL::PKey::RSA, " \
|
|
51
|
+
"got #{key.class}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
module Algorithms
|
|
6
|
+
# Base class for signature methods and MACs.
|
|
7
|
+
#
|
|
8
|
+
# Subclasses declare an identifier URI per W3C spec §6.4 / §6.3 and
|
|
9
|
+
# implement #compute_signature and #verify_signature.
|
|
10
|
+
#
|
|
11
|
+
# `key` is whatever OpenSSL expects (OpenSSL::PKey::RSA for RSA, a
|
|
12
|
+
# String secret for HMAC, etc.).
|
|
13
|
+
class SignatureMethodBase
|
|
14
|
+
class << self
|
|
15
|
+
attr_reader :identifier_uri, :digest_uri
|
|
16
|
+
|
|
17
|
+
def identifier(uri, digest_uri: nil)
|
|
18
|
+
@identifier_uri = uri
|
|
19
|
+
@digest_uri = digest_uri
|
|
20
|
+
Algorithms.register(:signature_method, uri, self)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Optional constructor for parameterized methods (e.g. HMACOutputLength).
|
|
25
|
+
def initialize(parameters = nil)
|
|
26
|
+
@parameters = parameters
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def sign(data, key)
|
|
30
|
+
compute_signature(data, key)
|
|
31
|
+
rescue OpenSSL::PKey::PKeyError => e
|
|
32
|
+
raise SigningError.new(
|
|
33
|
+
"signing failed: #{e.class}",
|
|
34
|
+
algorithm: self.class.identifier_uri,
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def verify(data, key, signature)
|
|
39
|
+
verify_signature(data, key, signature)
|
|
40
|
+
rescue OpenSSL::PKey::PKeyError => e
|
|
41
|
+
raise VerificationError.new(
|
|
42
|
+
"verification raised: #{e.class}",
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def compute_signature(_data, _key)
|
|
47
|
+
raise NotImplementedError,
|
|
48
|
+
"#{self.class} must implement #compute_signature"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def verify_signature(_data, _key, _signature)
|
|
52
|
+
raise NotImplementedError,
|
|
53
|
+
"#{self.class} must implement #verify_signature"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
module Algorithms
|
|
6
|
+
# Base class for XML Signature transforms.
|
|
7
|
+
#
|
|
8
|
+
# Each transform declares input and output types (:octets or :nodeset)
|
|
9
|
+
# plus an identifier URI. The reference resolver chains them with
|
|
10
|
+
# default type conversion when types mismatch (octets → nodeset via
|
|
11
|
+
# XML parse; nodeset → octets via inclusive C14N 1.0).
|
|
12
|
+
class TransformBase
|
|
13
|
+
class << self
|
|
14
|
+
attr_reader :identifier_uri
|
|
15
|
+
|
|
16
|
+
def identifier(uri)
|
|
17
|
+
@identifier_uri = uri
|
|
18
|
+
Algorithms.register(:transform, uri, self)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def input_type; :octets; end
|
|
22
|
+
def output_type; :octets; end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# `parameters`: optional hash parsed from the ds:Transform element
|
|
26
|
+
# (e.g. { xpaths: [...] } for the XPath Filter transform).
|
|
27
|
+
def initialize(parameters: nil, context: nil, signature_element: nil)
|
|
28
|
+
@parameters = parameters || {}
|
|
29
|
+
@context = context
|
|
30
|
+
@signature_element = signature_element
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def transform(_input)
|
|
34
|
+
raise NotImplementedError,
|
|
35
|
+
"#{self.class} must implement #transform"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
# Open-closed registry of W3C XML Signature algorithms keyed by URI.
|
|
6
|
+
#
|
|
7
|
+
# Adding a new algorithm:
|
|
8
|
+
# 1. Subclass the relevant base (DigestBase, SignatureMethodBase, etc.)
|
|
9
|
+
# 2. Declare `identifier "http://..."` on the subclass.
|
|
10
|
+
# 3. Add an autoload entry below and a reference in `load_builtins!`.
|
|
11
|
+
#
|
|
12
|
+
# No edits to existing classes are required to add a new algorithm.
|
|
13
|
+
module Algorithms
|
|
14
|
+
CATEGORIES = %i[digest signature_method canonicalization transform].freeze
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def registry
|
|
18
|
+
@registry ||= CATEGORIES.to_h { |c| [c, {}] }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def register(category, uri, klass)
|
|
22
|
+
validate_category!(category)
|
|
23
|
+
registry[category][uri] = klass
|
|
24
|
+
klass
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def lookup(category, uri)
|
|
28
|
+
load_builtins! unless @builtins_loaded
|
|
29
|
+
registry[category][uri] ||
|
|
30
|
+
raise(UnknownAlgorithm.new(category, uri))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def registered?(category, uri)
|
|
34
|
+
load_builtins! unless @builtins_loaded
|
|
35
|
+
registry[category].key?(uri)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def [](category)
|
|
39
|
+
validate_category!(category)
|
|
40
|
+
registry[category]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Forces autoload of every built-in algorithm class so that
|
|
44
|
+
# self-registration runs. Pure autoload — no `require` calls.
|
|
45
|
+
def load_builtins!
|
|
46
|
+
return if @builtins_loaded
|
|
47
|
+
|
|
48
|
+
# Digests
|
|
49
|
+
SHA1
|
|
50
|
+
SHA224
|
|
51
|
+
SHA256
|
|
52
|
+
SHA384
|
|
53
|
+
SHA512
|
|
54
|
+
# Signature methods
|
|
55
|
+
RsaPkcs1Sha
|
|
56
|
+
HmacSha
|
|
57
|
+
EcdsaSha
|
|
58
|
+
DsaSha
|
|
59
|
+
# Canonicalization
|
|
60
|
+
ExcC14n10
|
|
61
|
+
InclusiveC14n10
|
|
62
|
+
InclusiveC14n11
|
|
63
|
+
# Transforms
|
|
64
|
+
Base64Transform
|
|
65
|
+
EnvelopedSignatureTransform
|
|
66
|
+
|
|
67
|
+
@builtins_loaded = true
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def validate_category!(category)
|
|
73
|
+
return if CATEGORIES.include?(category)
|
|
74
|
+
|
|
75
|
+
raise ArgumentError,
|
|
76
|
+
"unknown algorithm category #{category.inspect}; " \
|
|
77
|
+
"expected one of #{CATEGORIES.inspect}"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Base classes — interfaces only.
|
|
82
|
+
autoload :DigestBase, "moxml/signature/algorithms/digest_base"
|
|
83
|
+
autoload :SignatureMethodBase,
|
|
84
|
+
"moxml/signature/algorithms/signature_method_base"
|
|
85
|
+
autoload :CanonicalizationBase,
|
|
86
|
+
"moxml/signature/algorithms/canonicalization_base"
|
|
87
|
+
autoload :TransformBase, "moxml/signature/algorithms/transform_base"
|
|
88
|
+
|
|
89
|
+
# Digests
|
|
90
|
+
autoload :SHA1, "moxml/signature/algorithms/sha1"
|
|
91
|
+
autoload :SHA224, "moxml/signature/algorithms/sha224"
|
|
92
|
+
autoload :SHA256, "moxml/signature/algorithms/sha256"
|
|
93
|
+
autoload :SHA384, "moxml/signature/algorithms/sha384"
|
|
94
|
+
autoload :SHA512, "moxml/signature/algorithms/sha512"
|
|
95
|
+
|
|
96
|
+
# Signature methods
|
|
97
|
+
autoload :RsaPkcs1Sha, "moxml/signature/algorithms/rsa_pkcs1_sha"
|
|
98
|
+
autoload :HmacSha, "moxml/signature/algorithms/hmac_sha"
|
|
99
|
+
autoload :EcdsaSha, "moxml/signature/algorithms/ecdsa_sha"
|
|
100
|
+
autoload :DsaSha, "moxml/signature/algorithms/dsa_sha"
|
|
101
|
+
|
|
102
|
+
# Canonicalization (delegates to Moxml::C14n engine)
|
|
103
|
+
autoload :ExcC14n10, "moxml/signature/algorithms/exc_c14n_10"
|
|
104
|
+
autoload :InclusiveC14n10, "moxml/signature/algorithms/inclusive_c14n_10"
|
|
105
|
+
autoload :InclusiveC14n11, "moxml/signature/algorithms/inclusive_c14n_11"
|
|
106
|
+
|
|
107
|
+
# Transforms
|
|
108
|
+
autoload :Base64Transform, "moxml/signature/algorithms/base64_transform"
|
|
109
|
+
autoload :EnvelopedSignatureTransform,
|
|
110
|
+
"moxml/signature/algorithms/enveloped_signature_transform"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
class Error < ::Moxml::Error; end
|
|
6
|
+
|
|
7
|
+
class SignatureError < Error; end
|
|
8
|
+
|
|
9
|
+
class UnknownAlgorithm < Error
|
|
10
|
+
attr_reader :category, :uri
|
|
11
|
+
|
|
12
|
+
def initialize(category, uri)
|
|
13
|
+
@category = category
|
|
14
|
+
@uri = uri
|
|
15
|
+
super("Unknown #{category} algorithm: #{uri}")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class DuplicateAlgorithm < Error
|
|
20
|
+
attr_reader :category, :uri
|
|
21
|
+
|
|
22
|
+
def initialize(category, uri)
|
|
23
|
+
@category = category
|
|
24
|
+
@uri = uri
|
|
25
|
+
super("Algorithm already registered for #{category}: #{uri}")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class SigningError < Error
|
|
30
|
+
attr_reader :algorithm
|
|
31
|
+
|
|
32
|
+
def initialize(message, algorithm: nil)
|
|
33
|
+
@algorithm = algorithm
|
|
34
|
+
super(message)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class VerificationError < Error
|
|
39
|
+
attr_reader :signature_id
|
|
40
|
+
|
|
41
|
+
def initialize(message, signature_id: nil)
|
|
42
|
+
@signature_id = signature_id
|
|
43
|
+
super(message)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class ReferenceDigestMismatch < VerificationError
|
|
48
|
+
attr_reader :reference_uri, :expected, :computed
|
|
49
|
+
|
|
50
|
+
def initialize(reference_uri:, expected:, computed:, signature_id: nil)
|
|
51
|
+
@reference_uri = reference_uri
|
|
52
|
+
@expected = expected
|
|
53
|
+
@computed = computed
|
|
54
|
+
super(
|
|
55
|
+
"Digest mismatch for reference #{reference_uri.inspect}",
|
|
56
|
+
signature_id: signature_id,
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class SignatureValueMismatch < VerificationError
|
|
62
|
+
attr_reader :algorithm
|
|
63
|
+
|
|
64
|
+
def initialize(algorithm: nil, signature_id: nil)
|
|
65
|
+
@algorithm = algorithm
|
|
66
|
+
super("SignatureValue did not verify", signature_id: signature_id)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class TransformError < Error
|
|
71
|
+
attr_reader :algorithm
|
|
72
|
+
|
|
73
|
+
def initialize(message, algorithm: nil)
|
|
74
|
+
@algorithm = algorithm
|
|
75
|
+
super(message)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
class CanonicalizationError < Error
|
|
80
|
+
attr_reader :algorithm
|
|
81
|
+
|
|
82
|
+
def initialize(message, algorithm: nil)
|
|
83
|
+
@algorithm = algorithm
|
|
84
|
+
super(message)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
class MalformedSignatureError < Error
|
|
89
|
+
attr_reader :detail
|
|
90
|
+
|
|
91
|
+
def initialize(message, detail: nil)
|
|
92
|
+
@detail = detail
|
|
93
|
+
super(message)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
class SignatureKeyError < Error; end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "base64"
|
|
5
|
+
|
|
6
|
+
module Moxml
|
|
7
|
+
module Signature
|
|
8
|
+
# Extracts an OpenSSL verification key from a Model::KeyInfo.
|
|
9
|
+
#
|
|
10
|
+
# Strategy (per spec §4.5): X509Certificate is preferred for key
|
|
11
|
+
# reconstruction because it carries the certified binding. Falls back
|
|
12
|
+
# to RSAKeyValue / DSAKeyValue / ECKeyValue if no certificate is
|
|
13
|
+
# present. KeyName is resolved via an application-supplied key map.
|
|
14
|
+
class KeyExtractor
|
|
15
|
+
attr_reader :key_map, :cert_store
|
|
16
|
+
|
|
17
|
+
def initialize(key_map: {}, cert_store: nil)
|
|
18
|
+
@key_map = key_map || {}
|
|
19
|
+
@cert_store = cert_store
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def extract(key_info)
|
|
23
|
+
return nil if key_info.nil?
|
|
24
|
+
|
|
25
|
+
from_x509_data(key_info.x509_data) ||
|
|
26
|
+
from_key_value(key_info.key_value) ||
|
|
27
|
+
from_key_name(key_info.key_name)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def from_x509_data(x509_data)
|
|
33
|
+
return nil unless x509_data
|
|
34
|
+
|
|
35
|
+
cert_b64 = x509_data.certificates.first
|
|
36
|
+
return nil unless cert_b64
|
|
37
|
+
|
|
38
|
+
# Certificates are stored as base64-encoded DER (spec §4.5.4).
|
|
39
|
+
# Decode here so we hand OpenSSL raw DER bytes.
|
|
40
|
+
cert_der = Base64.strict_decode64(cert_b64.to_s.gsub(/\s+/, ""))
|
|
41
|
+
cert = OpenSSL::X509::Certificate.new(cert_der)
|
|
42
|
+
cert.public_key
|
|
43
|
+
rescue OpenSSL::X509::CertificateError, ArgumentError
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def from_key_value(key_value)
|
|
48
|
+
return nil unless key_value
|
|
49
|
+
|
|
50
|
+
if key_value.rsa_key_value
|
|
51
|
+
rsa_public_key(key_value.rsa_key_value)
|
|
52
|
+
elsif key_value.dsa_key_value
|
|
53
|
+
dsa_public_key(key_value.dsa_key_value)
|
|
54
|
+
elsif key_value.ec_key_value
|
|
55
|
+
ec_public_key(key_value.ec_key_value)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def from_key_name(key_name)
|
|
60
|
+
return nil unless key_name
|
|
61
|
+
|
|
62
|
+
key_map[key_name]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def rsa_public_key(rsa_kv)
|
|
66
|
+
n = crypto_binary_to_integer(rsa_kv.modulus)
|
|
67
|
+
e = crypto_binary_to_integer(rsa_kv.exponent)
|
|
68
|
+
return nil unless n && e
|
|
69
|
+
|
|
70
|
+
# OpenSSL 3.x removed RSA.new(n, e). Reconstruct via the
|
|
71
|
+
# SubjectPublicKeyInfo form (PKCS#1 RSA public key DER).
|
|
72
|
+
asn1 = OpenSSL::ASN1::Sequence.new(
|
|
73
|
+
[
|
|
74
|
+
OpenSSL::ASN1::Integer.new(n),
|
|
75
|
+
OpenSSL::ASN1::Integer.new(e),
|
|
76
|
+
],
|
|
77
|
+
)
|
|
78
|
+
OpenSSL::PKey::RSA.new(asn1.to_der)
|
|
79
|
+
rescue OpenSSL::PKey::RSAError
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def dsa_public_key(dsa_kv)
|
|
84
|
+
p = crypto_binary_to_integer(dsa_kv.p)
|
|
85
|
+
q = crypto_binary_to_integer(dsa_kv.q)
|
|
86
|
+
g = crypto_binary_to_integer(dsa_kv.g) if dsa_kv.g
|
|
87
|
+
y = crypto_binary_to_integer(dsa_kv.y)
|
|
88
|
+
return nil unless p && q && y
|
|
89
|
+
|
|
90
|
+
params = OpenSSL::ASN1::Sequence.new(
|
|
91
|
+
[
|
|
92
|
+
OpenSSL::ASN1::Integer.new(p),
|
|
93
|
+
OpenSSL::ASN1::Integer.new(q),
|
|
94
|
+
OpenSSL::ASN1::Integer.new(g || 0),
|
|
95
|
+
OpenSSL::ASN1::Integer.new(y),
|
|
96
|
+
],
|
|
97
|
+
)
|
|
98
|
+
OpenSSL::PKey::DSA.new(params.to_der)
|
|
99
|
+
rescue OpenSSL::PKey::DSAError
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def ec_public_key(ec_kv)
|
|
104
|
+
return nil unless ec_kv.named_curve_uri && ec_kv.public_key
|
|
105
|
+
|
|
106
|
+
curve_name = curve_name_for(ec_kv.named_curve_uri)
|
|
107
|
+
return nil unless curve_name
|
|
108
|
+
|
|
109
|
+
# The XML signature PublicKey element already contains the
|
|
110
|
+
# uncompressed-point form (0x04 || x || y) per spec §4.5.2.3.
|
|
111
|
+
public_key_decoded = Base64.strict_decode64(ec_kv.public_key)
|
|
112
|
+
spki_der = build_ec_subject_public_key_info(curve_name, public_key_decoded)
|
|
113
|
+
OpenSSL::PKey::EC.new(spki_der)
|
|
114
|
+
rescue OpenSSL::PKey::ECError, ArgumentError
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Build a SubjectPublicKeyInfo DER for an EC public key.
|
|
119
|
+
# OpenSSL 3.0 makes PKey instances immutable; we can no longer
|
|
120
|
+
# create an empty EC key and assign public_key=. Constructing the
|
|
121
|
+
# full SPKI and reading it back via OpenSSL::PKey::EC.new(der)
|
|
122
|
+
# is the supported path.
|
|
123
|
+
def build_ec_subject_public_key_info(curve_name, point_octets)
|
|
124
|
+
algorithm = OpenSSL::ASN1::Sequence.new(
|
|
125
|
+
[
|
|
126
|
+
OpenSSL::ASN1::ObjectId.new("id-ecPublicKey"),
|
|
127
|
+
OpenSSL::ASN1::ObjectId.new(curve_name_to_oid(curve_name)),
|
|
128
|
+
],
|
|
129
|
+
)
|
|
130
|
+
OpenSSL::ASN1::Sequence.new(
|
|
131
|
+
[
|
|
132
|
+
algorithm,
|
|
133
|
+
OpenSSL::ASN1::BitString.new(point_octets),
|
|
134
|
+
],
|
|
135
|
+
).to_der
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
CURVE_NAME_TO_OID = {
|
|
139
|
+
"prime256v1" => "1.2.840.10045.3.1.7",
|
|
140
|
+
"secp384r1" => "1.3.132.0.34",
|
|
141
|
+
"secp521r1" => "1.3.132.0.35",
|
|
142
|
+
}.freeze
|
|
143
|
+
private_constant :CURVE_NAME_TO_OID
|
|
144
|
+
|
|
145
|
+
def curve_name_to_oid(curve_name)
|
|
146
|
+
CURVE_NAME_TO_OID.fetch(curve_name)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# ds:CryptoBinary is base64-encoded I2OSP output.
|
|
150
|
+
def crypto_binary_to_integer(base64_text)
|
|
151
|
+
return nil unless base64_text
|
|
152
|
+
|
|
153
|
+
raw = Base64.strict_decode64(base64_text.to_s.gsub(/\s+/, ""))
|
|
154
|
+
raw.bytes.reduce(0) { |acc, byte| (acc << 8) | byte }
|
|
155
|
+
rescue ArgumentError
|
|
156
|
+
nil
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Map NamedCurve URN OIDs to OpenSSL curve names (RFC 5480 §2.1).
|
|
160
|
+
CURVE_OID_TO_NAME = {
|
|
161
|
+
"urn:oid:1.2.840.10045.3.1.7" => "prime256v1", # P-256
|
|
162
|
+
"urn:oid:1.3.132.0.34" => "secp384r1", # P-384
|
|
163
|
+
"urn:oid:1.3.132.0.35" => "secp521r1", # P-521
|
|
164
|
+
}.freeze
|
|
165
|
+
private_constant :CURVE_OID_TO_NAME
|
|
166
|
+
|
|
167
|
+
def curve_name_for(uri)
|
|
168
|
+
CURVE_OID_TO_NAME[uri]
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
module Model
|
|
6
|
+
# Generic algorithm method element. Used for CanonicalizationMethod and
|
|
7
|
+
# SignatureMethod, both of which are `<Foo Algorithm="uri" />` shape.
|
|
8
|
+
class AlgorithmMethod
|
|
9
|
+
attr_accessor :algorithm, :parameters
|
|
10
|
+
|
|
11
|
+
def initialize(algorithm:, parameters: {})
|
|
12
|
+
@algorithm = algorithm
|
|
13
|
+
@parameters = parameters
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module Signature
|
|
5
|
+
module Model
|
|
6
|
+
module Key
|
|
7
|
+
# ds:DSAKeyValue — P, Q, G, Y required; J, Seed, PgenCounter optional.
|
|
8
|
+
# Attribute names mirror the W3C spec §4.5.2.1 element names
|
|
9
|
+
# (P, Q, G, Y, J, Seed, PgenCounter).
|
|
10
|
+
class DSAKeyValue
|
|
11
|
+
attr_accessor :p, :q, :g, :y, :j, :seed, :pgen_counter
|
|
12
|
+
|
|
13
|
+
# rubocop:disable Naming/MethodParameterName -- P/Q/G/Y/J are W3C spec names
|
|
14
|
+
def initialize(p:, q:, y:, g: nil, j: nil, seed: nil,
|
|
15
|
+
pgen_counter: nil)
|
|
16
|
+
@p = p
|
|
17
|
+
@q = q
|
|
18
|
+
@y = y
|
|
19
|
+
@g = g
|
|
20
|
+
@j = j
|
|
21
|
+
@seed = seed
|
|
22
|
+
@pgen_counter = pgen_counter
|
|
23
|
+
end
|
|
24
|
+
# rubocop:enable Naming/MethodParameterName
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|