scep 0.0.1
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 +15 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/README.md +144 -0
- data/Rakefile +2 -0
- data/lib/scep/endpoint.rb +169 -0
- data/lib/scep/keypair.rb +69 -0
- data/lib/scep/pkcs7_cert_only.rb +86 -0
- data/lib/scep/pki_operation/base.rb +121 -0
- data/lib/scep/pki_operation/proxy.rb +117 -0
- data/lib/scep/pki_operation/request.rb +70 -0
- data/lib/scep/pki_operation/response.rb +86 -0
- data/lib/scep/pki_operation.rb +10 -0
- data/lib/scep/version.rb +3 -0
- data/lib/scep.rb +41 -0
- data/scep.gemspec +32 -0
- data/spec/console +7 -0
- data/spec/fixtures/self-signed.crt +18 -0
- data/spec/fixtures/self-signed.csr +14 -0
- data/spec/fixtures/self-signed.key +15 -0
- data/spec/lib/scep/endpoint_spec.rb +131 -0
- data/spec/lib/scep/keypair_spec.rb +30 -0
- data/spec/lib/scep/pkcs7_cert_only_spec.rb +42 -0
- data/spec/lib/scep/pki_operation/base_spec.rb +56 -0
- data/spec/lib/scep/pki_operation/proxy_spec.rb +45 -0
- data/spec/lib/scep/pki_operation/request_spec.rb +55 -0
- data/spec/lib/scep/pki_operation/response_spec.rb +36 -0
- data/spec/spec_helper.rb +61 -0
- metadata +171 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ceb79fd8756349928c7653f5c59191e3cc8cbfe7
|
4
|
+
data.tar.gz: 9f322dce3a3d7a45f7ae7c220d67b2908dc57380
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 26c7b9378ffd100b39788cd61973e568b4413887bb6dac749ff68bb5d29419fb34028eade582235cd29970f9f27cdeeb39d73eb542ba011a0bddf4c5dc2f44fc
|
7
|
+
data.tar.gz: c864e7169c4ab8403d706b294cd48bfc535d010807d5e793b7347bd5463659095ea702db69cef2be1c2956afab5400c4e435b4d3a5d0809c220a972018b87d43
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.2.0
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
SCEP Gem
|
2
|
+
========
|
3
|
+
Libraries that allow you to be a SCEP server, be a SCEP proxy, etc.
|
4
|
+
|
5
|
+
If you believe you have discovered a security vulnerability in this gem, please email security@onelogin.com with a description. You can also use the form based submission located here: https://www.onelogin.com/security. We follow responsible disclosure guidelines, and will work with you to quickly find a resolution.
|
6
|
+
|
7
|
+
## Terminology
|
8
|
+
|
9
|
+
* CSR - Certificate Signing Request. "I want you to sign my public key so you know who I am in the future"
|
10
|
+
* CA - Certificate Authority. The ultimate authority who can sign certificates.
|
11
|
+
* RA - Registration Authority. RA Certificates must be signed by CA. Accepts CSR on behalf of CA.
|
12
|
+
Forwards CSR to CA for signing. You can proxy through to multiple CA
|
13
|
+
|
14
|
+
Note that a CA can be an RA.
|
15
|
+
|
16
|
+
|
17
|
+
## SCEP Requests
|
18
|
+
Contains a CSR that requires signing
|
19
|
+
|
20
|
+
### Encrypt request
|
21
|
+
When we want to forward a CSR to a RA. We may or may not be an RA ourselves.
|
22
|
+
|
23
|
+
First,collect some certificates:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
their_ra_cert = OpenSSL::X509::Certificate.new File.read(their-ra.crt')
|
27
|
+
our_keypair = SCEP::Keypair.read 'our.crt', 'our.key'
|
28
|
+
csr = OpenSSL::X509::Request.new File.read('some.csr')
|
29
|
+
```
|
30
|
+
|
31
|
+
Now create a request object and get the encrypted result:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
request = SCEP::PKIOperation::Request.new(our_keypair)
|
35
|
+
request.csr = csr
|
36
|
+
encrypted = request.encrypt(their_ra_cert)
|
37
|
+
```
|
38
|
+
|
39
|
+
We can then send `encrypted` to an RA
|
40
|
+
|
41
|
+
### Decrypt Request
|
42
|
+
When we are an RA.
|
43
|
+
|
44
|
+
First, collect some certificates:
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
their_cert = OpenSSL::X509::Certificate.new File.read('their.crt')
|
48
|
+
our_ra_keypair = SCEP::Keypair.read 'our-ra.crt', 'our-ra.key'
|
49
|
+
```
|
50
|
+
|
51
|
+
Now we can make a request object and get the original CSR:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
request = SCEP::PKIOperation::Request.new(our_ra_keypair)
|
55
|
+
request.verify_against(their_cert) # Make sure the response was signed by someone we trust
|
56
|
+
|
57
|
+
# Fails decryption if not signed by `their_cert`
|
58
|
+
request.decrypt(encrypted_request)
|
59
|
+
|
60
|
+
# Will always decrypt and ignore signature verification
|
61
|
+
request.decrypt(encrypted_request, false)
|
62
|
+
|
63
|
+
# Now get the encoded CSR
|
64
|
+
puts request.csr # => OpenSSL::X509::Request
|
65
|
+
```
|
66
|
+
|
67
|
+
### Proxying a Request
|
68
|
+
If we are an RA, we can decrypt a request intended for us and re-encrypt it for another RA
|
69
|
+
and grab the CSR in the process
|
70
|
+
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
their_cert = OpenSSL::X509::Certificate.new File.read('their.crt')
|
74
|
+
their_ra_cert = OpenSSL::X509::Certificate.new File.read('their-ra.crt')
|
75
|
+
our_ra_keypair = SCEP::Keypair.read 'our-ra.crt', 'our-ra.key'
|
76
|
+
|
77
|
+
request = SCEP::PKIOperation::Request.new(our_ra_keypair)
|
78
|
+
request.verify_against(their_cert)
|
79
|
+
newly_encrypted = request.proxy(original_encrypted_result, their_ra_cert)
|
80
|
+
p request.csr # => OpenSSL::X509::Request
|
81
|
+
```
|
82
|
+
|
83
|
+
## SCEP Response
|
84
|
+
Contains the signed X509 Certificate
|
85
|
+
|
86
|
+
### Encrypting a Response
|
87
|
+
When we are an RA
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
their_cert = OpenSSL::X509::Certificate.new File.read('their.crt')
|
91
|
+
our_ra_keypair = SCEP::Keypair.read 'our-ra.crt', 'our-ra.key'
|
92
|
+
|
93
|
+
signed_cert = OpenSSL::X509::Certificate.new File.read('cert-signed-by-ca.crt')
|
94
|
+
|
95
|
+
response = SCEP::PKIOperation::Response.new(our_ra_keypair)
|
96
|
+
encrypted = response.encrypt(their_cert)
|
97
|
+
```
|
98
|
+
|
99
|
+
### Decrypting a Response
|
100
|
+
When we are decrypting information from an RA
|
101
|
+
|
102
|
+
```ruby
|
103
|
+
their_ra_cert = OpenSSL::X509::Certificate.new File.read('their-ra.crt')
|
104
|
+
our_keypair = SCEP::Keypair.read 'our.crt', 'our.key'
|
105
|
+
|
106
|
+
response = SCEP::PKIOperation::Response.new(our_keypair)
|
107
|
+
response.verify_against(their_ra_cert)
|
108
|
+
response.decrypt(encrypted_response)
|
109
|
+
p response.signed_certificates # => [OpenSSL::X509::Certificate]
|
110
|
+
```
|
111
|
+
|
112
|
+
## SCEP Proxy
|
113
|
+
|
114
|
+
Easily be a SCEP proxy (psuedo sinatra syntax):
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
require 'scep'
|
118
|
+
|
119
|
+
ra_keypair = SCEP::Keypair.read('certs/ra.crt', 'certs/ra.key')
|
120
|
+
scep_server = SCEP::Endpoint.new 'https://some-final-endpoint.com'
|
121
|
+
|
122
|
+
post '/scep?operation=PKIOperation' do
|
123
|
+
proxy = SCEP::PKIOperation::Proxy.new(server, ra_keypair)
|
124
|
+
|
125
|
+
# Options to verify certificates:
|
126
|
+
|
127
|
+
# Verify request came from apple certificate
|
128
|
+
proxy.add_request_verification_certificate(@apple_cert) # OpenSSL::X509::Certificate
|
129
|
+
|
130
|
+
# Verify response came from CA certificate
|
131
|
+
proxy.add_response_verificaion_certificate(@ca_certificate) # OpenSSL::X509::Certificate
|
132
|
+
|
133
|
+
# Or don't verify anything
|
134
|
+
proxy.no_verify!
|
135
|
+
|
136
|
+
result = proxy.forward_pki_request(request.raw_post)
|
137
|
+
|
138
|
+
puts result.csr # The CSR they sent
|
139
|
+
puts result.signed_certificates # Returned signed certs from the SCEP server
|
140
|
+
|
141
|
+
headers['content-type'] = 'application/x-pki-message'
|
142
|
+
render results.p7enc_response.to_der
|
143
|
+
end
|
144
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module SCEP
|
5
|
+
# Handles making requests to a SCEP server and storing the RA and CA certs. Currently uses
|
6
|
+
# the URL defined in the `config/endpoints.yml` file.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# scep_endpoint = SCEP::Endpoint.new('https://scep-server-url.com')
|
10
|
+
# # Downloads RA, CA certs
|
11
|
+
# puts scep_endpoint.ca_certificate # => OpenSSL::X509::Certificate
|
12
|
+
# puts scep_endpoint.ra_certificate
|
13
|
+
#
|
14
|
+
# @todo GetCACaps
|
15
|
+
class Endpoint
|
16
|
+
include HTTParty
|
17
|
+
include SCEP::Loggable
|
18
|
+
|
19
|
+
# An exception raised if the SCEP server does not properly support the SCEP protocol.
|
20
|
+
class ProtocolError < StandardError; end
|
21
|
+
|
22
|
+
default_timeout 2
|
23
|
+
|
24
|
+
attr_writer :ra_certificate
|
25
|
+
|
26
|
+
attr_writer :ca_certificate
|
27
|
+
|
28
|
+
attr_accessor :default_options
|
29
|
+
|
30
|
+
def initialize(base_uri, default_options = {})
|
31
|
+
@default_options = default_options.merge(:base_uri => base_uri)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Gets the CA certificate. Will automatically download the CA certificate from
|
35
|
+
# the server if it has not yet been downloaded.
|
36
|
+
# @return [OpenSSL::X509::Certificate]
|
37
|
+
# @raise [ProtocolError] if the SCEP server does not return valid certs
|
38
|
+
def ca_certificate
|
39
|
+
download_certificates if @ca_certificate.nil?
|
40
|
+
return @ca_certificate
|
41
|
+
end
|
42
|
+
|
43
|
+
# Gets the RA certificate.
|
44
|
+
# @return [OpenSSL::X509::Certificate]
|
45
|
+
# @raise [ProtocolError] if the SCEP server does not return valid certs
|
46
|
+
# @note This will return the {#ca_certificate CA certificate} if the SCEP server does not
|
47
|
+
# support RA certs.
|
48
|
+
def ra_certificate
|
49
|
+
# Force download of CA, possibly RA certificate
|
50
|
+
@ra_certificate || ca_certificate
|
51
|
+
end
|
52
|
+
|
53
|
+
# Checks to see if the SCEP server supports the RA certificate
|
54
|
+
# @return [Boolean]
|
55
|
+
def supports_ra_certificate?
|
56
|
+
ca_certificate != ra_certificate
|
57
|
+
end
|
58
|
+
|
59
|
+
# Downloads RA and CA certificates from the SCEP server using the `GetCACert` operation.
|
60
|
+
# Will give {#ra_certificate} and {#ca_certificate} values.
|
61
|
+
# @return [HTTParty::Response] the response from the SCEP server.
|
62
|
+
# @raise [ProtocolError] if the
|
63
|
+
def download_certificates
|
64
|
+
logger.debug 'Downloading CA, possibly RA certificate from SCEP server'
|
65
|
+
response = scep_request 'GetCACert'
|
66
|
+
if response.content_type == 'application/x-x509-ca-cert' # Only a CA cert
|
67
|
+
handle_ca_only_cert(response.body)
|
68
|
+
elsif response.content_type == 'application/x-x509-ca-ra-cert'
|
69
|
+
handle_ca_ra_cert(response.body)
|
70
|
+
else
|
71
|
+
fail ProtocolError, "SCEP server returned invalid content type of #{response.content_type}"
|
72
|
+
end
|
73
|
+
return response
|
74
|
+
end
|
75
|
+
alias_method :get_ca_cert, :download_certificates
|
76
|
+
|
77
|
+
|
78
|
+
# Gets server capabilities. Memoized version of {#fetch_capabilities}
|
79
|
+
# @return [Set<String>] a set of capabilities
|
80
|
+
def capabilities
|
81
|
+
@capabilities || fetch_capabilities
|
82
|
+
end
|
83
|
+
|
84
|
+
# Gets server capabilities. Always triggers a download of capabilities
|
85
|
+
# @return [Set<String>] a set of capabilities
|
86
|
+
def fetch_capabilities
|
87
|
+
logger.debug 'Getting SCEP endpoint capabilities'
|
88
|
+
response = scep_request 'GetCACaps'
|
89
|
+
caps = response.body.strip.split("\n")
|
90
|
+
@capabilities = Set.new(caps)
|
91
|
+
logger.debug "SCEP endpoint supports capabilities: #{@capabilities.inspect}"
|
92
|
+
return @capabilities
|
93
|
+
end
|
94
|
+
|
95
|
+
# Whether the SCEP endpoint supports the POSTPKIOperation
|
96
|
+
# @return [Boolean] TRUE if it is supported, FALSE otherwise
|
97
|
+
def post_pki_operation?
|
98
|
+
capabilities.include?('POSTPKIOperation')
|
99
|
+
end
|
100
|
+
|
101
|
+
# Executes a SCEP request.
|
102
|
+
# @param [String] operation the SCEP operation to perform
|
103
|
+
# @param [String] message an optional message to send
|
104
|
+
# @return [HTTParty::Response] the httparty response
|
105
|
+
def scep_request(operation, message = nil, is_post = false)
|
106
|
+
query = { :operation => operation }
|
107
|
+
query[:message] = message unless message.nil?
|
108
|
+
if is_post
|
109
|
+
logger.debug "Executing POST ?operation=#{operation}"
|
110
|
+
response = self.class.post '/', { :query => { :operation => operation}, :body => message }.merge(default_options)
|
111
|
+
else
|
112
|
+
logger.debug "Executing GET ?operation=#{operation}&message=#{message}"
|
113
|
+
response = self.class.get '/', { :query => query }.merge(default_options)
|
114
|
+
end
|
115
|
+
|
116
|
+
if response.code != 200
|
117
|
+
raise ProtocolError, "SCEP request returned non-200 code of #{response.code}"
|
118
|
+
end
|
119
|
+
|
120
|
+
return response
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
# @todo: handle GET PKIOperations
|
125
|
+
# @todo: verify actually signed by CA?
|
126
|
+
# @param [String] payload the raw payload to send
|
127
|
+
# @return [String] the response body
|
128
|
+
def pki_operation(payload)
|
129
|
+
response = scep_request('PKIOperation', payload, true)
|
130
|
+
if response.content_type != 'application/x-pki-message'
|
131
|
+
raise ProtocolError,
|
132
|
+
"SCEP PKIOperation didn't return content-type of application/x-pki-message (returned #{response.content_type})"
|
133
|
+
end
|
134
|
+
return response.body
|
135
|
+
end
|
136
|
+
|
137
|
+
protected
|
138
|
+
|
139
|
+
def handle_ca_only_cert(response_body)
|
140
|
+
logger.debug 'SCEP server does not support RA certificate - only using CA cert'
|
141
|
+
@ca_certificate = OpenSSL::X509::Certificate.new(response_body)
|
142
|
+
rescue StandardError
|
143
|
+
fail ProtocolError, 'SCEP server did not return parseable X509::Certificate'
|
144
|
+
end
|
145
|
+
|
146
|
+
def handle_ca_ra_cert(response_body)
|
147
|
+
logger.debug 'SCEP server has both RA and CA certificate'
|
148
|
+
|
149
|
+
begin
|
150
|
+
pcerts = PKCS7CertOnly.decode(response_body)
|
151
|
+
rescue StandardError
|
152
|
+
fail ProtocolError, 'SCEP server did not return a parseable PKCS#7'
|
153
|
+
end
|
154
|
+
|
155
|
+
fail ProtocolError,
|
156
|
+
'SCEP server did not return two certificates in PKCS#7 cert chain' unless
|
157
|
+
pcerts.certificates.length == 2
|
158
|
+
|
159
|
+
|
160
|
+
unless pcerts.certificates[1].verify(pcerts.certificates[0].public_key)
|
161
|
+
fail ProtocolError,
|
162
|
+
'RA certificate must be signed by CA certificate when using RA/CA cert combination'
|
163
|
+
end
|
164
|
+
|
165
|
+
@ca_certificate = pcerts.certificates[0]
|
166
|
+
@ra_certificate = pcerts.certificates[1]
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
data/lib/scep/keypair.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
module SCEP
|
2
|
+
# A public / private keypair
|
3
|
+
class Keypair
|
4
|
+
|
5
|
+
# The various cryptosystems we support. Used mostly for ruby 1.8.7, which cannot
|
6
|
+
# automatically determine which cryptosystem is being used.
|
7
|
+
SUPPORTED_CRYPTOSYSTEMS = [
|
8
|
+
OpenSSL::PKey::RSA,
|
9
|
+
OpenSSL::PKey::DSA,
|
10
|
+
OpenSSL::PKey::EC,
|
11
|
+
OpenSSL::PKey::DH
|
12
|
+
]
|
13
|
+
|
14
|
+
# @return [OpenSSL::X509::Certificate]
|
15
|
+
attr_accessor :certificate
|
16
|
+
alias_method :cert, :certificate
|
17
|
+
|
18
|
+
# @return [OpenSSL::PKey]
|
19
|
+
attr_accessor :private_key
|
20
|
+
|
21
|
+
def initialize(certificate, private_key)
|
22
|
+
raise ArgumentError, '`certificate` must be an OpenSSL::X509::Certificate' unless
|
23
|
+
certificate.is_a?(OpenSSL::X509::Certificate)
|
24
|
+
|
25
|
+
@certificate = certificate
|
26
|
+
@private_key = private_key
|
27
|
+
end
|
28
|
+
|
29
|
+
# Loads a keypair from a file
|
30
|
+
# @param [String] certificate_filepath
|
31
|
+
# @param [String] private_key_filepath
|
32
|
+
# @param [String] private_key_passphrase add this if you
|
33
|
+
# @return [Keypair]
|
34
|
+
def self.read(certificate_filepath, private_key_filepath, private_key_passphrase = nil)
|
35
|
+
x509_cert = OpenSSL::X509::Certificate.new File.read(certificate_filepath.to_s)
|
36
|
+
pkey = read_private_key(File.open(private_key_filepath.to_s).read, private_key_passphrase)
|
37
|
+
new(x509_cert, pkey)
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
|
42
|
+
# Reads a DER or PEM encoded private key that is one of the {SUPPORTED_CRYPTOSYSTEMS}. In
|
43
|
+
# ruby 1.9+ we can do this easily. In ruby 1.8.7 we have to keep on guessing until we get
|
44
|
+
# it right.
|
45
|
+
def self.read_private_key(encoded_key, passphrase = nil)
|
46
|
+
# Ruby 1.9.3+
|
47
|
+
if OpenSSL::PKey.respond_to?(:read)
|
48
|
+
OpenSSL::PKey.read encoded_key, passphrase
|
49
|
+
|
50
|
+
# Ruby 1.8.7 - keep on guessing which cryptosystem until we're correct
|
51
|
+
else
|
52
|
+
SUPPORTED_CRYPTOSYSTEMS.each do |system|
|
53
|
+
begin
|
54
|
+
return system.new(encoded_key, passphrase)
|
55
|
+
rescue
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# If we're here, then the file is probably invalid
|
60
|
+
raise UnsupportedCryptosystem,
|
61
|
+
"Either private key is invalid, passphrase is invalid, or does not support one " \
|
62
|
+
"of cryptosystems: #{SUPPORTED_CRYPTOSYSTEMS.map(&:name).join(', ')}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
class UnsupportedCryptosystem < StandardError; end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module SCEP
|
2
|
+
# Workaround for issue generating PKCS#7 certificate only in ruby 2.x.
|
3
|
+
#
|
4
|
+
# Normally you could generate the certificate chain using the following code:
|
5
|
+
#
|
6
|
+
# ```ruby
|
7
|
+
# p7certs = OpenSSL::PKCS7.new
|
8
|
+
# p7certs.type = 'signed'
|
9
|
+
# p7certs.certificates = [x509_cert_1, x509_cert_2]
|
10
|
+
# ````
|
11
|
+
#
|
12
|
+
# But producing this in a der format is not valid:
|
13
|
+
#
|
14
|
+
# ```ruby
|
15
|
+
# der = p7certs.to_der
|
16
|
+
# p7decoded = OpenSSL::PKCS7.new(der) # exception!
|
17
|
+
# ```
|
18
|
+
#
|
19
|
+
# This class manually creates the ASN1 notation and creates a correctly formatted result:
|
20
|
+
#
|
21
|
+
# ```ruby
|
22
|
+
# p7certs = Pkcs7CertOnly.new([x509_cert_1, x509_cert_2])
|
23
|
+
# der = p7certs.to_der
|
24
|
+
# p7decoded = OpenSSL::PKCS7.new(der) # works!
|
25
|
+
# p7decoded.certificates # => [ array of the original x509 certificates ]
|
26
|
+
# ```
|
27
|
+
#
|
28
|
+
# @see https://groups.google.com/forum/#!topic/mailing.openssl.users/AIZndhJuG7I
|
29
|
+
# @see https://gist.github.com/cgthornt/fe1f9d68e18cc4d1ba20
|
30
|
+
class PKCS7CertOnly
|
31
|
+
include OpenSSL::ASN1
|
32
|
+
|
33
|
+
# @return [Array<OpenSSL::X509::Certificate>]
|
34
|
+
attr_accessor :certificates
|
35
|
+
|
36
|
+
def initialize(certificates = [])
|
37
|
+
@certificates = certificates
|
38
|
+
end
|
39
|
+
|
40
|
+
# Takes a binary encoded DER PKCS#7 certificates only payload and decodes it
|
41
|
+
# @param [String] der_encoded the encoded payload
|
42
|
+
# @return [Pkcs7CertOnly]
|
43
|
+
def self.decode(der_encoded)
|
44
|
+
p7certs = OpenSSL::PKCS7.new(der_encoded)
|
45
|
+
new(p7certs.certificates)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Converts this into an ASN1 sequence
|
49
|
+
# @return [OpenSSL::ASN1::Sequence]
|
50
|
+
def to_asn1
|
51
|
+
|
52
|
+
# Converts to an array of ASN1 encoded certs
|
53
|
+
asn1_certs = certificates.map do |cert|
|
54
|
+
decode(cert.to_der)
|
55
|
+
end
|
56
|
+
|
57
|
+
Sequence.new([
|
58
|
+
OpenSSL::ASN1::ObjectId.new('1.2.840.113549.1.7.2'),
|
59
|
+
ASN1Data.new([
|
60
|
+
Sequence.new([
|
61
|
+
OpenSSL::ASN1::Integer.new(1),
|
62
|
+
OpenSSL::ASN1::Set.new([]),
|
63
|
+
Sequence.new([
|
64
|
+
OpenSSL::ASN1::ObjectId.new('1.2.840.113549.1.7.1')
|
65
|
+
]),
|
66
|
+
ASN1Data.new(asn1_certs, 0, :CONTEXT_SPECIFIC),
|
67
|
+
ASN1Data.new([], 1, :CONTEXT_SPECIFIC),
|
68
|
+
OpenSSL::ASN1::Set.new([])
|
69
|
+
])
|
70
|
+
], 0, :CONTEXT_SPECIFIC)
|
71
|
+
])
|
72
|
+
end
|
73
|
+
|
74
|
+
# Gets this in a der (binary) format
|
75
|
+
# @return [String] binary encoded format
|
76
|
+
def to_der
|
77
|
+
to_asn1.to_der
|
78
|
+
end
|
79
|
+
|
80
|
+
# Gets this as a PKCS7 object
|
81
|
+
# @return [OpenSSL::PKCS7]
|
82
|
+
def to_pkcs7
|
83
|
+
OpenSSL::PKCS7.new(to_der)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module SCEP
|
2
|
+
module PKIOperation
|
3
|
+
|
4
|
+
# Base class that contains commonalities between both requests and repsonses:
|
5
|
+
#
|
6
|
+
# * {#ra_certificate RA Certificate}
|
7
|
+
# * {#ra_private_key RA Private Key}
|
8
|
+
#
|
9
|
+
class Base
|
10
|
+
DEFAULT_CIPHER_ALGORITHM = 'aes-256-cbc'
|
11
|
+
|
12
|
+
# Our keypair
|
13
|
+
# @return [Keypair]
|
14
|
+
attr_accessor :ra_keypair
|
15
|
+
|
16
|
+
# The last signed payload
|
17
|
+
# @return [OpenSSL::PKCS7]
|
18
|
+
attr_reader :p7sign
|
19
|
+
|
20
|
+
# The last encrypted payload
|
21
|
+
# @return [OpenSSL::PKCS7]
|
22
|
+
attr_reader :p7enc
|
23
|
+
|
24
|
+
# The store of trusted certs
|
25
|
+
# @return [OpenSSL::X509::Store]
|
26
|
+
attr_writer :x509_store
|
27
|
+
|
28
|
+
# Creates a new payload
|
29
|
+
# @param [Keypair] ra_keypair
|
30
|
+
def initialize(ra_keypair)
|
31
|
+
@ra_keypair = ra_keypair
|
32
|
+
end
|
33
|
+
|
34
|
+
# Gets an x509 store. Defaults to a store with system default paths. Used for
|
35
|
+
# {#unsign_and_unencrypt_raw decryption}.
|
36
|
+
# @return [OpenSSL::X509::Store]
|
37
|
+
def x509_store
|
38
|
+
@x509_store ||= OpenSSL::X509::Store.new
|
39
|
+
end
|
40
|
+
|
41
|
+
# Adds a certificate to verify against
|
42
|
+
# @param [OpenSSL::X509::Certificate] cert
|
43
|
+
def add_verification_certificate(cert)
|
44
|
+
x509_store.add_cert(cert)
|
45
|
+
end
|
46
|
+
alias_method :verify_against, :add_verification_certificate
|
47
|
+
|
48
|
+
protected
|
49
|
+
|
50
|
+
# Takes a raw binary string and returns the raw, unencrypted data
|
51
|
+
# @param [String] signed_and_encrypted_csr the signed and encrypted data
|
52
|
+
# @param [Boolean] verify if TRUE, verifies the signed PKCS7 payload against the {#x509_store}
|
53
|
+
# @raise [SCEP::PKIOperation::VerificationFailed] if `verify` is TRUE and the signed payload
|
54
|
+
# was *not* verified against the {#x509_store}.
|
55
|
+
# @return [String] the decrypted and unsigned data (original format)
|
56
|
+
def unsign_and_unencrypt_raw(signed_and_encrypted_csr, verify = true)
|
57
|
+
# Remove signature
|
58
|
+
@p7sign = OpenSSL::PKCS7.new(signed_and_encrypted_csr)
|
59
|
+
|
60
|
+
flags = OpenSSL::PKCS7::BINARY
|
61
|
+
flags |= OpenSSL::PKCS7::NOVERIFY unless verify
|
62
|
+
|
63
|
+
# See http://openssl.6102.n7.nabble.com/pkcs7-verification-with-ruby-td28455.html
|
64
|
+
verified = @p7sign.verify([], x509_store, nil, flags)
|
65
|
+
|
66
|
+
if !verified
|
67
|
+
raise SCEP::PKIOperation::VerificationFailed,
|
68
|
+
'Unable to verify signature against certificate store - did you add the correct certificates?'
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
# Decrypt
|
73
|
+
@p7enc = OpenSSL::PKCS7.new(@p7sign.data)
|
74
|
+
@p7enc.decrypt(ra_keypair.private_key, ra_keypair.certificate, OpenSSL::PKCS7::BINARY)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Signs and encrypts the given raw data
|
78
|
+
# @param [String] raw_data the raw data to sign and encrypt
|
79
|
+
# @param [OpenSSL::X509::Certificate] target_encryption_certs the cert(s) to encrypt for
|
80
|
+
# @param [OpenSSL::Cipher::Cipher] cipher the cipher to use. Defaults to {.create_default_cipher}
|
81
|
+
# @return [OpenSSL::PKCS7] the signed and encrypted payload
|
82
|
+
def sign_and_encrypt_raw(raw_data, target_encryption_certs, cipher = nil)
|
83
|
+
cipher ||= self.class.create_default_cipher
|
84
|
+
|
85
|
+
encrypted = OpenSSL::PKCS7.encrypt(
|
86
|
+
wrap_array(target_encryption_certs),
|
87
|
+
raw_data,
|
88
|
+
cipher,
|
89
|
+
OpenSSL::PKCS7::BINARY)
|
90
|
+
|
91
|
+
OpenSSL::PKCS7.sign(
|
92
|
+
ra_keypair.certificate,
|
93
|
+
ra_keypair.private_key,
|
94
|
+
encrypted.to_der,
|
95
|
+
[ra_keypair.certificate],
|
96
|
+
OpenSSL::PKCS7::BINARY)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Creates an {OpenSSL::Cipher} using the {DEFAULT_CIPHER_ALGORITHM}. It's best to create a new Cipher object
|
100
|
+
# for every new encryption call so that we don't re-use sensitive data (IV's) [citation needed].
|
101
|
+
# @return [OpenSSL::Cipher]
|
102
|
+
def self.create_default_cipher
|
103
|
+
OpenSSL::Cipher.new(DEFAULT_CIPHER_ALGORITHM)
|
104
|
+
end
|
105
|
+
|
106
|
+
protected
|
107
|
+
|
108
|
+
# Same as `Array.wrap`
|
109
|
+
# @see http://apidock.com/rails/Array/wrap/class
|
110
|
+
def wrap_array(object)
|
111
|
+
if object.nil?
|
112
|
+
[]
|
113
|
+
elsif object.respond_to?(:to_ary)
|
114
|
+
object.to_ary || [object]
|
115
|
+
else
|
116
|
+
[object]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|