Class: InformationCard::SamlToken

Inherits:
IdentityToken show all
Includes:
REXML
Defined in:
lib/information_card/saml_token.rb

Instance Attribute Summary

Attributes inherited from IdentityToken

#claims, #errors

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from IdentityToken

#valid?

Constructor Details

#initialize(saml_doc) ⇒ SamlToken

Returns a new instance of SamlToken.



28
29
30
31
# File 'lib/information_card/saml_token.rb', line 28

def initialize(saml_doc)
  super()
  @doc = saml_doc
end

Class Method Details

.create(saml_input) ⇒ Object



14
15
16
17
18
19
20
21
22
23
24
25
26
# File 'lib/information_card/saml_token.rb', line 14

def self.create(saml_input)
  saml_doc = REXML::Document.new(saml_input)
  saml_token = SamlToken.new(saml_doc) 
       
  saml_token.validate_document_conditions
  saml_token.validate_document_integrity
  return saml_token unless saml_token.valid?
  
  saml_token.process_claims
  saml_token.validate_claims
  
  saml_token
end

Instance Method Details

#get_public_key(mod, exponent) ⇒ Object



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# File 'lib/information_card/saml_token.rb', line 162

def get_public_key(mod, exponent)
  mod_binary = Base64.decode64(mod)     
  exponent_binary = Base64.decode64(exponent)
 
  exponent_encoding = make_asn_segment(0x02, exponent_binary)
  modulusEncoding = make_asn_segment(0x02, mod_binary)
  sequenceEncoding = make_asn_segment(0x30, modulusEncoding + exponent_encoding)
  bitstringEncoding = make_asn_segment(0x03, sequenceEncoding)
  hex_array = []
  "300D06092A864886F70D0101010500".gsub(/../) { |m| hex_array << m.to_i(16) }
  rsaAlgorithmIdentifier = hex_array.pack('C*')       
  combined = rsaAlgorithmIdentifier + bitstringEncoding
  publicKeyInfo = make_asn_segment(0x30, rsaAlgorithmIdentifier + bitstringEncoding)
  
  #encode the publicKeyInfo in base64 and add PEM brackets
  public_key_64 = Base64.encode64(publicKeyInfo)
  encoding = "-----BEGIN PUBLIC KEY-----\n"
  offset = 0;
  # strip out the newlines
  public_key_64.delete!("=\n") 
  while (segment = public_key_64[offset, 64])
     encoding = encoding + segment + "\n"
     offset += 64
  end
  encoding = encoding + "-----END PUBLIC KEY-----\n"
  @pub_key = OpenSSL::PKey::RSA.new(encoding)
  @pub_key
end

#make_asn_segment(type, string) ⇒ Object



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/information_card/saml_token.rb', line 191

def make_asn_segment(type, string)
  case (type)
    when 0x02
      string = 0.chr + string if string[0] > 0x7f
    when 0x03
      string = 0.chr + string
  end
  length = string.length
  
  if (length < 128)
     output = sprintf("%c%c%s", type, length, string)   
  elsif (length < 0x0100)
     output = sprintf("%c%c%c%s", type, 0x81, length, string)    
  elsif (length < 0x010000)
     output = sprintf("%c%c%c%c%s", type, 0x82, length/0x0100, length%0x0100, string)    
  else
      output = nil
  end    
  output
end

#ppidObject



44
45
46
# File 'lib/information_card/saml_token.rb', line 44

def ppid
  claims[:ppid]
end

#process_claimsObject



48
49
50
51
52
53
54
# File 'lib/information_card/saml_token.rb', line 48

def process_claims            
  attribute_nodes = XPath.match(@doc, "//saml:AttributeStatement/saml:Attribute", {"saml" => Namespaces::SAML_ASSERTION})
  attribute_nodes.each do |node|
    key = ClaimTypes.lookup(node.attributes['AttributeNamespace'], node.attributes['AttributeName'])      
    @claims[key] = XPath.first(node, "saml:AttributeValue", "saml" => Namespaces::SAML_ASSERTION).text        
  end
end

#unique_idObject



33
34
35
36
37
38
39
40
41
42
# File 'lib/information_card/saml_token.rb', line 33

def unique_id
  identity_claim_value = @claims[InformationCard::Config.identity_claim]
  return identity_claim_value unless InformationCard::Config.identity_claim == :ppid
  
  combined_key = ''
  combined_key << @mod
  combined_key << @exponent
  combined_key << identity_claim_value
  Digest::SHA1.hexdigest(combined_key)
end

#valid_audiences?(audiences) ⇒ Boolean

Returns:

  • (Boolean)


144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/information_card/saml_token.rb', line 144

def valid_audiences?(audiences)
  audience_scope = InformationCard::Config.audience_scope
  registered_audiences = InformationCard::Config.audiences
  
  return false if registered_audiences.nil? or registered_audiences.empty?
  
  if audience_scope == :page
    audiences.each{|audience| return true if registered_audiences.include?(audience.text)}
  elsif audience_scope == :site
    audiences.each do |audience|
      registered_audiences.each do |registered_audience|
        return true if audience.text.index(registered_audience) == 0
      end
    end
  end      
  false
end

#validate_audiencesObject



