maestrano 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +34 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +43 -0
- data/LICENSE +21 -0
- data/README.md +4 -0
- data/Rakefile +32 -0
- data/bin/maestrano-console +9 -0
- data/lib/maestrano.rb +114 -0
- data/lib/maestrano/account/bill.rb +14 -0
- data/lib/maestrano/api/error/authentication_error.rb +8 -0
- data/lib/maestrano/api/error/base_error.rb +24 -0
- data/lib/maestrano/api/error/connection_error.rb +8 -0
- data/lib/maestrano/api/error/invalid_request_error.rb +14 -0
- data/lib/maestrano/api/list_object.rb +37 -0
- data/lib/maestrano/api/object.rb +187 -0
- data/lib/maestrano/api/operation/base.rb +216 -0
- data/lib/maestrano/api/operation/create.rb +18 -0
- data/lib/maestrano/api/operation/delete.rb +13 -0
- data/lib/maestrano/api/operation/list.rb +18 -0
- data/lib/maestrano/api/operation/update.rb +59 -0
- data/lib/maestrano/api/resource.rb +39 -0
- data/lib/maestrano/api/util.rb +121 -0
- data/lib/maestrano/saml/attribute_value.rb +15 -0
- data/lib/maestrano/saml/metadata.rb +64 -0
- data/lib/maestrano/saml/request.rb +93 -0
- data/lib/maestrano/saml/response.rb +201 -0
- data/lib/maestrano/saml/schemas/saml20assertion_schema.xsd +283 -0
- data/lib/maestrano/saml/schemas/saml20protocol_schema.xsd +302 -0
- data/lib/maestrano/saml/schemas/xenc_schema.xsd +146 -0
- data/lib/maestrano/saml/schemas/xmldsig_schema.xsd +318 -0
- data/lib/maestrano/saml/settings.rb +37 -0
- data/lib/maestrano/saml/validation_error.rb +7 -0
- data/lib/maestrano/sso.rb +81 -0
- data/lib/maestrano/sso/base_group.rb +31 -0
- data/lib/maestrano/sso/base_user.rb +75 -0
- data/lib/maestrano/sso/group.rb +24 -0
- data/lib/maestrano/sso/session.rb +63 -0
- data/lib/maestrano/sso/user.rb +34 -0
- data/lib/maestrano/version.rb +3 -0
- data/lib/maestrano/xml_security/signed_document.rb +170 -0
- data/maestrano.gemspec +32 -0
- data/test/helpers/api_helpers.rb +82 -0
- data/test/helpers/saml_helpers.rb +62 -0
- data/test/maestrano/account/bill_test.rb +48 -0
- data/test/maestrano/api/list_object_test.rb +20 -0
- data/test/maestrano/api/object_test.rb +28 -0
- data/test/maestrano/api/resource_test.rb +343 -0
- data/test/maestrano/api/util_test.rb +31 -0
- data/test/maestrano/maestrano_test.rb +49 -0
- data/test/maestrano/saml/request_test.rb +168 -0
- data/test/maestrano/saml/response_test.rb +290 -0
- data/test/maestrano/saml/settings_test.rb +51 -0
- data/test/maestrano/sso/base_group_test.rb +54 -0
- data/test/maestrano/sso/base_user_test.rb +114 -0
- data/test/maestrano/sso/group_test.rb +47 -0
- data/test/maestrano/sso/session_test.rb +108 -0
- data/test/maestrano/sso/user_test.rb +65 -0
- data/test/maestrano/sso_test.rb +81 -0
- data/test/maestrano/xml_security/signed_document.rb +163 -0
- data/test/support/saml/certificates/certificate1 +12 -0
- data/test/support/saml/certificates/r1_certificate2_base64 +1 -0
- data/test/support/saml/responses/adfs_response_sha1.xml +46 -0
- data/test/support/saml/responses/adfs_response_sha256.xml +46 -0
- data/test/support/saml/responses/adfs_response_sha384.xml +46 -0
- data/test/support/saml/responses/adfs_response_sha512.xml +46 -0
- data/test/support/saml/responses/no_signature_ns.xml +48 -0
- data/test/support/saml/responses/open_saml_response.xml +56 -0
- data/test/support/saml/responses/r1_response6.xml.base64 +1 -0
- data/test/support/saml/responses/response1.xml.base64 +1 -0
- data/test/support/saml/responses/response2.xml.base64 +79 -0
- data/test/support/saml/responses/response3.xml.base64 +66 -0
- data/test/support/saml/responses/response4.xml.base64 +93 -0
- data/test/support/saml/responses/response5.xml.base64 +102 -0
- data/test/support/saml/responses/response_with_ampersands.xml +139 -0
- data/test/support/saml/responses/response_with_ampersands.xml.base64 +93 -0
- data/test/support/saml/responses/response_with_multiple_attribute_values.xml +57 -0
- data/test/support/saml/responses/simple_saml_php.xml +71 -0
- data/test/support/saml/responses/starfield_response.xml.base64 +1 -0
- data/test/support/saml/responses/wrapped_response_2.xml.base64 +150 -0
- data/test/test_helper.rb +46 -0
- metadata +305 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
module Maestrano
|
2
|
+
module Saml
|
3
|
+
class Settings
|
4
|
+
NAMEID_EMAIL_ADDRESS = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'
|
5
|
+
NAMEID_X509_SUBJECT_NAME = 'urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName'
|
6
|
+
NAMEID_WINDOWS_DOMAIN_QUALIFIED_NAME = 'urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName'
|
7
|
+
NAMEID_KERBEROS = 'urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos'
|
8
|
+
NAMEID_ENTITY = 'urn:oasis:names:tc:SAML:2.0:nameid-format:entity'
|
9
|
+
NAMEID_TRANSIENT = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
|
10
|
+
NAMEID_PERSISTENT = 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
|
11
|
+
PROTOCOL_BINDING_POST = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
|
12
|
+
|
13
|
+
def initialize(overrides = {})
|
14
|
+
config = DEFAULTS.merge(overrides)
|
15
|
+
config.each do |k,v|
|
16
|
+
acc = "#{k.to_s}=".to_sym
|
17
|
+
self.send(acc, v) if self.respond_to? acc
|
18
|
+
end
|
19
|
+
end
|
20
|
+
attr_accessor :assertion_consumer_service_url, :issuer, :sp_name_qualifier
|
21
|
+
attr_accessor :idp_sso_target_url, :idp_cert_fingerprint, :idp_cert, :name_identifier_format
|
22
|
+
attr_accessor :authn_context
|
23
|
+
attr_accessor :idp_slo_target_url
|
24
|
+
attr_accessor :name_identifier_value
|
25
|
+
attr_accessor :sessionindex
|
26
|
+
attr_accessor :assertion_consumer_logout_service_url
|
27
|
+
attr_accessor :compress_request
|
28
|
+
attr_accessor :double_quote_xml_attribute_values
|
29
|
+
attr_accessor :passive
|
30
|
+
attr_accessor :protocol_binding
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
DEFAULTS = {:compress_request => true, :double_quote_xml_attribute_values => false}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Maestrano
|
2
|
+
module SSO
|
3
|
+
# Return the saml_settings based on
|
4
|
+
# Maestrano configuration
|
5
|
+
def self.saml_settings
|
6
|
+
settings = Maestrano::Saml::Settings.new
|
7
|
+
settings.assertion_consumer_service_url = self.consume_url
|
8
|
+
settings.issuer = Maestrano.param('app_host')
|
9
|
+
settings.idp_sso_target_url = self.idp_url
|
10
|
+
settings.idp_cert_fingerprint = Maestrano.param('sso_x509_fingerprint')
|
11
|
+
settings.name_identifier_format = Maestrano.param('sso_name_id_format')
|
12
|
+
settings
|
13
|
+
end
|
14
|
+
|
15
|
+
# Build a new SAML Request
|
16
|
+
def self.build_request(get_params = {})
|
17
|
+
Maestrano::Saml::Request.new(get_params)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Build a new SAML response
|
21
|
+
def self.build_response(saml_post_param)
|
22
|
+
Maestrano::Saml::Response.new(saml_post_param)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.enabled?
|
26
|
+
!!Maestrano.param('sso_enabled')
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.init_url
|
30
|
+
host = Maestrano.param('app_host')
|
31
|
+
path = Maestrano.param('sso_app_init_path')
|
32
|
+
return "#{host}#{path}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.consume_url
|
36
|
+
host = Maestrano.param('app_host')
|
37
|
+
path = Maestrano.param('sso_app_consume_path')
|
38
|
+
return "#{host}#{path}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.logout_url
|
42
|
+
host = Maestrano.param('api_host')
|
43
|
+
path = '/app_logout'
|
44
|
+
return "#{host}#{path}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.unauthorized_url
|
48
|
+
host = Maestrano.param('api_host')
|
49
|
+
path = '/app_access_unauthorized'
|
50
|
+
return "#{host}#{path}";
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.idp_url
|
54
|
+
host = Maestrano.param('api_host')
|
55
|
+
api_base = Maestrano.param('api_base')
|
56
|
+
endpoint = 'auth/saml'
|
57
|
+
return "#{host}#{api_base}#{endpoint}"
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.session_check_url(user_uid,sso_session)
|
61
|
+
host = Maestrano.param('api_host')
|
62
|
+
api_base = Maestrano.param('api_base')
|
63
|
+
endpoint = 'auth/saml'
|
64
|
+
return URI.escape("#{host}#{api_base}#{endpoint}/#{user_uid}?session=#{sso_session}")
|
65
|
+
end
|
66
|
+
|
67
|
+
# Set maestrano attributes in session
|
68
|
+
# Takes the BaseUser hash representation and current session
|
69
|
+
# in arguments
|
70
|
+
def self.set_session(session, auth)
|
71
|
+
if auth && (extra = (auth[:extra] || auth['extra'])) && (sso_session = (extra[:session] || extra['session']))
|
72
|
+
session[:mno_uid] = (sso_session[:uid] || sso_session['uid'])
|
73
|
+
session[:mno_session] = (sso_session[:token] || sso_session['token'])
|
74
|
+
if recheck = (sso_session[:recheck] || sso_session['recheck'])
|
75
|
+
session[:mno_session_recheck] = recheck.utc.iso8601
|
76
|
+
end
|
77
|
+
session[:mno_group_uid] = (sso_session[:group_uid] || sso_session['group_uid'])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Maestrano
|
2
|
+
module SSO
|
3
|
+
class BaseGroup
|
4
|
+
attr_accessor :local_id
|
5
|
+
attr_reader :uid,:country, :company_name, :free_trial_end_at
|
6
|
+
|
7
|
+
# Initializer
|
8
|
+
# @param Maestrano::SAML::Response
|
9
|
+
def initialize(saml_response)
|
10
|
+
att = saml_response.attributes
|
11
|
+
@uid = att['group_uid']
|
12
|
+
@country = att['country']
|
13
|
+
@free_trial_end_at = Time.iso8601(att['group_end_free_trial'])
|
14
|
+
@company_name = att['company_name']
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_hash
|
18
|
+
{
|
19
|
+
provider: 'maestrano',
|
20
|
+
uid: self.uid,
|
21
|
+
info: {
|
22
|
+
free_trial_end_at: self.free_trial_end_at,
|
23
|
+
company_name: self.company_name,
|
24
|
+
country: self.country,
|
25
|
+
},
|
26
|
+
extra: {}
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Maestrano
|
2
|
+
module SSO
|
3
|
+
class BaseUser
|
4
|
+
attr_accessor :local_id
|
5
|
+
attr_reader :sso_session,:sso_session_recheck,
|
6
|
+
:group_uid,:group_role,:uid,:virtual_uid,:email,
|
7
|
+
:virtual_email,:first_name, :last_name,:country, :company_name
|
8
|
+
|
9
|
+
# Initializer
|
10
|
+
# @param Maestrano::SAML::Response
|
11
|
+
def initialize(saml_response)
|
12
|
+
att = saml_response.attributes
|
13
|
+
@sso_session = att['mno_session']
|
14
|
+
@sso_session_recheck = Time.iso8601(att['mno_session_recheck'])
|
15
|
+
@group_uid = att['group_uid']
|
16
|
+
@group_role = att['group_role']
|
17
|
+
@uid = att['uid']
|
18
|
+
@virtual_uid = att['virtual_uid']
|
19
|
+
@email = att['email']
|
20
|
+
@virtual_email = att['virtual_email']
|
21
|
+
@first_name = att['name']
|
22
|
+
@last_name = att['surname']
|
23
|
+
@country = att['country']
|
24
|
+
@company_name = att['company_name']
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_uid
|
28
|
+
if Maestrano.param('user_creation_mode') == 'real'
|
29
|
+
return self.uid
|
30
|
+
else
|
31
|
+
return self.virtual_uid
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_email
|
36
|
+
if Maestrano.param('user_creation_mode') == 'real'
|
37
|
+
return self.email
|
38
|
+
else
|
39
|
+
return self.virtual_email
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Hash representation of the resource
|
44
|
+
def to_hash
|
45
|
+
{
|
46
|
+
provider: 'maestrano',
|
47
|
+
uid: self.to_uid,
|
48
|
+
info: {
|
49
|
+
email: self.to_email,
|
50
|
+
first_name: self.first_name,
|
51
|
+
last_name: self.last_name,
|
52
|
+
country: self.country,
|
53
|
+
company_name: self.company_name,
|
54
|
+
},
|
55
|
+
extra: {
|
56
|
+
uid: self.uid,
|
57
|
+
virtual_uid: self.virtual_uid,
|
58
|
+
real_email: self.email,
|
59
|
+
virtual_email: self.virtual_email,
|
60
|
+
group: {
|
61
|
+
uid: self.group_uid,
|
62
|
+
role: self.group_role,
|
63
|
+
},
|
64
|
+
session: {
|
65
|
+
uid: self.uid,
|
66
|
+
token: self.sso_session,
|
67
|
+
recheck: self.sso_session_recheck,
|
68
|
+
group_uid: self.group_uid
|
69
|
+
},
|
70
|
+
}
|
71
|
+
}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Maestrano
|
2
|
+
module SSO
|
3
|
+
module Group
|
4
|
+
def find_for_maestrano_auth(auth)
|
5
|
+
# E.g with Rails
|
6
|
+
# where(auth.slice(:provider, :uid)).first_or_create do |group|
|
7
|
+
# group.provider = auth[:provider]
|
8
|
+
# group.uid = auth[:uid]
|
9
|
+
# group.name = (auth[:info][:company_name] || 'Your Group')
|
10
|
+
# group.country = auth[:info][:country]
|
11
|
+
# end
|
12
|
+
raise NoMethodError, "You need to override find_for_maestrano_auth in your #{self.class.name} model"
|
13
|
+
end
|
14
|
+
|
15
|
+
def maestrano?
|
16
|
+
if self.respond_to?(:provider)
|
17
|
+
return self.provider.to_s == 'maestrano'
|
18
|
+
else
|
19
|
+
raise NoMethodError, "You need to override maestrano? in your #{self.class.name} model"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Maestrano
|
2
|
+
module SSO
|
3
|
+
class Session
|
4
|
+
attr_accessor :session, :uid, :session_token, :recheck
|
5
|
+
|
6
|
+
def initialize(session)
|
7
|
+
self.session = session
|
8
|
+
self.uid = (self.session['mno_uid'] || self.session[:mno_uid])
|
9
|
+
self.session_token = (self.session['mno_session'] || self.session[:mno_session])
|
10
|
+
if recheck = (self.session['mno_session_recheck'] || self.session[:mno_session_recheck])
|
11
|
+
self.recheck = Time.iso8601(recheck)
|
12
|
+
end
|
13
|
+
|
14
|
+
if self.uid.nil? || self.session_token.nil? || self.recheck.nil?
|
15
|
+
$stderr.puts "WARNING: Maestrano session information missing. User will have to relogin"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def remote_check_required?
|
20
|
+
if self.uid && self.session_token && self.recheck
|
21
|
+
return (self.recheck <= Time.now)
|
22
|
+
end
|
23
|
+
return true
|
24
|
+
end
|
25
|
+
|
26
|
+
# Check remote maestrano session and update the
|
27
|
+
# recheck attribute if the session is still valid
|
28
|
+
# Return true if the session is still valid and
|
29
|
+
# false otherwise
|
30
|
+
def perform_remote_check
|
31
|
+
# Get remote session info
|
32
|
+
url = Maestrano::SSO.session_check_url(self.uid, self.session_token)
|
33
|
+
begin
|
34
|
+
response = RestClient.get(url)
|
35
|
+
response = JSON.parse(response)
|
36
|
+
rescue Exception => e
|
37
|
+
response = {}
|
38
|
+
end
|
39
|
+
|
40
|
+
# Process response
|
41
|
+
if response['valid'] && response['recheck']
|
42
|
+
self.recheck = Time.iso8601(response['recheck'])
|
43
|
+
return true
|
44
|
+
end
|
45
|
+
|
46
|
+
return false
|
47
|
+
end
|
48
|
+
|
49
|
+
def valid?
|
50
|
+
if self.remote_check_required?
|
51
|
+
if perform_remote_check
|
52
|
+
self.session[:mno_session_recheck] = self.recheck.utc.iso8601
|
53
|
+
return true
|
54
|
+
else
|
55
|
+
return false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
return true
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Maestrano
|
2
|
+
module SSO
|
3
|
+
module User
|
4
|
+
def find_for_maestrano_auth(auth)
|
5
|
+
# E.g with Rails
|
6
|
+
# where(auth.slice(:provider, :uid)).first_or_create do |user|
|
7
|
+
# user.provider = auth[:provider]
|
8
|
+
# user.uid = auth[:uid]
|
9
|
+
# user.email = auth[:info][:email]
|
10
|
+
# user.name = auth[:info][:first_name]
|
11
|
+
# user.surname = auth[:info][:last_name]
|
12
|
+
# user.country = auth[:info][:country]
|
13
|
+
# user.company = auth[:info][:company_name]
|
14
|
+
# end
|
15
|
+
raise NoMethodError, "You need to override find_for_maestrano_auth in your #{self.class.name} model"
|
16
|
+
end
|
17
|
+
|
18
|
+
# Check whether the user is a maestrano one
|
19
|
+
def maestrano?
|
20
|
+
if self.respond_to?(:provider)
|
21
|
+
return self.provider.to_s == 'maestrano'
|
22
|
+
else
|
23
|
+
raise NoMethodError, "You need to override maestrano? in your #{self.class.name} model"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Check whether the SSO session is still valid
|
28
|
+
# or not
|
29
|
+
def maestrano_session_valid?(session)
|
30
|
+
Maestrano::SSO::Session.new(session).valid?
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# The contents of this file are subject to the terms
|
2
|
+
# of the Common Development and Distribution License
|
3
|
+
# (the License). You may not use this file except in
|
4
|
+
# compliance with the License.
|
5
|
+
#
|
6
|
+
# You can obtain a copy of the License at
|
7
|
+
# https://opensso.dev.java.net/public/CDDLv1.0.html or
|
8
|
+
# opensso/legal/CDDLv1.0.txt
|
9
|
+
# See the License for the specific language governing
|
10
|
+
# permission and limitations under the License.
|
11
|
+
#
|
12
|
+
# When distributing Covered Code, include this CDDL
|
13
|
+
# Header Notice in each file and include the License file
|
14
|
+
# at opensso/legal/CDDLv1.0.txt.
|
15
|
+
# If applicable, add the following below the CDDL Header,
|
16
|
+
# with the fields enclosed by brackets [] replaced by
|
17
|
+
# your own identifying information:
|
18
|
+
# "Portions Copyrighted [year] [name of copyright owner]"
|
19
|
+
#
|
20
|
+
# $Id: xml_sec.rb,v 1.6 2007/10/24 00:28:41 todddd Exp $
|
21
|
+
#
|
22
|
+
# Copyright 2007 Sun Microsystems Inc. All Rights Reserved
|
23
|
+
# Portions Copyrighted 2007 Todd W Saxton.
|
24
|
+
|
25
|
+
require 'rubygems'
|
26
|
+
require "rexml/document"
|
27
|
+
require "rexml/xpath"
|
28
|
+
require "openssl"
|
29
|
+
require 'nokogiri'
|
30
|
+
require "digest/sha1"
|
31
|
+
require "digest/sha2"
|
32
|
+
require "maestrano/saml/validation_error"
|
33
|
+
|
34
|
+
module Maestrano
|
35
|
+
module XMLSecurity
|
36
|
+
class SignedDocument < REXML::Document
|
37
|
+
C14N = "http://www.w3.org/2001/10/xml-exc-c14n#"
|
38
|
+
DSIG = "http://www.w3.org/2000/09/xmldsig#"
|
39
|
+
|
40
|
+
attr_accessor :signed_element_id
|
41
|
+
|
42
|
+
def initialize(response)
|
43
|
+
super(response)
|
44
|
+
extract_signed_element_id
|
45
|
+
end
|
46
|
+
|
47
|
+
def validate_document(idp_cert_fingerprint, soft = true)
|
48
|
+
# get cert from response
|
49
|
+
cert_element = REXML::XPath.first(self, "//ds:X509Certificate", { "ds"=>DSIG })
|
50
|
+
raise Maestrano::Saml::ValidationError.new("Certificate element missing in response (ds:X509Certificate)") unless cert_element
|
51
|
+
base64_cert = cert_element.text
|
52
|
+
cert_text = Base64.decode64(base64_cert)
|
53
|
+
cert = OpenSSL::X509::Certificate.new(cert_text)
|
54
|
+
|
55
|
+
# check cert matches registered idp cert
|
56
|
+
fingerprint = Digest::SHA1.hexdigest(cert.to_der)
|
57
|
+
|
58
|
+
if fingerprint != idp_cert_fingerprint.gsub(/[^a-zA-Z0-9]/,"").downcase
|
59
|
+
return soft ? false : (raise Maestrano::Saml::ValidationError.new("Fingerprint mismatch"))
|
60
|
+
end
|
61
|
+
|
62
|
+
validate_signature(base64_cert, soft)
|
63
|
+
end
|
64
|
+
|
65
|
+
def validate_signature(base64_cert, soft = true)
|
66
|
+
# validate references
|
67
|
+
|
68
|
+
# check for inclusive namespaces
|
69
|
+
inclusive_namespaces = extract_inclusive_namespaces
|
70
|
+
|
71
|
+
document = Nokogiri.parse(self.to_s)
|
72
|
+
|
73
|
+
# create a working copy so we don't modify the original
|
74
|
+
@working_copy ||= REXML::Document.new(self.to_s).root
|
75
|
+
|
76
|
+
# store and remove signature node
|
77
|
+
@sig_element ||= begin
|
78
|
+
element = REXML::XPath.first(@working_copy, "//ds:Signature", {"ds"=>DSIG})
|
79
|
+
element.remove
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
# verify signature
|
84
|
+
signed_info_element = REXML::XPath.first(@sig_element, "//ds:SignedInfo", {"ds"=>DSIG})
|
85
|
+
noko_sig_element = document.at_xpath('//ds:Signature', 'ds' => DSIG)
|
86
|
+
noko_signed_info_element = noko_sig_element.at_xpath('./ds:SignedInfo', 'ds' => DSIG)
|
87
|
+
canon_algorithm = canon_algorithm REXML::XPath.first(@sig_element, '//ds:CanonicalizationMethod', 'ds' => DSIG)
|
88
|
+
canon_string = noko_signed_info_element.canonicalize(canon_algorithm)
|
89
|
+
noko_sig_element.remove
|
90
|
+
|
91
|
+
# check digests
|
92
|
+
REXML::XPath.each(@sig_element, "//ds:Reference", {"ds"=>DSIG}) do |ref|
|
93
|
+
uri = ref.attributes.get_attribute("URI").value
|
94
|
+
|
95
|
+
hashed_element = document.at_xpath("//*[@ID='#{uri[1..-1]}']")
|
96
|
+
canon_algorithm = canon_algorithm REXML::XPath.first(ref, '//ds:CanonicalizationMethod', 'ds' => DSIG)
|
97
|
+
canon_hashed_element = hashed_element.canonicalize(canon_algorithm, inclusive_namespaces)
|
98
|
+
|
99
|
+
digest_algorithm = algorithm(REXML::XPath.first(ref, "//ds:DigestMethod"))
|
100
|
+
|
101
|
+
hash = digest_algorithm.digest(canon_hashed_element)
|
102
|
+
digest_value = Base64.decode64(REXML::XPath.first(ref, "//ds:DigestValue", {"ds"=>DSIG}).text)
|
103
|
+
|
104
|
+
unless digests_match?(hash, digest_value)
|
105
|
+
return soft ? false : (raise Maestrano::Saml::ValidationError.new("Digest mismatch"))
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
base64_signature = REXML::XPath.first(@sig_element, "//ds:SignatureValue", {"ds"=>DSIG}).text
|
110
|
+
signature = Base64.decode64(base64_signature)
|
111
|
+
|
112
|
+
# get certificate object
|
113
|
+
cert_text = Base64.decode64(base64_cert)
|
114
|
+
cert = OpenSSL::X509::Certificate.new(cert_text)
|
115
|
+
|
116
|
+
# signature method
|
117
|
+
signature_algorithm = algorithm(REXML::XPath.first(signed_info_element, "//ds:SignatureMethod", {"ds"=>DSIG}))
|
118
|
+
|
119
|
+
unless cert.public_key.verify(signature_algorithm.new, signature, canon_string)
|
120
|
+
return soft ? false : (raise Maestrano::Saml::ValidationError.new("Key validation error"))
|
121
|
+
end
|
122
|
+
|
123
|
+
return true
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def digests_match?(hash, digest_value)
|
129
|
+
hash == digest_value
|
130
|
+
end
|
131
|
+
|
132
|
+
def extract_signed_element_id
|
133
|
+
reference_element = REXML::XPath.first(self, "//ds:Signature/ds:SignedInfo/ds:Reference", {"ds"=>DSIG})
|
134
|
+
self.signed_element_id = reference_element.attribute("URI").value[1..-1] unless reference_element.nil?
|
135
|
+
end
|
136
|
+
|
137
|
+
def canon_algorithm(element)
|
138
|
+
algorithm = element.attribute('Algorithm').value if element
|
139
|
+
case algorithm
|
140
|
+
when "http://www.w3.org/2001/10/xml-exc-c14n#" then Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
|
141
|
+
when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" then Nokogiri::XML::XML_C14N_1_0
|
142
|
+
when "http://www.w3.org/2006/12/xml-c14n11" then Nokogiri::XML::XML_C14N_1_1
|
143
|
+
else Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def algorithm(element)
|
148
|
+
algorithm = element.attribute("Algorithm").value if element
|
149
|
+
algorithm = algorithm && algorithm =~ /sha(.*?)$/i && $1.to_i
|
150
|
+
case algorithm
|
151
|
+
when 256 then OpenSSL::Digest::SHA256
|
152
|
+
when 384 then OpenSSL::Digest::SHA384
|
153
|
+
when 512 then OpenSSL::Digest::SHA512
|
154
|
+
else
|
155
|
+
OpenSSL::Digest::SHA1
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def extract_inclusive_namespaces
|
160
|
+
if element = REXML::XPath.first(self, "//ec:InclusiveNamespaces", { "ec" => C14N })
|
161
|
+
prefix_list = element.attributes.get_attribute("PrefixList").value
|
162
|
+
prefix_list.split(" ")
|
163
|
+
else
|
164
|
+
[]
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|