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,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/signature"
|
|
5
|
+
|
|
6
|
+
RSpec.describe "Moxml C14N engine" do
|
|
7
|
+
describe Moxml::C14n::Exclusive do
|
|
8
|
+
let(:ctx) { Moxml.new(:nokogiri) }
|
|
9
|
+
let(:canon) { described_class.new }
|
|
10
|
+
|
|
11
|
+
it "renders a simple element without namespaces" do
|
|
12
|
+
doc = ctx.parse("<root>hello</root>")
|
|
13
|
+
expect(canon.canonicalize(doc.root)).to eq("<root>hello</root>")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "preserves nested element structure" do
|
|
17
|
+
doc = ctx.parse("<root><child>text</child></root>")
|
|
18
|
+
expect(canon.canonicalize(doc.root)).to eq("<root><child>text</child></root>")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "renders declared namespaces" do
|
|
22
|
+
doc = ctx.parse(<<~XML.strip)
|
|
23
|
+
<root xmlns:foo="http://example.com/foo"><foo:child>x</foo:child></root>
|
|
24
|
+
XML
|
|
25
|
+
result = canon.canonicalize(doc.root)
|
|
26
|
+
# Exclusive C14N renders the namespace on the element where it's visibly
|
|
27
|
+
# used (foo:child), not on ancestors that don't use it.
|
|
28
|
+
expect(result).to include('<foo:child xmlns:foo="http://example.com/foo">')
|
|
29
|
+
expect(result).to include("</foo:child>")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "excludes ancestor-inherited namespaces when not visibly used (exclusive)" do
|
|
33
|
+
doc = ctx.parse(<<~XML.strip)
|
|
34
|
+
<outer xmlns:unused="http://example.com/unused"><inner>text</inner></outer>
|
|
35
|
+
XML
|
|
36
|
+
inner = doc.at_xpath("//inner")
|
|
37
|
+
result = canon.canonicalize(inner)
|
|
38
|
+
expect(result).to eq("<inner>text</inner>")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "escapes special characters in text content" do
|
|
42
|
+
doc = ctx.parse("<root>a<b>c&d</root>")
|
|
43
|
+
expect(canon.canonicalize(doc.root)).to eq("<root>a<b>c&d</root>")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "escapes special characters in attribute values" do
|
|
47
|
+
doc = ctx.parse(%(<root attr="a&b"c"/>))
|
|
48
|
+
expect(canon.canonicalize(doc.root))
|
|
49
|
+
.to include(%(attr="a&b"c"))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "sorts attributes by namespace URI then local name" do
|
|
53
|
+
doc = ctx.parse(%(<root b="2" a="1" c="3"/>))
|
|
54
|
+
result = canon.canonicalize(doc.root)
|
|
55
|
+
expect(result).to include('a="1" b="2" c="3"')
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "omits comments by default" do
|
|
59
|
+
doc = ctx.parse("<root>a<!-- comment -->b</root>")
|
|
60
|
+
expect(canon.canonicalize(doc.root)).to eq("<root>ab</root>")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it "includes comments when with_comments is true" do
|
|
64
|
+
doc = ctx.parse("<root>a<!-- comment -->b</root>")
|
|
65
|
+
expect(canon.canonicalize(doc.root, with_comments: true))
|
|
66
|
+
.to eq("<root>a<!-- comment -->b</root>")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe ".escape_text" do
|
|
71
|
+
it "escapes &, <, >" do
|
|
72
|
+
expect(Moxml::C14n.escape_text("a & b < c > d"))
|
|
73
|
+
.to eq("a & b < c > d")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "escapes bare CR as 
" do
|
|
77
|
+
expect(Moxml::C14n.escape_text("a\rb"))
|
|
78
|
+
.to eq("a
b")
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe ".escape_attribute" do
|
|
83
|
+
it "escapes quotes and tabs/newlines" do
|
|
84
|
+
expect(Moxml::C14n.escape_attribute(%(a"b\tc\nd)))
|
|
85
|
+
.to eq(%(a"b	c
d))
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/c14n"
|
|
5
|
+
|
|
6
|
+
# Edge cases for comment and processing-instruction handling per
|
|
7
|
+
# W3C C14N 1.0 §2.6, §2.7.
|
|
8
|
+
RSpec.describe "C14N comments and PIs" do
|
|
9
|
+
let(:ctx) { Moxml.new(:nokogiri) }
|
|
10
|
+
|
|
11
|
+
describe "comments inside elements" do
|
|
12
|
+
it "omits comments by default" do
|
|
13
|
+
doc = ctx.parse("<root>a<!-- c -->b</root>")
|
|
14
|
+
expect(Moxml::C14n.canonicalize(doc.root)).to eq("<root>ab</root>")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "includes comments when with_comments: true" do
|
|
18
|
+
doc = ctx.parse("<root>a<!-- c -->b</root>")
|
|
19
|
+
expect(Moxml::C14n.canonicalize(doc.root, with_comments: true))
|
|
20
|
+
.to eq("<root>a<!-- c -->b</root>")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "preserves comment content verbatim" do
|
|
24
|
+
doc = ctx.parse("<root><!-- & <stuff> --></root>")
|
|
25
|
+
expect(Moxml::C14n.canonicalize(doc.root, with_comments: true))
|
|
26
|
+
.to eq("<root><!-- & <stuff> --></root>")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
describe "processing instructions" do
|
|
31
|
+
it "always renders PIs (not affected by with_comments)" do
|
|
32
|
+
doc = ctx.parse("<root><?pi data?>x</root>")
|
|
33
|
+
expect(Moxml::C14n.canonicalize(doc.root))
|
|
34
|
+
.to eq("<root><?pi data?>x</root>")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "renders PI without data when data is empty" do
|
|
38
|
+
doc = ctx.parse("<root><?pi?>x</root>")
|
|
39
|
+
expect(Moxml::C14n.canonicalize(doc.root))
|
|
40
|
+
.to eq("<root><?pi?>x</root>")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "preserves PI target and data verbatim" do
|
|
44
|
+
doc = ctx.parse(%(<root><?xml-stylesheet href="a.css" type="text/css"?>x</root>))
|
|
45
|
+
expect(Moxml::C14n.canonicalize(doc.root))
|
|
46
|
+
.to include(%(<?xml-stylesheet href="a.css" type="text/css"?>))
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe "PIs at document level" do
|
|
51
|
+
# TODO.c14n/09: Document-level PIs (outside the document element) are
|
|
52
|
+
# not yet rendered correctly. The moxml PI parser mis-parses document-level
|
|
53
|
+
# PIs, and the DataModel.from_document path drops them. Fixing requires
|
|
54
|
+
# adapter-level work and is tracked separately.
|
|
55
|
+
it "renders PIs outside the document element (KNOWN LIMITATION)" do
|
|
56
|
+
skip "document-level PI rendering not yet implemented"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
describe "comments at document level" do
|
|
61
|
+
it "renders document-level comments when with_comments: true (KNOWN LIMITATION)" do
|
|
62
|
+
skip "document-level comment rendering not yet implemented"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/signature"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Moxml::C14n::Inclusive10 do
|
|
7
|
+
let(:ctx) { Moxml.new(:nokogiri) }
|
|
8
|
+
let(:canon) { described_class.new }
|
|
9
|
+
|
|
10
|
+
it "renders a simple element" do
|
|
11
|
+
doc = ctx.parse("<root>hello</root>")
|
|
12
|
+
expect(canon.canonicalize(doc.root)).to eq("<root>hello</root>")
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it "attracts ancestor namespaces at the apex (the key inclusive behavior)" do
|
|
16
|
+
xml = <<~XML.strip
|
|
17
|
+
<outer xmlns:foo="http://example.com/foo" xmlns:bar="http://example.com/bar">
|
|
18
|
+
<inner x="1"><foo:child>hello</foo:child></inner>
|
|
19
|
+
</outer>
|
|
20
|
+
XML
|
|
21
|
+
doc = ctx.parse(xml)
|
|
22
|
+
inner = doc.at_xpath("//*[local-name()='inner']")
|
|
23
|
+
result = canon.canonicalize(inner)
|
|
24
|
+
# Both inherited namespaces are rendered on <inner>, even though
|
|
25
|
+
# <inner> itself does not visibly use them. This is the inclusive
|
|
26
|
+
# "ancestor attraction" behavior.
|
|
27
|
+
expect(result).to include('xmlns:bar="http://example.com/bar"')
|
|
28
|
+
expect(result).to include('xmlns:foo="http://example.com/foo"')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "renders each namespace declaration only once per chain" do
|
|
32
|
+
xml = <<~XML.strip
|
|
33
|
+
<root xmlns:a="http://example.com/a">
|
|
34
|
+
<child xmlns:a="http://example.com/a"><grand/></child>
|
|
35
|
+
</root>
|
|
36
|
+
XML
|
|
37
|
+
doc = ctx.parse(xml)
|
|
38
|
+
result = canon.canonicalize(doc.root)
|
|
39
|
+
expect(result.scan('xmlns:a="http://example.com/a"').count).to eq(1)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "re-renders a namespace when re-declared with a different URI" do
|
|
43
|
+
xml = <<~XML.strip
|
|
44
|
+
<root xmlns:a="http://example.com/a">
|
|
45
|
+
<child xmlns:a="http://example.com/other"><grand/></child>
|
|
46
|
+
</root>
|
|
47
|
+
XML
|
|
48
|
+
doc = ctx.parse(xml)
|
|
49
|
+
result = canon.canonicalize(doc.root)
|
|
50
|
+
expect(result.scan("xmlns:a=").count).to eq(2)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "renders the default namespace at apex when inherited" do
|
|
54
|
+
xml = '<outer xmlns="http://example.com"><inner/></outer>'
|
|
55
|
+
doc = ctx.parse(xml)
|
|
56
|
+
inner = doc.at_xpath("//*[local-name()='inner']")
|
|
57
|
+
result = canon.canonicalize(inner)
|
|
58
|
+
expect(result).to include('xmlns="http://example.com"')
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/c14n"
|
|
5
|
+
|
|
6
|
+
# Edge cases for namespace rendering per W3C C14N 1.0 §2.3 / 1.1 §2.3.
|
|
7
|
+
# These cover the bugs that historically plague C14N implementations.
|
|
8
|
+
RSpec.describe "C14N namespace rendering" do
|
|
9
|
+
let(:ctx) { Moxml.new(:nokogiri) }
|
|
10
|
+
|
|
11
|
+
describe "default namespace" do
|
|
12
|
+
it "renders xmlns= declaration when default ns is set" do
|
|
13
|
+
doc = ctx.parse(%(<root xmlns="urn:foo"/>))
|
|
14
|
+
result = Moxml::C14n.canonicalize(doc.root)
|
|
15
|
+
expect(result).to eq('<root xmlns="urn:foo"></root>')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it "renders xmlns='' when transitioning to empty default" do
|
|
19
|
+
doc = ctx.parse(%(<a xmlns="urn:foo"><b xmlns=""><c/></b></a>))
|
|
20
|
+
result = Moxml::C14n.canonicalize(doc.root)
|
|
21
|
+
# <b> has empty default; <a> has urn:foo → xmlns="" appears
|
|
22
|
+
expect(result).to include('xmlns=""')
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "does not render xmlns when default ns is empty everywhere" do
|
|
26
|
+
doc = ctx.parse(%(<root><child/></root>))
|
|
27
|
+
result = Moxml::C14n.canonicalize(doc.root)
|
|
28
|
+
expect(result).to eq("<root><child></child></root>")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
describe "prefix redeclaration" do
|
|
33
|
+
it "renders both declarations when URI changes" do
|
|
34
|
+
doc = ctx.parse(%(<a xmlns:x="urn:1"><b xmlns:x="urn:2"/></a>))
|
|
35
|
+
result = Moxml::C14n.canonicalize(doc.root)
|
|
36
|
+
expect(result.scan("xmlns:x=").length).to eq(2)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "renders only once when URI is unchanged" do
|
|
40
|
+
doc = ctx.parse(%(<a xmlns:x="urn:1"><b xmlns:x="urn:1"/></a>))
|
|
41
|
+
result = Moxml::C14n.canonicalize(doc.root)
|
|
42
|
+
expect(result.scan("xmlns:x=").length).to eq(1)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
describe "xml namespace" do
|
|
47
|
+
it "does not render xmlns:xml declaration" do
|
|
48
|
+
doc = ctx.parse(%(<root xmlns:xml="http://www.w3.org/XML/1998/namespace"/>))
|
|
49
|
+
result = Moxml::C14n.canonicalize(doc.root)
|
|
50
|
+
expect(result).not_to include("xmlns:xml=")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe "namespace sorting" do
|
|
55
|
+
it "sorts xmlns declarations by prefix" do
|
|
56
|
+
doc = ctx.parse(%(<root xmlns:z="urn:z" xmlns:a="urn:a"/>))
|
|
57
|
+
result = Moxml::C14n.canonicalize(doc.root)
|
|
58
|
+
# a should appear before z
|
|
59
|
+
expect(result.index("xmlns:a=")).to be < result.index("xmlns:z=")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "renders default namespace first" do
|
|
63
|
+
doc = ctx.parse(%(<root xmlns="urn:default" xmlns:a="urn:a"/>))
|
|
64
|
+
result = Moxml::C14n.canonicalize(doc.root)
|
|
65
|
+
# Default (xmlns=) before prefixed
|
|
66
|
+
expect(result.index("xmlns=")).to be < result.index("xmlns:a=")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
describe "deeply nested inheritance" do
|
|
71
|
+
it "renders each prefix at the level it is first declared" do
|
|
72
|
+
xml = <<~XML.strip
|
|
73
|
+
<root xmlns:a="urn:a">
|
|
74
|
+
<level1 xmlns:b="urn:b">
|
|
75
|
+
<level2 xmlns:c="urn:c">
|
|
76
|
+
<apex/>
|
|
77
|
+
</level2>
|
|
78
|
+
</level1>
|
|
79
|
+
</root>
|
|
80
|
+
XML
|
|
81
|
+
doc = ctx.parse(xml)
|
|
82
|
+
result = Moxml::C14n.canonicalize(doc.root)
|
|
83
|
+
# Inclusive: all in-scope ns render on apex. But we're canonicalizing
|
|
84
|
+
# the whole tree, so each ns renders at its declaration point.
|
|
85
|
+
expect(result.scan("xmlns:").length).to eq(3)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/c14n"
|
|
5
|
+
|
|
6
|
+
# xml:lang and xml:space are "simple inheritable" attributes per W3C C14N
|
|
7
|
+
# 1.1 §2.4. When an element is in the node-set but an ancestor is not,
|
|
8
|
+
# the canonical form inherits these attributes from the nearest ancestor
|
|
9
|
+
# in which they were declared.
|
|
10
|
+
#
|
|
11
|
+
# These specs exercise the inclusive behavior at full-document
|
|
12
|
+
# canonicalization (where everything is in the set, so inheritance
|
|
13
|
+
# doesn't trigger). For subset scenarios, see TODO.c14n/07.
|
|
14
|
+
RSpec.describe "C14N xml:* inheritable attributes" do
|
|
15
|
+
let(:ctx) { Moxml.new(:nokogiri) }
|
|
16
|
+
|
|
17
|
+
it "renders xml:lang attribute" do
|
|
18
|
+
doc = ctx.parse(%(<root xml:lang="en">hello</root>))
|
|
19
|
+
expect(Moxml::C14n.canonicalize(doc.root))
|
|
20
|
+
.to eq(%(<root xml:lang="en">hello</root>))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "renders xml:space attribute" do
|
|
24
|
+
doc = ctx.parse(%(<root xml:space="preserve">data</root>))
|
|
25
|
+
expect(Moxml::C14n.canonicalize(doc.root))
|
|
26
|
+
.to eq(%(<root xml:space="preserve">data</root>))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "renders xml:lang on descendant that declares its own" do
|
|
30
|
+
doc = ctx.parse(%(<a xml:lang="en"><b xml:lang="fr"/></a>))
|
|
31
|
+
result = Moxml::C14n.canonicalize(doc.root)
|
|
32
|
+
expect(result).to include('<b xml:lang="fr">')
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "renders xml:id attribute" do
|
|
36
|
+
doc = ctx.parse(%(<root xml:id="foo"/>))
|
|
37
|
+
expect(Moxml::C14n.canonicalize(doc.root))
|
|
38
|
+
.to eq(%(<root xml:id="foo"></root>))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "renders xml:base attribute" do
|
|
42
|
+
doc = ctx.parse(%(<root xml:base="http://example.com/"/>))
|
|
43
|
+
expect(Moxml::C14n.canonicalize(doc.root))
|
|
44
|
+
.to eq(%(<root xml:base="http://example.com/"></root>))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "sorts xml:* attributes by namespace URI then local name" do
|
|
48
|
+
doc = ctx.parse(%(<root xml:space="default" xml:lang="en" xml:id="x"/>))
|
|
49
|
+
result = Moxml::C14n.canonicalize(doc.root)
|
|
50
|
+
# All in XML namespace; sort by local name: id < lang < space
|
|
51
|
+
expect(result.index("xml:id=")).to be < result.index("xml:lang=")
|
|
52
|
+
expect(result.index("xml:lang=")).to be < result.index("xml:space=")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/signature"
|
|
5
|
+
require "base64"
|
|
6
|
+
|
|
7
|
+
RSpec.describe Moxml::Signature::Algorithms::Base64Transform do
|
|
8
|
+
let(:ctx) { Moxml.new(:nokogiri) }
|
|
9
|
+
let(:transform) { described_class.new(context: ctx) }
|
|
10
|
+
|
|
11
|
+
it "decodes base64 octet input" do
|
|
12
|
+
encoded = Base64.strict_encode64("hello world")
|
|
13
|
+
expect(transform.transform(encoded)).to eq("hello world")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "strips whitespace before decoding" do
|
|
17
|
+
encoded = Base64.strict_encode64("hello world").chars.each_slice(4).map(&:join).join("\n")
|
|
18
|
+
expect(transform.transform(encoded)).to eq("hello world")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "raises TransformError on invalid base64" do
|
|
22
|
+
expect do
|
|
23
|
+
transform.transform("!!!not base64!!!")
|
|
24
|
+
end.to raise_error(Moxml::Signature::TransformError)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/signature"
|
|
5
|
+
require "openssl"
|
|
6
|
+
require "base64"
|
|
7
|
+
|
|
8
|
+
RSpec.describe Moxml::Signature::Algorithms::DigestBase do
|
|
9
|
+
describe "SHA-256" do
|
|
10
|
+
let(:digest) { Moxml::Signature::Algorithms::SHA256.new }
|
|
11
|
+
|
|
12
|
+
it "computes raw bytes matching OpenSSL" do
|
|
13
|
+
data = "hello"
|
|
14
|
+
expect(digest.digest(data)).to eq(OpenSSL::Digest::SHA256.digest(data))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "computes base64-encoded digest matching OpenSSL" do
|
|
18
|
+
data = "hello"
|
|
19
|
+
expected = Base64.strict_encode64(OpenSSL::Digest::SHA256.digest(data))
|
|
20
|
+
expect(digest.digest_base64(data)).to eq(expected)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "matches the FIPS 180-3 test vector for empty string" do
|
|
24
|
+
# SHA-256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
|
25
|
+
expected_hex = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
26
|
+
expect(digest.digest("").unpack1("H*")).to eq(expected_hex)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "matches the FIPS 180-3 test vector for 'abc'" do
|
|
30
|
+
expected_hex = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
|
|
31
|
+
expect(digest.digest("abc").unpack1("H*")).to eq(expected_hex)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe "SHA-1" do
|
|
36
|
+
it "matches the FIPS test vector for 'abc'" do
|
|
37
|
+
digest = Moxml::Signature::Algorithms::SHA1.new
|
|
38
|
+
expected_hex = "a9993e364706816aba3e25717850c26c9cd0d89d"
|
|
39
|
+
expect(digest.digest("abc").unpack1("H*")).to eq(expected_hex)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "is registered under its W3C URI" do
|
|
43
|
+
klass = Moxml::Signature::Algorithms.lookup(
|
|
44
|
+
:digest, "http://www.w3.org/2000/09/xmldsig#sha1"
|
|
45
|
+
)
|
|
46
|
+
expect(klass).to eq(Moxml::Signature::Algorithms::SHA1)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe "all five SHA digests" do
|
|
51
|
+
expected_uris = {
|
|
52
|
+
"http://www.w3.org/2000/09/xmldsig#sha1" =>
|
|
53
|
+
[Moxml::Signature::Algorithms::SHA1, 20],
|
|
54
|
+
"http://www.w3.org/2001/04/xmldsig-more#sha224" =>
|
|
55
|
+
[Moxml::Signature::Algorithms::SHA224, 28],
|
|
56
|
+
"http://www.w3.org/2001/04/xmlenc#sha256" =>
|
|
57
|
+
[Moxml::Signature::Algorithms::SHA256, 32],
|
|
58
|
+
"http://www.w3.org/2001/04/xmldsig-more#sha384" =>
|
|
59
|
+
[Moxml::Signature::Algorithms::SHA384, 48],
|
|
60
|
+
"http://www.w3.org/2001/04/xmlenc#sha512" =>
|
|
61
|
+
[Moxml::Signature::Algorithms::SHA512, 64],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
expected_uris.each do |uri, (klass, byte_length)|
|
|
65
|
+
it "#{uri} resolves to #{klass.name.split('::').last} with #{byte_length}-byte output" do
|
|
66
|
+
instance = klass.new
|
|
67
|
+
result = instance.digest("test")
|
|
68
|
+
expect(result.bytesize).to eq(byte_length)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/signature"
|
|
5
|
+
require "openssl"
|
|
6
|
+
|
|
7
|
+
RSpec.describe Moxml::Signature::Algorithms::DsaSha do
|
|
8
|
+
let(:key) { OpenSSL::PKey::DSA.generate(2048) }
|
|
9
|
+
let(:data) { "the quick brown fox jumps over the lazy dog" }
|
|
10
|
+
|
|
11
|
+
it "round-trips DSA-SHA1" do
|
|
12
|
+
algo = described_class.new(
|
|
13
|
+
identifier_uri: "http://www.w3.org/2000/09/xmldsig#dsa-sha1",
|
|
14
|
+
)
|
|
15
|
+
sig = algo.sign(data, key)
|
|
16
|
+
# DSA q for 2048-bit DSA is 256 bits → 32-byte halves → 64-byte raw sig.
|
|
17
|
+
expect(sig.bytesize).to eq(64)
|
|
18
|
+
expect(algo.verify(data, key, sig)).to be true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it "round-trips DSA-SHA256" do
|
|
22
|
+
algo = described_class.new(
|
|
23
|
+
identifier_uri: "http://www.w3.org/2009/xmldsig11#dsa-sha256",
|
|
24
|
+
)
|
|
25
|
+
sig = algo.sign(data, key)
|
|
26
|
+
expect(sig.bytesize).to eq(64)
|
|
27
|
+
expect(algo.verify(data, key, sig)).to be true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "rejects tampered payload" do
|
|
31
|
+
algo = described_class.new(
|
|
32
|
+
identifier_uri: "http://www.w3.org/2009/xmldsig11#dsa-sha256",
|
|
33
|
+
)
|
|
34
|
+
sig = algo.sign(data, key)
|
|
35
|
+
expect(algo.verify("#{data}!", key, sig)).to be false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "rejects non-DSA keys" do
|
|
39
|
+
algo = described_class.new(
|
|
40
|
+
identifier_uri: "http://www.w3.org/2009/xmldsig11#dsa-sha256",
|
|
41
|
+
)
|
|
42
|
+
expect do
|
|
43
|
+
algo.sign(data, OpenSSL::PKey::RSA.generate(2048))
|
|
44
|
+
end.to raise_error(Moxml::Signature::SignatureKeyError)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/signature"
|
|
5
|
+
require "openssl"
|
|
6
|
+
|
|
7
|
+
RSpec.describe Moxml::Signature::Algorithms::EcdsaSha do
|
|
8
|
+
let(:key) { OpenSSL::PKey::EC.generate("prime256v1") }
|
|
9
|
+
let(:data) { "the quick brown fox jumps over the lazy dog" }
|
|
10
|
+
|
|
11
|
+
%w[
|
|
12
|
+
http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1
|
|
13
|
+
http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha224
|
|
14
|
+
http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256
|
|
15
|
+
http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384
|
|
16
|
+
http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512
|
|
17
|
+
].each do |uri|
|
|
18
|
+
it "#{uri} round-trips sign/verify on P-256" do
|
|
19
|
+
algo = described_class.new(identifier_uri: uri)
|
|
20
|
+
sig = algo.sign(data, key)
|
|
21
|
+
# P-256: 32-byte r ‖ 32-byte s = 64 bytes
|
|
22
|
+
expect(sig.bytesize).to eq(64)
|
|
23
|
+
expect(algo.verify(data, key, sig)).to be true
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it "rejects tampered payload" do
|
|
28
|
+
algo = described_class.new(
|
|
29
|
+
identifier_uri: "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
|
|
30
|
+
)
|
|
31
|
+
sig = algo.sign(data, key)
|
|
32
|
+
expect(algo.verify("#{data}!", key, sig)).to be false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "rejects verification with a different key" do
|
|
36
|
+
other = OpenSSL::PKey::EC.generate("prime256v1")
|
|
37
|
+
algo = described_class.new(
|
|
38
|
+
identifier_uri: "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
|
|
39
|
+
)
|
|
40
|
+
sig = algo.sign(data, key)
|
|
41
|
+
expect(algo.verify(data, other, sig)).to be false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "supports P-384 with 48-byte coordinates" do
|
|
45
|
+
p384 = OpenSSL::PKey::EC.generate("secp384r1")
|
|
46
|
+
algo = described_class.new(
|
|
47
|
+
identifier_uri: "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384",
|
|
48
|
+
)
|
|
49
|
+
sig = algo.sign(data, p384)
|
|
50
|
+
expect(sig.bytesize).to eq(96)
|
|
51
|
+
expect(algo.verify(data, p384, sig)).to be true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "supports P-521 with 66-byte coordinates" do
|
|
55
|
+
p521 = OpenSSL::PKey::EC.generate("secp521r1")
|
|
56
|
+
algo = described_class.new(
|
|
57
|
+
identifier_uri: "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512",
|
|
58
|
+
)
|
|
59
|
+
sig = algo.sign(data, p521)
|
|
60
|
+
expect(sig.bytesize).to eq(132)
|
|
61
|
+
expect(algo.verify(data, p521, sig)).to be true
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "rejects non-EC keys" do
|
|
65
|
+
algo = described_class.new(
|
|
66
|
+
identifier_uri: "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
|
|
67
|
+
)
|
|
68
|
+
expect do
|
|
69
|
+
algo.sign(data, OpenSSL::PKey::RSA.generate(2048))
|
|
70
|
+
end.to raise_error(Moxml::Signature::SignatureKeyError)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/signature"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Moxml::Signature::Algorithms::EnvelopedSignatureTransform do
|
|
7
|
+
let(:ctx) { Moxml.new(:nokogiri) }
|
|
8
|
+
|
|
9
|
+
it "is a no-op when signature_element is nil (signing case)" do
|
|
10
|
+
doc = ctx.parse("<root><child/></root>")
|
|
11
|
+
root = doc.root
|
|
12
|
+
transform = described_class.new(context: ctx, signature_element: nil)
|
|
13
|
+
expect(transform.transform(root)).to equal(root)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "removes the containing signature from a copy (verification case)" do
|
|
17
|
+
xml = <<~XML.strip
|
|
18
|
+
<root>
|
|
19
|
+
<payload>important</payload>
|
|
20
|
+
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
|
|
21
|
+
<ds:SignedInfo/>
|
|
22
|
+
</ds:Signature>
|
|
23
|
+
</root>
|
|
24
|
+
XML
|
|
25
|
+
doc = ctx.parse(xml)
|
|
26
|
+
sig_elem = doc.at_xpath("//ds:Signature",
|
|
27
|
+
"ds" => "http://www.w3.org/2000/09/xmldsig#")
|
|
28
|
+
|
|
29
|
+
transform = described_class.new(context: ctx, signature_element: sig_elem)
|
|
30
|
+
result = transform.transform(doc.root)
|
|
31
|
+
|
|
32
|
+
expect(doc.at_xpath("//ds:Signature",
|
|
33
|
+
"ds" => "http://www.w3.org/2000/09/xmldsig#")).not_to be_nil
|
|
34
|
+
|
|
35
|
+
c14n = Moxml::C14n::Exclusive.new
|
|
36
|
+
canonical = c14n.canonicalize(result)
|
|
37
|
+
expect(canonical).not_to include("Signature")
|
|
38
|
+
expect(canonical).to include("important")
|
|
39
|
+
end
|
|
40
|
+
end
|