information_card 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +2 -0
- data/LICENSE +27 -0
- data/README +45 -0
- data/Rakefile +35 -0
- data/lib/information_card.rb +10 -0
- data/lib/information_card/certificate_util.rb +17 -0
- data/lib/information_card/claim_types.rb +43 -0
- data/lib/information_card/config.rb +52 -0
- data/lib/information_card/decrypter.rb +53 -0
- data/lib/information_card/identity_token.rb +23 -0
- data/lib/information_card/invalid_token.rb +8 -0
- data/lib/information_card/namespaces.rb +7 -0
- data/lib/information_card/processor.rb +15 -0
- data/lib/information_card/saml_token.rb +212 -0
- data/lib/information_card/xml_canonicalizer.rb +95 -0
- data/test/certificate_util_test.rb +21 -0
- data/test/claim_types_test.rb +39 -0
- data/test/decrypter_test.rb +12 -0
- data/test/fixtures/certificates/test.crt +14 -0
- data/test/fixtures/certificates/test.key +15 -0
- data/test/fixtures/encrypted_information_cards/jack_deer.xml +1 -0
- data/test/fixtures/encrypted_information_cards/john_smith.xml +1 -0
- data/test/fixtures/saml_tokens/jack_deer.xml +1 -0
- data/test/fixtures/saml_tokens/john_smith.xml +1 -0
- data/test/processor_test.rb +34 -0
- data/test/saml_token_test.rb +165 -0
- data/test/test_helper.rb +73 -0
- data/test/xml_canonicalizer_test.rb +188 -0
- metadata +78 -0
data/CHANGELOG
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
Copyright (c) 2007, ThoughtWorks
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
6
|
+
|
7
|
+
* Redistributions of source code must retain the above copyright
|
8
|
+
notice, this list of conditions and the following disclaimer.
|
9
|
+
|
10
|
+
* Redistributions in binary form must reproduce the above copyright
|
11
|
+
notice, this list of conditions and the following disclaimer in the
|
12
|
+
documentation and/or other materials provided with the distribution.
|
13
|
+
|
14
|
+
* Neither the name of ThoughtWorks nor the
|
15
|
+
names of its contributors may be used to endorse or promote products
|
16
|
+
derived from this software without specific prior written permission.
|
17
|
+
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
19
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
20
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
21
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
22
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
23
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
24
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
25
|
+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
26
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
27
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
=Information Card
|
2
|
+
|
3
|
+
A Ruby library for processing information cards.
|
4
|
+
|
5
|
+
==Features
|
6
|
+
* Easy to use API for decrypting, validating and processing SAML formatted information cards
|
7
|
+
* Basic XML canonicalization (not yet fully c14n compliant)
|
8
|
+
* Flexible configuration
|
9
|
+
* Comprehensive test suite
|
10
|
+
|
11
|
+
==Installing
|
12
|
+
gem intall information_card
|
13
|
+
|
14
|
+
The library is known to work with Ruby 1.8.4 and up on Win32, Max OSX and Ubuntu 6.06
|
15
|
+
Examples were tested with Rails 1.2.3
|
16
|
+
|
17
|
+
==Getting Started
|
18
|
+
For documentation, visit http://informationcardruby.com/documents
|
19
|
+
|
20
|
+
==Homepage
|
21
|
+
http://informationcardruby.com
|
22
|
+
|
23
|
+
See also:
|
24
|
+
http://rubyforge.org/projects/informationcard
|
25
|
+
http://www.codeplex.com/informationcardruby
|
26
|
+
|
27
|
+
==Community
|
28
|
+
Discussion regarding the Information Card library takes place on the RubyForge mailing lists
|
29
|
+
|
30
|
+
http://rubyforge.org/mailman/listinfo/informationcard-users
|
31
|
+
|
32
|
+
Please join us to discuss, ask questions and report bugs
|
33
|
+
|
34
|
+
==Authors
|
35
|
+
Joe Poon
|
36
|
+
Jason Sallis
|
37
|
+
|
38
|
+
==License
|
39
|
+
Copyright (c) 2007 ThoughtWorks, released under the BSD license
|
40
|
+
|
41
|
+
|
42
|
+
|
43
|
+
|
44
|
+
|
45
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
Gem::manage_gems
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
require 'rake/testtask'
|
5
|
+
require 'rake/clean'
|
6
|
+
|
7
|
+
CLEAN.include("pkg")
|
8
|
+
|
9
|
+
# Gemspec reference - http://rubygems.org/read/chapter/20
|
10
|
+
spec = Gem::Specification.new do |s|
|
11
|
+
s.name = "information_card"
|
12
|
+
s.version = "0.1.0"
|
13
|
+
s.author = "Joe Poon, Jason Sallis"
|
14
|
+
s.email = "informationcard-users@rubyforge.org"
|
15
|
+
s.homepage = "http://informationcardruby.com"
|
16
|
+
s.platform = Gem::Platform::RUBY
|
17
|
+
s.summary = "A library for processing information cards"
|
18
|
+
s.files = FileList["lib/**/*", "docs/**/*", "test/**/*", "Rakefile", "LICENSE", "CHANGELOG"].exclude("rdoc").to_a
|
19
|
+
s.require_path = "lib"
|
20
|
+
s.autorequire = "information_card"
|
21
|
+
s.has_rdoc = true
|
22
|
+
s.extra_rdoc_files = ["README"]
|
23
|
+
end
|
24
|
+
|
25
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
26
|
+
pkg.need_tar = true
|
27
|
+
end
|
28
|
+
|
29
|
+
Rake::TestTask.new do |t|
|
30
|
+
t.libs << "test"
|
31
|
+
t.test_files = FileList["test/*test.rb"]
|
32
|
+
t.verbose = true
|
33
|
+
end
|
34
|
+
|
35
|
+
task :default => [:test]
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'information_card/claim_types'
|
2
|
+
require 'information_card/config'
|
3
|
+
require 'information_card/decrypter'
|
4
|
+
require 'information_card/identity_token'
|
5
|
+
require 'information_card/saml_token'
|
6
|
+
require 'information_card/invalid_token'
|
7
|
+
require 'information_card/processor'
|
8
|
+
require 'information_card/xml_canonicalizer'
|
9
|
+
require 'information_card/certificate_util'
|
10
|
+
require 'information_card/namespaces'
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
module InformationCard
|
4
|
+
class CertificateUtil
|
5
|
+
|
6
|
+
def self.lookup_private_key(directory, subject)
|
7
|
+
path = File.join(directory, '*.crt')
|
8
|
+
Dir[path].each do |cert_file|
|
9
|
+
cert = OpenSSL::X509::Certificate.new(File.read(cert_file))
|
10
|
+
return OpenSSL::PKey::RSA.new(File.read(cert_file.gsub(/.crt$/, '') + ".key")) if (cert.subject.to_s == subject)
|
11
|
+
end
|
12
|
+
raise "No private key found in #{path.gsub(/\*.crt/, '')} with subject #{subject}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module InformationCard
|
2
|
+
class ClaimTypes
|
3
|
+
|
4
|
+
@@claims = {
|
5
|
+
:given_name => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
|
6
|
+
:email_address => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
|
7
|
+
:surname => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
|
8
|
+
:street_address => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/streetaddress",
|
9
|
+
:locality => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/locality",
|
10
|
+
:state_province => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/stateorprovince",
|
11
|
+
:postal_code => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/postalcode",
|
12
|
+
:country => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/country",
|
13
|
+
:home_phone => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/homephone",
|
14
|
+
:other_phone => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/otherphone",
|
15
|
+
:mobile_phone => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobilephone",
|
16
|
+
:date_of_birth => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/dateofbirth",
|
17
|
+
:gender => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/gender",
|
18
|
+
:ppid => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/privatepersonalidentifier",
|
19
|
+
:webpage => "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/webpage"
|
20
|
+
}
|
21
|
+
|
22
|
+
def self.map(specified_claims)
|
23
|
+
values = []
|
24
|
+
specified_claims.each do |claim_key|
|
25
|
+
raise "Undefined claim #{claim_key}" if not @@claims.include?(claim_key)
|
26
|
+
values << @@claims[claim_key]
|
27
|
+
end
|
28
|
+
values
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.lookup(namespace, attribute_name)
|
32
|
+
# Some identity selector implementations specify the attribute name as part of the namespace.
|
33
|
+
# As a result, we need to remove the duplicated attribute name from the namespace.
|
34
|
+
# ex. namespace => http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress
|
35
|
+
# attribute_name => emailaddress
|
36
|
+
desired_claim = "#{namespace}/#{attribute_name}".gsub(/#{attribute_name}\/#{attribute_name}/, attribute_name)
|
37
|
+
@@claims.each_pair do |key, value|
|
38
|
+
return key if value == desired_claim
|
39
|
+
end
|
40
|
+
raise "Undefined claim #{desired_claim}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module InformationCard
|
2
|
+
class Config
|
3
|
+
def self.certificate_location=(certificate_location)
|
4
|
+
@certificate_location = certificate_location
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.certificate_location
|
8
|
+
@certificate_location
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.certificate_subject=(certificate_subject)
|
12
|
+
@certificate_subject = certificate_subject
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.certificate_subject
|
16
|
+
@certificate_subject
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.audience_scope=(audience_scope)
|
20
|
+
@audience_scope = audience_scope
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.audience_scope
|
24
|
+
@audience_scope
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.audiences=(audiences)
|
28
|
+
@audiences = audiences
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.audiences
|
32
|
+
@audiences
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.required_claims=(required_claims)
|
36
|
+
@required_claims = required_claims
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.required_claims
|
40
|
+
@required_claims
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.identity_claim=(identity_claim)
|
44
|
+
@identity_claim = identity_claim
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.identity_claim
|
48
|
+
@identity_claim
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module InformationCard
|
2
|
+
class Decrypter
|
3
|
+
|
4
|
+
attr_reader :errors
|
5
|
+
|
6
|
+
def initialize(encrypted_information_card_xml, certificate_location, certificate_subject)
|
7
|
+
@xml_document = REXML::Document.new(encrypted_information_card_xml)
|
8
|
+
@certificate_location = certificate_location
|
9
|
+
@certificate_subject = certificate_subject
|
10
|
+
@errors = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def decrypt
|
14
|
+
private_key = CertificateUtil.lookup_private_key(@certificate_location, @certificate_subject)
|
15
|
+
encrypted_data = REXML::XPath.first(@xml_document, "enc:EncryptedData", {"enc" => Namespaces::XENC})
|
16
|
+
key_info = REXML::XPath.first(encrypted_data, "x:KeyInfo", {"x" => Namespaces::DS})
|
17
|
+
encrypted_key = REXML::XPath.first(key_info, "e:EncryptedKey", {"e" => Namespaces::XENC})
|
18
|
+
key_cipher = REXML::XPath.first(encrypted_key, "e:CipherData/e:CipherValue", {"e" => Namespaces::XENC})
|
19
|
+
key = decrypt_key(key_cipher.text, private_key)
|
20
|
+
|
21
|
+
cipher_data = REXML::XPath.first(@xml_document, "enc:EncryptedData/enc:CipherData/enc:CipherValue", {"enc" => Namespaces::XENC})
|
22
|
+
decrypt_cipher_data(key, cipher_data.text)
|
23
|
+
end
|
24
|
+
|
25
|
+
def valid?
|
26
|
+
# TODO: Should perform more validation and handle errors more gracefully.
|
27
|
+
# ex. What if algorithm is not supported?
|
28
|
+
errors.empty?
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def decrypt_key(key_wrap_cipher, private_key, ssl_padding=OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)
|
34
|
+
# TODO: Encrypted method is assumed t obe rsa-oaep-mgf1p
|
35
|
+
from_key = OpenSSL::PKey::RSA.new(private_key)
|
36
|
+
key_wrap_str = Base64.decode64(key_wrap_cipher)
|
37
|
+
from_key.private_decrypt(key_wrap_str, ssl_padding)
|
38
|
+
end
|
39
|
+
|
40
|
+
def decrypt_cipher_data(key_cipher, cipher_data)
|
41
|
+
cipher_data_str = Base64.decode64(cipher_data)
|
42
|
+
mcrypt_iv = cipher_data_str[0..15]
|
43
|
+
cipher_data_str = cipher_data_str[16..-1]
|
44
|
+
# TODO: Encryption method algorithm is assumed to be aes256-cbc.
|
45
|
+
cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
|
46
|
+
cipher.decrypt
|
47
|
+
cipher.key = key_cipher
|
48
|
+
cipher.iv = mcrypt_iv
|
49
|
+
result = cipher.update(cipher_data_str)
|
50
|
+
result << cipher.final
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module InformationCard
|
2
|
+
class IdentityToken
|
3
|
+
attr_reader :errors, :claims
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@errors = {}
|
7
|
+
@claims = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def valid?
|
11
|
+
@errors.empty?
|
12
|
+
end
|
13
|
+
|
14
|
+
def unique_id
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def ppid
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module InformationCard
|
2
|
+
class Processor
|
3
|
+
def self.process(encrypted_information_card_xml)
|
4
|
+
begin
|
5
|
+
decrypter = Decrypter.new(encrypted_information_card_xml,
|
6
|
+
InformationCard::Config.certificate_location,
|
7
|
+
InformationCard::Config.certificate_subject)
|
8
|
+
decrypted_information_card = decrypter.decrypt
|
9
|
+
rescue => e
|
10
|
+
return InvalidToken.new({:decryption => e.message})
|
11
|
+
end
|
12
|
+
SamlToken.create(decrypted_information_card)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
# Portions of this class were inspired by the XmlSecurity::SignedDocument class written by Todd Saxton
|
2
|
+
# and the 'Self-Issued InfoCard Tutorial and Demo' written by Kim Cameron
|
3
|
+
|
4
|
+
require 'rexml/document'
|
5
|
+
require 'digest/sha1'
|
6
|
+
require 'base64'
|
7
|
+
|
8
|
+
module InformationCard
|
9
|
+
class SamlToken < IdentityToken
|
10
|
+
include REXML
|
11
|
+
|
12
|
+
private :initialize
|
13
|
+
|
14
|
+
def self.create(saml_input)
|
15
|
+
saml_doc = REXML::Document.new(saml_input)
|
16
|
+
saml_token = SamlToken.new(saml_doc)
|
17
|
+
|
18
|
+
saml_token.validate_document_conditions
|
19
|
+
saml_token.validate_document_integrity
|
20
|
+
return saml_token unless saml_token.valid?
|
21
|
+
|
22
|
+
saml_token.process_claims
|
23
|
+
saml_token.validate_claims
|
24
|
+
|
25
|
+
saml_token
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(saml_doc)
|
29
|
+
super()
|
30
|
+
@doc = saml_doc
|
31
|
+
end
|
32
|
+
|
33
|
+
def unique_id
|
34
|
+
identity_claim_value = @claims[InformationCard::Config.identity_claim]
|
35
|
+
return identity_claim_value unless InformationCard::Config.identity_claim == :ppid
|
36
|
+
|
37
|
+
combined_key = ''
|
38
|
+
combined_key << @mod
|
39
|
+
combined_key << @exponent
|
40
|
+
combined_key << identity_claim_value
|
41
|
+
Digest::SHA1.hexdigest(combined_key)
|
42
|
+
end
|
43
|
+
|
44
|
+
def ppid
|
45
|
+
claims[:ppid]
|
46
|
+
end
|
47
|
+
|
48
|
+
def process_claims
|
49
|
+
attribute_nodes = XPath.match(@doc, "//saml:AttributeStatement/saml:Attribute", {"saml" => Namespaces::SAML_ASSERTION})
|
50
|
+
attribute_nodes.each do |node|
|
51
|
+
key = ClaimTypes.lookup(node.attributes['AttributeNamespace'], node.attributes['AttributeName'])
|
52
|
+
@claims[key] = XPath.first(node, "saml:AttributeValue", "saml" => Namespaces::SAML_ASSERTION).text
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def validate_claims
|
57
|
+
return if Config::required_claims.nil? or Config::required_claims.empty?
|
58
|
+
|
59
|
+
claims_errors = []
|
60
|
+
Config::required_claims.each { |claim| claims_errors << claim if not @claims.key?(claim) }
|
61
|
+
@errors[:missing_claims] = claims_errors unless claims_errors.empty?
|
62
|
+
end
|
63
|
+
|
64
|
+
def validate_document_conditions
|
65
|
+
validate_audiences
|
66
|
+
validate_conditions
|
67
|
+
end
|
68
|
+
|
69
|
+
def validate_document_integrity
|
70
|
+
verify_digest
|
71
|
+
verify_signature
|
72
|
+
end
|
73
|
+
|
74
|
+
def validate_audiences
|
75
|
+
conditions = XPath.first(@doc, "//saml:Conditions", "saml" => Namespaces::SAML_ASSERTION)
|
76
|
+
audiences = XPath.match(@doc, "//saml:AudienceRestrictionCondition/saml:Audience", {"saml" => Namespaces::SAML_ASSERTION})
|
77
|
+
@errors[:audience] = "AudienceRestriction is not valid" unless valid_audiences?(audiences)
|
78
|
+
end
|
79
|
+
|
80
|
+
def validate_conditions
|
81
|
+
conditions = XPath.first(@doc, "//saml:Conditions", "saml" => Namespaces::SAML_ASSERTION)
|
82
|
+
|
83
|
+
condition_errors = {}
|
84
|
+
not_before_time = Time.parse(conditions.attributes['NotBefore'])
|
85
|
+
condition_errors[:not_before] = "Time is before #{not_before_time}" if Time.now < not_before_time
|
86
|
+
|
87
|
+
not_on_or_after_time = Time.parse(conditions.attributes['NotOnOrAfter'])
|
88
|
+
condition_errors[:not_on_or_after] = "Time is on or after #{not_on_or_after_time}" if Time.now >= not_on_or_after_time
|
89
|
+
|
90
|
+
@errors[:conditions] = condition_errors unless condition_errors.empty?
|
91
|
+
end
|
92
|
+
|
93
|
+
def verify_digest
|
94
|
+
working_doc = REXML::Document.new(@doc.to_s)
|
95
|
+
|
96
|
+
assertion_node = XPath.first(working_doc, "saml:Assertion", {"saml" => Namespaces::SAML_ASSERTION})
|
97
|
+
signature_node = XPath.first(assertion_node, "ds:Signature", {"ds" => Namespaces::DS})
|
98
|
+
signed_info_node = XPath.first(signature_node, "ds:SignedInfo", {"ds" => Namespaces::DS})
|
99
|
+
digest_value_node = XPath.first(signed_info_node, "ds:Reference/ds:DigestValue", {"ds" => Namespaces::DS})
|
100
|
+
|
101
|
+
digest_value = digest_value_node.text
|
102
|
+
|
103
|
+
signature_node.remove
|
104
|
+
digest_errors = []
|
105
|
+
canonicalizer = InformationCard::XmlCanonicalizer.new
|
106
|
+
|
107
|
+
reference_nodes = XPath.match(signed_info_node, "ds:Reference", {"ds" => Namespaces::DS})
|
108
|
+
# TODO: Check specification to see if digest is required.
|
109
|
+
@errors[:digest] = "No reference nodes to check digest" and return if reference_nodes.nil? or reference_nodes.empty?
|
110
|
+
|
111
|
+
reference_nodes.each do |node|
|
112
|
+
uri = node.attributes['URI']
|
113
|
+
nodes_to_verify = XPath.match(working_doc, "saml:Assertion[@AssertionID='#{uri[1..uri.size]}']", {"saml" => Namespaces::SAML_ASSERTION})
|
114
|
+
|
115
|
+
nodes_to_verify.each do |node|
|
116
|
+
canonicalized_signed_info = canonicalizer.canonicalize(node)
|
117
|
+
signed_node_hash = Base64.encode64(Digest::SHA1.digest(canonicalized_signed_info)).chomp
|
118
|
+
digest_errors << "Invalid Digest for #{uri}. Expected #{signed_node_hash} but was #{digest_value}" unless signed_node_hash == digest_value
|
119
|
+
end
|
120
|
+
|
121
|
+
@errors[:digest] = digest_errors unless digest_errors.empty?
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def verify_signature
|
126
|
+
assertion_node = XPath.first(@doc, "saml:Assertion", {"saml" => Namespaces::SAML_ASSERTION})
|
127
|
+
signature_node = XPath.first(assertion_node, "ds:Signature", {"ds" => Namespaces::DS})
|
128
|
+
modulus_node = XPath.first(signature_node, "ds:KeyInfo/ds:KeyValue/ds:RSAKeyValue/ds:Modulus", {"ds" => Namespaces::DS})
|
129
|
+
exponent_node = XPath.first(signature_node, "ds:KeyInfo/ds:KeyValue/ds:RSAKeyValue/ds:Exponent", {"ds" => Namespaces::DS})
|
130
|
+
|
131
|
+
@mod = modulus_node.text
|
132
|
+
@exponent = exponent_node.text
|
133
|
+
public_key_string = get_public_key(@mod, @exponent)
|
134
|
+
|
135
|
+
signed_info_node = XPath.first(signature_node, "ds:SignedInfo", {"ds" => Namespaces::DS})
|
136
|
+
signature_value_node = XPath.first(signature_node, "ds:SignatureValue", {"ds" => Namespaces::DS})
|
137
|
+
|
138
|
+
signature = Base64.decode64(signature_value_node.text)
|
139
|
+
canonicalized_signed_info = InformationCard::XmlCanonicalizer.new.canonicalize(signed_info_node)
|
140
|
+
|
141
|
+
@errors[:signature] = "Invalid Signature" unless public_key_string.verify(OpenSSL::Digest::SHA1.new, signature, canonicalized_signed_info)
|
142
|
+
end
|
143
|
+
|
144
|
+
def valid_audiences?(audiences)
|
145
|
+
audience_scope = InformationCard::Config.audience_scope
|
146
|
+
registered_audiences = InformationCard::Config.audiences
|
147
|
+
|
148
|
+
return false if registered_audiences.nil? or registered_audiences.empty?
|
149
|
+
|
150
|
+
if audience_scope == :page
|
151
|
+
audiences.each{|audience| return true if registered_audiences.include?(audience.text)}
|
152
|
+
elsif audience_scope == :site
|
153
|
+
audiences.each do |audience|
|
154
|
+
registered_audiences.each do |registered_audience|
|
155
|
+
return true if audience.text.index(registered_audience) == 0
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
false
|
160
|
+
end
|
161
|
+
|
162
|
+
def get_public_key(mod, exponent)
|
163
|
+
mod_binary = Base64.decode64(mod)
|
164
|
+
exponent_binary = Base64.decode64(exponent)
|
165
|
+
|
166
|
+
exponent_encoding = make_asn_segment(0x02, exponent_binary)
|
167
|
+
modulusEncoding = make_asn_segment(0x02, mod_binary)
|
168
|
+
sequenceEncoding = make_asn_segment(0x30, modulusEncoding + exponent_encoding)
|
169
|
+
bitstringEncoding = make_asn_segment(0x03, sequenceEncoding)
|
170
|
+
hex_array = []
|
171
|
+
"300D06092A864886F70D0101010500".gsub(/../) { |m| hex_array << m.to_i(16) }
|
172
|
+
rsaAlgorithmIdentifier = hex_array.pack('C*')
|
173
|
+
combined = rsaAlgorithmIdentifier + bitstringEncoding
|
174
|
+
publicKeyInfo = make_asn_segment(0x30, rsaAlgorithmIdentifier + bitstringEncoding)
|
175
|
+
|
176
|
+
#encode the publicKeyInfo in base64 and add PEM brackets
|
177
|
+
public_key_64 = Base64.encode64(publicKeyInfo)
|
178
|
+
encoding = "-----BEGIN PUBLIC KEY-----\n"
|
179
|
+
offset = 0;
|
180
|
+
# strip out the newlines
|
181
|
+
public_key_64.delete!("=\n")
|
182
|
+
while (segment = public_key_64[offset, 64])
|
183
|
+
encoding = encoding + segment + "\n"
|
184
|
+
offset += 64
|
185
|
+
end
|
186
|
+
encoding = encoding + "-----END PUBLIC KEY-----\n"
|
187
|
+
@pub_key = OpenSSL::PKey::RSA.new(encoding)
|
188
|
+
@pub_key
|
189
|
+
end
|
190
|
+
|
191
|
+
def make_asn_segment(type, string)
|
192
|
+
case (type)
|
193
|
+
when 0x02
|
194
|
+
string = 0.chr + string if string[0] > 0x7f
|
195
|
+
when 0x03
|
196
|
+
string = 0.chr + string
|
197
|
+
end
|
198
|
+
length = string.length
|
199
|
+
|
200
|
+
if (length < 128)
|
201
|
+
output = sprintf("%c%c%s", type, length, string)
|
202
|
+
elsif (length < 0x0100)
|
203
|
+
output = sprintf("%c%c%c%s", type, 0x81, length, string)
|
204
|
+
elsif (length < 0x010000)
|
205
|
+
output = sprintf("%c%c%c%c%s", type, 0x82, length/0x0100, length%0x0100, string)
|
206
|
+
else
|
207
|
+
output = nil
|
208
|
+
end
|
209
|
+
output
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|