Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion lib/akami/wsse.rb
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,15 @@ def wsse_signature
# First key/value is tag/hash
tag, hash = signature_hash.shift

security_hash nil, tag, hash, signature_hash, true
sig = security_hash nil, tag, hash, signature_hash, true
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.
Expand Down
67 changes: 58 additions & 9 deletions lib/akami/wsse/signature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,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
Expand All @@ -32,8 +35,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?
Expand All @@ -50,6 +56,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
Expand All @@ -61,6 +87,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?

Expand Down Expand Up @@ -125,20 +158,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")
Expand All @@ -152,6 +194,13 @@ def body_digest
Base64.encode64(OpenSSL::Digest::SHA1.digest(body)).strip
end

def timestamp_digest
return nil unless self.timestamp

timestamp = canonicalize(at_xpath(@document, "//Envelope/Header/Security/Timestamp"))
Base64.encode64(OpenSSL::Digest::SHA1.digest(timestamp)).strip if timestamp
end

def signed_info_digest_method
{ "DigestMethod/" => nil, :attributes! => { "DigestMethod/" => { "Algorithm" => SHA1DigestAlgorithm } } }
end
Expand Down
99 changes: 99 additions & 0 deletions spec/akami/wsse_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,105 @@
expect(wsse.to_xml).to match(/<wsu:Timestamp.*<wsse:UsernameToken/i)
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("<wsu:Created>")
end

it "contains a wsu:Expires node" do
expect(wsse.to_xml).to include("<wsu:Expires>")
end

it "contains an id on Timestamp" do
id = wsse.signature.timestamp_id
expect(wsse.to_xml).to include(%Q|<wsu:Timestamp wsu:Id="#{id}"|)
end

it "contains two references" do
body_id = wsse.signature.body_id
expect(wsse.to_xml).to include(%Q|<Reference URI="##{body_id}"|)
timestamp_id = wsse.signature.timestamp_id
expect(wsse.to_xml).to include(%Q|<Reference URI="##{timestamp_id}"|)
end

context "with #created_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,
created_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 with the given time" do
expect(wsse.to_xml).to include("<wsu:Created>#{wsse.signature.created_at.utc.xmlschema}</wsu:Created>")
end

it "contains a wsu:Expires node set to #created_at + 60 seconds" do
expect(wsse.to_xml).to include("<wsu:Expires>#{(wsse.signature.created_at + 60).utc.xmlschema}</wsu:Expires>")
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("<wsu:Created>#{created_at.utc.xmlschema}</wsu:Created>")
end
end

it "contains a wsu:Expires node set to the given time" do
expect(wsse.to_xml).to include("<wsu:Expires>#{wsse.signature.expires_at.utc.xmlschema}</wsu:Expires>")
end
end
end
end

end