information_card 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|