From 5ee703615f401936ebe81273b3b9fa93fb91e546 Mon Sep 17 00:00:00 2001
From: "Albert J. Wong" <[email protected]>
Date: Fri, 19 Jun 2015 23:56:20 -0700
Subject: [PATCH 1/3] Implement SOAP decryption in pure Ruby.
* Creates pkcs12 versions of the test keys.
* Uses xmlenc gem for decryption.
* Adds new ruby only decrypt_message_xml_ruby method.
* Does NOT do WSSE timestamp validation.
* Does NOT verify XML Dsig.
---
Gemfile | 2 ++
Gemfile.lock | 24 ++++++++++++++++++++++++
spec/fixtures/test_keystore_importkey.p12 | Bin 0 -> 2594 bytes
spec/fixtures/test_keystore_vbms_server_key.p12 | Bin 0 -> 2622 bytes
spec/ruby_crypto_spec.rb | 21 +++++++++++----------
src/vbms/common.rb | 16 ++++++++++++++++
6 files changed, 53 insertions(+), 10 deletions(-)
create mode 100644 spec/fixtures/test_keystore_importkey.p12
create mode 100644 spec/fixtures/test_keystore_vbms_server_key.p12
diff --git a/Gemfile b/Gemfile
index 2169f6a..0650638 100644
--- a/Gemfile
+++ b/Gemfile
@@ -2,6 +2,8 @@ source 'https://rubygems.org'
gem 'httpi'
gem 'nokogiri'
+gem 'pg'
+gem 'xmlenc'
# to install without postgres, "bundle install --without postgres"
group :postgres do
diff --git a/Gemfile.lock b/Gemfile.lock
index 38aa4a5..07497c4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,6 +1,16 @@
GEM
remote: https://rubygems.org/
specs:
+ activemodel (4.2.1)
+ activesupport (= 4.2.1)
+ builder (~> 3.1)
+ activesupport (4.2.1)
+ i18n (~> 0.7)
+ json (~> 1.7, >= 1.7.7)
+ minitest (~> 5.1)
+ thread_safe (~> 0.3, >= 0.3.4)
+ tzinfo (~> 1.1)
+ builder (3.2.2)
byebug (5.0.0)
columnize (= 0.9.0)
coderay (1.1.0)
@@ -10,8 +20,11 @@ GEM
nokogiri (>= 1.4.3)
httpi (2.3.0)
rack
+ i18n (0.7.0)
+ json (1.8.3)
method_source (0.8.2)
mini_portile (0.6.2)
+ minitest (5.7.0)
nokogiri (1.6.6.2)
mini_portile (~> 0.6.0)
pg (0.18.1)
@@ -36,6 +49,13 @@ GEM
rspec-support (~> 3.2.0)
rspec-support (3.2.2)
slop (3.6.0)
+ thread_safe (0.3.5)
+ tzinfo (1.2.2)
+ thread_safe (~> 0.1)
+ xmlenc (0.2.0)
+ activemodel (>= 3.0.0)
+ activesupport (>= 3.0.0)
+ nokogiri (~> 1.6.0)
PLATFORMS
ruby
@@ -48,3 +68,7 @@ DEPENDENCIES
pg
pry-nav
rspec
+ xmlenc
+
+BUNDLED WITH
+ 1.10.4
diff --git a/spec/fixtures/test_keystore_importkey.p12 b/spec/fixtures/test_keystore_importkey.p12
new file mode 100644
index 0000000000000000000000000000000000000000..8596bb40c18e78f22a6f276a2766b5f5bfb3d266
GIT binary patch
literal 2594
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
literal 0
HcmV?d00001
diff --git a/spec/fixtures/test_keystore_vbms_server_key.p12 b/spec/fixtures/test_keystore_vbms_server_key.p12
new file mode 100644
index 0000000000000000000000000000000000000000..bc08028d962241c90e9d88c4549a311292127fb7
GIT binary patch
literal 2622
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
I<ZqPx7pr;A#{d8T
literal 0
HcmV?d00001
diff --git a/spec/ruby_crypto_spec.rb b/spec/ruby_crypto_spec.rb
index bcd552f..56d922f 100644
--- a/spec/ruby_crypto_spec.rb
+++ b/spec/ruby_crypto_spec.rb
@@ -5,14 +5,16 @@
let (:plaintext_xml) { fixture_path('plaintext_basic_soap.xml') }
let (:plaintext_unicode_xml) { fixture_path('plaintext_unicode_soap.xml') }
let (:plaintext_request_name) { "getDocumentTypes" }
- let (:test_keystore) { fixture_path('test_keystore.jks') }
+ let (:test_jks_keystore) { fixture_path('test_keystore.jks') }
+ let (:test_pc12_server_key) { fixture_path('test_keystore_vbms_server_key.p12') }
+ let (:test_pc12_client_key) { fixture_path('test_keystore_importkey.p12') }
let (:test_keystore_pass) { "importkey" }
it "encrypts in ruby, and decrypts using java" do
# TODO(awong): Implement encrypt in ruby.
encrypted_xml = VBMS.encrypted_soap_document(
- plaintext_xml, test_keystore, test_keystore_pass, plaintext_request_name)
- decrypted_xml = VBMS.decrypt_message_xml(encrypted_xml, test_keystore,
+ plaintext_xml, test_jks_keystore, test_keystore_pass, plaintext_request_name)
+ decrypted_xml = VBMS.decrypt_message_xml(encrypted_xml, test_jks_keystore,
test_keystore_pass, plaintext_request_name)
# Compare the decrypted request node with the original request node.
@@ -28,11 +30,10 @@
end
it "encrypts in java, and decrypts using ruby" do
- # TODO(awong): Implement decrypt in ruby.
encrypted_xml = VBMS.encrypted_soap_document(
- plaintext_xml, test_keystore, test_keystore_pass, plaintext_request_name)
- decrypted_xml = VBMS.decrypt_message_xml(encrypted_xml, test_keystore,
- test_keystore_pass, plaintext_request_name)
+ plaintext_xml, test_jks_keystore, test_keystore_pass, plaintext_request_name)
+ decrypted_xml = VBMS.decrypt_message_xml_ruby(encrypted_xml, test_pc12_server_key,
+ test_keystore_pass)
# Compare the decrypted request node with the original request node.
original_doc = Nokogiri::XML(fixture('plaintext_basic_soap.xml'))
@@ -49,8 +50,8 @@
it "handles roundtripping utf-8 content." do
pending("Correct Unicode Handling")
encrypted_xml = VBMS.encrypted_soap_document(
- plaintext_unicode_xml, test_keystore, test_keystore_pass, plaintext_request_name)
- decrypted_xml = VBMS.decrypt_message_xml(encrypted_xml, test_keystore,
+ plaintext_unicode_xml, test_jks_keystore, test_keystore_pass, plaintext_request_name)
+ decrypted_xml = VBMS.decrypt_message_xml(encrypted_xml, test_jks_keystore,
test_keystore_pass, plaintext_request_name)
# Compare the decrypted request node with the original request node.
@@ -62,6 +63,6 @@
decrypted_request_node = decrypted_doc.xpath(
'/soapenv:Envelope/soapenv:Body/v4:getDocumentTypes',
VBMS::XML_NAMESPACES)
- expect(original_request_node).to be_equivalent_to(decrypted_request_node).respecting_element_order
+ expect(decrypted_request_node).to be_equivalent_to(original_request_node).respecting_element_order
end
end
diff --git a/src/vbms/common.rb b/src/vbms/common.rb
index 0fc29c1..f5838b5 100644
--- a/src/vbms/common.rb
+++ b/src/vbms/common.rb
@@ -1,4 +1,5 @@
require 'open3'
+require 'xmlenc'
module VBMS
FILEDIR = File.dirname(File.absolute_path(__FILE__))
@@ -67,6 +68,21 @@ def self.decrypt_message_xml(in_xml, keyfile, keypass, logfile, ignore_timestamp
end
end
+ def self.decrypt_message_xml_ruby(encrypted_xml, keyfile_p12, keypass)
+ encrypted_doc = Xmlenc::EncryptedDocument.new(encrypted_xml)
+
+ # TODO(awong): Associate a keystore class with this API instead of
+ # passing path per request. The keystore client should take in a ds:KeyInfo
+ # node and know how to find the associated private key.
+ encryption_key = OpenSSL::PKCS12.new(File.read(keyfile_p12), keypass)
+ decrypted_doc = encrypted_doc.decrypt(encryption_key.key)
+
+ # TODO(awong): Signature verification.
+ # TODO(awong): Timestamp validation.
+ #
+ decrypted_doc
+ end
+
def self.encrypted_soap_document(infile, keyfile, keypass, request_name)
output, errors, status = Open3.capture3(DO_WSSE, '-e', '-i', infile, '-k', keyfile, '-p', keypass, '-n', request_name)
if status != 0
From 6ab5eed82bd858b947d3a0a19eb61b7cf42f0cf0 Mon Sep 17 00:00:00 2001
From: "Albert J. Wong" <[email protected]>
Date: Mon, 22 Jun 2015 09:36:14 -0700
Subject: [PATCH 2/3] Implement SoapScum Keystore.
Allows for loading multiple pc12 certificates into a keystore
and then finding the matching cert based on a
wsse:SecurityTokenReference.
---
.../soap-scum/client_x509_subject_keyinfo.xml | 1 +
.../soap-scum/server_x509_subject_keyinfo.xml | 1 +
spec/soap-scum_spec.rb | 36 +++++++++++++
src/soap-scum.rb | 59 ++++++++++++++++++++++
4 files changed, 97 insertions(+)
create mode 100644 spec/fixtures/soap-scum/client_x509_subject_keyinfo.xml
create mode 100644 spec/fixtures/soap-scum/server_x509_subject_keyinfo.xml
create mode 100644 spec/soap-scum_spec.rb
create mode 100644 src/soap-scum.rb
diff --git a/spec/fixtures/soap-scum/client_x509_subject_keyinfo.xml b/spec/fixtures/soap-scum/client_x509_subject_keyinfo.xml
new file mode 100644
index 0000000..fb9542b
--- /dev/null
+++ b/spec/fixtures/soap-scum/client_x509_subject_keyinfo.xml
@@ -0,0 +1 @@
+<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><wsse:SecurityTokenReference><ds:X509Data><ds:X509IssuerSerial><ds:X509IssuerName>CN=Unknown,OU=Unknown,O=Unknown,L=Unknown,ST=Unknown,C=Unknown</ds:X509IssuerName><ds:X509SerialNumber>1743189802</ds:X509SerialNumber></ds:X509IssuerSerial></ds:X509Data></wsse:SecurityTokenReference></ds:KeyInfo>
diff --git a/spec/fixtures/soap-scum/server_x509_subject_keyinfo.xml b/spec/fixtures/soap-scum/server_x509_subject_keyinfo.xml
new file mode 100644
index 0000000..239deb0
--- /dev/null
+++ b/spec/fixtures/soap-scum/server_x509_subject_keyinfo.xml
@@ -0,0 +1 @@
+<ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><wsse:SecurityTokenReference><ds:X509Data><ds:X509IssuerSerial><ds:X509IssuerName>CN=Unknown,OU=Unknown,O=Unknown,L=Unknown,ST=Unknown,C=Unknown</ds:X509IssuerName><ds:X509SerialNumber>1294758391</ds:X509SerialNumber></ds:X509IssuerSerial></ds:X509Data></wsse:SecurityTokenReference></ds:KeyInfo>
diff --git a/spec/soap-scum_spec.rb b/spec/soap-scum_spec.rb
new file mode 100644
index 0000000..3eaf7eb
--- /dev/null
+++ b/spec/soap-scum_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+require 'soap-scum'
+
+describe :SoapScum do
+ describe "KeyStore" do
+ let (:server_x509_subject) { Nokogiri::XML(fixture('soap-scum/server_x509_subject_keyinfo.xml')) }
+ let (:client_x509_subject) { Nokogiri::XML(fixture('soap-scum/client_x509_subject_keyinfo.xml')) }
+ let (:server_pc12) { fixture_path('test_keystore_vbms_server_key.p12') }
+ let (:client_pc12) { fixture_path('test_keystore_importkey.p12') }
+ let (:keypass) { 'importkey' }
+
+ it "loads a pc12 file and cert" do
+ keystore = SoapScum::KeyStore.new
+ keystore.add_pc12(server_pc12, keypass)
+ server_pkcs12 = OpenSSL::PKCS12.new(File.read(server_pc12), keypass)
+
+ expect(keystore.get_key(server_x509_subject).to_der).to eql(server_pkcs12.key.to_der)
+ expect(keystore.get_certificate(server_x509_subject).to_der).to eql(server_pkcs12.certificate.to_der)
+ end
+
+ it "returns correct cert and key by subject" do
+ keystore = SoapScum::KeyStore.new
+ keystore.add_pc12(server_pc12, keypass)
+ keystore.add_pc12(client_pc12, keypass)
+
+ server_pkcs12 = OpenSSL::PKCS12.new(File.read(server_pc12), keypass)
+ client_pkcs12 = OpenSSL::PKCS12.new(File.read(client_pc12), keypass)
+
+ expect(keystore.get_key(server_x509_subject).to_der).to eql(server_pkcs12.key.to_der)
+ expect(keystore.get_certificate(server_x509_subject).to_der).to eql(server_pkcs12.certificate.to_der)
+
+ expect(keystore.get_key(client_x509_subject).to_der).to eql(client_pkcs12.key.to_der)
+ expect(keystore.get_certificate(client_x509_subject).to_der).to eql(client_pkcs12.certificate.to_der)
+ end
+ end
+end
diff --git a/src/soap-scum.rb b/src/soap-scum.rb
new file mode 100644
index 0000000..023003c
--- /dev/null
+++ b/src/soap-scum.rb
@@ -0,0 +1,59 @@
+require 'nokogiri'
+
+module SoapScum
+ module XMLNamespaces
+ SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/"
+ WSSE = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
+ WSU = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
+ DS = "http://www.w3.org/2000/09/xmldsig#"
+ end
+
+ class KeyStore
+ CertificateAndKey = Struct.new(:certificate, :key)
+ def initialize()
+ @by_subject = {}
+ end
+
+ def add_pc12(path, keypass = "")
+ pkcs12 = OpenSSL::PKCS12.new(File.read(path), keypass)
+ entry = CertificateAndKey.new(pkcs12.certificate, pkcs12.key)
+
+ @by_subject[x509_to_normalized_subject(pkcs12.certificate)] = entry
+ end
+
+ def get_key(keyinfo_node)
+ needle = keyinfo_to_normalized_subject(keyinfo_node)
+ @by_subject[needle].key
+ end
+
+ def get_certificate(keyinfo_node)
+ needle = keyinfo_to_normalized_subject(keyinfo_node)
+ @by_subject[needle].certificate
+ end
+
+ private
+ # Takes an x509 certificate and returns an array sorted in an order that
+ # allows for matching against other normalized subjects.
+ def x509_to_normalized_subject(certificate)
+ normalized_subject = certificate.subject.to_a.map {|name, value, _| [name, value] }.sort_by {|x| x[0] }
+ normalized_subject << ['SerialNumber', certificate.serial.to_s ]
+ end
+
+ def keyinfo_to_normalized_subject(keyinfo_node)
+ subject = keyinfo_node.at(
+ '/ds:KeyInfo/wsse:SecurityTokenReference/ds:X509Data/ds:X509IssuerSerial/ds:X509IssuerName',
+ ds: XMLNamespaces::DS,
+ wsse: XMLNamespaces::WSSE)
+ serial = keyinfo_node.at(
+ '/ds:KeyInfo/wsse:SecurityTokenReference/ds:X509Data/ds:X509IssuerSerial/ds:X509SerialNumber',
+ ds: XMLNamespaces::DS,
+ wsse: XMLNamespaces::WSSE)
+
+ normalized_subject = subject.inner_text.split(',').map {|x| x.split('=')}.sort_by{|x| x[0]}
+ normalized_subject << ['SerialNumber', serial.inner_text ]
+ end
+
+ def keyinfo_has_cert?(keyinfo_node)
+ end
+ end
+end
From d4f29bc02d59cdbe2a4f946dd44d94da0819418a Mon Sep 17 00:00:00 2001
From: "Albert J. Wong" <[email protected]>
Date: Mon, 22 Jun 2015 18:02:56 -0700
Subject: [PATCH 3/3] Implement MessageProcessor encryption.
No signature yet, but getting close.
---
spec/soap-scum_spec.rb | 49 ++++++++++--
src/soap-scum.rb | 205 +++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 248 insertions(+), 6 deletions(-)
diff --git a/spec/soap-scum_spec.rb b/spec/soap-scum_spec.rb
index 3eaf7eb..19b21af 100644
--- a/spec/soap-scum_spec.rb
+++ b/spec/soap-scum_spec.rb
@@ -1,14 +1,17 @@
require 'spec_helper'
require 'soap-scum'
+require 'xmlenc'
describe :SoapScum do
- describe "KeyStore" do
- let (:server_x509_subject) { Nokogiri::XML(fixture('soap-scum/server_x509_subject_keyinfo.xml')) }
- let (:client_x509_subject) { Nokogiri::XML(fixture('soap-scum/client_x509_subject_keyinfo.xml')) }
- let (:server_pc12) { fixture_path('test_keystore_vbms_server_key.p12') }
- let (:client_pc12) { fixture_path('test_keystore_importkey.p12') }
- let (:keypass) { 'importkey' }
+ let (:server_x509_subject) { Nokogiri::XML(fixture('soap-scum/server_x509_subject_keyinfo.xml')) }
+ let (:client_x509_subject) { Nokogiri::XML(fixture('soap-scum/client_x509_subject_keyinfo.xml')) }
+ let (:server_pc12) { fixture_path('test_keystore_vbms_server_key.p12') }
+ let (:client_pc12) { fixture_path('test_keystore_importkey.p12') }
+ let (:test_jks_keystore) { fixture_path('test_keystore.jks') }
+ let (:test_keystore_pass) { "importkey" }
+ let (:keypass) { 'importkey' }
+ describe "KeyStore" do
it "loads a pc12 file and cert" do
keystore = SoapScum::KeyStore.new
keystore.add_pc12(server_pc12, keypass)
@@ -33,4 +36,38 @@
expect(keystore.get_certificate(client_x509_subject).to_der).to eql(client_pkcs12.certificate.to_der)
end
end
+
+ describe "MessageProcessor" do
+ let (:keystore) {
+ keystore = SoapScum::KeyStore.new
+ keystore.add_pc12(server_pc12, keypass)
+ keystore
+ }
+ let (:message_processor) {
+ SoapScum::MessageProcessor.new(keystore)
+ }
+ let (:content_document) {
+ Nokogiri::XML('<hi-mom xmlns:example="http://example.com"><example:a-doc /></hi-mom>')
+ }
+
+ it "Creates basic soap envelope" do
+ soap_doc = message_processor.wrap_in_soap(content_document)
+ # TODO(awong): Verify structure.
+ end
+
+ it "Encrypts and signs a soap message" do
+ soap_doc = message_processor.wrap_in_soap(content_document)
+ encrypted_doc = message_processor.encrypt(soap_doc, keystore.all.first.certificate,
+ SoapScum::MessageProcessor::CryptoAlgorithms::RSA1_5,
+ SoapScum::MessageProcessor::CryptoAlgorithms::AES128,
+ soap_doc.at_xpath('/soapenv:Envelope/soapenv:Body',
+ soapenv: SoapScum::XMLNamespaces::SOAPENV).children)
+ encrypted_xml = encrypted_doc.serialize(encoding: 'UTF-8', save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
+ puts Xmlenc::EncryptedDocument.new(encrypted_xml).decrypt(keystore.all.first.key)
+ decrypted_xml = VBMS.decrypt_message_xml(encrypted_xml, test_jks_keystore,
+ test_keystore_pass, 'hi-mom')
+ puts decrypted_xml
+ end
+ end
+
end
diff --git a/src/soap-scum.rb b/src/soap-scum.rb
index 023003c..4cdd6c2 100644
--- a/src/soap-scum.rb
+++ b/src/soap-scum.rb
@@ -1,15 +1,22 @@
+require 'base64'
require 'nokogiri'
module SoapScum
module XMLNamespaces
SOAPENV = "http://schemas.xmlsoap.org/soap/envelope/"
WSSE = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
+ WSSE11 = "http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd"
WSU = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
DS = "http://www.w3.org/2000/09/xmldsig#"
+ XENC = "http://www.w3.org/2001/04/xmlenc#"
end
class KeyStore
CertificateAndKey = Struct.new(:certificate, :key)
+ def all()
+ @by_subject.map{|_, x| x}
+ end
+
def initialize()
@by_subject = {}
end
@@ -56,4 +63,202 @@ def keyinfo_to_normalized_subject(keyinfo_node)
def keyinfo_has_cert?(keyinfo_node)
end
end
+
+ class MessageProcessor
+ module CryptoAlgorithms
+ RSA1_5 = 'http://www.w3.org/2001/04/xmlenc#rsa-1_5'
+ RSA_OAEP = 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p'
+ # TODO(awong): Add triple-des support for xmlenc 1.0 compliance.
+ AES128 = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'
+ AES256 = 'http://www.w3.org/2001/04/xmlenc#aes256-cbc'
+ end
+
+ def initialize(keystore)
+ @keystore = keystore
+ end
+
+ # Creats a soap message wrapping the root node of the contents_doc.
+ #
+ # SOAP messages have the format
+ #
+ # <Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
+ # <Body>
+ # <!-- Content goes here -->
+ # </Body>
+ # </Envelope>
+ #
+ # The header element is optional without mustUnderstand so this does not
+ # populate it.
+ #
+ # TODO(awong): Add mustUnderstand support.
+ def wrap_in_soap(contents_doc)
+ builder = Nokogiri::XML::Builder.new do |xml|
+ xml['soapenv'].Envelope('xmlns:soapenv' => XMLNamespaces::SOAPENV) do
+ xml['soapenv'].Body do
+ if !contents_doc.nil?
+ xml.parent << contents_doc.root.clone
+ end
+ end
+ end
+ end
+ builder.doc
+ end
+
+ def encrypt(soap_doc, certificate, keytransport_algorithm, cipher_algorithm, nodes_to_encrypt, validity: 5.minutes)
+ envelope = soap_doc.xpath('/soapenv:Envelope', soapenv: XMLNamespaces::SOAPENV)
+
+ # Ensure there is a header node.
+ header = envelope.at_xpath('/soapenv:Header', soapenv: XMLNamespaces::SOAPENV)
+ if header.nil?
+ header_builder = Nokogiri::XML::Builder.new do |xml|
+ xml["soapenv"].Header('xmlns:soapenv' => XMLNamespaces::SOAPENV)
+ end
+ envelope.children.first.add_previous_sibling(header_builder.doc.root)
+ end
+
+ # Create the wsse:Security header.
+ # * Generate timestammp element.
+ # * Create xmlenc template
+ # * Create xmldsig template
+ Nokogiri::XML::Builder.with(soap_doc.at('/soap:Envelope/soap:Header', soap: XMLNamespaces::SOAPENV)) do |xml|
+ # TODO(awong): Do we need mustUnderstand?
+ xml['wsse'].Security('xmlns:wsse' => XMLNamespaces::WSSE,
+ 'xmlns:wsu' => XMLNamespaces::WSU,
+ 'soapenv:mustUnderstand' => '1') do
+ # TODO(awong): Allow configurable digest and signature methods.
+# add_xmldsig_template(xml, certificate, "http://www.w3.org/2000/09/xmldsig#sha1", "http://www.w3.org/2000/09/xmldsig#rsa-sha1", nodes_to_sign)
+ add_xmlenc_template(xml, certificate, keytransport_algorithm, cipher_algorithm, nodes_to_encrypt)
+
+ # Add wsu:Timestamp
+ xml['wsu'].Timestamp('wsu:Id' => "TS-#{generate_id}") do
+ # Using localtime technically follows spec but seems to break
+ # various parsers.
+ now = Time.now.utc
+ xml['wsu'].Created now.xmlschema
+ xml['wsu'].Expires (now + validity).xmlschema
+ end
+ end
+ end
+
+ # Ensure IDs exist on elements like body.
+ # Perform actual signature on plaintext.
+ # Perform actual encryption
+ soap_doc
+ end
+
+ private
+ def generate_encrypted_data(node, encrypted_node_id, key_id, symmetric_key, cipher_algorithm, cipher)
+ cipher.reset
+ cipher.encrypt
+ cipher.key = symmetric_key
+ iv = cipher.random_iv
+
+ raw_xml = node.serialize(encoding: 'UTF-8', save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
+ cipher_text = iv + cipher.update(raw_xml) + cipher.final
+ builder = Nokogiri::XML::Builder.new do |xml|
+ xml['xenc'].EncryptedData('xmlns:xenc' => 'http://www.w3.org/2001/04/xmlenc#', Id: encrypted_node_id, Type: "http://www.w3.org/2001/04/xmlenc#Content") do
+ xml['xenc'].EncryptionMethod(Algorithm: cipher_algorithm)
+ xml['ds'].KeyInfo('xmlns:ds' => XMLNamespaces::DS) do
+ xml['wsse'].SecurityTokenReference('xmlns:wsse' => XMLNamespaces::WSSE,
+ 'xmlns:wsse11' => XMLNamespaces::WSSE11,
+ 'wsse11:TokenType' => 'http://docs.oasis-open.org/wss/oasis-wss-soap-message-security-1.1#EncryptedKey') do
+ xml['wsse'].Reference(URI: "##{key_id}")
+ end
+ end
+ xml['xenc'].CipherData do
+ xml['xenc'].CipherValue Base64.strict_encode64(cipher_text)
+ end
+ end
+ end
+ builder.doc.root
+ end
+
+ def get_block_cipher(cipher_algorithm)
+ case cipher_algorithm
+ when CryptoAlgorithms::AES128
+ return OpenSSL::Cipher::AES128.new(:CBC)
+ when CryptoAlgorithms::AES256
+ return OpenSSL::Cipher::AES256.new(:CBC)
+ else
+ raise "Unknown Cipher: #{cipher_algorithm}"
+ end
+ end
+
+ def generate_id
+ SecureRandom.hex(5)
+ end
+
+ # Takes an XMLBuilder and adds the XML Encryption template.
+ def add_xmlenc_template(xml, certificate, keytransport_algorithm, cipher_algorithm, nodes_to_encrypt)
+ # #5.4.1 Lists the valid ciphers and block sizes.
+ # Generate a secure key as per ruby docs.
+ cipher = get_block_cipher(cipher_algorithm)
+ cipher.encrypt
+ symmetric_key = cipher.random_key
+
+ key_id = "EK-#{generate_id}"
+ xml['xenc'].EncryptedKey('xmlns:xenc' => 'http://www.w3.org/2001/04/xmlenc#', Id: key_id) do
+ xml['xenc'].EncryptionMethod(Algorithm: keytransport_algorithm)
+ xml['ds'].KeyInfo('xmlns:ds' => XMLNamespaces::DS) do
+ xml['wsse'].SecurityTokenReference do
+ xml['ds'].X509Data do
+ xml['ds'].X509IssuerSerial do
+ xml['ds'].X509IssuerName certificate.subject.to_a.map { |name,value,_| "#{name}=#{value}" }.join(',')
+ xml['ds'].X509SerialNumber certificate.serial.to_s
+ end
+ end
+ end
+ end
+
+ xml['xenc'].CipherData do
+ xml['xenc'].CipherValue Base64.strict_encode64(certificate.public_key.public_encrypt(symmetric_key))
+ end
+
+ xml['xenc'].ReferenceList do
+ nodes_to_encrypt.each do |node|
+ encrypted_node_id = "ED-#{generate_id}"
+ encrypted_node = generate_encrypted_data(node, encrypted_node_id, key_id, symmetric_key, cipher_algorithm, cipher)
+ xml['xenc'].DataReference(URI: "##{encrypted_node_id}")
+ node.add_previous_sibling(encrypted_node)
+ node.remove
+ end
+ end
+ end
+ end
+
+ def add_dsig_template(xml, certificate, digest_method, signature_method, nodes_to_sign)
+ signature_id = "EK-#{generate_id}"
+ xml['ds'].Signature('xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#', Id: signature_id) do
+ xml['ds'].SignedInfo do
+ # TODO(awong): Allow modification of CanonicalizationMethod.
+ xml['ds'].CanonicalizationMethod(Algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#") do
+ xml['ec'].InclusiveNamespaces('xmlns:ec' => "http://www.w3.org/2001/10/xml-exc-c14n#", PrefixList: "soapenv v4")
+ end
+ xml['ds'].SignatureMethod(Algorithm: signature_method)
+ nodes_to_sign.each do |node|
+ xml['ds'].Reference(URI: "##{node.Id}") do
+ xml['ds'].Transforms do
+ xml['ds'].Transform(Algorithm: "http://www.w3.org/2001/10/xml-exc-c14n#") do
+ xml['ec'].InclusiveNamespaces('xmlns:ec' => "http://www.w3.org/2001/10/xml-exc-c14n#", PrefixList: "soapenv v4")
+ end
+ end
+ xml['ds'].DigestMethod(Algorithm: digest_method)
+ xml['ds'].DigestValue
+ end
+ end
+ end
+ xml['ds'].SignatureValue
+ xml['ds'].KeyInfo('xmlns:ds' => XMLNamespaces::DS, Id: "KI-#{generate_id}") do
+ xml['wsse'].SecurityTokenReference('wsu:Id' => "STR-#{generate_id}") do
+ xml['ds'].X509Data do
+ xml['ds'].X509IssuerSerial do
+ xml['ds'].X509IssuerName certificate.subject.to_a.map { |name,value,_| "#{name}=#{value}" }.join(',')
+ xml['ds'].X509SerialNumber certificate.serial.to_s
+ end
+ end
+ end
+ end
+ end
+ end
+ end
end