Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added ability to add signed timestamp to header #25

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -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.
Expand Down
66 changes: 57 additions & 9 deletions lib/akami/wsse/signature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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?
Expand All @@ -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
Expand All @@ -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?

Expand Down Expand Up @@ -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")
Expand All @@ -151,6 +193,12 @@ def body_digest
Base64.encode64(OpenSSL::Digest::SHA1.digest(body)).strip
end

def timestamp_digest
return nil unless timestamp

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should tis be return nil unless self.timestamp. It seems that timestamp refers to the variable in the scope of the method due tot he assignment on the next line.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if timestamp is nil, which it can be, an exception is raised. Should this line be ended with ... 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 @@ -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("<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