Signer
WS Security XML Certificate signing for Ruby
Installation
gem install signer
Usage
require "signer"
signer = Signer.new(File.read("example.xml"))
signer.cert = OpenSSL::X509::Certificate.new(File.read("cert.pem"))
signer.private_key = OpenSSL::PKey::RSA.new(File.read("key.pem"), "password")
signer.document.xpath("//u:Timestamp", { "u" => "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" }).each do |node|
signer.digest!(node)
end
signer.document.xpath("//a:To", { "a" => "http://www.w3.org/2005/08/addressing" }).each do |node|
signer.digest!(node)
end
signer.sign!(:security_token => true)
signer.to_xml
Usage with Savon
client = Savon::Client.new do |wsdl, http|
wsdl.document = "..."
wsdl.endpoint = "..."
end
response = client.request(:search_documents) do
soap.version = 2
soap.xml do
builder = Nokogiri::XML::Builder.new do |xml|
xml.send("s:Envelope",
"xmlns:s" => "http://www.w3.org/2003/05/soap-envelope",
"xmlns:a" => "http://www.w3.org/2005/08/addressing",
"xmlns:u" => "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
) do
xml.send("s:Header") do
xml.send("a:Action", "s:mustUnderstand" => "1") do
xml.text "http://tempuri.org/IDocumentService/SearchDocuments"
end
xml.send("a:MessageID") do
xml.text "urn:uuid:30db5d4f-ab84-46be-907c-be690a92979b"
end
xml.send("a:ReplyTo") do
xml.send("a:Address") do
xml.text "http://www.w3.org/2005/08/addressing/anonymous"
end
end
xml.send("a:To", "a:mustUnderstand" => "1") do
xml.text "..."
end
xml.send("o:Security",
"xmlns:o" => "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
"s:mustUnderstand" => "1"
) do
xml.send("u:Timestamp") do
time = Time.now.utc
xml.send("u:Created") do
xml.text(time.xmlschema)
end
xml.send("u:Expires") do
xml.text((time + 5.minutes).xmlschema)
end
end
end
end
xml.send("s:Body") do
xml.send("SearchDocuments", "xmlns" => "http://tempuri.org/") do
xml.send("searchCriteria",
"xmlns:b" => "http://schemas.datacontract.org/2004/07/ZMDVS.BusinessLogic.Data.Documents.Integration",
"xmlns:i" => "http://www.w3.org/2001/XMLSchema-instance"
) do
xml.send("b:RegistrationNo") do
xml.text "1"
end
end
end
end
end
end
signer = Signer.new(builder.to_xml)
signer.cert = OpenSSL::X509::Certificate.new(File.read("cert.pem"))
signer.private_key = OpenSSL::PKey::RSA.new(File.read("key.pem"), "test")
signer.document.xpath("//u:Timestamp", { "u" => "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" }).each do |node|
signer.digest!(node)
end
signer.document.xpath("//a:To", { "a" => "http://www.w3.org/2005/08/addressing" }).each do |node|
signer.digest!(node)
end
signer.sign!
signer.to_xml
end
end
Example
Input:
<?xml version="1.0"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<s:Header>
<a:Action s:mustUnderstand="1">http://tempuri.org/IDocumentService/SearchDocuments</a:Action>
<a:MessageID>urn:uuid:30db5d4f-ab84-46be-907c-be690a92979b</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<To xmlns="http://www.w3.org/2005/08/addressing" xmlns:a="http://www.w3.org/2003/05/soap-envelope" a:mustUnderstand="1">http://tempuri.org/PublicServices/Test/1.0.12/PublicServices/DocumentService.svc</To>
<o:Security xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" s:mustUnderstand="1">
<u:Timestamp>
<u:Created>2012-05-02T18:17:14.467Z</u:Created>
<u:Expires>2012-05-02T18:22:14.467Z</u:Expires>
</u:Timestamp>
</o:Security>
</s:Header>
<s:Body>
<SearchDocuments xmlns="http://tempuri.org/">
<searchCriteria xmlns:b="http://schemas.datacontract.org/2004/07/BusinessLogic.Data.Documents.Integration" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<b:RegistrationNo>1</b:RegistrationNo>
</searchCriteria>
</SearchDocuments>
</s:Body>
</s:Envelope>
Output:
<?xml version="1.0"?>
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<s:Header>
<a:Action s:mustUnderstand="1">http://tempuri.org/IDocumentService/SearchDocuments</a:Action>
<a:MessageID>urn:uuid:30db5d4f-ab84-46be-907c-be690a92979b</a:MessageID>
<a:ReplyTo>
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
</a:ReplyTo>
<To xmlns="http://www.w3.org/2005/08/addressing" xmlns:a="http://www.w3.org/2003/05/soap-envelope" a:mustUnderstand="1" u:Id="_7e75a8ded22253b163ca76a40b6cc0c670ed0c33">http://tempuri.org/PublicServices/Test/1.0.12/PublicServices/DocumentService.svc</To>
<o:Security xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" s:mustUnderstand="1">
<u:Timestamp u:Id="_23dd13bb673d95ac7c29f0bebcca8268d39675b1">
<u:Created>2012-05-02T18:17:14.467Z</u:Created>
<u:Expires>2012-05-02T18:22:14.467Z</u:Expires>
</u:Timestamp>
<o:BinarySecurityToken u:Id="uuid-639b8970-7644-4f9e-9bc4-9c2e367808fc-1" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">MIICsDCCAhmgAwIBAgIJAOUHvh4oho0tMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTIwNTAzMTMxODIyWhcNMTMwNTAzMTMxODIyWjBFMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCvK5hMPv/R5IFmwWyJOyEaFUrF/ZsmN+Gip8hvR6rLP3YPNx9iFYvPcZllFmuVwyaz7YT2N5BsqTwLdyi5v4HY4fUtuz0p8jIPoSd6dfDvcnSpf4QLTOgOaL3ciPEbgDHH2tnIksukoWzqCYva+qFZ74NFl19swXotW9fA4Jzs4QIDAQABo4GnMIGkMB0GA1UdDgQWBBRU1WEHDnP8Hr7ZulxrSzEwOcYpMzB1BgNVHSMEbjBsgBRU1WEHDnP8Hr7ZulxrSzEwOcYpM6FJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAOUHvh4oho0tMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEASY/9SAOK57q9mGnNJJeyDbmyGrAHSJTod646xTHYkMvhUqwHyk9PTr5bdfmswpmyVn+AQ43U2tU5vnpTBmKpHWD2+HSHgGa92mMLrfBOd8EBZ329NL3N2HDPIaHr4NPGyhNrSK3QVOnAq2D0jlyrGYJlLli1NxHiBz7FCEJaVI8=</o:BinarySecurityToken>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<Reference URI="#_23dd13bb673d95ac7c29f0bebcca8268d39675b1">
<Transforms>
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue>Oz29YgZk14+nchoqv9zGzhJcDUo=</DigestValue>
</Reference>
<Reference URI="#_7e75a8ded22253b163ca76a40b6cc0c670ed0c33">
<Transforms>
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue>leV/RNYhwuCuD7/DBzn3IgQzUxI=</DigestValue>
</Reference>
</SignedInfo>
<SignatureValue>en7YYAIn90ofH08aF917jNngMuse+vK6bihF0v6UsXFnGGMOflWfRTZ6mFmC2HwLmb2lSrhZ3eth3cs2fCBlEr/K2ZDMQfJo6CPxmbzfX/fxR/isCTDz+HIJd13J0HK4n+CzkndwplkCmT8SQlduUruUFUUmlQiiZQ7nryR+XyM=</SignatureValue>
<KeyInfo>
<o:SecurityTokenReference>
<o:Reference ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3" URI="#uuid-639b8970-7644-4f9e-9bc4-9c2e367808fc-1"/>
</o:SecurityTokenReference>
</KeyInfo>
</Signature>
</o:Security>
</s:Header>
<s:Body>
<SearchDocuments xmlns="http://tempuri.org/">
<searchCriteria xmlns:b="http://schemas.datacontract.org/2004/07/BusinessLogic.Data.Documents.Integration" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<b:RegistrationNo>1</b:RegistrationNo>
</searchCriteria>
</SearchDocuments>
</s:Body>
</s:Envelope>
Different signature and digest algorithms support
You can change digest algorithms used for both node digesting and signing. Default for both is SHA1. Currently SHA1 :sha1
, SHA256 :sha256
, and GOST R 34.11-94 :gostr3411
are supported out of the box.
signer.digest_algorithm = :sha256
signer.signature_digest_algorithm = :sha256
You can provide you own digest support by passing in these methods a Hash
with :id
and :digester
keys. In :id
should be a string for XML //Reference/DigestMethod[Algorithm]
, in :digester
should be a Ruby object, compatible by interface with OpenSSL::Digest
class, at least it should respond to digest
and reset
methods.
Signature algorithm is dependent from keypair used for signing and can't be changed. Usually it's RSA. Currently gem recognizes GOST R 34.10-2001 certificates and sets up a XML identifier for it. If used signature algorithm and signature digest doesn't corresponds with XML identifier, you can change identifier with signature_algorithm_id
method.
Please note, that these settings will be changed or reset on certificate assignment, please change them after setting certificate!
NOTE: To sign XMLs with GOST R 34.10-2001, you need to have Ruby compiled with patches from https://bugs.ruby-lang.org/issues/9830 and correctly configured OpenSSL (see https://github.com/openssl/openssl/blob/master/engines/ccgost/README.gost)
Miscellaneous
Every new instance of signer has Nokogiri noblanks
set as default in process of parsing xml file. If you need to disable it, pass optional argument noblanks: false
.
Signer.new(File.read("example.xml"), noblanks: false)
Available options for the sign!
method:
- [:security_token] - Serializes certificate in DER format, encodes it with Base64 and inserts it within a
BinarySecurityToken
tag
If you need to digest a BinarySecurityToken
tag, you need to construct it yourself before signing.
signer.digest!(signer.binary_security_token_node)
signer.sign!
- [:inclusive_namespaces] - Array of namespace prefixes which definitions should be added to signed info node during canonicalization
If you need Signature
tags to be in explicit namespace (say, <ds:Signature>
) instead of to be in implicit default namespace you can specify next option:
signer.ds_namespace_prefix = 'ds'
If you need to use canonicalization with inclusive namespaces you can pass array of namespace prefixes in :inclusive_namespaces
option in both digest!
and sign!
methods.
- [:issuer_serial] - flag to include a
X509Data
node to include information from a X509Certificate
- [:issuer_in_security_token] - flag to include the
X509Data
inside a SecurityTokenReference
element