From 7b4ea654cdc9bcde3949e9e7c74f7c0803e6df67 Mon Sep 17 00:00:00 2001 From: Sandro Liebscher Date: Mon, 16 Nov 2015 11:04:51 +0100 Subject: [PATCH] Added ability to add signed timestamp to header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To do this you have to give the option to the *wsse_signature*! Like so: Savon.client({ … wsse_signature: Akami::WSSE::Signature.new( Akami::WSSE::Certs.new(…), timestamp: true) } It is also possible to give created_at and/or expires_at like this Savon.client({ … wsse_signature: Akami::WSSE::Signature.new( Akami::WSSE::Certs.new(…), {timestamp: true, created_at: Time.now-10, expires_at: Time.now + 600}) } --- lib/akami/wsse.rb | 10 +++- lib/akami/wsse/signature.rb | 66 +++++++++++++++++++++---- spec/akami/wsse_spec.rb | 99 +++++++++++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+), 10 deletions(-) diff --git a/lib/akami/wsse.rb b/lib/akami/wsse.rb index 7212861..38ef832 100644 --- a/lib/akami/wsse.rb +++ b/lib/akami/wsse.rb @@ -136,7 +136,15 @@ def wsse_signature # First key/value is tag/hash tag, hash = signature_hash.shift - security_hash nil, tag, hash, signature_hash + sig = security_hash nil, tag, hash, signature_hash + if signature.timestamp + wsu_timestamp_hash = signature.wsu_timestamp_hash + sig["wsse:Security"]["wsu:Timestamp"] = wsu_timestamp_hash["wsu:Timestamp"] # attributes should be merged and not overridden + sig["wsse:Security"][:attributes!].merge!(wsu_timestamp_hash[:attributes!]) + sig["wsse:Security"][:order!] << "wsu:Timestamp" + end + + sig end # Returns a Hash containing wsu:Timestamp details. diff --git a/lib/akami/wsse/signature.rb b/lib/akami/wsse/signature.rb index 0eaaff2..c80086e 100644 --- a/lib/akami/wsse/signature.rb +++ b/lib/akami/wsse/signature.rb @@ -11,6 +11,9 @@ class MissingCertificate < RuntimeError; end # For a +Savon::WSSE::Certs+ object. To hold the certs we need to sign. attr_accessor :certs + # Wheater to sign the timestamp or not ant their time + attr_accessor :timestamp, :created_at, :expires_at + # Without a document, the document cannot be signed. # Generate the document once, and then set document and recall #to_token def document @@ -31,8 +34,11 @@ def document=(document) SignatureNamespace = 'http://www.w3.org/2000/09/xmldsig#'.freeze - def initialize(certs = Certs.new) - @certs = certs + def initialize(certs = Certs.new, options = {}) + @certs = certs + @timestamp = options[:timestamp] ? options[:timestamp] : false + @created_at = options[:created_at] + @expires_at = options[:expires_at] end def have_document? @@ -49,6 +55,26 @@ def body_id @body_id ||= "Body-#{uid}".freeze end + def timestamp_id + @timestamp_id ||= "TS-#{uid}".freeze + end + + def wsu_timestamp_hash + return {} unless timestamp + { + "wsu:Timestamp" => { + "wsu:Created" => (@created_at ||= Time.now).utc.xmlschema, + "wsu:Expires" => (@expires_at ||= created_at + 60).utc.xmlschema + }, + :attributes! => { + "wsu:Timestamp" => { + "wsu:Id" => timestamp_id, + "xmlns:wsu" => WSU_NAMESPACE + } + } + } + end + def security_token_id @security_token_id ||= "SecurityToken-#{uid}".freeze end @@ -60,6 +86,13 @@ def body_attributes } end + def timestamp_attributes + { + "xmlns:wsu" => Akami::WSSE::WSU_NAMESPACE, + "wsu:Id" => timestamp_id, + } + end + def to_token return {} unless have_document? @@ -124,20 +157,29 @@ def signed_info "SignedInfo" => { "CanonicalizationMethod/" => nil, "SignatureMethod/" => nil, - "Reference" => [ - #signed_info_transforms.merge(signed_info_digest_method).merge({ "DigestValue" => timestamp_digest }), - signed_info_transforms.merge(signed_info_digest_method).merge({ "DigestValue" => body_digest }), - ], + "Reference" => references, :attributes! => { - "CanonicalizationMethod/" => { "Algorithm" => ExclusiveXMLCanonicalizationAlgorithm }, - "SignatureMethod/" => { "Algorithm" => RSASHA1SignatureAlgorithm }, - "Reference" => { "URI" => ["##{body_id}"] }, + "CanonicalizationMethod/" => { "Algorithm" => ExclusiveXMLCanonicalizationAlgorithm }, + "SignatureMethod/" => { "Algorithm" => RSASHA1SignatureAlgorithm }, + "Reference" => { "URI" => reference_uris }, }, :order! => [ "CanonicalizationMethod/", "SignatureMethod/", "Reference" ], }, } end + def references + ref = [signed_info_transforms.merge(signed_info_digest_method).merge({ "DigestValue" => body_digest })] + ref << signed_info_transforms.merge(signed_info_digest_method).merge({ "DigestValue" => timestamp_digest }) if timestamp + ref + end + + def reference_uris + ref_uris = ["##{body_id}"] + ref_uris << "##{timestamp_id}" if timestamp + ref_uris + end + def the_signature raise MissingCertificate, "Expected a private_key for signing" unless certs.private_key signed_info = at_xpath(@document, "//Envelope/Header/Security/Signature/SignedInfo") @@ -151,6 +193,12 @@ def body_digest Base64.encode64(OpenSSL::Digest::SHA1.digest(body)).strip end + def timestamp_digest + return nil unless timestamp + timestamp = canonicalize(at_xpath(@document, "//Envelope/Header/Security/Timestamp")) + Base64.encode64(OpenSSL::Digest::SHA1.digest(timestamp)).strip + end + def signed_info_digest_method { "DigestMethod/" => nil, :attributes! => { "DigestMethod/" => { "Algorithm" => SHA1DigestAlgorithm } } } end diff --git a/spec/akami/wsse_spec.rb b/spec/akami/wsse_spec.rb index 2a6c1ec..b8c3991 100644 --- a/spec/akami/wsse_spec.rb +++ b/spec/akami/wsse_spec.rb @@ -261,6 +261,105 @@ expect(wsse.to_xml).to include("username", "password") end end + + context "whith signature and timestamp on it" do + let(:fixtures_path) do + File.join(Bundler.root, 'spec', 'fixtures', 'akami', 'wsse', 'signature' ) + end + let(:cert_file_path) { File.join(fixtures_path, 'cert.pem') } + let(:private_key_path) { File.join(fixtures_path, 'private_key') } + + before do + wsse.signature = Akami::WSSE::Signature.new( + Akami::WSSE::Certs.new( + cert_file: cert_file_path, + private_key_file: private_key_path), + timestamp: true) + # some stubs because we have no document body + wsse.signature.stub(:have_document?) { true } + wsse.signature.stub(:body_digest) { "stubbedBodyDigest" } + wsse.signature.stub(:timestamp_digest) { "stubbedTimestampDigest" } + end + + it "contains SignedInfo node" do + expect(wsse.to_xml).to include('SignedInfo') + end + + it "contains SignatureValue node" do + expect(wsse.to_xml).to include('SignatureValue') + end + + it "contains KeyInfo node" do + expect(wsse.to_xml).to include('KeyInfo') + end + + it "contains a wsu:Created node" do + expect(wsse.to_xml).to include("") + end + + it "contains a wsu:Expires node" do + expect(wsse.to_xml).to include("") + end + + it "contains an id on Timestamp" do + id = wsse.signature.timestamp_id + expect(wsse.to_xml).to include(%Q|#{wsse.signature.created_at.utc.xmlschema}") + end + + it "contains a wsu:Expires node set to #created_at + 60 seconds" do + expect(wsse.to_xml).to include("#{(wsse.signature.created_at + 60).utc.xmlschema}") + end + end + + context "with #expires_at" do + before do + wsse.signature = Akami::WSSE::Signature.new( + Akami::WSSE::Certs.new( + cert_file: cert_file_path, + private_key_file: private_key_path), + {timestamp: true, + expires_at: (Time.now + 86400)}) + wsse.signature.stub(:have_document?) { true } + wsse.signature.stub(:body_digest) { "stubbedBodyDigest" } + wsse.signature.stub(:timestamp_digest) { "stubbedTimestampDigest" } + end + + it "contains a wsu:Created node defaulting to Time.now" do + created_at = Time.now + Timecop.freeze created_at do + expect(wsse.to_xml).to include("#{created_at.utc.xmlschema}") + end + end + + it "contains a wsu:Expires node set to the given time" do + expect(wsse.to_xml).to include("#{wsse.signature.expires_at.utc.xmlschema}") + end + end + end end end