ruby-saml-mod 0.1.30 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/onelogin/saml.rb +3 -2
- data/lib/onelogin/saml/auth_request.rb +37 -43
- data/lib/onelogin/saml/base_assertion.rb +159 -0
- data/lib/onelogin/saml/logout_request.rb +44 -0
- data/lib/onelogin/saml/logout_response.rb +24 -25
- data/lib/onelogin/saml/response.rb +2 -4
- data/lib/onelogin/saml/settings.rb +23 -17
- data/spec/base_assertion_spec.rb +5 -0
- data/spec/fixtures/logout_request.xml +11 -0
- data/spec/fixtures/logout_response.xml +13 -0
- data/spec/fixtures/test1-cert.pem +21 -0
- data/spec/fixtures/test1-key.pem +15 -0
- data/spec/fixtures/test1-response.xml +62 -0
- data/spec/fixtures/test2-response.xml +70 -0
- data/spec/fixtures/test3-response.xml +9 -0
- data/spec/fixtures/test4-response.xml +57 -0
- data/spec/fixtures/test5-response.xml +48 -0
- data/spec/fixtures/test6-response.xml +9 -0
- data/spec/fixtures/wrong-key.pem +15 -0
- data/spec/fixtures/xml_signature_wrapping_attack_duplicate_ids.xml +11 -0
- data/spec/fixtures/xml_signature_wrapping_attack_response_attributes.xml +45 -0
- data/spec/fixtures/xml_signature_wrapping_attack_response_nameid.xml +44 -0
- data/spec/logout_request_spec.rb +89 -0
- data/spec/logout_response_spec.rb +76 -0
- data/spec/meta_data_spec.rb +39 -0
- data/spec/response_spec.rb +193 -0
- data/spec/spec_helper.rb +33 -0
- data/spec/support/test_server.rb +73 -0
- metadata +77 -10
- data/LICENSE +0 -19
- data/README +0 -7
- data/lib/onelogin/saml/log_out_request.rb +0 -54
- data/ruby-saml-mod.gemspec +0 -33
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c9b8339af98b853334c4cd343eff1454a9ed537a
|
4
|
+
data.tar.gz: 7702ec985b556013a1d812f646e415ec16cfe58f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1e1accc268ecc2fad5023f99552d8c2a53cca7ddd548092236534bcb0d6016ec51a40d0960b8706afceacd9fe5ecfeb204751985f0b15c90e78e1bbaeeb5cade
|
7
|
+
data.tar.gz: 868e76914f77d9766ebc31449b9262c28dd689a4ea815ed23c6269ada3a0e4422ec2941e73c87bfd3e9d53671ee0e5c0c1f6b58ab2151ba969cee116a3599f7d
|
data/lib/onelogin/saml.rb
CHANGED
@@ -35,12 +35,13 @@ module Onelogin
|
|
35
35
|
}
|
36
36
|
end
|
37
37
|
|
38
|
+
require 'onelogin/saml/base_assertion'
|
38
39
|
require 'onelogin/saml/auth_request'
|
39
|
-
require 'onelogin/saml/authn_contexts
|
40
|
+
require 'onelogin/saml/authn_contexts'
|
40
41
|
require 'onelogin/saml/response'
|
41
42
|
require 'onelogin/saml/settings'
|
42
43
|
require 'onelogin/saml/name_identifiers'
|
43
44
|
require 'onelogin/saml/status_codes'
|
44
45
|
require 'onelogin/saml/meta_data'
|
45
|
-
require 'onelogin/saml/
|
46
|
+
require 'onelogin/saml/logout_request'
|
46
47
|
require 'onelogin/saml/logout_response'
|
@@ -1,53 +1,47 @@
|
|
1
1
|
module Onelogin::Saml
|
2
|
-
class AuthRequest
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
2
|
+
class AuthRequest < BaseAssertion
|
3
|
+
attr_accessor :requested_authn_context,
|
4
|
+
:assertion_consumer_service_url,
|
5
|
+
:name_identifier_format
|
6
|
+
|
7
|
+
def self.parse(raw_assertion, settings = nil)
|
8
|
+
raise NotImplementedError
|
8
9
|
end
|
9
|
-
|
10
|
-
def self.
|
11
|
-
|
12
|
-
|
10
|
+
|
11
|
+
def self.generate(settings)
|
12
|
+
super(settings, {
|
13
|
+
destination: settings.idp_sso_target_url,
|
14
|
+
requested_authn_context: settings.requested_authn_context,
|
15
|
+
assertion_consumer_service_url: Array(settings.assertion_consumer_service_url).first,
|
16
|
+
name_identifier_format: settings.name_identifier_format
|
17
|
+
})
|
13
18
|
end
|
14
|
-
|
15
|
-
def generate_request
|
16
|
-
@id = Onelogin::Saml::AuthRequest.generate_unique_id(42)
|
17
|
-
issue_instant = Onelogin::Saml::AuthRequest.get_timestamp
|
18
19
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
@request_xml += "<saml:AuthnContextClassRef xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">#{@settings.requested_authn_context}</saml:AuthnContextClassRef>"
|
27
|
-
@request_xml += "</samlp:RequestedAuthnContext>\n"
|
20
|
+
def generate
|
21
|
+
if self.requested_authn_context
|
22
|
+
xml = <<-XML
|
23
|
+
<samlp:RequestedAuthnContext Comparison="exact">
|
24
|
+
<saml:AuthnContextClassRef>#{self.requested_authn_context}</saml:AuthnContextClassRef>
|
25
|
+
</samlp:RequestedAuthnContext>
|
26
|
+
XML
|
28
27
|
end
|
29
|
-
|
30
|
-
@request_xml += "</samlp:AuthnRequest>"
|
31
28
|
|
32
|
-
|
33
|
-
|
34
|
-
|
29
|
+
<<-XML
|
30
|
+
<samlp:AuthnRequest
|
31
|
+
xmlns:samlp="#{Onelogin::NAMESPACES['samlp']}"
|
32
|
+
xmlns:saml="#{Onelogin::NAMESPACES['saml']}"
|
33
|
+
ID="#{self.id}"
|
34
|
+
Version="2.0"
|
35
|
+
ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
|
36
|
+
AssertionConsumerServiceURL=\"#{self.assertion_consumer_service_url}\"
|
37
|
+
IssueInstant="#{self.issue_instant}">
|
35
38
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
chars = ("a".."f").to_a + ("0".."9").to_a
|
43
|
-
chars_len = chars.size
|
44
|
-
unique_id = ("a".."f").to_a[rand(6)]
|
45
|
-
2.upto(length) { |i| unique_id << chars[rand(chars_len)] }
|
46
|
-
unique_id
|
47
|
-
end
|
48
|
-
|
49
|
-
def self.get_timestamp
|
50
|
-
Time.new.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
39
|
+
<saml:Issuer>#{self.issuer}</saml:Issuer>
|
40
|
+
<samlp:NameIDPolicy Format="#{self.name_identifier_format}" AllowCreate="true"></samlp:NameIDPolicy>
|
41
|
+
|
42
|
+
#{xml}
|
43
|
+
</samlp:AuthnRequest>
|
44
|
+
XML
|
51
45
|
end
|
52
46
|
end
|
53
47
|
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
module Onelogin::Saml
|
2
|
+
class BaseAssertion
|
3
|
+
attr_accessor :settings
|
4
|
+
attr_reader :xml
|
5
|
+
attr_writer :id,
|
6
|
+
:issuer,
|
7
|
+
:issue_instant,
|
8
|
+
:destination,
|
9
|
+
:in_response_to,
|
10
|
+
:base64_assertion
|
11
|
+
def id
|
12
|
+
@id ||= root_attribute_value('ID')
|
13
|
+
end
|
14
|
+
|
15
|
+
def issue_instant
|
16
|
+
@issue_instant ||= root_attribute_value('IssueInstance')
|
17
|
+
end
|
18
|
+
|
19
|
+
def issuer
|
20
|
+
@issuer ||= node_content('saml:Issuer')
|
21
|
+
end
|
22
|
+
|
23
|
+
def destination
|
24
|
+
@destination ||= root_attribute_value('Destination')
|
25
|
+
end
|
26
|
+
|
27
|
+
def in_response_to
|
28
|
+
@in_response_to ||= root_attribute_value('InResponseTo')
|
29
|
+
end
|
30
|
+
|
31
|
+
def document
|
32
|
+
@document ||= LibXML::XML::Document.string(xml) if xml
|
33
|
+
end
|
34
|
+
|
35
|
+
def xml=(value)
|
36
|
+
@xml = value.strip
|
37
|
+
end
|
38
|
+
|
39
|
+
def base64_assertion
|
40
|
+
@base64_assertion ||= begin
|
41
|
+
deflated_assertion = Zlib::Deflate.deflate(self.xml, 9)[2..-5]
|
42
|
+
Base64.strict_encode64(deflated_assertion)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def assertion_type
|
47
|
+
return unless document
|
48
|
+
|
49
|
+
if document.root.name =~ /Request$/
|
50
|
+
:request
|
51
|
+
elsif document.root.name =~ /Response$/
|
52
|
+
:response
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def process(settings)
|
57
|
+
# TODO: Verify signature and decrypt.
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.parse(raw_assertion, settings = nil)
|
61
|
+
assertion = new
|
62
|
+
assertion.base64_assertion = raw_assertion
|
63
|
+
|
64
|
+
decoded_xml = Base64.decode64(raw_assertion)
|
65
|
+
zlib = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
66
|
+
|
67
|
+
assertion.xml = zlib.inflate(decoded_xml)
|
68
|
+
|
69
|
+
assertion.process(settings) if settings
|
70
|
+
assertion
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.generate(settings, attributes = {})
|
74
|
+
assertion = new
|
75
|
+
|
76
|
+
assertion.settings = settings
|
77
|
+
assertion.id = generate_unique_id
|
78
|
+
assertion.issue_instant = get_timestamp
|
79
|
+
assertion.issuer = settings.issuer
|
80
|
+
|
81
|
+
attributes.each do |key, value|
|
82
|
+
if assertion.respond_to? "#{key}="
|
83
|
+
assertion.send "#{key}=", value
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
assertion.xml = assertion.generate
|
88
|
+
|
89
|
+
assertion
|
90
|
+
end
|
91
|
+
|
92
|
+
def forward_url
|
93
|
+
@forward_url ||= begin
|
94
|
+
url, existing_query_string = destination.split('?')
|
95
|
+
query_string = query_string_append(existing_query_string, query_string_param, base64_assertion)
|
96
|
+
|
97
|
+
if settings.sign?
|
98
|
+
query_string = query_string_append(query_string, "SigAlg", "http://www.w3.org/2000/09/xmldsig#rsa-sha1")
|
99
|
+
signature = generate_signature(query_string, settings.xmlsec_privatekey)
|
100
|
+
query_string = query_string_append(query_string, "Signature", signature)
|
101
|
+
end
|
102
|
+
|
103
|
+
if settings.relay_state
|
104
|
+
query_string = query_string_append(query_string, "RelayState", settings.relay_state)
|
105
|
+
end
|
106
|
+
|
107
|
+
[url, query_string].join("?")
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def generate
|
112
|
+
raise "Subclass does not implement abstract method generate."
|
113
|
+
end
|
114
|
+
|
115
|
+
def root_attribute_value(attribute)
|
116
|
+
document.root[attribute] if document
|
117
|
+
end
|
118
|
+
|
119
|
+
def node_attribute_value(xpath, attribute)
|
120
|
+
document.root.find_first(xpath, Onelogin::NAMESPACES)[attribute] rescue nil
|
121
|
+
end
|
122
|
+
|
123
|
+
def node_content(xpath)
|
124
|
+
document.root.find_first(xpath, Onelogin::NAMESPACES).content rescue nil
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.generate_unique_id(length = 42)
|
128
|
+
chars = ("a".."f").to_a + ("0".."9").to_a
|
129
|
+
chars_len = chars.size
|
130
|
+
unique_id = ("a".."f").to_a[rand(6)]
|
131
|
+
2.upto(length) { |i| unique_id << chars[rand(chars_len)] }
|
132
|
+
unique_id
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.get_timestamp
|
136
|
+
Time.new.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
|
137
|
+
end
|
138
|
+
|
139
|
+
private
|
140
|
+
|
141
|
+
def query_string_param
|
142
|
+
if assertion_type == :request
|
143
|
+
'SAMLRequest'
|
144
|
+
elsif assertion_type == :response
|
145
|
+
'SAMLResponse'
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def generate_signature(string, private_key)
|
150
|
+
pkey = OpenSSL::PKey::RSA.new(File.read(private_key))
|
151
|
+
sign = pkey.sign(OpenSSL::Digest::SHA1.new, string)
|
152
|
+
Base64.encode64(sign).gsub(/\s/, '')
|
153
|
+
end
|
154
|
+
|
155
|
+
def query_string_append(query_string, key, value)
|
156
|
+
[query_string, "#{CGI.escape(key)}=#{CGI.escape(value)}"].compact.join('&')
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Onelogin::Saml
|
2
|
+
class LogoutRequest < BaseAssertion
|
3
|
+
attr_writer :name_id,
|
4
|
+
:session_index,
|
5
|
+
:name_qualifier,
|
6
|
+
:name_identifier_format
|
7
|
+
|
8
|
+
def name_id
|
9
|
+
@name_id ||= node_content('saml:NameID')
|
10
|
+
end
|
11
|
+
|
12
|
+
def name_identifier_format
|
13
|
+
@name_identifier_format ||= node_attribute_value('samlp:NameID', 'Format')
|
14
|
+
end
|
15
|
+
|
16
|
+
def name_qualifier
|
17
|
+
@name_qualifier ||= node_attribute_value('samlp:NameID', 'NameQualifier')
|
18
|
+
end
|
19
|
+
|
20
|
+
def session_index
|
21
|
+
@session_index ||= node_content('samlp:SessionIndex')
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.generate(name_qualifier, name_id, session_index, settings)
|
25
|
+
super(settings, {
|
26
|
+
destination: settings.idp_slo_target_url,
|
27
|
+
name_identifier_format: settings.name_identifier_format,
|
28
|
+
name_id: name_id,
|
29
|
+
name_qualifier: name_qualifier,
|
30
|
+
session_index: session_index
|
31
|
+
})
|
32
|
+
end
|
33
|
+
|
34
|
+
def generate
|
35
|
+
<<-XML
|
36
|
+
<samlp:LogoutRequest xmlns:samlp="#{Onelogin::NAMESPACES['samlp']}" xmlns:saml="#{Onelogin::NAMESPACES['saml']}" ID="#{self.id}" Version="2.0" IssueInstant="#{self.issue_instant}" Destination="#{self.destination}">
|
37
|
+
<saml:Issuer>#{self.issuer}</saml:Issuer>
|
38
|
+
<saml:NameID NameQualifier="#{self.name_qualifier}" SPNameQualifier="#{self.issuer}" Format="#{self.name_identifier_format}">#{self.name_id}</saml:NameID>
|
39
|
+
<samlp:SessionIndex>#{self.session_index}</samlp:SessionIndex>
|
40
|
+
</samlp:LogoutRequest>
|
41
|
+
XML
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -1,38 +1,37 @@
|
|
1
1
|
module Onelogin::Saml
|
2
|
-
class LogoutResponse
|
3
|
-
|
4
|
-
attr_reader :settings, :document, :xml, :response
|
5
|
-
attr_reader :status_code, :status_message, :issuer
|
6
|
-
attr_reader :in_response_to, :destination, :request_id
|
7
|
-
def initialize(response, settings=nil)
|
8
|
-
@response = response
|
2
|
+
class LogoutResponse < BaseAssertion
|
9
3
|
|
10
|
-
|
11
|
-
zlib = Zlib::Inflate.new(-Zlib::MAX_WBITS)
|
12
|
-
@xml = zlib.inflate(@xml)
|
13
|
-
@document = LibXML::XML::Document.string(@xml)
|
4
|
+
STATUS_MESSAGE = 'Successfully Signed Out'
|
14
5
|
|
15
|
-
|
16
|
-
|
17
|
-
@in_response_to = @document.find_first("/samlp:LogoutResponse", Onelogin::NAMESPACES)['InResponseTo'] rescue nil
|
18
|
-
@destination = @document.find_first("/samlp:LogoutResponse", Onelogin::NAMESPACES)['Destination'] rescue nil
|
19
|
-
@status_code = @document.find_first("/samlp:LogoutResponse/samlp:Status/samlp:StatusCode", Onelogin::NAMESPACES)['Value'] rescue nil
|
20
|
-
@status_message = @document.find_first("/samlp:LogoutResponse/samlp:Status/samlp:StatusMessage", Onelogin::NAMESPACES).content rescue nil
|
6
|
+
attr_writer :status_code,
|
7
|
+
:status_message
|
21
8
|
|
22
|
-
|
9
|
+
def status_code
|
10
|
+
@status_code ||= node_attribute_value('samlp:Status/samlp:StatusCode', 'Value')
|
23
11
|
end
|
24
12
|
|
25
|
-
def
|
26
|
-
@
|
27
|
-
return unless @response
|
13
|
+
def status_message
|
14
|
+
@status_message ||= node_content("samlp:Status/samlp:StatusMessage")
|
28
15
|
end
|
29
16
|
|
30
|
-
def
|
31
|
-
|
17
|
+
def self.generate(in_response_to, settings)
|
18
|
+
super(settings, in_response_to: in_response_to, destination: settings.idp_slo_target_url)
|
32
19
|
end
|
33
|
-
|
20
|
+
|
21
|
+
def generate
|
22
|
+
<<-XML
|
23
|
+
<samlp:LogoutResponse xmlns:samlp="#{Onelogin::NAMESPACES['samlp']}" xmlns:saml="#{Onelogin::NAMESPACES['saml']}" ID="#{self.id}" Version="2.0" IssueInstant="#{self.issue_instant}" Destination="#{self.destination}" InResponseTo="#{self.in_response_to}">
|
24
|
+
<saml:Issuer>#{self.issuer}</saml:Issuer>
|
25
|
+
<samlp:Status>
|
26
|
+
<samlp:StatusCode Value="#{Onelogin::Saml::StatusCodes::SUCCESS_URI}"></samlp:StatusCode>
|
27
|
+
<samlp:StatusMessage>#{STATUS_MESSAGE}</samlp:StatusMessage>
|
28
|
+
</samlp:Status>
|
29
|
+
</samlp:LogoutResponse>
|
30
|
+
XML
|
31
|
+
end
|
32
|
+
|
34
33
|
def success_status?
|
35
|
-
|
34
|
+
self.status_code == Onelogin::Saml::StatusCodes::SUCCESS_URI
|
36
35
|
end
|
37
36
|
end
|
38
37
|
end
|
@@ -7,6 +7,7 @@ module Onelogin::Saml
|
|
7
7
|
attr_reader :status_code, :status_message
|
8
8
|
attr_reader :in_response_to, :destination, :issuer
|
9
9
|
attr_reader :validation_error
|
10
|
+
|
10
11
|
def initialize(response, settings=nil)
|
11
12
|
@response = response
|
12
13
|
|
@@ -28,6 +29,7 @@ module Onelogin::Saml
|
|
28
29
|
|
29
30
|
def process(settings)
|
30
31
|
@settings = settings
|
32
|
+
@logger = settings.logger
|
31
33
|
return unless @response
|
32
34
|
|
33
35
|
@in_response_to = untrusted_find_first("/samlp:Response")['InResponseTo'] rescue nil
|
@@ -72,10 +74,6 @@ module Onelogin::Saml
|
|
72
74
|
end.flatten.compact
|
73
75
|
end
|
74
76
|
|
75
|
-
def logger=(val)
|
76
|
-
@logger = val
|
77
|
-
end
|
78
|
-
|
79
77
|
def is_valid?
|
80
78
|
@is_valid ||= validate
|
81
79
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module Onelogin::Saml
|
2
2
|
class Settings
|
3
|
-
|
3
|
+
|
4
4
|
def initialize(atts={})
|
5
5
|
atts.each do |key, val|
|
6
6
|
if self.respond_to? "#{key}="
|
@@ -8,48 +8,54 @@ module Onelogin::Saml
|
|
8
8
|
end
|
9
9
|
end
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
12
|
# The URL at which the SAML assertion should be received.
|
13
13
|
attr_accessor :assertion_consumer_service_url
|
14
|
-
|
14
|
+
|
15
15
|
# The name of your application.
|
16
16
|
attr_accessor :issuer
|
17
|
-
|
18
|
-
#
|
17
|
+
|
18
|
+
# Logger
|
19
|
+
attr_accessor :logger
|
20
|
+
|
21
|
+
#
|
19
22
|
attr_accessor :sp_name_qualifier
|
20
|
-
|
23
|
+
|
21
24
|
# The IdP URL to which the authentication request should be sent.
|
22
25
|
attr_accessor :idp_sso_target_url
|
23
|
-
|
26
|
+
|
24
27
|
# The IdP URL to which the logout request should be sent.
|
25
28
|
attr_accessor :idp_slo_target_url
|
26
|
-
|
29
|
+
|
27
30
|
# The certificate fingerprint. This is provided from the identity provider when setting up the relationship.
|
28
31
|
attr_accessor :idp_cert_fingerprint
|
29
|
-
|
32
|
+
|
30
33
|
# Describes the format of the username required by this application.
|
31
34
|
# For email: Onelogin::Saml::NameIdentifiers::EMAIL
|
32
35
|
attr_accessor :name_identifier_format
|
33
|
-
|
36
|
+
|
34
37
|
# The type of authentication requested (see Onelogin::Saml::AuthnContexts)
|
35
38
|
attr_accessor :requested_authn_context
|
36
|
-
|
39
|
+
|
40
|
+
# The relay state to use when generating assertions.
|
41
|
+
attr_accessor :relay_state
|
42
|
+
|
37
43
|
## Attributes for the metadata
|
38
|
-
|
44
|
+
|
39
45
|
# The logout url of your application
|
40
46
|
attr_accessor :sp_slo_url
|
41
|
-
|
42
|
-
# The name of the technical contact for your application
|
47
|
+
|
48
|
+
# The name of the technical contact for your application
|
43
49
|
attr_accessor :tech_contact_name
|
44
|
-
|
50
|
+
|
45
51
|
# The email of the technical contact for your application
|
46
52
|
attr_accessor :tech_contact_email
|
47
|
-
|
53
|
+
|
48
54
|
## Attributes for xml encryption
|
49
55
|
|
50
56
|
# The PEM-encoded certificate
|
51
57
|
attr_accessor :xmlsec_certificate
|
52
|
-
|
58
|
+
|
53
59
|
# The PEM-encoded private key
|
54
60
|
attr_accessor :xmlsec_privatekey
|
55
61
|
|