moxml 0.1.25 → 0.2.0
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/.github/workflows/windows.yml +62 -0
- data/Gemfile +3 -1
- data/README.adoc +133 -6
- data/docs/signature/algorithms.md +108 -0
- data/docs/signature/architecture.md +148 -0
- data/docs/signature/c14n.md +110 -0
- data/docs/signature/examples.md +109 -0
- data/docs/signature/flows.md +132 -0
- data/docs/signature/quick-reference.md +115 -0
- data/docs/signature/security.md +143 -0
- data/examples/signature/auto_key_extraction.rb +24 -0
- data/examples/signature/enveloped_rsa.rb +45 -0
- data/examples/signature/hmac.rb +33 -0
- data/lib/compat/opal/moxml_boot.rb +5 -0
- data/lib/moxml/adapter/base.rb +53 -49
- data/lib/moxml/adapter/customized_leptris/declaration.rb +32 -0
- data/lib/moxml/adapter/customized_leptris/doctype.rb +31 -0
- data/lib/moxml/adapter/customized_leptris/entity_reference.rb +12 -0
- data/lib/moxml/adapter/customized_leptris/text_segment.rb +26 -0
- data/lib/moxml/adapter/customized_leptris.rb +16 -0
- data/lib/moxml/adapter/customized_libxml/cdata.rb +3 -12
- data/lib/moxml/adapter/customized_libxml/comment.rb +2 -9
- data/lib/moxml/adapter/customized_libxml/declaration.rb +1 -10
- data/lib/moxml/adapter/customized_libxml/node.rb +8 -0
- data/lib/moxml/adapter/customized_libxml/processing_instruction.rb +2 -10
- data/lib/moxml/adapter/customized_libxml/text.rb +2 -9
- data/lib/moxml/adapter/customized_oga/entity_decoder.rb +37 -0
- data/lib/moxml/adapter/customized_oga/raw_value_override.rb +52 -0
- data/lib/moxml/adapter/customized_oga/xml_generator.rb +4 -34
- data/lib/moxml/adapter/customized_oga.rb +2 -0
- data/lib/moxml/adapter/customized_ox/entity_reference.rb +1 -17
- data/lib/moxml/adapter/customized_rexml/entity_reference.rb +1 -11
- data/lib/moxml/adapter/headed_ox.rb +9 -128
- data/lib/moxml/adapter/leptris.rb +965 -0
- data/lib/moxml/adapter/libxml.rb +131 -99
- data/lib/moxml/adapter/nokogiri.rb +29 -8
- data/lib/moxml/adapter/oga.rb +91 -54
- data/lib/moxml/adapter/ox.rb +93 -137
- data/lib/moxml/adapter/rexml.rb +61 -50
- data/lib/moxml/adapter.rb +2 -1
- data/lib/moxml/attribute.rb +8 -4
- data/lib/moxml/attribute_resolver.rb +189 -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/config.rb +29 -3
- data/lib/moxml/context.rb +12 -0
- data/lib/moxml/element.rb +29 -12
- data/lib/moxml/entity/reference.rb +28 -0
- data/lib/moxml/entity.rb +125 -0
- data/lib/moxml/node.rb +90 -1
- data/lib/moxml/sax/namespace_splitter.rb +5 -2
- 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 +28 -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/xml_emitter.rb +71 -0
- data/lib/moxml/xpath/compiler.rb +60 -7
- data/lib/moxml/xpath/parser.rb +22 -15
- data/lib/moxml.rb +5 -0
- data/reference-docs/w3c-xmldsig-bestpractices.md +216 -0
- data/reference-docs/w3c-xmldsig-core.md +400 -0
- data/spec/examples/readme_examples_spec.rb +0 -4
- data/spec/examples/xpath_examples_spec.rb +0 -11
- 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/integration/all_adapters_spec.rb +17 -0
- data/spec/integration/sax_parity_spec.rb +58 -0
- data/spec/integration/shared_examples/edge_cases.rb +0 -10
- data/spec/integration/shared_examples/integration_workflows.rb +0 -10
- data/spec/integration/shared_examples/node_wrappers/attribute_behavior.rb +98 -0
- data/spec/integration/shared_examples/node_wrappers/namespace_behavior.rb +26 -0
- data/spec/integration/shared_examples/node_wrappers/node_behavior.rb +0 -8
- data/spec/moxml/adapter/leptris_spec.rb +75 -0
- data/spec/moxml/adapter/ox_spec.rb +20 -6
- data/spec/moxml/adapter/platform_spec.rb +15 -2
- data/spec/moxml/adapter/shared_examples/adapter_contract.rb +180 -0
- data/spec/moxml/attribute_resolver_spec.rb +108 -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/doctype_spec.rb +1 -1
- data/spec/moxml/entity_spec.rb +76 -0
- data/spec/moxml/node_spec.rb +104 -0
- data/spec/moxml/sax_entity_parity_spec.rb +358 -0
- data/spec/moxml/signature/algorithms/base64_transform_spec.rb +26 -0
- data/spec/moxml/signature/algorithms/digest_base_spec.rb +72 -0
- data/spec/moxml/signature/algorithms/dsa_sha_spec.rb +46 -0
- data/spec/moxml/signature/algorithms/ecdsa_sha_spec.rb +72 -0
- data/spec/moxml/signature/algorithms/enveloped_signature_transform_spec.rb +40 -0
- data/spec/moxml/signature/algorithms/hmac_sha_spec.rb +71 -0
- data/spec/moxml/signature/algorithms/rsa_pkcs1_sha_spec.rb +55 -0
- data/spec/moxml/signature/algorithms_spec.rb +76 -0
- data/spec/moxml/signature/cross_verify_spec.rb +64 -0
- data/spec/moxml/signature/edge_cases_spec.rb +284 -0
- data/spec/moxml/signature/fixtures_spec.rb +80 -0
- data/spec/moxml/signature/key_extractor_spec.rb +96 -0
- data/spec/moxml/signature/model_spec.rb +54 -0
- data/spec/moxml/signature/round_trip_spec.rb +113 -0
- data/spec/moxml/xml_emitter_spec.rb +64 -0
- data/spec/moxml/xpath/axes_spec.rb +72 -0
- data/spec/moxml/xpath/parser_spec.rb +6 -0
- data/spec/moxml/xpath_capabilities_spec.rb +5 -3
- data/spec/performance/benchmark_spec.rb +13 -6
- data/spec/performance/thread_safety_spec.rb +0 -4
- data/spec/spec_helper.rb +5 -1
- metadata +130 -2
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/signature"
|
|
5
|
+
require "openssl"
|
|
6
|
+
|
|
7
|
+
RSpec.describe Moxml::Signature::KeyExtractor do
|
|
8
|
+
let(:extractor) { described_class.new }
|
|
9
|
+
|
|
10
|
+
describe "RSAKeyValue" do
|
|
11
|
+
let(:rsa_key) { OpenSSL::PKey::RSA.generate(2048) }
|
|
12
|
+
|
|
13
|
+
it "reconstructs a public RSA key from Modulus and Exponent" do
|
|
14
|
+
require "base64"
|
|
15
|
+
n_b64 = Base64.strict_encode64(rsa_key.n.to_s(2))
|
|
16
|
+
e_b64 = Base64.strict_encode64(rsa_key.e.to_s(2))
|
|
17
|
+
key_value = Moxml::Signature::Model::KeyValue.new(
|
|
18
|
+
rsa_key_value: Moxml::Signature::Model::Key::RSAKeyValue.new(
|
|
19
|
+
modulus: n_b64,
|
|
20
|
+
exponent: e_b64,
|
|
21
|
+
),
|
|
22
|
+
)
|
|
23
|
+
key_info = Moxml::Signature::Model::KeyInfo.new(key_value: key_value)
|
|
24
|
+
extracted = extractor.extract(key_info)
|
|
25
|
+
expect(extracted).to be_a(OpenSSL::PKey::RSA)
|
|
26
|
+
expect(extracted.n).to eq(rsa_key.n)
|
|
27
|
+
expect(extracted.e).to eq(rsa_key.e)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe "X509Certificate" do
|
|
32
|
+
let(:rsa_key) { OpenSSL::PKey::RSA.generate(2048) }
|
|
33
|
+
let(:cert) do
|
|
34
|
+
# Generate a self-signed cert so we can round-trip the DER.
|
|
35
|
+
cert = OpenSSL::X509::Certificate.new
|
|
36
|
+
cert.version = 2
|
|
37
|
+
cert.serial = 1
|
|
38
|
+
cert.subject = OpenSSL::X509::Name.parse("/CN=test")
|
|
39
|
+
cert.issuer = cert.subject
|
|
40
|
+
cert.public_key = rsa_key
|
|
41
|
+
cert.not_before = Time.now
|
|
42
|
+
cert.not_after = Time.now + 3600
|
|
43
|
+
cert.sign(rsa_key, OpenSSL::Digest.new("SHA256"))
|
|
44
|
+
cert
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it "extracts the public key from the certificate" do
|
|
48
|
+
require "base64"
|
|
49
|
+
key_info = Moxml::Signature::Model::KeyInfo.new(
|
|
50
|
+
x509_data: Moxml::Signature::Model::Key::X509Data.new(
|
|
51
|
+
# X509Certificate is stored as base64-encoded DER (spec §4.5.4).
|
|
52
|
+
certificates: [Base64.strict_encode64(cert.to_der)],
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
extracted = extractor.extract(key_info)
|
|
56
|
+
expect(extracted).to be_a(OpenSSL::PKey::RSA)
|
|
57
|
+
expect(extracted.n).to eq(rsa_key.n)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
describe "KeyName" do
|
|
62
|
+
it "looks up the key in the application-supplied map" do
|
|
63
|
+
key = OpenSSL::PKey::RSA.generate(2048)
|
|
64
|
+
extractor_with_map = described_class.new(key_map: { "my-key" => key })
|
|
65
|
+
key_info = Moxml::Signature::Model::KeyInfo.new(key_name: "my-key")
|
|
66
|
+
expect(extractor_with_map.extract(key_info)).to equal(key)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "returns nil for an unknown key name" do
|
|
70
|
+
key_info = Moxml::Signature::Model::KeyInfo.new(key_name: "unknown")
|
|
71
|
+
expect(extractor.extract(key_info)).to be_nil
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
describe "ECKeyValue" do
|
|
76
|
+
it "reconstructs a P-256 public EC key" do
|
|
77
|
+
ec = OpenSSL::PKey::EC.generate("prime256v1")
|
|
78
|
+
# Spec §4.5.2.3: PublicKey contains the uncompressed-point form
|
|
79
|
+
# 0x04 || x || y, base64-encoded.
|
|
80
|
+
point_octets = ec.public_key.to_octet_string(:uncompressed)
|
|
81
|
+
ec_pub_b64 = Base64.strict_encode64(point_octets)
|
|
82
|
+
|
|
83
|
+
key_info = Moxml::Signature::Model::KeyInfo.new(
|
|
84
|
+
key_value: Moxml::Signature::Model::KeyValue.new(
|
|
85
|
+
ec_key_value: Moxml::Signature::Model::Key::ECKeyValue.new(
|
|
86
|
+
named_curve_uri: "urn:oid:1.2.840.10045.3.1.7",
|
|
87
|
+
public_key: ec_pub_b64,
|
|
88
|
+
),
|
|
89
|
+
),
|
|
90
|
+
)
|
|
91
|
+
extracted = extractor.extract(key_info)
|
|
92
|
+
expect(extracted).to be_a(OpenSSL::PKey::EC)
|
|
93
|
+
expect(extracted.public_key).to eq(ec.public_key)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/signature"
|
|
5
|
+
|
|
6
|
+
RSpec.describe Moxml::Signature::Model do
|
|
7
|
+
describe Moxml::Signature::Model::Signature do
|
|
8
|
+
it "stores its components" do
|
|
9
|
+
si = Moxml::Signature::Model::SignedInfo.new
|
|
10
|
+
sv = Moxml::Signature::Model::SignatureValue.new(value: "abc")
|
|
11
|
+
sig = described_class.new(id: "S1", signed_info: si, signature_value: sv)
|
|
12
|
+
expect(sig.id).to eq("S1")
|
|
13
|
+
expect(sig.signed_info).to equal(si)
|
|
14
|
+
expect(sig.signature_value).to equal(sv)
|
|
15
|
+
expect(sig.objects).to eq([])
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
describe Moxml::Signature::Model::Reference do
|
|
20
|
+
it "accepts uri, transforms, and digest method" do
|
|
21
|
+
t = Moxml::Signature::Model::Transforms.new(
|
|
22
|
+
transforms: [Moxml::Signature::Model::Transform.new(
|
|
23
|
+
algorithm: "http://www.w3.org/2000/09/xmldsig#enveloped-signature",
|
|
24
|
+
)],
|
|
25
|
+
)
|
|
26
|
+
dm = Moxml::Signature::Model::DigestMethod.new(
|
|
27
|
+
algorithm: "http://www.w3.org/2001/04/xmlenc#sha256",
|
|
28
|
+
)
|
|
29
|
+
ref = described_class.new(uri: "", transforms: t, digest_method: dm,
|
|
30
|
+
digest_value: "abc=")
|
|
31
|
+
expect(ref.uri).to eq("")
|
|
32
|
+
expect(ref.transforms.transforms.size).to eq(1)
|
|
33
|
+
expect(ref.digest_method.algorithm).to eq(
|
|
34
|
+
"http://www.w3.org/2001/04/xmlenc#sha256",
|
|
35
|
+
)
|
|
36
|
+
expect(ref.digest_value).to eq("abc=")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe Moxml::Signature::Model::Transforms do
|
|
41
|
+
it "is empty by default" do
|
|
42
|
+
expect(described_class.new).to be_empty
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "appends transforms with <<" do
|
|
46
|
+
transforms = described_class.new
|
|
47
|
+
t = Moxml::Signature::Model::Transform.new(
|
|
48
|
+
algorithm: "http://www.w3.org/2000/09/xmldsig#base64",
|
|
49
|
+
)
|
|
50
|
+
transforms << t
|
|
51
|
+
expect(transforms.size).to eq(1)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "moxml/signature"
|
|
5
|
+
require "openssl"
|
|
6
|
+
|
|
7
|
+
# End-to-end signing + verification, including tamper detection and
|
|
8
|
+
# wrong-key rejection. Uses real model instances and real OpenSSL keys
|
|
9
|
+
# per the project's "no doubles" rule.
|
|
10
|
+
RSpec.describe "Moxml XML Signature end-to-end round trip" do
|
|
11
|
+
let(:ctx) { Moxml.new(:nokogiri) }
|
|
12
|
+
let(:private_key) { OpenSSL::PKey::RSA.generate(2048) }
|
|
13
|
+
let(:document_xml) { "<doc><greeting>Hello, World!</greeting></doc>" }
|
|
14
|
+
|
|
15
|
+
let(:common_options) do
|
|
16
|
+
{
|
|
17
|
+
context: ctx,
|
|
18
|
+
signature_method: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
|
|
19
|
+
canonicalization_method: "http://www.w3.org/2001/10/xml-exc-c14n#",
|
|
20
|
+
digest_method: "http://www.w3.org/2001/04/xmlenc#sha256",
|
|
21
|
+
reference_uri: "",
|
|
22
|
+
transforms: ["http://www.w3.org/2000/09/xmldsig#enveloped-signature"],
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def sign_and_attach(xml, key, **opts)
|
|
27
|
+
doc = ctx.parse(xml)
|
|
28
|
+
signature = Moxml::Signature.sign(document: doc, key: key, **common_options.merge(opts))
|
|
29
|
+
serialized = Moxml::Signature::Serializer.new(context: ctx).serialize(signature)
|
|
30
|
+
doc.root.add_child(serialized.root)
|
|
31
|
+
doc
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
describe "RSA-SHA256 round trip" do
|
|
35
|
+
it "verifies a freshly-signed document" do
|
|
36
|
+
doc = sign_and_attach(document_xml, private_key)
|
|
37
|
+
result = Moxml::Signature.verify(context: ctx, document: doc, key: private_key)
|
|
38
|
+
expect(result.valid?).to be true
|
|
39
|
+
expect(result.signature_count).to eq(1)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "verifies with the public key alone" do
|
|
43
|
+
doc = sign_and_attach(document_xml, private_key)
|
|
44
|
+
pub = OpenSSL::PKey::RSA.new(private_key.public_to_pem)
|
|
45
|
+
result = Moxml::Signature.verify(context: ctx, document: doc, key: pub)
|
|
46
|
+
expect(result.valid?).to be true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "detects payload tampering" do
|
|
50
|
+
doc = sign_and_attach(document_xml, private_key)
|
|
51
|
+
greeting = doc.at_xpath("//greeting")
|
|
52
|
+
greeting.text = "Goodbye, World!"
|
|
53
|
+
result = Moxml::Signature.verify(context: ctx, document: doc, key: private_key)
|
|
54
|
+
expect(result.valid?).to be false
|
|
55
|
+
# SignatureValue still matches (SignedInfo unchanged) but reference
|
|
56
|
+
# digest mismatches.
|
|
57
|
+
expect(result.results.first.signature_valid?).to be true
|
|
58
|
+
expect(result.results.first.references.first.valid?).to be false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it "detects SignedInfo tampering" do
|
|
62
|
+
doc = sign_and_attach(document_xml, private_key)
|
|
63
|
+
dv = doc.at_xpath("//ds:DigestValue",
|
|
64
|
+
"ds" => "http://www.w3.org/2000/09/xmldsig#")
|
|
65
|
+
dv.text = Base64.strict_encode64("x" * 32)
|
|
66
|
+
result = Moxml::Signature.verify(context: ctx, document: doc, key: private_key)
|
|
67
|
+
expect(result.valid?).to be false
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it "rejects an unrelated verification key" do
|
|
71
|
+
doc = sign_and_attach(document_xml, private_key)
|
|
72
|
+
other = OpenSSL::PKey::RSA.generate(2048)
|
|
73
|
+
result = Moxml::Signature.verify(context: ctx, document: doc, key: other)
|
|
74
|
+
expect(result.valid?).to be false
|
|
75
|
+
expect(result.results.first.signature_valid?).to be false
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
describe "HMAC-SHA256 round trip" do
|
|
80
|
+
let(:hmac_options) do
|
|
81
|
+
common_options.merge(
|
|
82
|
+
signature_method: "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256",
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "verifies with the shared secret" do
|
|
87
|
+
doc = sign_and_attach(document_xml, "shared-secret", **hmac_options)
|
|
88
|
+
result = Moxml::Signature.verify(context: ctx, document: doc, key: "shared-secret")
|
|
89
|
+
expect(result.valid?).to be true
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "rejects the wrong secret" do
|
|
93
|
+
doc = sign_and_attach(document_xml, "shared-secret", **hmac_options)
|
|
94
|
+
result = Moxml::Signature.verify(context: ctx, document: doc, key: "wrong-secret")
|
|
95
|
+
expect(result.valid?).to be false
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
describe "fixture key" do
|
|
100
|
+
let(:pem_path) do
|
|
101
|
+
File.expand_path("../../fixtures/xmldsig/keys/rsa_private.pem", __dir__)
|
|
102
|
+
end
|
|
103
|
+
let(:fixture_key) { OpenSSL::PKey::RSA.new(File.read(pem_path)) }
|
|
104
|
+
|
|
105
|
+
it "loads and round-trips" do
|
|
106
|
+
skip "fixture key not present" unless File.exist?(pem_path)
|
|
107
|
+
|
|
108
|
+
doc = sign_and_attach(document_xml, fixture_key)
|
|
109
|
+
result = Moxml::Signature.verify(context: ctx, document: doc, key: fixture_key)
|
|
110
|
+
expect(result.valid?).to be true
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Moxml::XmlEmitter do
|
|
6
|
+
describe ".escape_text" do
|
|
7
|
+
it "escapes only & < > in a single pass" do
|
|
8
|
+
expect(described_class.escape_text("a & < > ' \""))
|
|
9
|
+
.to eq("a & < > ' \"")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "returns clean strings unallocated" do
|
|
13
|
+
input = "clean"
|
|
14
|
+
expect(described_class.escape_text(input)).to equal(input)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "coerces non-strings" do
|
|
18
|
+
expect(described_class.escape_text(1)).to eq("1")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe ".escape_attribute" do
|
|
23
|
+
it "escapes & < > and double quotes, keeping apostrophes literal" do
|
|
24
|
+
expect(described_class.escape_attribute(%q{& < > ' "}))
|
|
25
|
+
.to eq("& < > ' "")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
describe ".cdata" do
|
|
30
|
+
it "wraps content in a CDATA section" do
|
|
31
|
+
expect(described_class.cdata("data")).to eq("<![CDATA[data]]>")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "splits an embedded end sequence into adjacent sections" do
|
|
35
|
+
expect(described_class.cdata("a]]>b"))
|
|
36
|
+
.to eq("<![CDATA[a]]]]><![CDATA[>b]]>")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe ".declaration_xml" do
|
|
41
|
+
it "renders only the set attributes" do
|
|
42
|
+
expect(described_class.declaration_xml("1.0", nil, nil))
|
|
43
|
+
.to eq('<?xml version="1.0"?>')
|
|
44
|
+
expect(described_class.declaration_xml("1.0", "UTF-8", "yes"))
|
|
45
|
+
.to eq('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>')
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "omits empty encodings" do
|
|
49
|
+
expect(described_class.declaration_xml("1.0", "", nil))
|
|
50
|
+
.to eq('<?xml version="1.0"?>')
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe ".doctype_xml" do
|
|
55
|
+
it "renders PUBLIC and SYSTEM forms and bare names" do
|
|
56
|
+
expect(described_class.doctype_xml("r", "pub", "sys"))
|
|
57
|
+
.to eq('<!DOCTYPE r PUBLIC "pub" "sys">')
|
|
58
|
+
expect(described_class.doctype_xml("r", nil, "sys"))
|
|
59
|
+
.to eq('<!DOCTYPE r SYSTEM "sys">')
|
|
60
|
+
expect(described_class.doctype_xml("r", nil, nil))
|
|
61
|
+
.to eq("<!DOCTYPE r>")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -163,6 +163,78 @@ RSpec.describe "XPath Axes" do
|
|
|
163
163
|
end
|
|
164
164
|
end
|
|
165
165
|
|
|
166
|
+
describe "attribute axis with namespaces" do
|
|
167
|
+
let(:ns_doc) do
|
|
168
|
+
xml = <<~XML
|
|
169
|
+
<root xmlns:p="http://x.org" xmlns:q="http://x.org">
|
|
170
|
+
<item q:type="namespaced" type="bare" other="o"/>
|
|
171
|
+
</root>
|
|
172
|
+
XML
|
|
173
|
+
context.parse(xml)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def select(expression, namespaces = nil)
|
|
177
|
+
ast = Moxml::XPath::Parser.parse(expression)
|
|
178
|
+
proc = Moxml::XPath::Compiler.compile_with_cache(ast,
|
|
179
|
+
namespaces: namespaces)
|
|
180
|
+
proc.call(ns_doc).map(&:value)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it "matches bare names against no-namespace attributes only" do
|
|
184
|
+
expect(select("/root/item/@type")).to eq(["bare"])
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
it "matches prefixed names by namespace URI, not by prefix spelling" do
|
|
188
|
+
expect(select("/root/item/@p:type")).to eq(["namespaced"])
|
|
189
|
+
expect(select("/root/item/@q:type")).to eq(["namespaced"])
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
it "matches any local name within a prefix's namespace" do
|
|
193
|
+
expect(select("/root/item/@q:*")).to eq(["namespaced"])
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
it "matches nothing for an undeclared prefix" do
|
|
197
|
+
expect(select("/root/item/@z:type")).to be_empty
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "prefers context prefix mappings over document declarations" do
|
|
201
|
+
expect(select("/root/item/@p:type", "p" => "http://other.org"))
|
|
202
|
+
.to be_empty
|
|
203
|
+
expect(select("/root/item/@zzz:type", "zzz" => "http://x.org"))
|
|
204
|
+
.to eq(["namespaced"])
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
describe "descendant-or-self shorthand from an element" do
|
|
209
|
+
it "selects descendants with .//step" do
|
|
210
|
+
ast = Moxml::XPath::Parser.parse(".//grandchild")
|
|
211
|
+
proc = Moxml::XPath::Compiler.compile_with_cache(ast)
|
|
212
|
+
result = proc.call(doc.root.children.find(&:element?))
|
|
213
|
+
|
|
214
|
+
expect(result.map { |n| n.attribute("id").value })
|
|
215
|
+
.to contain_exactly("g1", "g2")
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
describe "node type tests as steps" do
|
|
220
|
+
it "selects text nodes with text()" do
|
|
221
|
+
ast = Moxml::XPath::Parser.parse("//grandchild/text()")
|
|
222
|
+
proc = Moxml::XPath::Compiler.compile_with_cache(ast)
|
|
223
|
+
result = proc.call(doc)
|
|
224
|
+
|
|
225
|
+
expect(result.map(&:content)).to contain_exactly("text1", "text2")
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
it "selects comments with comment()" do
|
|
229
|
+
commented = context.parse("<root><!-- c1 --><a/><!-- c2 --></root>")
|
|
230
|
+
ast = Moxml::XPath::Parser.parse("//comment()")
|
|
231
|
+
proc = Moxml::XPath::Compiler.compile_with_cache(ast)
|
|
232
|
+
result = proc.call(commented)
|
|
233
|
+
|
|
234
|
+
expect(result.map(&:content)).to contain_exactly(" c1 ", " c2 ")
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
166
238
|
describe "descendant axis" do
|
|
167
239
|
it "finds descendants without self" do
|
|
168
240
|
ast = Moxml::XPath::Parser.parse("/root/descendant::child")
|
|
@@ -60,6 +60,12 @@ RSpec.describe Moxml::XPath::Parser do
|
|
|
60
60
|
expect(ast.type).to eq(:relative_path)
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
it "parses prefixed attribute names" do
|
|
64
|
+
ast = described_class.parse("@xmi:type")
|
|
65
|
+
expect(ast.type).to eq(:relative_path)
|
|
66
|
+
expect { described_class.parse("@xmi:*") }.not_to raise_error
|
|
67
|
+
end
|
|
68
|
+
|
|
63
69
|
it "parses path with parent reference" do
|
|
64
70
|
ast = described_class.parse("../book")
|
|
65
71
|
expect(ast.type).to eq(:relative_path)
|
|
@@ -131,11 +131,13 @@ RSpec.describe "XPath Capabilities" do
|
|
|
131
131
|
end
|
|
132
132
|
|
|
133
133
|
describe "XPath functions" do
|
|
134
|
-
it "supports count() function" do
|
|
134
|
+
it "supports count() function, returning the scalar unwrapped" do
|
|
135
135
|
doc = Moxml.new.parse(simple_xml)
|
|
136
|
-
# NOTE: count() returns a number, not nodes
|
|
137
136
|
result = doc.xpath("count(//book)")
|
|
138
|
-
|
|
137
|
+
# REXML's XPath has no function support and yields an empty set
|
|
138
|
+
skip "count() function not supported on #{adapter_name}" if result.is_a?(Moxml::NodeSet)
|
|
139
|
+
|
|
140
|
+
expect(result).to eq(2)
|
|
139
141
|
rescue Moxml::XPathError, NoMethodError
|
|
140
142
|
skip "count() function not supported on #{adapter_name}"
|
|
141
143
|
end
|
|
@@ -23,14 +23,21 @@ RSpec.shared_examples "Performance Examples" do
|
|
|
23
23
|
context "measures performance" do
|
|
24
24
|
let(:doc) { context.parse(large_xml) }
|
|
25
25
|
|
|
26
|
+
# Absolute ips varies more than 2x across machines and even between
|
|
27
|
+
# runs on the same machine (observed: rexml serializer 1.5-2.9 ips on
|
|
28
|
+
# one machine). These thresholds sit at roughly half of typical
|
|
29
|
+
# modern-runner throughput so they only trip on asymptotic
|
|
30
|
+
# regressions (accidental O(n^2), broken caching), not on hardware
|
|
31
|
+
# variance or a few percent of legitimate overhead.
|
|
26
32
|
let(:thresholds) do
|
|
27
33
|
{
|
|
28
|
-
nokogiri: { parser:
|
|
29
|
-
oga: { parser:
|
|
30
|
-
rexml: { parser: 0, serializer:
|
|
31
|
-
ox: { parser:
|
|
32
|
-
headed_ox: { parser:
|
|
33
|
-
libxml: { parser:
|
|
34
|
+
nokogiri: { parser: 8, serializer: 500 },
|
|
35
|
+
oga: { parser: 5, serializer: 50 },
|
|
36
|
+
rexml: { parser: 0, serializer: 1 },
|
|
37
|
+
ox: { parser: 1, serializer: 400 },
|
|
38
|
+
headed_ox: { parser: 1, serializer: 400 },
|
|
39
|
+
libxml: { parser: 200, serializer: 30 },
|
|
40
|
+
leptris: { parser: 1300, serializer: 600 },
|
|
34
41
|
}
|
|
35
42
|
end
|
|
36
43
|
|
|
@@ -20,10 +20,6 @@ RSpec.shared_examples "Thread Safety Examples" do
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
it "handles concurrent processing" do
|
|
23
|
-
if Moxml.new.config.adapter_name == :ox
|
|
24
|
-
skip "Ox doesn't have a native XPath"
|
|
25
|
-
end
|
|
26
|
-
|
|
27
23
|
processor = processor_class.new
|
|
28
24
|
threads = []
|
|
29
25
|
results = Queue.new
|
data/spec/spec_helper.rb
CHANGED
|
@@ -101,7 +101,11 @@ RSpec.configure do |config|
|
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
Moxml.configure do |config|
|
|
104
|
-
|
|
104
|
+
# Pin the suite baseline to the fallback adapter so results are
|
|
105
|
+
# reproducible regardless of whether the local environment has a
|
|
106
|
+
# leptris new enough to win the runtime-default probe. Leptris gets
|
|
107
|
+
# full coverage through its explicit adapter/integration matrices.
|
|
108
|
+
config.adapter = RUBY_ENGINE == "opal" ? Moxml::Config::OPAL_DEFAULT_ADAPTER : Moxml::Config::FALLBACK_ADAPTER
|
|
105
109
|
config.strict_parsing = true
|
|
106
110
|
config.default_encoding = "UTF-8"
|
|
107
111
|
config.entity_load_mode = :optional if RUBY_ENGINE == "opal"
|