74
75
76
77
78
# File 'lib/information_card/saml_token.rb', line 74

def validate_audiences
  conditions = XPath.first(@doc, "//saml:Conditions", "saml" => Namespaces::SAML_ASSERTION)
  audiences = XPath.match(@doc, "//saml:AudienceRestrictionCondition/saml:Audience", {"saml" => Namespaces::SAML_ASSERTION})
  @errors[:audience] = "AudienceRestriction is not valid" unless valid_audiences?(audiences)      
end

#validate_claimsObject



56
57
58
59
60
61
62
# File 'lib/information_card/saml_token.rb', line 56

def validate_claims
  return if Config::required_claims.nil? or Config::required_claims.empty?
  
  claims_errors = []
  Config::required_claims.each { |claim| claims_errors << claim if not @claims.key?(claim) }
  @errors[:missing_claims] = claims_errors unless claims_errors.empty?
end

#validate_conditionsObject



80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/information_card/saml_token.rb', line 80

def validate_conditions    
  conditions = XPath.first(@doc, "//saml:Conditions", "saml" => Namespaces::SAML_ASSERTION)
  
  condition_errors = {}
  not_before_time = Time.parse(conditions.attributes['NotBefore'])
  condition_errors[:not_before] = "Time is before #{not_before_time}" if Time.now < not_before_time     
  
  not_on_or_after_time = Time.parse(conditions.attributes['NotOnOrAfter'])
  condition_errors[:not_on_or_after] = "Time is on or after #{not_on_or_after_time}" if Time.now >= not_on_or_after_time

  @errors[:conditions] = condition_errors unless condition_errors.empty?    
end

#validate_document_conditionsObject



64
65
66
67
# File 'lib/information_card/saml_token.rb', line 64

def validate_document_conditions
  validate_audiences
  validate_conditions
end

#validate_document_integrityObject



69
70
71
72
# File 'lib/information_card/saml_token.rb', line 69

def validate_document_integrity
  verify_digest
  verify_signature     
end

#verify_digestObject



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/information_card/saml_token.rb', line 93

def verify_digest     
  working_doc = REXML::Document.new(@doc.to_s)
  
  assertion_node = XPath.first(working_doc, "saml:Assertion", {"saml" => Namespaces::SAML_ASSERTION}) 
  signature_node =  XPath.first(assertion_node, "ds:Signature", {"ds" => Namespaces::DS}) 
  signed_info_node = XPath.first(signature_node, "ds:SignedInfo", {"ds" => Namespaces::DS})    
  digest_value_node = XPath.first(signed_info_node, "ds:Reference/ds:DigestValue", {"ds" => Namespaces::DS})
  
  digest_value = digest_value_node.text

  signature_node.remove
  digest_errors = []
  canonicalizer = InformationCard::XmlCanonicalizer.new
  
  reference_nodes = XPath.match(signed_info_node, "ds:Reference", {"ds" => Namespaces::DS})
  # TODO: Check specification to see if digest is required.
  @errors[:digest] = "No reference nodes to check digest" and return if reference_nodes.nil? or reference_nodes.empty?
  
  reference_nodes.each do |node|
    uri = node.attributes['URI']
    nodes_to_verify = XPath.match(working_doc, "saml:Assertion[@AssertionID='#{uri[1..uri.size]}']", {"saml" => Namespaces::SAML_ASSERTION})
  
    nodes_to_verify.each do |node|
      canonicalized_signed_info = canonicalizer.canonicalize(node)          
      signed_node_hash = Base64.encode64(Digest::SHA1.digest(canonicalized_signed_info)).chomp                    
      digest_errors << "Invalid Digest for #{uri}. Expected #{signed_node_hash} but was #{digest_value}" unless signed_node_hash == digest_value
    end
                   
    @errors[:digest] = digest_errors unless digest_errors.empty?
  end  
end

#verify_signatureObject



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/information_card/saml_token.rb', line 125

def verify_signature
  assertion_node = XPath.first(@doc, "saml:Assertion", {"saml" => Namespaces::SAML_ASSERTION}) 
  signature_node =  XPath.first(assertion_node, "ds:Signature", {"ds" => Namespaces::DS})    
  modulus_node = XPath.first(signature_node, "ds:KeyInfo/ds:KeyValue/ds:RSAKeyValue/ds:Modulus", {"ds" => Namespaces::DS})
  exponent_node = XPath.first(signature_node, "ds:KeyInfo/ds:KeyValue/ds:RSAKeyValue/ds:Exponent", {"ds" => Namespaces::DS})
  
  @mod = modulus_node.text
  @exponent = exponent_node.text
  public_key_string = get_public_key(@mod, @exponent)

  signed_info_node = XPath.first(signature_node, "ds:SignedInfo", {"ds" => Namespaces::DS})
  signature_value_node = XPath.first(signature_node, "ds:SignatureValue", {"ds" => Namespaces::DS})

  signature = Base64.decode64(signature_value_node.text)
  canonicalized_signed_info = InformationCard::XmlCanonicalizer.new.canonicalize(signed_info_node)
  
  @errors[:signature] = "Invalid Signature" unless public_key_string.verify(OpenSSL::Digest::SHA1.new, signature, canonicalized_signed_info)  
end