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.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +9 -0
  3. data/README.adoc +64 -0
  4. data/docs/signature/algorithms.md +108 -0
  5. data/docs/signature/architecture.md +148 -0
  6. data/docs/signature/c14n.md +110 -0
  7. data/docs/signature/examples.md +109 -0
  8. data/docs/signature/flows.md +132 -0
  9. data/docs/signature/quick-reference.md +115 -0
  10. data/docs/signature/security.md +143 -0
  11. data/examples/signature/auto_key_extraction.rb +24 -0
  12. data/examples/signature/enveloped_rsa.rb +45 -0
  13. data/examples/signature/hmac.rb +33 -0
  14. data/lib/moxml/c14n/attribute_handler.rb +71 -0
  15. data/lib/moxml/c14n/character_encoder.rb +29 -0
  16. data/lib/moxml/c14n/data_model.rb +145 -0
  17. data/lib/moxml/c14n/exclusive.rb +188 -0
  18. data/lib/moxml/c14n/inclusive_10.rb +42 -0
  19. data/lib/moxml/c14n/inclusive_11.rb +21 -0
  20. data/lib/moxml/c14n/namespace_context.rb +68 -0
  21. data/lib/moxml/c14n/namespace_handler.rb +71 -0
  22. data/lib/moxml/c14n/node.rb +42 -0
  23. data/lib/moxml/c14n/nodes/attribute_node.rb +50 -0
  24. data/lib/moxml/c14n/nodes/comment_node.rb +29 -0
  25. data/lib/moxml/c14n/nodes/element_node.rb +62 -0
  26. data/lib/moxml/c14n/nodes/namespace_node.rb +41 -0
  27. data/lib/moxml/c14n/nodes/processing_instruction_node.rb +30 -0
  28. data/lib/moxml/c14n/nodes/root_node.rb +22 -0
  29. data/lib/moxml/c14n/nodes/text_node.rb +29 -0
  30. data/lib/moxml/c14n/nodes.rb +17 -0
  31. data/lib/moxml/c14n/processor.rb +118 -0
  32. data/lib/moxml/c14n/writer.rb +121 -0
  33. data/lib/moxml/c14n/xml_base_handler.rb +140 -0
  34. data/lib/moxml/c14n.rb +96 -0
  35. data/lib/moxml/signature/algorithms/base64_transform.rb +40 -0
  36. data/lib/moxml/signature/algorithms/canonicalization_base.rb +87 -0
  37. data/lib/moxml/signature/algorithms/digest_base.rb +51 -0
  38. data/lib/moxml/signature/algorithms/dsa_sha.rb +107 -0
  39. data/lib/moxml/signature/algorithms/ecdsa_sha.rb +125 -0
  40. data/lib/moxml/signature/algorithms/enveloped_signature_transform.rb +79 -0
  41. data/lib/moxml/signature/algorithms/exc_c14n_10.rb +23 -0
  42. data/lib/moxml/signature/algorithms/hmac_sha.rb +117 -0
  43. data/lib/moxml/signature/algorithms/inclusive_c14n_10.rb +21 -0
  44. data/lib/moxml/signature/algorithms/inclusive_c14n_11.rb +23 -0
  45. data/lib/moxml/signature/algorithms/rsa_pkcs1_sha.rb +56 -0
  46. data/lib/moxml/signature/algorithms/sha1.rb +13 -0
  47. data/lib/moxml/signature/algorithms/sha224.rb +13 -0
  48. data/lib/moxml/signature/algorithms/sha256.rb +13 -0
  49. data/lib/moxml/signature/algorithms/sha384.rb +13 -0
  50. data/lib/moxml/signature/algorithms/sha512.rb +13 -0
  51. data/lib/moxml/signature/algorithms/signature_method_base.rb +58 -0
  52. data/lib/moxml/signature/algorithms/transform_base.rb +40 -0
  53. data/lib/moxml/signature/algorithms.rb +113 -0
  54. data/lib/moxml/signature/errors.rb +99 -0
  55. data/lib/moxml/signature/key_extractor.rb +172 -0
  56. data/lib/moxml/signature/model/algorithm_method.rb +18 -0
  57. data/lib/moxml/signature/model/digest_method.rb +15 -0
  58. data/lib/moxml/signature/model/key/dsa_key_value.rb +29 -0
  59. data/lib/moxml/signature/model/key/ec_key_value.rb +22 -0
  60. data/lib/moxml/signature/model/key/rsa_key_value.rb +19 -0
  61. data/lib/moxml/signature/model/key/x509_data.rb +32 -0
  62. data/lib/moxml/signature/model/key/x509_digest.rb +19 -0
  63. data/lib/moxml/signature/model/key/x509_issuer_serial.rb +19 -0
  64. data/lib/moxml/signature/model/key_info.rb +21 -0
  65. data/lib/moxml/signature/model/key_value.rb +20 -0
  66. data/lib/moxml/signature/model/object_element.rb +18 -0
  67. data/lib/moxml/signature/model/reference.rb +22 -0
  68. data/lib/moxml/signature/model/signature.rb +21 -0
  69. data/lib/moxml/signature/model/signature_value.rb +16 -0
  70. data/lib/moxml/signature/model/signed_info.rb +20 -0
  71. data/lib/moxml/signature/model/transform.rb +16 -0
  72. data/lib/moxml/signature/model/transforms.rb +36 -0
  73. data/lib/moxml/signature/model.rb +35 -0
  74. data/lib/moxml/signature/parser.rb +270 -0
  75. data/lib/moxml/signature/reference_resolver.rb +79 -0
  76. data/lib/moxml/signature/reference_result.rb +22 -0
  77. data/lib/moxml/signature/serializer.rb +163 -0
  78. data/lib/moxml/signature/signer.rb +73 -0
  79. data/lib/moxml/signature/single_verification_result.rb +31 -0
  80. data/lib/moxml/signature/transform_pipeline.rb +106 -0
  81. data/lib/moxml/signature/verification_result.rb +27 -0
  82. data/lib/moxml/signature/verifier.rb +129 -0
  83. data/lib/moxml/signature.rb +94 -0
  84. data/lib/moxml/version.rb +1 -1
  85. data/lib/moxml.rb +2 -0
  86. data/reference-docs/w3c-xmldsig-bestpractices.md +216 -0
  87. data/reference-docs/w3c-xmldsig-core.md +400 -0
  88. data/spec/fixtures/xmldsig/keys/rsa_private.pem +28 -0
  89. data/spec/fixtures/xmldsig/keys/rsa_public.pem +9 -0
  90. data/spec/fixtures/xmldsig/keys/rsa_ref.pem +15 -0
  91. data/spec/fixtures/xmldsig/keys/rsa_ref.pub +6 -0
  92. data/spec/fixtures/xmldsig/sign2-doc.xml +6 -0
  93. data/spec/fixtures/xmldsig/sign2-result.xml +25 -0
  94. data/spec/fixtures/xmldsig/sign3-result.xml +39 -0
  95. data/spec/moxml/c14n/api_spec.rb +117 -0
  96. data/spec/moxml/c14n/c14n_spec.rb +88 -0
  97. data/spec/moxml/c14n/comments_pis_spec.rb +65 -0
  98. data/spec/moxml/c14n/inclusive10_spec.rb +60 -0
  99. data/spec/moxml/c14n/namespace_edge_cases_spec.rb +88 -0
  100. data/spec/moxml/c14n/xml_attributes_spec.rb +54 -0
  101. data/spec/moxml/signature/algorithms/base64_transform_spec.rb +26 -0
  102. data/spec/moxml/signature/algorithms/digest_base_spec.rb +72 -0
  103. data/spec/moxml/signature/algorithms/dsa_sha_spec.rb +46 -0
  104. data/spec/moxml/signature/algorithms/ecdsa_sha_spec.rb +72 -0
  105. data/spec/moxml/signature/algorithms/enveloped_signature_transform_spec.rb +40 -0
  106. data/spec/moxml/signature/algorithms/hmac_sha_spec.rb +71 -0
  107. data/spec/moxml/signature/algorithms/rsa_pkcs1_sha_spec.rb +55 -0
  108. data/spec/moxml/signature/algorithms_spec.rb +76 -0
  109. data/spec/moxml/signature/cross_verify_spec.rb +64 -0
  110. data/spec/moxml/signature/edge_cases_spec.rb +284 -0
  111. data/spec/moxml/signature/fixtures_spec.rb +80 -0
  112. data/spec/moxml/signature/key_extractor_spec.rb +96 -0
  113. data/spec/moxml/signature/model_spec.rb +54 -0
  114. data/spec/moxml/signature/round_trip_spec.rb +113 -0
  115. 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,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Signature
