maestrano 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.
- 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
|