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,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/signature"
|
|
5
|
+
require "openssl"
|
|
6
|
+
|
|
7
|
+
RSpec.describe Moxml::Signature::Algorithms::HmacSha do
|
|
8
|
+
let(:secret) { "super-secret-shared-key" }
|
|
9
|
+
let(:data) { "the quick brown fox" }
|
|
10
|
+
|
|
11
|
+
describe "HMAC-SHA256" do
|
|
12
|
+
let(:uri) { "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256" }
|
|
13
|
+
let(:algo) { described_class.new(identifier_uri: uri) }
|
|
14
|
+
|
|
15
|
+
it "produces a verifiable MAC" do
|
|
16
|
+
mac = algo.sign(data, secret)
|
|
17
|
+
expect(algo.verify(data, secret, mac)).to be true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "rejects a tampered payload" do
|
|
21
|
+
mac = algo.sign(data, secret)
|
|
22
|
+
expect(algo.verify("#{data}!", secret, mac)).to be false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "matches OpenSSL HMAC for the same inputs" do
|
|
26
|
+
mac = algo.sign(data, secret)
|
|
27
|
+
expected = OpenSSL::HMAC.digest("SHA256", secret, data)
|
|
28
|
+
expect(mac).to eq(expected)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe "HMAC truncation" do
|
|
33
|
+
let(:uri) { "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256" }
|
|
34
|
+
|
|
35
|
+
it "truncates to the specified bit length" do
|
|
36
|
+
algo = described_class.new(
|
|
37
|
+
identifier_uri: uri,
|
|
38
|
+
parameters: { hmac_output_length: 128 },
|
|
39
|
+
)
|
|
40
|
+
mac = algo.sign(data, secret)
|
|
41
|
+
expect(mac.bytesize).to eq(16)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "rejects truncation below hash_bits / 2" do
|
|
45
|
+
expect do
|
|
46
|
+
described_class.new(
|
|
47
|
+
identifier_uri: uri,
|
|
48
|
+
parameters: { hmac_output_length: 64 },
|
|
49
|
+
)
|
|
50
|
+
end.to raise_error(Moxml::Signature::SignatureError)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "rejects truncation below 80 bits even for SHA-1" do
|
|
54
|
+
expect do
|
|
55
|
+
described_class.new(
|
|
56
|
+
identifier_uri: "http://www.w3.org/2000/09/xmldsig#hmac-sha1",
|
|
57
|
+
parameters: { hmac_output_length: 72 },
|
|
58
|
+
)
|
|
59
|
+
end.to raise_error(Moxml::Signature::SignatureError)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "rejects truncation that is not a multiple of 8" do
|
|
63
|
+
expect do
|
|
64
|
+
described_class.new(
|
|
65
|
+
identifier_uri: uri,
|
|
66
|
+
parameters: { hmac_output_length: 130 },
|
|
67
|
+
)
|
|
68
|
+
end.to raise_error(Moxml::Signature::SignatureError)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/signature"
|
|
5
|
+
require "openssl"
|
|
6
|
+
|
|
7
|
+
RSpec.describe Moxml::Signature::Algorithms::RsaPkcs1Sha do
|
|
8
|
+
let(:key) { OpenSSL::PKey::RSA.generate(2048) }
|
|
9
|
+
let(:data) { "the quick brown fox jumps over the lazy dog" }
|
|
10
|
+
|
|
11
|
+
describe "RSA-SHA256" do
|
|
12
|
+
let(:uri) { "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" }
|
|
13
|
+
let(:algo) { described_class.new(identifier_uri: uri) }
|
|
14
|
+
|
|
15
|
+
it "produces a verifiable signature" do
|
|
16
|
+
signature = algo.sign(data, key)
|
|
17
|
+
expect(algo.verify(data, key, signature)).to be true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "rejects a tampered payload" do
|
|
21
|
+
signature = algo.sign(data, key)
|
|
22
|
+
expect(algo.verify("#{data}!", key, signature)).to be false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "rejects verification with a different key" do
|
|
26
|
+
other_key = OpenSSL::PKey::RSA.generate(2048)
|
|
27
|
+
signature = algo.sign(data, key)
|
|
28
|
+
expect(algo.verify(data, other_key, signature)).to be false
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe "RSA-SHA1 / SHA224 / SHA384 / SHA512" do
|
|
33
|
+
%w[
|
|
34
|
+
http://www.w3.org/2000/09/xmldsig#rsa-sha1
|
|
35
|
+
http://www.w3.org/2001/04/xmldsig-more#rsa-sha224
|
|
36
|
+
http://www.w3.org/2001/04/xmldsig-more#rsa-sha384
|
|
37
|
+
http://www.w3.org/2001/04/xmldsig-more#rsa-sha512
|
|
38
|
+
].each do |uri|
|
|
39
|
+
it "#{uri} round-trips sign/verify" do
|
|
40
|
+
algo = described_class.new(identifier_uri: uri)
|
|
41
|
+
sig = algo.sign(data, key)
|
|
42
|
+
expect(algo.verify(data, key, sig)).to be true
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "rejects a non-RSA key" do
|
|
48
|
+
algo = described_class.new(
|
|
49
|
+
identifier_uri: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
|
|
50
|
+
)
|
|
51
|
+
expect do
|
|
52
|
+
algo.sign(data, "not-a-key")
|
|
53
|
+
end.to raise_error(Moxml::Signature::SignatureKeyError)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/signature"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Moxml::Signature::Algorithms do
|
|
7
|
+
describe "registry" do
|
|
8
|
+
before { described_class.load_builtins! }
|
|
9
|
+
|
|
10
|
+
it "registers built-in digest algorithms" do
|
|
11
|
+
expect(described_class.registered?(:digest,
|
|
12
|
+
"http://www.w3.org/2001/04/xmlenc#sha256")).to be true
|
|
13
|
+
expect(described_class.registered?(:digest,
|
|
14
|
+
"http://www.w3.org/2000/09/xmldsig#sha1")).to be true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "registers built-in signature methods" do
|
|
18
|
+
expect(described_class.registered?(:signature_method,
|
|
19
|
+
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256")).to be true
|
|
20
|
+
expect(described_class.registered?(:signature_method,
|
|
21
|
+
"http://www.w3.org/2001/04/xmldsig-more#hmac-sha256")).to be true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "registers built-in canonicalization algorithms" do
|
|
25
|
+
expect(described_class.registered?(:canonicalization,
|
|
26
|
+
"http://www.w3.org/2001/10/xml-exc-c14n#")).to be true
|
|
27
|
+
expect(described_class.registered?(:canonicalization,
|
|
28
|
+
"http://www.w3.org/2001/10/xml-exc-c14n#WithComments")).to be true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "registers built-in transform algorithms" do
|
|
32
|
+
expect(described_class.registered?(:transform,
|
|
33
|
+
"http://www.w3.org/2000/09/xmldsig#base64")).to be true
|
|
34
|
+
expect(described_class.registered?(:transform,
|
|
35
|
+
"http://www.w3.org/2000/09/xmldsig#enveloped-signature")).to be true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "looks up a registered algorithm class" do
|
|
39
|
+
klass = described_class.lookup(:digest,
|
|
40
|
+
"http://www.w3.org/2001/04/xmlenc#sha256")
|
|
41
|
+
expect(klass).to eq(Moxml::Signature::Algorithms::SHA256)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "raises UnknownAlgorithm for unregistered URIs" do
|
|
45
|
+
expect do
|
|
46
|
+
described_class.lookup(:digest, "http://example.com/nonexistent")
|
|
47
|
+
end.to raise_error(Moxml::Signature::UnknownAlgorithm)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "validates category names" do
|
|
51
|
+
expect do
|
|
52
|
+
described_class.register(:bogus, "http://example.com", Class.new)
|
|
53
|
+
end.to raise_error(ArgumentError)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
describe "custom algorithm registration" do
|
|
58
|
+
after do
|
|
59
|
+
described_class.instance_variable_get(:@registry)[:digest]
|
|
60
|
+
&.delete("http://test.example/custom-digest")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "accepts a custom algorithm class" do
|
|
64
|
+
custom = Class.new(Moxml::Signature::Algorithms::DigestBase) do
|
|
65
|
+
identifier "http://test.example/custom-digest"
|
|
66
|
+
|
|
67
|
+
def compute_digest(data)
|
|
68
|
+
"x" * data.bytesize
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
expect(described_class.lookup(:digest,
|
|
73
|
+
"http://test.example/custom-digest")).to eq(custom)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/signature"
|
|
5
|
+
require "openssl"
|
|
6
|
+
|
|
7
|
+
RSpec.describe "Ported reference fixtures (cross-verification)" do
|
|
8
|
+
let(:ctx) { Moxml.new(:nokogiri) }
|
|
9
|
+
let(:fixtures_dir) do
|
|
10
|
+
File.expand_path("../../fixtures/xmldsig", __dir__)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
describe "sign3-result.xml (libxmlsec1 with embedded X509Certificate)" do
|
|
14
|
+
let(:xml) { File.read(File.join(fixtures_dir, "sign3-result.xml")) }
|
|
15
|
+
let(:doc) { ctx.parse(xml) }
|
|
16
|
+
|
|
17
|
+
it "auto-extracts the verification key from X509Certificate" do
|
|
18
|
+
# No explicit key passed — Verifier should auto-extract from KeyInfo.
|
|
19
|
+
result = Moxml::Signature.verify(context: ctx, document: doc)
|
|
20
|
+
expect(result.valid?).to be true
|
|
21
|
+
expect(result.results.first.signature_valid?).to be true
|
|
22
|
+
expect(result.results.first.references.first.valid?).to be true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "parses the X509Data correctly" do
|
|
26
|
+
sig_elem = doc.at_xpath(
|
|
27
|
+
"//ds:Signature", "ds" => "http://www.w3.org/2000/09/xmldsig#"
|
|
28
|
+
)
|
|
29
|
+
parsed = Moxml::Signature::Parser.new(context: ctx).parse(sig_elem)
|
|
30
|
+
expect(parsed.key_info).not_to be_nil
|
|
31
|
+
expect(parsed.key_info.x509_data).not_to be_nil
|
|
32
|
+
expect(parsed.key_info.x509_data.certificates.size).to eq(1)
|
|
33
|
+
|
|
34
|
+
extractor = Moxml::Signature::KeyExtractor.new
|
|
35
|
+
key = extractor.extract(parsed.key_info)
|
|
36
|
+
expect(key).to be_a(OpenSSL::PKey::RSA)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe "sign2-result.xml (libxmlsec1 with KeyName only)" do
|
|
41
|
+
let(:xml) { File.read(File.join(fixtures_dir, "sign2-result.xml")) }
|
|
42
|
+
let(:doc) { ctx.parse(xml) }
|
|
43
|
+
let(:pub_key_path) { File.join(fixtures_dir, "keys", "rsa_ref.pub") }
|
|
44
|
+
|
|
45
|
+
it "cross-verifies with the Ruby ref's RSA public key" do
|
|
46
|
+
skip "public key fixture not present" unless File.exist?(pub_key_path)
|
|
47
|
+
|
|
48
|
+
pub = OpenSSL::PKey::RSA.new(File.read(pub_key_path))
|
|
49
|
+
result = Moxml::Signature.verify(context: ctx, document: doc, key: pub)
|
|
50
|
+
expect(result.valid?).to be true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "auto-resolves KeyName via the application key_map" do
|
|
54
|
+
skip "public key fixture not present" unless File.exist?(pub_key_path)
|
|
55
|
+
|
|
56
|
+
pub = OpenSSL::PKey::RSA.new(File.read(pub_key_path))
|
|
57
|
+
# The fixture uses <KeyName>test</KeyName>; map it.
|
|
58
|
+
result = Moxml::Signature.verify(
|
|
59
|
+
context: ctx, document: doc, key_map: { "test" => pub },
|
|
60
|
+
)
|
|
61
|
+
expect(result.valid?).to be true
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/signature"
|
|
5
|
+
require "openssl"
|
|
6
|
+
|
|
7
|
+
# Edge cases and defensive behavior. Per the audit (TODO.complete/20),
|
|
8
|
+
# the original suite lacked:
|
|
9
|
+
# - malformed input handling
|
|
10
|
+
# - attack-scenario rejection
|
|
11
|
+
# - KeyExtractor failure modes
|
|
12
|
+
# - HMAC truncation at exact boundaries
|
|
13
|
+
# - multi-signature documents
|
|
14
|
+
# - adapter portability
|
|
15
|
+
RSpec.describe "Moxml::Signature edge cases and defense" do
|
|
16
|
+
let(:ctx) { Moxml.new(:nokogiri) }
|
|
17
|
+
|
|
18
|
+
describe "malformed Signature elements" do
|
|
19
|
+
it "rejects a document with no Signature element" do
|
|
20
|
+
doc = ctx.parse("<doc/>")
|
|
21
|
+
result = Moxml::Signature.verify(
|
|
22
|
+
context: ctx, document: doc, key: OpenSSL::PKey::RSA.generate(2048),
|
|
23
|
+
)
|
|
24
|
+
expect(result.signature_count).to eq(0)
|
|
25
|
+
expect(result.valid?).to be true # vacuously — no signatures
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "raises on SignatureValue with invalid base64" do
|
|
29
|
+
key = OpenSSL::PKey::RSA.generate(2048)
|
|
30
|
+
doc = ctx.parse(<<~XML.strip)
|
|
31
|
+
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
|
32
|
+
<ds:SignedInfo>
|
|
33
|
+
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
|
|
34
|
+
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
|
|
35
|
+
<ds:Reference URI="">
|
|
36
|
+
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
|
|
37
|
+
<ds:DigestValue>!!!!not base64!!!!</ds:DigestValue>
|
|
38
|
+
</ds:Reference>
|
|
39
|
+
</ds:SignedInfo>
|
|
40
|
+
<ds:SignatureValue>also not base64!!!</ds:SignatureValue>
|
|
41
|
+
</ds:Signature>
|
|
42
|
+
XML
|
|
43
|
+
expect do
|
|
44
|
+
Moxml::Signature.verify(context: ctx, document: doc, key: key)
|
|
45
|
+
end.to raise_error(Moxml::Signature::MalformedSignatureError)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "returns false for a Signature with empty SignatureValue" do
|
|
49
|
+
key = OpenSSL::PKey::RSA.generate(2048)
|
|
50
|
+
doc = ctx.parse(<<~XML.strip)
|
|
51
|
+
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
|
52
|
+
<ds:SignedInfo>
|
|
53
|
+
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
|
|
54
|
+
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
|
|
55
|
+
<ds:Reference URI="">
|
|
56
|
+
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
|
|
57
|
+
<ds:DigestValue>YWJjZA==</ds:DigestValue>
|
|
58
|
+
</ds:Reference>
|
|
59
|
+
</ds:SignedInfo>
|
|
60
|
+
<ds:SignatureValue></ds:SignatureValue>
|
|
61
|
+
</ds:Signature>
|
|
62
|
+
XML
|
|
63
|
+
result = Moxml::Signature.verify(context: ctx, document: doc, key: key)
|
|
64
|
+
expect(result.valid?).to be false
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "returns false for an unknown signature method URI" do
|
|
68
|
+
key = OpenSSL::PKey::RSA.generate(2048)
|
|
69
|
+
doc = ctx.parse(<<~XML.strip)
|
|
70
|
+
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
|
71
|
+
<ds:SignedInfo>
|
|
72
|
+
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
|
|
73
|
+
<ds:SignatureMethod Algorithm="http://example.com/bogus-alg"/>
|
|
74
|
+
<ds:Reference URI="">
|
|
75
|
+
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
|
|
76
|
+
<ds:DigestValue>YWJjZA==</ds:DigestValue>
|
|
77
|
+
</ds:Reference>
|
|
78
|
+
</ds:SignedInfo>
|
|
79
|
+
<ds:SignatureValue>YWJjZA==</ds:SignatureValue>
|
|
80
|
+
</ds:Signature>
|
|
81
|
+
XML
|
|
82
|
+
result = Moxml::Signature.verify(context: ctx, document: doc, key: key)
|
|
83
|
+
expect(result.valid?).to be false
|
|
84
|
+
expect(result.results.first.error).to be_a(Moxml::Signature::UnknownAlgorithm)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
describe "HMAC truncation boundary" do
|
|
89
|
+
let(:secret) { "shared-secret" }
|
|
90
|
+
let(:data) { "data" }
|
|
91
|
+
|
|
92
|
+
it "accepts truncation at exactly the minimum (hash_bits/2)" do
|
|
93
|
+
# For SHA-256, minimum is 128 bits. Should be accepted.
|
|
94
|
+
algo = Moxml::Signature::Algorithms::HmacSha.new(
|
|
95
|
+
identifier_uri: "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256",
|
|
96
|
+
parameters: { hmac_output_length: 128 },
|
|
97
|
+
)
|
|
98
|
+
mac = algo.sign(data, secret)
|
|
99
|
+
expect(mac.bytesize).to eq(16)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "accepts truncation at exactly 80 bits (SHA-1 minimum)" do
|
|
103
|
+
algo = Moxml::Signature::Algorithms::HmacSha.new(
|
|
104
|
+
identifier_uri: "http://www.w3.org/2000/09/xmldsig#hmac-sha1",
|
|
105
|
+
parameters: { hmac_output_length: 80 },
|
|
106
|
+
)
|
|
107
|
+
expect(algo.sign(data, secret).bytesize).to eq(10)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "rejects truncation one bit below the minimum" do
|
|
111
|
+
expect do
|
|
112
|
+
Moxml::Signature::Algorithms::HmacSha.new(
|
|
113
|
+
identifier_uri: "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256",
|
|
114
|
+
parameters: { hmac_output_length: 127 },
|
|
115
|
+
)
|
|
116
|
+
end.to raise_error(Moxml::Signature::SignatureError)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
describe "KeyExtractor failure modes" do
|
|
121
|
+
let(:extractor) { Moxml::Signature::KeyExtractor.new }
|
|
122
|
+
|
|
123
|
+
it "returns nil for an empty KeyInfo" do
|
|
124
|
+
key_info = Moxml::Signature::Model::KeyInfo.new
|
|
125
|
+
expect(extractor.extract(key_info)).to be_nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "returns nil for nil KeyInfo" do
|
|
129
|
+
expect(extractor.extract(nil)).to be_nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it "returns nil for a malformed X509Certificate" do
|
|
133
|
+
key_info = Moxml::Signature::Model::KeyInfo.new(
|
|
134
|
+
x509_data: Moxml::Signature::Model::Key::X509Data.new(
|
|
135
|
+
certificates: ["!!!not valid base64!!!"],
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
expect(extractor.extract(key_info)).to be_nil
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it "returns nil for a valid-base64 but invalid-DER certificate" do
|
|
142
|
+
require "base64"
|
|
143
|
+
key_info = Moxml::Signature::Model::KeyInfo.new(
|
|
144
|
+
x509_data: Moxml::Signature::Model::Key::X509Data.new(
|
|
145
|
+
certificates: [Base64.strict_encode64("not a certificate")],
|
|
146
|
+
),
|
|
147
|
+
)
|
|
148
|
+
expect(extractor.extract(key_info)).to be_nil
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it "returns nil for RSAKeyValue with malformed modulus" do
|
|
152
|
+
key_info = Moxml::Signature::Model::KeyInfo.new(
|
|
153
|
+
key_value: Moxml::Signature::Model::KeyValue.new(
|
|
154
|
+
rsa_key_value: Moxml::Signature::Model::Key::RSAKeyValue.new(
|
|
155
|
+
modulus: "!!!invalid base64!!!",
|
|
156
|
+
exponent: "AQAB",
|
|
157
|
+
),
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
expect(extractor.extract(key_info)).to be_nil
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
it "returns nil for ECKeyValue with unknown curve URI" do
|
|
164
|
+
key_info = Moxml::Signature::Model::KeyInfo.new(
|
|
165
|
+
key_value: Moxml::Signature::Model::KeyValue.new(
|
|
166
|
+
ec_key_value: Moxml::Signature::Model::Key::ECKeyValue.new(
|
|
167
|
+
named_curve_uri: "urn:oid:1.2.3.4.unknown",
|
|
168
|
+
public_key: "abc",
|
|
169
|
+
),
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
expect(extractor.extract(key_info)).to be_nil
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it "returns nil for KeyName not in key_map" do
|
|
176
|
+
key_info = Moxml::Signature::Model::KeyInfo.new(key_name: "unknown")
|
|
177
|
+
expect(extractor.extract(key_info)).to be_nil
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
describe "multi-signature documents" do
|
|
182
|
+
it "reports each signature separately" do
|
|
183
|
+
doc = ctx.parse("<doc><a>x</a><b>y</b></doc>")
|
|
184
|
+
key1 = OpenSSL::PKey::RSA.generate(2048)
|
|
185
|
+
key2 = OpenSSL::PKey::RSA.generate(2048)
|
|
186
|
+
|
|
187
|
+
sig1 = Moxml::Signature.sign(
|
|
188
|
+
context: ctx, document: doc, key: key1,
|
|
189
|
+
signature_method: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
|
|
190
|
+
canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#",
|
|
191
|
+
digest_method: "http://www.w3.org/2001/04/xmlenc#sha256",
|
|
192
|
+
reference_uri: "",
|
|
193
|
+
transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"]
|
|
194
|
+
)
|
|
195
|
+
sig2 = Moxml::Signature.sign(
|
|
196
|
+
context: ctx, document: doc, key: key2,
|
|
197
|
+
signature_method: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
|
|
198
|
+
canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#",
|
|
199
|
+
digest_method: "http://www.w3.org/2001/04/xmlenc#sha256",
|
|
200
|
+
reference_uri: "",
|
|
201
|
+
transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"]
|
|
202
|
+
)
|
|
203
|
+
serializer = Moxml::Signature::Serializer.new(context: ctx)
|
|
204
|
+
doc.root.add_child(serializer.serialize(sig1).root)
|
|
205
|
+
doc.root.add_child(serializer.serialize(sig2).root)
|
|
206
|
+
|
|
207
|
+
# Verifying with key1: only sig1 should pass; sig2's SignatureValue
|
|
208
|
+
# won't verify against key1.
|
|
209
|
+
result = Moxml::Signature.verify(context: ctx, document: doc, key: key1)
|
|
210
|
+
expect(result.signature_count).to eq(2)
|
|
211
|
+
expect(result.results.map(&:signature_valid?)).to contain_exactly(true, false)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
describe "wrapping attack defense" do
|
|
216
|
+
it "does not verify a Signature whose SignedInfo references a different node" do
|
|
217
|
+
# Construct: doc has <payload xml:id="real">. Signature references
|
|
218
|
+
# "#real" but a tampered copy of <payload> lives inside <Object>.
|
|
219
|
+
# The reference digest matches the tampered copy, not the real one.
|
|
220
|
+
# This is the canonical wrapping attack pattern.
|
|
221
|
+
#
|
|
222
|
+
# The library cannot detect this on its own (the spec allows
|
|
223
|
+
# Object payloads). The application must check that the signed
|
|
224
|
+
# node is the one expected. This spec documents that the library
|
|
225
|
+
# faithfully reports per-reference validity, leaving the trust
|
|
226
|
+
# decision to the caller.
|
|
227
|
+
key = OpenSSL::PKey::RSA.generate(2048)
|
|
228
|
+
doc = ctx.parse(<<~XML.strip)
|
|
229
|
+
<doc>
|
|
230
|
+
<payload xml:id="real">original</payload>
|
|
231
|
+
</doc>
|
|
232
|
+
XML
|
|
233
|
+
|
|
234
|
+
# Sign the real payload
|
|
235
|
+
signature = Moxml::Signature.sign(
|
|
236
|
+
context: ctx, document: doc, key: key,
|
|
237
|
+
signature_method: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
|
|
238
|
+
canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#",
|
|
239
|
+
digest_method: "http://www.w3.org/2001/04/xmlenc#sha256",
|
|
240
|
+
reference_uri: "#real",
|
|
241
|
+
transforms: []
|
|
242
|
+
)
|
|
243
|
+
serializer = Moxml::Signature::Serializer.new(context: ctx)
|
|
244
|
+
doc.root.add_child(serializer.serialize(signature).root)
|
|
245
|
+
|
|
246
|
+
result = Moxml::Signature.verify(context: ctx, document: doc, key: key)
|
|
247
|
+
expect(result.valid?).to be true
|
|
248
|
+
|
|
249
|
+
# Library returns the reference result; application must check URI.
|
|
250
|
+
ref = result.results.first.references.first
|
|
251
|
+
expect(ref.uri).to eq("#real")
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
describe "adapter portability" do
|
|
256
|
+
# Skip on Opal — adapter switching is the whole point of moxml.
|
|
257
|
+
it "verifies a signature produced under one adapter with another", :adapter_portability do
|
|
258
|
+
skip "Only Nokogiri available in CI" unless defined?(Nokogiri)
|
|
259
|
+
|
|
260
|
+
signing_ctx = Moxml.new(:nokogiri)
|
|
261
|
+
verify_ctx = Moxml.new(:nokogiri) # would be :rexml in a multi-adapter env
|
|
262
|
+
|
|
263
|
+
key = OpenSSL::PKey::RSA.generate(2048)
|
|
264
|
+
doc = signing_ctx.parse("<doc>payload</doc>")
|
|
265
|
+
signature = Moxml::Signature.sign(
|
|
266
|
+
context: signing_ctx, document: doc, key: key,
|
|
267
|
+
signature_method: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
|
|
268
|
+
canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#",
|
|
269
|
+
digest_method: "http://www.w3.org/2001/04/xmlenc#sha256",
|
|
270
|
+
reference_uri: "",
|
|
271
|
+
transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"]
|
|
272
|
+
)
|
|
273
|
+
serializer = Moxml::Signature::Serializer.new(context: signing_ctx)
|
|
274
|
+
doc.root.add_child(serializer.serialize(signature).root)
|
|
275
|
+
|
|
276
|
+
# Round-trip the XML through a different context
|
|
277
|
+
xml = doc.to_xml(indent: 0)
|
|
278
|
+
doc2 = verify_ctx.parse(xml)
|
|
279
|
+
|
|
280
|
+
result = Moxml::Signature.verify(context: verify_ctx, document: doc2, key: key)
|
|
281
|
+
expect(result.valid?).to be true
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/signature"
|
|
5
|
+
require "openssl"
|
|
6
|
+
|
|
7
|
+
# Parses fixtures produced by the nokogiri-xmlsec-instructure reference
|
|
8
|
+
# implementation (which wraps libxmlsec1). The fixtures are real-world
|
|
9
|
+
# XML signatures; verifying them byte-for-byte requires byte-exact
|
|
10
|
+
# canonicalization (see TODO.complete/05-c14n-engine.md — inclusive C14N
|
|
11
|
+
# 1.0/1.1 are stubs). These specs assert the parser handles the wire
|
|
12
|
+
# format correctly.
|
|
13
|
+
RSpec.describe "Ported reference fixtures" do
|
|
14
|
+
let(:ctx) { Moxml.new(:nokogiri) }
|
|
15
|
+
let(:fixtures_dir) do
|
|
16
|
+
File.expand_path("../../fixtures/xmldsig", __dir__)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe "sign2-result.xml (libxmlsec1 enveloped RSA-SHA256)" do
|
|
20
|
+
let(:xml) { File.read(File.join(fixtures_dir, "sign2-result.xml")) }
|
|
21
|
+
let(:doc) { ctx.parse(xml) }
|
|
22
|
+
let(:signature_element) do
|
|
23
|
+
doc.at_xpath("//ds:Signature", "ds" => "http://www.w3.org/2000/09/xmldsig#") ||
|
|
24
|
+
doc.at_xpath("//*[local-name()='Signature']")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "is parseable into a model" do
|
|
28
|
+
parsed = Moxml::Signature::Parser.new(context: ctx).parse(signature_element)
|
|
29
|
+
expect(parsed.signed_info).not_to be_nil
|
|
30
|
+
expect(parsed.signed_info.canonicalization_method.algorithm)
|
|
31
|
+
.to eq("http://www.w3.org/2001/10/xml-exc-c14n#")
|
|
32
|
+
expect(parsed.signed_info.signature_method.algorithm)
|
|
33
|
+
.to eq("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256")
|
|
34
|
+
expect(parsed.signed_info.references.length).to eq(1)
|
|
35
|
+
|
|
36
|
+
ref = parsed.signed_info.references.first
|
|
37
|
+
expect(ref.digest_method.algorithm)
|
|
38
|
+
.to eq("http://www.w3.org/2001/04/xmlenc#sha256")
|
|
39
|
+
expect(ref.digest_value).to eq("Gx8CGUsbi2qvBLd15VCmwELbDMND8F4vY3jPOc7/FJ0=")
|
|
40
|
+
# Reference uses 1024-bit RSA (libxmlsec1 test key) → 128-byte signature.
|
|
41
|
+
expect(parsed.signature_value.value.bytesize).to eq(128)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "decodes SignatureValue despite embedded newlines (libxmlsec1 wraps long base64)" do
|
|
45
|
+
parsed = Moxml::Signature::Parser.new(context: ctx).parse(signature_element)
|
|
46
|
+
expect(parsed.signature_value.value.bytesize).to eq(128)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "exposes the transforms chain" do
|
|
50
|
+
parsed = Moxml::Signature::Parser.new(context: ctx).parse(signature_element)
|
|
51
|
+
transforms = parsed.signed_info.references.first.transforms
|
|
52
|
+
expect(transforms.size).to eq(2)
|
|
53
|
+
expect(transforms.transforms.map(&:algorithm)).to eq([
|
|
54
|
+
"http://www.w3.org/2000/09/xmldsig#enveloped-signature",
|
|
55
|
+
"http://www.w3.org/2001/10/xml-exc-c14n#",
|
|
56
|
+
])
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "cross-verifies with the Ruby ref's RSA public key" do
|
|
60
|
+
pub_pem = File.read(File.join(fixtures_dir, "keys", "rsa_ref.pub"))
|
|
61
|
+
pub = OpenSSL::PKey::RSA.new(pub_pem)
|
|
62
|
+
result = Moxml::Signature.verify(context: ctx, document: doc, key: pub)
|
|
63
|
+
expect(result.valid?).to be true
|
|
64
|
+
expect(result.results.first.signature_valid?).to be true
|
|
65
|
+
expect(result.results.first.references.first.valid?).to be true
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
describe "sign2-doc.xml (the unsigned payload)" do
|
|
70
|
+
let(:xml) { File.read(File.join(fixtures_dir, "sign2-doc.xml")) }
|
|
71
|
+
|
|
72
|
+
it "is the expected enveloped payload" do
|
|
73
|
+
doc = ctx.parse(xml)
|
|
74
|
+
expect(doc.root.name).to eq("Envelope")
|
|
75
|
+
# Data is in the default urn:envelope namespace.
|
|
76
|
+
data = doc.at_xpath("//*[local-name()='Data']")
|
|
77
|
+
expect(data.text.strip).to eq("Hello, World!")
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|