5
+ module Algorithms
6
+ class SHA1 < DigestBase
7
+ identifier "http://www.w3.org/2000/09/xmldsig#sha1"
8
+ digest_bits 160
9
+ openssl_digest_name "SHA1"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Signature
5
+ module Algorithms
6
+ class SHA224 < DigestBase
7
+ identifier "http://www.w3.org/2001/04/xmldsig-more#sha224"
8
+ digest_bits 224
9
+ openssl_digest_name "SHA224"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Signature
5
+ module Algorithms
6
+ class SHA256 < DigestBase
7
+ identifier "http://www.w3.org/2001/04/xmlenc#sha256"
8
+ digest_bits 256
9
+ openssl_digest_name "SHA256"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Signature
5
+ module Algorithms
6
+ class SHA384 < DigestBase
7
+ identifier "http://www.w3.org/2001/04/xmldsig-more#sha384"
8
+ digest_bits 384
9
+ openssl_digest_name "SHA384"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Signature
5
+ module Algorithms
6
+ class SHA512 < DigestBase
7
+ identifier "http://www.w3.org/2001/04/xmlenc#sha512"
8
+ digest_bits 512
9
+ openssl_digest_name "SHA512"
10
+ end
11
+ end
12
+ end
13
+ 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,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Moxml
4
+ module Signature
5
+ module Model
6
+ class DigestMethod
7
+ attr_accessor :algorithm
8
+
9
+ def initialize(algorithm:)
10
+ @algorithm = algorithm
11
+ end
12
+ end
13
+ end
14
+ end
15
+ 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