signer 1.2.1 → 1.3.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 +8 -8
- data/CHANGELOG.md +4 -0
- data/README.md +17 -0
- data/lib/signer.rb +55 -5
- data/lib/signer/digester.rb +74 -0
- data/lib/signer/version.rb +1 -1
- data/spec/fixtures/output_1_sha256.xml +2 -0
- data/spec/signer_spec.rb +21 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
YjcwMWM0ZWJiMGRmZWVlZmYzMWJlZmNiZjk3MDViODMwMDk5ZGRiMg==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
YjgwNGI1ZDFmYTc4NWMyZWM1NTdhNTk1ZjFiY2MxMmY5MWYzYjMzZA==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
MTRmNjU2NDM4MjRhM2ZlZTA2MmFjMWQyNDZmMDVkNTEwOTAwMGE1MDBmMjc5
|
10
|
+
ZTE4Mzg5MjBjMWViYmU0NTMwYzgzY2E4YmVkMzc2NWNlNjhmZWIzM2JiMzI3
|
11
|
+
MjVhNDU3YTQwMmQ1ZjYwOTNmMWMxOGQwOWY1NDNlNzM3ZjA0NTI=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
YmM4ZDhlYTU4YmQ3NDllYzYyZDg0MTU5Y2Q2OGY1ODM3YmI2Yzg4ODc4Yjkz
|
14
|
+
OTlhNDQ1NmM5OTliNmRkZmVlMzI5MzZlOWQxZjE3YTUyZjQ2MWQ2ZWU0Y2U1
|
15
|
+
NzRlODhjMjBjOWZhZDBmNGI0NjdmYWUyOGU5NjMxNDc3OWViMWE=
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -200,6 +200,23 @@ Output:
|
|
200
200
|
</s:Envelope>
|
201
201
|
```
|
202
202
|
|
203
|
+
## Different signature and digest algorithms support
|
204
|
+
|
205
|
+
You can change digest algorithms used for both node digesting and signing. Default for both is SHA1. Currently __SHA1__ `:sha1`, __SHA256__ `:sha256`, and __GOST R 34.11-94__ `:gostr3411` are supported out of the box.
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
signer.digest_algorithm = :sha256 # Set algorithm for node digesting
|
209
|
+
signer.signature_digest_algorithm = :sha256 # Set algorithm for message digesting for signing
|
210
|
+
```
|
211
|
+
|
212
|
+
You can provide you own digest support by passing in these methods a `Hash` with `:id` and `:digester` keys. In `:id` should be a string for XML `//Reference/DigestMethod[Algorithm]`, in `:digester` should be a Ruby object, compatible by interface with `OpenSSL::Digest` class, at least it should respond to `digest` and `reset` methods.
|
213
|
+
|
214
|
+
Signature algorithm is dependent from keypair used for signing and can't be changed. Usually it's __RSA__. Currently gem recognizes __GOST R 34.10-2001__ certificates and sets up a XML identifier for it. If used signature algorithm and signature digest doesn't corresponds with XML identifier, you can change identifier with `signature_algorithm_id` method.
|
215
|
+
|
216
|
+
Please note, that these settings will be changed or reset on certificate assignment, please change them after setting certificate!
|
217
|
+
|
218
|
+
__NOTE__: To sign XMLs with __GOST R 34.10-2001__, you need to have Ruby compiled with patches from https://bugs.ruby-lang.org/issues/9830 and correctly configured OpenSSL (see https://github.com/openssl/openssl/blob/master/engines/ccgost/README.gost)
|
219
|
+
|
203
220
|
## Miscellaneous
|
204
221
|
|
205
222
|
If you need to digest a `BinarySecurityToken` tag, you need to construct it yourself **before** signing.
|
data/lib/signer.rb
CHANGED
@@ -3,22 +3,66 @@ require "base64"
|
|
3
3
|
require "digest/sha1"
|
4
4
|
require "openssl"
|
5
5
|
|
6
|
+
require "signer/digester"
|
6
7
|
require "signer/version"
|
7
8
|
|
8
9
|
class Signer
|
9
|
-
attr_accessor :document, :
|
10
|
+
attr_accessor :document, :private_key, :signature_algorithm_id
|
11
|
+
attr_reader :cert
|
10
12
|
attr_writer :security_node, :security_token_id
|
11
13
|
|
12
14
|
WSU_NAMESPACE = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd'
|
13
15
|
|
14
16
|
def initialize(document)
|
15
17
|
self.document = Nokogiri::XML(document.to_s, &:noblanks)
|
18
|
+
self.digest_algorithm = :sha1
|
19
|
+
self.set_default_signature_method!
|
16
20
|
end
|
17
21
|
|
18
22
|
def to_xml
|
19
23
|
document.to_xml(:save_with => 0)
|
20
24
|
end
|
21
25
|
|
26
|
+
# Return symbol name for supported digest algorithms and string name for custom ones.
|
27
|
+
def digest_algorithm
|
28
|
+
@digester.symbol || @digester.digest_name
|
29
|
+
end
|
30
|
+
|
31
|
+
# Allows to change algorithm for node digesting (default is SHA1).
|
32
|
+
#
|
33
|
+
# You may pass either a one of +:sha1+, +:sha256+ or +:gostr3411+ symbols
|
34
|
+
# or +Hash+ with keys +:id+ with a string, which will denote algorithm in XML Reference tag
|
35
|
+
# and +:digester+ with instance of class with interface compatible with +OpenSSL::Digest+ class.
|
36
|
+
def digest_algorithm=(algorithm)
|
37
|
+
@digester = Signer::Digester.new(algorithm)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Return symbol name for supported digest algorithms and string name for custom ones.
|
41
|
+
def signature_digest_algorithm
|
42
|
+
@sign_digester.symbol || @sign_digester.digest_name
|
43
|
+
end
|
44
|
+
|
45
|
+
# Allows to change digesting algorithm for signature creation. Same as +digest_algorithm=+
|
46
|
+
def signature_digest_algorithm=(algorithm)
|
47
|
+
@sign_digester = Signer::Digester.new(algorithm)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Receives certificate for signing and tries to guess a digest algorithm for signature creation.
|
51
|
+
#
|
52
|
+
# Will change +signature_digest_algorithm+ and +signature_algorithm_id+ for known certificate types and reset to defaults for others.
|
53
|
+
def cert=(certificate)
|
54
|
+
@cert = certificate
|
55
|
+
# Try to guess a digest algorithm for signature creation
|
56
|
+
case @cert.signature_algorithm
|
57
|
+
when 'GOST R 34.11-94 with GOST R 34.10-2001'
|
58
|
+
self.signature_digest_algorithm = :gostr3411
|
59
|
+
self.signature_algorithm_id = 'http://www.w3.org/2001/04/xmldsig-more#gostr34102001-gostr3411'
|
60
|
+
# Add clauses for other types of keys that require other digest algorithms and identifiers
|
61
|
+
else # most common 'sha1WithRSAEncryption' type included here
|
62
|
+
self.set_default_signature_method! # Reset any changes as they can become malformed
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
22
66
|
def security_token_id
|
23
67
|
@security_token_id ||= "uuid-639b8970-7644-4f9e-9bc4-9c2e367808fc-1"
|
24
68
|
end
|
@@ -56,7 +100,7 @@ class Signer
|
|
56
100
|
canonicalization_method_node['Algorithm'] = 'http://www.w3.org/2001/10/xml-exc-c14n#'
|
57
101
|
node.add_child(canonicalization_method_node)
|
58
102
|
signature_method_node = Nokogiri::XML::Node.new('SignatureMethod', document)
|
59
|
-
signature_method_node['Algorithm'] =
|
103
|
+
signature_method_node['Algorithm'] = self.signature_algorithm_id
|
60
104
|
node.add_child(signature_method_node)
|
61
105
|
end
|
62
106
|
node
|
@@ -146,7 +190,7 @@ class Signer
|
|
146
190
|
target_node["#{wsu_ns}:Id"] = id.to_s
|
147
191
|
end
|
148
192
|
target_canon = canonicalize(target_node)
|
149
|
-
target_digest = Base64.encode64(
|
193
|
+
target_digest = Base64.encode64(@digester.digest(target_canon)).strip
|
150
194
|
|
151
195
|
reference_node = Nokogiri::XML::Node.new('Reference', document)
|
152
196
|
reference_node['URI'] = id.to_s.size > 0 ? "##{id}" : ""
|
@@ -164,7 +208,7 @@ class Signer
|
|
164
208
|
transforms_node.add_child(transform_node)
|
165
209
|
|
166
210
|
digest_method_node = Nokogiri::XML::Node.new('DigestMethod', document)
|
167
|
-
digest_method_node['Algorithm'] =
|
211
|
+
digest_method_node['Algorithm'] = @digester.digest_id
|
168
212
|
reference_node.add_child(digest_method_node)
|
169
213
|
|
170
214
|
digest_value_node = Nokogiri::XML::Node.new('DigestValue', document)
|
@@ -185,7 +229,7 @@ class Signer
|
|
185
229
|
|
186
230
|
signed_info_canon = canonicalize(signed_info_node)
|
187
231
|
|
188
|
-
signature = private_key.sign(
|
232
|
+
signature = private_key.sign(@sign_digester.digester, signed_info_canon)
|
189
233
|
signature_value_digest = Base64.encode64(signature).gsub("\n", '')
|
190
234
|
|
191
235
|
signature_value_node = Nokogiri::XML::Node.new('SignatureValue', document)
|
@@ -196,6 +240,12 @@ class Signer
|
|
196
240
|
|
197
241
|
protected
|
198
242
|
|
243
|
+
# Reset digest algorithm for signature creation and signature algorithm identifier
|
244
|
+
def set_default_signature_method!
|
245
|
+
self.signature_digest_algorithm = :sha1
|
246
|
+
self.signature_algorithm_id = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'
|
247
|
+
end
|
248
|
+
|
199
249
|
##
|
200
250
|
# Searches in namespaces, defined on +target_node+ or its ancestors,
|
201
251
|
# for the +namespace+ with given URI and returns its prefix.
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
class Signer
|
4
|
+
|
5
|
+
# Digest algorithms supported "out of the box"
|
6
|
+
DIGEST_ALGORITHMS = {
|
7
|
+
# SHA 1
|
8
|
+
sha1: {
|
9
|
+
name: 'SHA1',
|
10
|
+
id: 'http://www.w3.org/2000/09/xmldsig#sha1',
|
11
|
+
digester: lambda { OpenSSL::Digest::SHA1.new },
|
12
|
+
},
|
13
|
+
# SHA 256
|
14
|
+
sha256: {
|
15
|
+
name: 'SHA256',
|
16
|
+
id: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
|
17
|
+
digester: lambda { OpenSSL::Digest::SHA256.new },
|
18
|
+
},
|
19
|
+
# GOST R 34-11 94
|
20
|
+
gostr3411: {
|
21
|
+
name: 'GOST R 34.11-94',
|
22
|
+
id: 'http://www.w3.org/2001/04/xmldsig-more#gostr3411',
|
23
|
+
digester: lambda {
|
24
|
+
OpenSSL::Engine.load
|
25
|
+
gost_engine = OpenSSL::Engine.by_id('gost')
|
26
|
+
gost_engine.set_default(0xFFFF)
|
27
|
+
gost_engine.digest('md_gost94')
|
28
|
+
},
|
29
|
+
},
|
30
|
+
}
|
31
|
+
|
32
|
+
# Class that holds +OpenSSL::Digest+ instance with some meta information for digesting in XML.
|
33
|
+
class Digester
|
34
|
+
|
35
|
+
# You may pass either a one of +:sha1+, +:sha256+ or +:gostr3411+ symbols
|
36
|
+
# or +Hash+ with keys +:id+ with a string, which will denote algorithm in XML Reference tag
|
37
|
+
# and +:digester+ with instance of class with interface compatible with +OpenSSL::Digest+ class.
|
38
|
+
def initialize(algorithm)
|
39
|
+
if algorithm.kind_of? Symbol
|
40
|
+
@digest_info = DIGEST_ALGORITHMS[algorithm].dup
|
41
|
+
@digest_info[:digester] = @digest_info[:digester].call
|
42
|
+
@symbol = algorithm
|
43
|
+
else
|
44
|
+
@digest_info = algorithm
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
attr_reader :symbol
|
49
|
+
|
50
|
+
# Digest
|
51
|
+
def digest(message)
|
52
|
+
self.digester.digest(message)
|
53
|
+
end
|
54
|
+
|
55
|
+
alias call digest
|
56
|
+
|
57
|
+
# Returns +OpenSSL::Digest+ (or derived class) instance
|
58
|
+
def digester
|
59
|
+
@digest_info[:digester].reset
|
60
|
+
end
|
61
|
+
|
62
|
+
# Human-friendly name
|
63
|
+
def digest_name
|
64
|
+
@digest_info[:name]
|
65
|
+
end
|
66
|
+
|
67
|
+
# XML-friendly name (for specifying in XML +DigestMethod+ node +Algorithm+ attribute)
|
68
|
+
def digest_id
|
69
|
+
@digest_info[:id]
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
data/lib/signer/version.rb
CHANGED
@@ -0,0 +1,2 @@
|
|
1
|
+
<?xml version="1.0"?>
|
2
|
+
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:wsurandom="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"><s:Header><a:Action s:mustUnderstand="1">http://tempuri.org/IDocumentService/SearchDocuments</a:Action><a:MessageID>urn:uuid:30db5d4f-ab84-46be-907c-be690a92979b</a:MessageID><a:ReplyTo><a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address></a:ReplyTo><To xmlns="http://www.w3.org/2005/08/addressing" xmlns:a="http://www.w3.org/2003/05/soap-envelope" a:mustUnderstand="1">http://tempuri.org/PublicServices/Test/1.0.12/PublicServices/DocumentService.svc</To><o:Security xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" s:mustUnderstand="1"><wsurandom:Timestamp><wsurandom:Created>2012-05-02T18:17:14.467Z</wsurandom:Created><wsurandom:Expires>2012-05-02T18:22:14.467Z</wsurandom:Expires></wsurandom:Timestamp><o:BinarySecurityToken ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary" wsurandom:Id="uuid-639b8970-7644-4f9e-9bc4-9c2e367808fc-1">MIICsDCCAhmgAwIBAgIJAOUHvh4oho0tMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTIwNTAzMTMxODIyWhcNMTMwNTAzMTMxODIyWjBFMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCvK5hMPv/R5IFmwWyJOyEaFUrF/ZsmN+Gip8hvR6rLP3YPNx9iFYvPcZllFmuVwyaz7YT2N5BsqTwLdyi5v4HY4fUtuz0p8jIPoSd6dfDvcnSpf4QLTOgOaL3ciPEbgDHH2tnIksukoWzqCYva+qFZ74NFl19swXotW9fA4Jzs4QIDAQABo4GnMIGkMB0GA1UdDgQWBBRU1WEHDnP8Hr7ZulxrSzEwOcYpMzB1BgNVHSMEbjBsgBRU1WEHDnP8Hr7ZulxrSzEwOcYpM6FJpEcwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZIIJAOUHvh4oho0tMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEASY/9SAOK57q9mGnNJJeyDbmyGrAHSJTod646xTHYkMvhUqwHyk9PTr5bdfmswpmyVn+AQ43U2tU5vnpTBmKpHWD2+HSHgGa92mMLrfBOd8EBZ329NL3N2HDPIaHr4NPGyhNrSK3QVOnAq2D0jlyrGYJlLli1NxHiBz7FCEJaVI8=</o:BinarySecurityToken><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><Reference URI="#uuid-639b8970-7644-4f9e-9bc4-9c2e367808fc-1"><Transforms><Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><DigestValue>frx76VWYEsTF+bL6gMKAs3NSkziiB2pRLVb2zZNOWZ0=</DigestValue></Reference></SignedInfo><SignatureValue>FqbJM5mu4LGECjGLNsGoV37qvMBbnukAbAUjHU9BeOkN8reoLfLdsoGvnl+T6+nv7Oaw5LrCDWVdA31RmbqVMn0qPm39CtIAPncUJD0Zr1XKvL2oIA8ySgR4pqQR/GM8fdOkQPRIXXAjTxg6UuVYmXn0vfpLeH2uqoQR1L98Rj8=</SignatureValue><KeyInfo><o:SecurityTokenReference><o:Reference ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3" URI="#uuid-639b8970-7644-4f9e-9bc4-9c2e367808fc-1"/></o:SecurityTokenReference></KeyInfo></Signature></o:Security></s:Header><s:Body><SearchDocuments xmlns="http://tempuri.org/"><searchCriteria xmlns:b="http://schemas.datacontract.org/2004/07/BusinessLogic.Data.Documents.Integration" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"><b:RegistrationNo>1</b:RegistrationNo></searchCriteria></SearchDocuments></s:Body></s:Envelope>
|
data/spec/signer_spec.rb
CHANGED
@@ -47,6 +47,27 @@ describe Signer do
|
|
47
47
|
signer.to_xml.should == Nokogiri::XML(File.read(output_xml_file), &:noblanks).to_xml(:save_with => 0)
|
48
48
|
end
|
49
49
|
|
50
|
+
it "should digest and sign SOAP XML with SHA256" do
|
51
|
+
input_xml_file = File.join(File.dirname(__FILE__), 'fixtures', 'input_1.xml')
|
52
|
+
cert_file = File.join(File.dirname(__FILE__), 'fixtures', 'cert.pem')
|
53
|
+
private_key_file = File.join(File.dirname(__FILE__), 'fixtures', 'key.pem')
|
54
|
+
|
55
|
+
signer = Signer.new(File.read(input_xml_file))
|
56
|
+
signer.cert = OpenSSL::X509::Certificate.new(File.read(cert_file))
|
57
|
+
signer.private_key = OpenSSL::PKey::RSA.new(File.read(private_key_file), "test")
|
58
|
+
signer.digest_algorithm = :sha256
|
59
|
+
signer.signature_digest_algorithm = :sha256
|
60
|
+
signer.signature_algorithm_id = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
|
61
|
+
|
62
|
+
signer.digest!(signer.binary_security_token_node)
|
63
|
+
|
64
|
+
signer.sign!
|
65
|
+
|
66
|
+
output_xml_file = File.join(File.dirname(__FILE__), 'fixtures', 'output_1_sha256.xml')
|
67
|
+
|
68
|
+
signer.to_xml.should == Nokogiri::XML(File.read(output_xml_file), &:noblanks).to_xml(:save_with => 0)
|
69
|
+
end
|
70
|
+
|
50
71
|
it "should sign simple XML" do
|
51
72
|
input_xml_file = File.join(File.dirname(__FILE__), 'fixtures', 'input_2.xml')
|
52
73
|
cert_file = File.join(File.dirname(__FILE__), 'fixtures', 'cert.pem')
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: signer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Edgars Beigarts
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-06-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -59,6 +59,7 @@ executables: []
|
|
59
59
|
extensions: []
|
60
60
|
extra_rdoc_files: []
|
61
61
|
files:
|
62
|
+
- lib/signer/digester.rb
|
62
63
|
- lib/signer/version.rb
|
63
64
|
- lib/signer.rb
|
64
65
|
- README.md
|
@@ -70,6 +71,7 @@ files:
|
|
70
71
|
- spec/fixtures/input_3_c14n_comments.xml
|
71
72
|
- spec/fixtures/key.pem
|
72
73
|
- spec/fixtures/output_1.xml
|
74
|
+
- spec/fixtures/output_1_sha256.xml
|
73
75
|
- spec/fixtures/output_2.xml
|
74
76
|
- spec/fixtures/output_3_c14n_comments.xml
|
75
77
|
- spec/signer_spec.rb
|
@@ -104,6 +106,7 @@ test_files:
|
|
104
106
|
- spec/fixtures/input_3_c14n_comments.xml
|
105
107
|
- spec/fixtures/key.pem
|
106
108
|
- spec/fixtures/output_1.xml
|
109
|
+
- spec/fixtures/output_1_sha256.xml
|
107
110
|
- spec/fixtures/output_2.xml
|
108
111
|
- spec/fixtures/output_3_c14n_comments.xml
|
109
112
|
- spec/signer_spec.rb
|