├── .gitignore
├── .rspec
├── .travis.yml
├── Gemfile
├── LICENSE.md
├── README.md
├── Rakefile
├── config.ru
├── config.yaml.example
├── lib
└── r509
│ └── certificateauthority
│ └── http
│ ├── config.rb
│ ├── factory.rb
│ ├── server.rb
│ ├── subjectparser.rb
│ ├── validityperiodconverter.rb
│ ├── version.rb
│ └── views
│ ├── test_issue.erb
│ ├── test_revoke.erb
│ └── test_unrevoke.erb
├── r509-ca-http.gemspec
└── spec
├── fixtures
├── test_ca.cer
├── test_ca.key
└── test_config.yaml
├── http_spec.rb
├── spec_helper.rb
├── subject_parser_spec.rb
└── validity_period_converter_spec.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .yardoc
3 | cert_data/test_ca/crl_number.txt
4 | coverage
5 | doc
6 | *.gem
7 | config.yaml
8 | log
9 | Gemfile.lock
10 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color -f doc
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | bundler_args: --without documentation
2 | language: ruby
3 | rvm:
4 | - 1.9.3
5 | - 2.0.0
6 | - 2.1.0
7 | - ruby-head
8 | - rbx
9 |
10 | notifications:
11 | irc:
12 | channels:
13 | - "irc.freenode.org#r509"
14 | use_notice: true
15 | skip_join: true
16 | on_success: always
17 | on_failure: always
18 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem 'coveralls', require: false
4 | platforms :rbx do
5 | gem "rubysl-ipaddr"
6 | gem "rubysl-singleton"
7 | gem "rubysl-base64"
8 | gem "rubinius-coverage"
9 | end
10 | gem "r509"
11 | #gem "r509-middleware-validity"
12 | #gem "r509-middleware-certwriter"
13 | #gem "r509-validity-redis"
14 | gem "dependo"
15 | gem "sinatra"
16 | gemspec
17 | group :documentation do
18 | gem "yard", "~>0.8"
19 | gem "redcarpet", "~>2.2.2"
20 | gem "github-markup", ">=0.7.5"
21 | end
22 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright 2011 Paul Kehrer, Sean Schulte
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #r509-ca-http [](http://travis-ci.org/r509/r509-ca-http) [](https://coveralls.io/r/r509/r509-ca-http)
2 |
3 | r509-ca-http is an HTTP server that runs a certificate authority, for signing SSL certificates. It supports issuance and revocation, and is intended to be part of a complete certificate authority for use in production environments.
4 |
5 | ##Requirements/Installation
6 |
7 | You need [r509](https://github.com/r509/r509) and sinatra. For development/tests you need rack-test and rspec.
8 |
9 | ## API
10 |
11 | ### GET /1/crl/:ca/get
12 |
13 | Deprecated; will be removed in a future version. Use generate instead.
14 |
15 |
16 | ### GET /1/crl/:ca/generate
17 |
18 | Generate and get a new CRL for the given ```:ca```.
19 |
20 | ### POST /1/certificate/issue
21 |
22 | Issue a certificate.
23 |
24 | Required POST parameters:
25 |
26 | - ca
27 | - profile
28 | - validityPeriod (in seconds)
29 | - csr (or spki)
30 | - subject
31 |
32 | The subject is provided like so:
33 |
34 | subject[CN]=domain.com&subject[O]=orgname&subject[L]=locality
35 |
36 | Optional POST parameters:
37 |
38 | - extensions[subjectAlternativeName]
39 | - message\_digest
40 |
41 | SAN names are provided like so:
42 |
43 | extensions[subjectAlternativeName][]=domain1.com&extensions[subjectAlternativeName][]=domain2.com
44 |
45 | The issue method will return the PEM text of the issued certificate.
46 |
47 | Please note that all fields subject/extension request fields encoded in a CSR are ignored in favor of the POST parameters.
48 |
49 | ### POST /1/certificate/revoke
50 |
51 | Revoke a certificate.
52 |
53 | Required POST parameters:
54 |
55 | - ca
56 | - serial
57 |
58 | Optional POST parameters:
59 |
60 | - reason (must be an integer or nil. nil by default)
61 |
62 | The revoke method returns the newly generated CRL, after revocation.
63 |
64 | ### POST /1/certificate/unrevoke
65 |
66 | Unrevoke a certificate. (IE, remove it from the CRL and return its OCSP status to valid.)
67 |
68 | Required POST parameters:
69 |
70 | - ca
71 | - serial
72 |
73 | The unrevoke method returns the newly generated CRL, after the certificate was removed from it.
74 |
75 | ## Helper pages
76 |
77 | These pages are present on the server, for you to work with the CA with a basic web interface. You should _not_ expose these endpoints to anyone.
78 |
79 | - /test/certificate/issue
80 |
81 | - /test/certificate/revoke
82 |
83 | - /test/certificate/unrevoke
84 |
85 | ## certificate\_authorities (config.yaml)
86 |
87 | You use the ```config.yaml``` file to specify information about your certificate authority. You can operate multiple certificate authorities, each of which can have multiple profiles, with one instance of r509-ca-http.
88 |
89 | Information about how to construct the YAML can be found at [the official r509 documentation](https://github.com/r509/r509).
90 |
91 | ## Middleware (config.ru)
92 |
93 | Running r509-ca-http will let you issue and revoke certificates. But that's not everything you need to do, if you're going to run a CA. You're going to need information about validity, and you may want to save a record of issued certificates to the filesystem.
94 |
95 | For that, we've created a few pieces of Rack middleware for your use.
96 |
97 | - [r509-middleware-validity](https://github.com/r509/r509-middleware-validity)
98 | - [r509-middleware-certwriter](https://github.com/r509/r509-middleware-certwriter)
99 |
100 | After installing one or both of them, you'll have to edit your ```config.ru``` and/or ```config.yaml``` files.
101 |
102 | ##Signals
103 |
104 | You can send a kill -USR2 signal to any running r509-ca-http process to cause it to reload and print its config to the logs (provided your app server isn't trapping USR2 first).
105 |
106 | ##Support
107 |
108 | You can file bugs on GitHub or join the #r509 channel on irc.freenode.net to ask questions.
109 |
110 | ## Rake tasks
111 |
112 | There are a few things you can do with Rake.
113 |
114 | ```rake spec```
115 |
116 | Run all the tests.
117 |
118 | ```rake gem:build```
119 |
120 | Build a gem file.
121 |
122 | ```rake gem:install```
123 |
124 | Install the gem you just built.
125 |
126 | ```rake gem:uninstall```
127 |
128 | Uninstall r509-ca-http.
129 |
130 | ```rake yard```
131 |
132 | Generate documentation.
133 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'rubygems'
2 | require 'rspec/core/rake_task'
3 | require "#{File.dirname(__FILE__)}/lib/r509/certificateauthority/http/version"
4 |
5 | task :default => :spec
6 | RSpec::Core::RakeTask.new(:spec) do
7 | ENV['RACK_ENV'] = 'test'
8 | end
9 |
10 | desc 'Run all rspec tests with rcov (1.8 only)'
11 | RSpec::Core::RakeTask.new(:rcov) do |t|
12 | t.rcov_opts = %q[--exclude "spec,gems"]
13 | t.rcov = true
14 | end
15 |
16 | namespace :gem do
17 | desc 'Build the gem'
18 | task :build do
19 | puts `yard`
20 | puts `gem build r509-ca-http.gemspec`
21 | end
22 |
23 | desc 'Install gem'
24 | task :install do
25 | puts `gem install r509-ca-http-#{R509::CertificateAuthority::HTTP::VERSION}.gem`
26 | end
27 |
28 | desc 'Uninstall gem'
29 | task :uninstall do
30 | puts `gem uninstall r509-ca-http`
31 | end
32 | end
33 |
34 | desc 'Build yard documentation'
35 | task :yard do
36 | puts `yard`
37 | `open doc/index.html`
38 | end
39 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | require 'r509'
2 | require 'dependo'
3 | require 'logger'
4 | require 'r509/certificateauthority/http/server'
5 |
6 | Dependo::Registry[:log] = Logger.new(STDOUT)
7 |
8 | begin
9 | gem 'r509-middleware-validity'
10 | require 'r509/middleware/validity'
11 | use R509::Middleware::Validity
12 | Dependo::Registry[:log].info "Using r509 middleware validity"
13 | rescue Gem::LoadError
14 | end
15 |
16 | begin
17 | gem 'r509-middleware-certwriter'
18 | require 'r509/middleware/certwriter'
19 | use R509::Middleware::Certwriter
20 | Dependo::Registry[:log].info "Using r509 middleware certwriter"
21 | rescue Gem::LoadError
22 | end
23 |
24 | R509::CertificateAuthority::HTTP::Config.load_config
25 |
26 | R509::CertificateAuthority::HTTP::Config.print_config
27 |
28 | server = R509::CertificateAuthority::HTTP::Server
29 | run server
30 |
--------------------------------------------------------------------------------
/config.yaml.example:
--------------------------------------------------------------------------------
1 | certificate_authorities: {
2 | test_ca: {
3 | ca_cert: {
4 | cert: "/some/path/to/ca.cer",
5 | key: "/some/path/to/ca_key.pem"
6 | },
7 | cdp_location: ['http://crl.domain.com/test_ca.crl'],
8 | message_digest: 'SHA1', #SHA1, SHA256, SHA512 supported. MD5 too, but you really shouldn't use that unless you have a good reason
9 | profiles: {
10 | server: {
11 | basic_constraints: { "ca" : true },
12 | key_usage: [digitalSignature,keyEncipherment],
13 | extended_key_usage: [serverAuth]
14 | }
15 | }
16 | }
17 | }
18 | certwriter: {
19 | path: "/path/to/issued/certs/directory"
20 | }
21 |
--------------------------------------------------------------------------------
/lib/r509/certificateauthority/http/config.rb:
--------------------------------------------------------------------------------
1 | module R509::CertificateAuthority::HTTP
2 | class Config
3 | def self.load_config(config_file = "config.yaml")
4 | config_data = File.read(config_file)
5 |
6 | Dependo::Registry[:config_pool] = R509::Config::CAConfigPool.from_yaml("certificate_authorities", config_data)
7 |
8 | Dependo::Registry[:crls] = {}
9 | Dependo::Registry[:options_builders] = {}
10 | Dependo::Registry[:certificate_authorities] = {}
11 | Dependo::Registry[:config_pool].names.each do |name|
12 | Dependo::Registry[:crls][name] = R509::CRL::Administrator.new(Dependo::Registry[:config_pool][name])
13 | Dependo::Registry[:options_builders][name] = R509::CertificateAuthority::OptionsBuilder.new(Dependo::Registry[:config_pool][name])
14 | Dependo::Registry[:certificate_authorities][name] = R509::CertificateAuthority::Signer.new(Dependo::Registry[:config_pool][name])
15 | end
16 | end
17 |
18 | def self.print_config
19 | Dependo::Registry[:log].warn "Config loaded"
20 | Dependo::Registry[:config_pool].all.each do |config|
21 | Dependo::Registry[:log].warn "Config: "
22 | Dependo::Registry[:log].warn "CA Cert:"+config.ca_cert.subject.to_s
23 | Dependo::Registry[:log].warn "OCSP Cert (may be the same as above):"+config.ocsp_cert.subject.to_s
24 | Dependo::Registry[:log].warn "OCSP Validity Hours: "+config.ocsp_validity_hours.to_s
25 | Dependo::Registry[:log].warn "CRL Validity Hours: "+config.crl_validity_hours.to_s
26 | Dependo::Registry[:log].warn "\n"
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/r509/certificateauthority/http/factory.rb:
--------------------------------------------------------------------------------
1 | module R509::CertificateAuthority::HTTP
2 | module Factory
3 | class CSRFactory
4 | def build(options)
5 | R509::CSR.new(options)
6 | end
7 | end
8 |
9 | class SPKIFactory
10 | def build(options)
11 | R509::SPKI.new(options)
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/r509/certificateauthority/http/server.rb:
--------------------------------------------------------------------------------
1 | require 'sinatra/base'
2 | require 'r509'
3 | require "#{File.dirname(__FILE__)}/config"
4 | require "#{File.dirname(__FILE__)}/subjectparser"
5 | require "#{File.dirname(__FILE__)}/validityperiodconverter"
6 | require "#{File.dirname(__FILE__)}/factory"
7 | require 'base64'
8 | require 'yaml'
9 | require 'logger'
10 | require 'dependo'
11 |
12 | # Capture USR2 calls so we can reload and print the config
13 | # I'd rather use HUP, but daemons like thin already capture that
14 | # so we can't use it.
15 | Signal.trap("USR2") do
16 | R509::CertificateAuthority::HTTP::Config.load_config
17 | R509::CertificateAuthority::HTTP::Config.print_config
18 | end
19 |
20 | module R509
21 | module CertificateAuthority
22 | module HTTP
23 | class Server < Sinatra::Base
24 | extend Dependo::Mixin
25 | include Dependo::Mixin
26 |
27 | configure do
28 | disable :protection #disable Rack::Protection (for speed)
29 | disable :logging
30 | set :environment, :production
31 |
32 | set :subject_parser, R509::CertificateAuthority::HTTP::SubjectParser.new
33 | set :validity_period_converter, R509::CertificateAuthority::HTTP::ValidityPeriodConverter.new
34 | set :csr_factory, R509::CertificateAuthority::HTTP::Factory::CSRFactory.new
35 | set :spki_factory, R509::CertificateAuthority::HTTP::Factory::SPKIFactory.new
36 | end
37 |
38 | before do
39 | content_type :text
40 | end
41 |
42 | helpers do
43 | def crl(name)
44 | Dependo::Registry[:crls][name]
45 | end
46 | def ca(name)
47 | Dependo::Registry[:certificate_authorities][name]
48 | end
49 | def builder(name)
50 | Dependo::Registry[:options_builders][name]
51 | end
52 | def subject_parser
53 | settings.subject_parser
54 | end
55 | def validity_period_converter
56 | settings.validity_period_converter
57 | end
58 | def csr_factory
59 | settings.csr_factory
60 | end
61 | def spki_factory
62 | settings.spki_factory
63 | end
64 | end
65 |
66 | error do
67 | log.error env["sinatra.error"].inspect
68 | log.error env["sinatra.error"].backtrace.join("\n")
69 | "Something is amiss with our CA. You should ... wait?"
70 | end
71 |
72 | error StandardError do
73 | log.error env["sinatra.error"].inspect
74 | log.error env["sinatra.error"].backtrace.join("\n")
75 | env["sinatra.error"].inspect
76 | end
77 |
78 | get '/favicon.ico' do
79 | log.debug "go away. no children."
80 | "go away. no children"
81 | end
82 |
83 | get '/1/crl/:ca/get/?' do
84 | log.info "DEPRECATED: Get CRL for #{params[:ca]}"
85 |
86 | if not crl(params[:ca])
87 | raise ArgumentError, "CA not found"
88 | end
89 |
90 | crl(params[:ca]).generate_crl.to_pem
91 | end
92 |
93 | get '/1/crl/:ca/generate/?' do
94 | log.info "Generate CRL for #{params[:ca]}"
95 |
96 | if not crl(params[:ca])
97 | raise ArgumentError, "CA not found"
98 | end
99 |
100 | crl(params[:ca]).generate_crl.to_pem
101 | end
102 |
103 | post '/1/certificate/issue/?' do
104 | log.info "Issue Certificate"
105 | raw = request.env["rack.input"].read
106 | env["rack.input"].rewind
107 | log.info raw
108 |
109 | log.info params.inspect
110 |
111 | if not params.has_key?("ca")
112 | raise ArgumentError, "Must provide a CA"
113 | end
114 | if not ca(params["ca"])
115 | raise ArgumentError, "CA not found"
116 | end
117 | if not params.has_key?("profile")
118 | raise ArgumentError, "Must provide a CA profile"
119 | end
120 | if not params.has_key?("validityPeriod")
121 | raise ArgumentError, "Must provide a validity period"
122 | end
123 | if not params.has_key?("csr") and not params.has_key?("spki")
124 | raise ArgumentError, "Must provide a CSR or SPKI"
125 | end
126 |
127 | subject = subject_parser.parse(raw, "subject")
128 | log.info subject.inspect
129 | log.info subject.to_s
130 | if subject.empty?
131 | raise ArgumentError, "Must provide a subject"
132 | end
133 |
134 | extensions = []
135 | if params.has_key?("extensions") and params["extensions"].has_key?("subjectAlternativeName")
136 | san_names = params["extensions"]["subjectAlternativeName"].select { |name| not name.empty? }
137 | if not san_names.empty?
138 | extensions.push(R509::Cert::Extensions::SubjectAlternativeName.new(:value => R509::ASN1.general_name_parser(san_names)))
139 | end
140 | elsif params.has_key?("extensions") and params["extensions"].has_key?("dNSNames")
141 | san_names = R509::ASN1::GeneralNames.new
142 | params["extensions"]["dNSNames"].select{ |name| not name.empty? }.each do |name|
143 | san_names.create_item(:tag => 2, :value => name.strip)
144 | end
145 | if not san_names.names.empty?
146 | extensions.push(R509::Cert::Extensions::SubjectAlternativeName.new(:value => san_names))
147 | end
148 | end
149 |
150 | validity_period = validity_period_converter.convert(params["validityPeriod"])
151 |
152 | if params.has_key?("csr")
153 | csr = csr_factory.build(:csr => params["csr"])
154 | signer_opts = builder(params["ca"]).build_and_enforce(
155 | :csr => csr,
156 | :profile_name => params["profile"],
157 | :subject => subject,
158 | :extensions => extensions,
159 | :message_digest => params["message_digest"],
160 | :not_before => validity_period[:not_before],
161 | :not_after => validity_period[:not_after],
162 | )
163 | cert = ca(params["ca"]).sign(signer_opts)
164 | elsif params.has_key?("spki")
165 | spki = spki_factory.build(:spki => params["spki"], :subject => subject)
166 | signer_opts = builder(params["ca"]).build_and_enforce(
167 | :spki => spki,
168 | :profile_name => params["profile"],
169 | :subject => subject,
170 | :extensions => extensions,
171 | :message_digest => params["message_digest"],
172 | :not_before => validity_period[:not_before],
173 | :not_after => validity_period[:not_after],
174 | )
175 | cert = ca(params["ca"]).sign(signer_opts)
176 | else
177 | raise ArgumentError, "Must provide a CSR or SPKI"
178 | end
179 |
180 | pem = cert.to_pem
181 | log.info pem
182 |
183 | pem
184 | end
185 |
186 | post '/1/certificate/revoke/?' do
187 | ca = params[:ca]
188 | serial = params[:serial]
189 | reason = params[:reason]
190 | log.info "Revoke for serial #{serial} on CA #{ca}"
191 |
192 | if not ca
193 | raise ArgumentError, "CA must be provided"
194 | end
195 | if not crl(ca)
196 | raise ArgumentError, "CA not found"
197 | end
198 | if not serial
199 | raise ArgumentError, "Serial must be provided"
200 | end
201 |
202 | if reason.nil? or reason.empty?
203 | reason = nil
204 | else
205 | reason = reason.to_i
206 | end
207 |
208 | crl(ca).revoke_cert(serial, reason)
209 |
210 | crl(ca).generate_crl.to_pem
211 | end
212 |
213 | post '/1/certificate/unrevoke/?' do
214 | ca = params[:ca]
215 | serial = params[:serial]
216 | log.info "Unrevoke for serial #{serial} on CA #{ca}"
217 |
218 | if not ca
219 | raise ArgumentError, "CA must be provided"
220 | end
221 | if not crl(ca)
222 | raise ArgumentError, "CA not found"
223 | end
224 | if not serial
225 | raise ArgumentError, "Serial must be provided"
226 | end
227 |
228 | crl(ca).unrevoke_cert(serial.to_i)
229 |
230 | crl(ca).generate_crl.to_pem
231 | end
232 |
233 | get '/test/certificate/issue/?' do
234 | log.info "Loaded test issuance interface"
235 | content_type :html
236 | erb :test_issue
237 | end
238 |
239 | get '/test/certificate/revoke/?' do
240 | log.info "Loaded test revoke interface"
241 | content_type :html
242 | erb :test_revoke
243 | end
244 |
245 | get '/test/certificate/unrevoke/?' do
246 | log.info "Loaded test unrevoke interface"
247 | content_type :html
248 | erb :test_unrevoke
249 | end
250 | end
251 | end
252 | end
253 | end
254 |
--------------------------------------------------------------------------------
/lib/r509/certificateauthority/http/subjectparser.rb:
--------------------------------------------------------------------------------
1 | module R509
2 | module CertificateAuthority
3 | module HTTP
4 | class SubjectParser
5 | def parse(raw, name="subject")
6 | if raw.nil?
7 | raise ArgumentError, "Must provide a query string"
8 | end
9 |
10 | subject = R509::Subject.new
11 | raw.split(/[&;] */n).each { |pair|
12 | key, value = pair.split('=', 2).map { |data| unescape(data) }
13 | match = key.match(/#{name}\[(.*)\]/)
14 | if not match.nil? and not value.empty?
15 | subject[match[1]] = value
16 | end
17 | }
18 | subject
19 | end
20 |
21 | if defined?(::Encoding)
22 | def unescape(s, encoding = Encoding::UTF_8)
23 | URI.decode_www_form_component(s, encoding)
24 | end
25 | else
26 | def unescape(s, encoding = nil)
27 | URI.decode_www_form_component(s, encoding)
28 | end
29 | end
30 | end
31 | end
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/r509/certificateauthority/http/validityperiodconverter.rb:
--------------------------------------------------------------------------------
1 | module R509::CertificateAuthority::HTTP
2 | class ValidityPeriodConverter
3 | def convert(validity_period)
4 | if validity_period.nil?
5 | raise ArgumentError, "Must provide validity period"
6 | end
7 | if validity_period.to_i <= 0
8 | raise ArgumentError, "Validity period must be positive"
9 | end
10 | {
11 | # Begin the validity period 6 hours into the past, to account for
12 | # possibly-slow clocks.
13 | :not_before => Time.now - (6 * 60 * 60),
14 | # Add validity_period number of seconds to the current time.
15 | :not_after => Time.now + validity_period.to_i,
16 | }
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/r509/certificateauthority/http/version.rb:
--------------------------------------------------------------------------------
1 | module R509
2 | module CertificateAuthority
3 | module HTTP
4 | VERSION="0.3.2"
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/r509/certificateauthority/http/views/test_issue.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | Issue
4 |
5 |
6 |
7 | Issue a certificate
8 |
9 |
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/lib/r509/certificateauthority/http/views/test_revoke.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | Revoke
4 |
5 |
6 |
7 | Revoke a certificate
8 |
9 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/lib/r509/certificateauthority/http/views/test_unrevoke.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | Unrevoke
4 |
5 |
6 |
7 | Unrevoke a certificate
8 |
9 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/r509-ca-http.gemspec:
--------------------------------------------------------------------------------
1 | $:.push File.expand_path("../lib", __FILE__)
2 | require "r509/certificateauthority/http/version"
3 |
4 | spec = Gem::Specification.new do |s|
5 | s.name = 'r509-ca-http'
6 | s.version = R509::CertificateAuthority::HTTP::VERSION
7 | s.platform = Gem::Platform::RUBY
8 | s.summary = "A (relatively) simple certificate authority API written to work with r509"
9 | s.description = 'A HTTP CA API for r509'
10 | s.add_dependency 'r509', '~> 0.10.0'
11 | s.add_dependency 'sinatra'
12 | s.add_dependency 'dependo'
13 | s.add_development_dependency 'rspec'
14 | s.add_development_dependency 'rack-test'
15 | s.add_development_dependency 'rake'
16 | s.add_development_dependency 'simplecov'
17 | s.author = "Sean Schulte"
18 | s.email = "sirsean@gmail.com"
19 | s.homepage = "http://r509.org"
20 | s.required_ruby_version = ">= 1.9.3"
21 | s.files = %w(README.md Rakefile) + Dir["{lib,script,spec,doc,cert_data}/**/*"]
22 | s.test_files= Dir.glob('test/*_spec.rb')
23 | s.require_path = "lib"
24 | end
25 |
26 |
--------------------------------------------------------------------------------
/spec/fixtures/test_ca.cer:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDjzCCAnegAwIBAgIJAP/ZxwuHN9GUMA0GCSqGSIb3DQEBBQUAMF4xCzAJBgNV
3 | BAYTAlVTMREwDwYDVQQIDAhJbGxpbm9pczEQMA4GA1UEBwwHQ2hpY2FnbzEYMBYG
4 | A1UECgwPUnVieSBDQSBQcm9qZWN0MRAwDgYDVQQDDAdUZXN0IENBMB4XDTExMDIy
5 | MDIwNDkxMloXDTMwMDQyMTIwNDkxMlowXjELMAkGA1UEBhMCVVMxETAPBgNVBAgM
6 | CElsbGlub2lzMRAwDgYDVQQHDAdDaGljYWdvMRgwFgYDVQQKDA9SdWJ5IENBIFBy
7 | b2plY3QxEDAOBgNVBAMMB1Rlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
8 | ggEKAoIBAQCot8/z3jxARkwyRk0csM84dlWs91W95wPpunx3P3otqUxCJaOAyQl/
9 | uY8deg0xDmsime2JQ6aG6m76y3LfT4J1tlkhtmiGKhNJir1kgL8sL0wTXYXvwZEw
10 | qEEYD5mKi7Na9eo4R0ydqd9KAquVIhKywXvV1+Y4RDTx+f1WXmMCBaYZ76/rXmIe
11 | oE0avriRiihOtlgto+VNJw7VRnvq+cEd81BT62wRk1fG1lcpCqTEfEtKEI6PqqZh
12 | E2f+6lNmhZZ3Tj7NeNgYQLd4q+L1y030Vj4onpAIJrwfQq3dxTmrLDqnN2WOFsbo
13 | 0qGh3s4yqUaOYd2wIBgCp7fEf5X/yh53AgMBAAGjUDBOMB0GA1UdDgQWBBR5dbuE
14 | Osss3noJvjEbQ7wcKk1TWDAfBgNVHSMEGDAWgBR5dbuEOsss3noJvjEbQ7wcKk1T
15 | WDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAp/Fh+WWjN7x/SZzMm
16 | H1INAS4loufHXzkqI+3bClAzlhzpBVbY7dHwD6H8oKh06EtppbN0Bw3iqXSRyoNd
17 | +lDXccPij5dCuTFQ97DOrv7kGlhiM95XVkx4mvnd+i6jKwgOpllgDE/nKOwC42bq
18 | lIikcp7XLQUPt7amyX9UeJ8lxQ/niSkT868ls2IZQkF3XEhCi+5VlefEoaiMQZmD
19 | RyEd/vlsSnpXI5LLpkFq+NFGvgdI7+UADNIyDplyivD4VyQ6+zisX3r1ojroa6Y+
20 | WyXxLx+AVJVnGjngr2fDKr4ggsYWlJEs8lJ0r90jliYrSPA6rIldTbs3k9AbUp8G
21 | kS6X
22 | -----END CERTIFICATE-----
23 |
--------------------------------------------------------------------------------
/spec/fixtures/test_ca.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCot8/z3jxARkwy
3 | Rk0csM84dlWs91W95wPpunx3P3otqUxCJaOAyQl/uY8deg0xDmsime2JQ6aG6m76
4 | y3LfT4J1tlkhtmiGKhNJir1kgL8sL0wTXYXvwZEwqEEYD5mKi7Na9eo4R0ydqd9K
5 | AquVIhKywXvV1+Y4RDTx+f1WXmMCBaYZ76/rXmIeoE0avriRiihOtlgto+VNJw7V
6 | Rnvq+cEd81BT62wRk1fG1lcpCqTEfEtKEI6PqqZhE2f+6lNmhZZ3Tj7NeNgYQLd4
7 | q+L1y030Vj4onpAIJrwfQq3dxTmrLDqnN2WOFsbo0qGh3s4yqUaOYd2wIBgCp7fE
8 | f5X/yh53AgMBAAECggEAMlRS5m6fDpVp2X17N1nPFwrF2AkYPMQTOL/2rSP0cHaW
9 | Vw0fTyWpfb5+4M4t7Tpd3z6Hy3Cw1oJMhOf35oGzayXwRMxDNfKLOl72zGpTnPym
10 | 9wfpEnJtu1QVxvWwWdH+uN2u9wbd5hJsl4lgYeZ+KXDqXgo/lP1TxfNLDV6urkVA
11 | wsR5URtG2ddOTOUjhUvizWPt1QCVE6Z5YVe9haTtQ4EtLjPywd8LB01AXRkvLIlL
12 | 2XpL/EeuzF8GLd3GRMjEwgnT7WWBLHe7zIZqsRVx318Pu+WoWRFXUbKxrwH05tUV
13 | kx9Gh//L01uNwGx7qHSyJxTmYibjJXQAyQqVpPY4oQKBgQDgTbRVX65AEj5x8J3A
14 | 1UEBDZE6ZQL/AnM+yPZGFSTfM04ebLUKyeIDvoSUxrbWMSf8JJwU1OCA+zJiXSld
15 | 0BNBwd/irr9aglEuOBx6ITJypaKtehR5TtZGWsrvJT9f7IZ5Tc29gC0wiKNOqQ09
16 | O4J7/tvk2j4viAawGHSlRvJDnwKBgQDAj0X1kLwaH+QlVHU9QyRbK8qJsouot8Km
17 | BEjkZQ4D+7BQ/3mfFszJ/AG6tMA6FgikskyEics7XrtNYPZOGZ1xxKpi1an6o8cm
18 | GKUavZnkR5eHNwUEl2c9V5lNMbtAbCbIz30sCBFluicw37M2O8n38xTDSnXcRTbN
19 | n3rqpu52KQKBgE3MRc8Sx7JrYYNNjLnUfZ5q4UNaw8ZFSEmvlFPMg6Ry/BZraAPc
20 | 7+qSixO7NLFoDVFUNVq4V0IFXn1liLKEOBmnsArEx5QR/SxFxALMPt4q+ximbjGB
21 | Gar/VMHLroaL2Dx8su6WZZYe3l2rHu9tE54EUKq407bSvFcZtGObDu5LAoGAfxmS
22 | ueYQ4sWOF73JrOg2hR9AjucVHAY/KsnFO0wglix5Ut1ub73i6qe2lIBeKXkFt4Ag
23 | 1ZMGXGfJBegsa5youcFwHdCeY9vaxaCayi2/+FfxAsUkQMWW1XyOqc9bo8g/SWj7
24 | XCbvJNBcsfvWFMQeKdV/LPBnHz9oTw0nWt9YoxECgYAA9f2l6btaIV2JuDEzR8LK
25 | 8MjXZMwmg9pEiUT0DsiiVIkJDsHdb2i54DhkfEKxi+ZDTO8AoFpqc4GTwobQHgPk
26 | N6Qa+wvig8SAWTS0dbPq2usvmx8cOiuqbn88VVl9jmFT6WTRJzNVRgVMM8xwl3uS
27 | odNmWsnOyWhM8h5LMVNb5w==
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/spec/fixtures/test_config.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | certificate_authorities:
3 | test_ca:
4 | ca_cert:
5 | cert: spec/fixtures/test_ca.cer
6 | key: spec/fixtures/test_ca.key
7 | profiles:
8 | server:
9 | basic_constraints:
10 | :ca: false
11 | key_usage:
12 | :value:
13 | - digitalSignature
14 | - keyEncipherment
15 | extended_key_usage:
16 | :value:
17 | - serverAuth
18 | crl_distribution_points:
19 | :value:
20 | - :type: URI
21 | :value: http://crl.domain.com/test_ca.crl
22 | default_md: SHA1
23 | allowed_mds:
24 | - SHA1
25 | - SHA256
26 |
--------------------------------------------------------------------------------
/spec/http_spec.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/spec_helper'
2 | require "openssl"
3 |
4 | describe R509::CertificateAuthority::HTTP::Server do
5 | before :each do
6 | # clear the dependo before each test
7 | Dependo::Registry.clear
8 | Dependo::Registry[:log] = Logger.new(nil)
9 | R509::CertificateAuthority::HTTP::Config.load_config(File.dirname(__FILE__)+"/fixtures/test_config.yaml")
10 | Dependo::Registry[:crls] = { "test_ca" => double("crl") }
11 | Dependo::Registry[:certificate_authorities] = { "test_ca" => double("test_ca") }
12 | Dependo::Registry[:options_builders] = { "test_ca" => double("options_builder") }
13 | @subject_parser = double("subject parser")
14 | #@validity_period_converter = double("validity period converter")
15 | @csr_factory = double("csr factory")
16 | @spki_factory = double("spki factory")
17 | end
18 |
19 | def app
20 | @app ||= R509::CertificateAuthority::HTTP::Server
21 | @app.send(:set, :subject_parser, @subject_parser)
22 | #@app.send(:set, :validity_period_converter, @validity_period_converter)
23 | @app.send(:set, :csr_factory, @csr_factory)
24 | @app.send(:set, :spki_factory, @spki_factory)
25 | end
26 |
27 | context "get CRL" do
28 | it "gets the CRL" do
29 | crl = double('crl')
30 | crl.should_receive(:to_pem).and_return("generated crl")
31 | Dependo::Registry[:crls]["test_ca"].should_receive(:generate_crl).and_return(crl)
32 | get "/1/crl/test_ca/get"
33 | last_response.should be_ok
34 | last_response.content_type.should match(/text\/plain/)
35 | last_response.body.should == "generated crl"
36 | end
37 | it "when CA is not found" do
38 | get "/1/crl/bogus/get/"
39 | last_response.status.should == 500
40 | last_response.body.should == "#"
41 | end
42 | end
43 |
44 | context "generate CRL" do
45 | it "generates the CRL" do
46 | crl = double('crl')
47 | crl.should_receive(:to_pem).and_return("generated crl")
48 | Dependo::Registry[:crls]["test_ca"].should_receive(:generate_crl).and_return(crl)
49 | get "/1/crl/test_ca/generate"
50 | last_response.should be_ok
51 | last_response.body.should == "generated crl"
52 | end
53 | it "when CA is not found" do
54 | get "/1/crl/bogus/generate/"
55 | last_response.status.should == 500
56 | last_response.body.should == "#"
57 | end
58 | end
59 |
60 | context "issue certificate" do
61 | it "when no parameters are given" do
62 | post "/1/certificate/issue"
63 | last_response.should_not be_ok
64 | last_response.body.should == "#"
65 | end
66 | it "when there's a profile, subject, CSR, validity period, but no ca" do
67 | post "/1/certificate/issue", "profile" => "my profile", "subject" => "subject", "csr" => "my csr", "validityPeriod" => 365
68 | last_response.should_not be_ok
69 | last_response.body.should == "#"
70 | end
71 | it "when there's a ca, profile, subject, CSR, but no validity period" do
72 | post "/1/certificate/issue", "ca" => "test_ca", "profile" => "my profile", "subject" => "subject", "csr" => "my csr"
73 | last_response.should_not be_ok
74 | last_response.body.should == "#"
75 | end
76 | it "when there's a ca, profile, subject, validity period, but no CSR" do
77 | post "/1/certificate/issue", "ca" => "test_ca", "profile" => "my profile", "subject" => "subject", "validityPeriod" => 365
78 | last_response.should_not be_ok
79 | last_response.body.should == "#"
80 | end
81 | it "when there's a ca, profile, CSR, validity period, but no subject" do
82 | @subject_parser.should_receive(:parse).with(anything, "subject").and_return(R509::Subject.new)
83 | post "/1/certificate/issue", "ca" => "test_ca", "profile" => "profile", "validityPeriod" => 365, "csr" => "csr"
84 | last_response.should_not be_ok
85 | last_response.body.should == "#"
86 | end
87 | it "when there's a ca, subject, CSR, validity period, but no profile" do
88 | post "/1/certificate/issue", "ca" => "test_ca", "subject" => "subject", "validityPeriod" => 365, "csr" => "csr"
89 | last_response.should_not be_ok
90 | last_response.body.should == "#"
91 | end
92 | it "when the given CA is not found" do
93 | post "/1/certificate/issue", "ca" => "some bogus CA"
94 | last_response.should_not be_ok
95 | last_response.body.should == "#"
96 | end
97 | it "fails to issue" do
98 | csr = double("csr")
99 | @csr_factory.should_receive(:build).with({:csr => "csr"}).and_return(csr)
100 | #@validity_period_converter.should_receive(:convert).with("365").and_return({:not_before => 1, :not_after => 2})
101 | subject = R509::Subject.new [["CN", "domain.com"]]
102 | @subject_parser.should_receive(:parse).with(anything, "subject").and_return(subject)
103 | Dependo::Registry[:options_builders]["test_ca"].should_receive(:build_and_enforce).with(:csr => csr, :profile_name => "profile", :extensions => [], :subject => subject, :message_digest =>nil, :not_before=> kind_of(Time), :not_after => kind_of(Time) ).and_raise(R509::R509Error.new("failed to issue because of: good reason"))
104 |
105 | post "/1/certificate/issue", "ca" => "test_ca", "profile" => "profile", "subject" => "subject", "validityPeriod" => 365, "csr" => "csr"
106 | last_response.should_not be_ok
107 | last_response.body.should == "#"
108 | end
109 | it "issues a CSR with no SAN extensions" do
110 | csr = double("csr")
111 | @csr_factory.should_receive(:build).with(:csr => "csr").and_return(csr)
112 | #@validity_period_converter.should_receive(:convert).with("365").and_return({:not_before => 1, :not_after => 2})
113 | subject = R509::Subject.new [["CN", "domain.com"]]
114 | @subject_parser.should_receive(:parse).with(anything, "subject").and_return(subject)
115 | cert = double("cert")
116 | Dependo::Registry[:options_builders]["test_ca"].should_receive(:build_and_enforce).with(:csr => csr, :profile_name => "profile", :extensions => [], :subject => subject, :message_digest =>nil, :not_before=> kind_of(Time), :not_after => kind_of(Time) ).and_return(:csr => csr, :profile_name => "profile", :subject => subject, :message_digest => "SHA1", :not_before=> kind_of(Time), :not_after => kind_of(Time) )
117 | Dependo::Registry[:certificate_authorities]["test_ca"].should_receive(:sign).and_return(cert)
118 | cert.should_receive(:to_pem).and_return("signed cert")
119 |
120 | post "/1/certificate/issue", "ca" => "test_ca", "profile" => "profile", "subject" => "subject", "validityPeriod" => 365, "csr" => "csr"
121 | last_response.should be_ok
122 | last_response.body.should == "signed cert"
123 | end
124 | it "issues a CSR with SAN extensions" do
125 | csr = double("csr")
126 | @csr_factory.should_receive(:build).with(:csr => "csr").and_return(csr)
127 | #@validity_period_converter.should_receive(:convert).with("365").and_return({:not_before => 1, :not_after => 2})
128 | subject = R509::Subject.new [["CN", "domain.com"]]
129 | @subject_parser.should_receive(:parse).with(anything, "subject").and_return(subject)
130 | cert = double("cert")
131 | Dependo::Registry[:options_builders]["test_ca"].should_receive(:build_and_enforce).with(:csr => csr, :profile_name => "profile", :extensions => kind_of(Array), :subject => subject, :extensions => kind_of(Array), :message_digest =>nil, :not_before=> kind_of(Time), :not_after => kind_of(Time) ).and_return(:csr => csr, :profile_name => "profile", :subject => subject, :message_digest => "SHA1", :not_before=> kind_of(Time), :not_after => kind_of(Time) )
132 | Dependo::Registry[:certificate_authorities]["test_ca"].should_receive(:sign).and_return(cert)
133 | cert.should_receive(:to_pem).and_return("signed cert")
134 |
135 | post "/1/certificate/issue", "ca" => "test_ca", "profile" => "profile", "subject" => "subject", "validityPeriod" => 365, "csr" => "csr", "extensions[subjectAlternativeName][]" => ["domain1.com","domain2.com"]
136 | last_response.should be_ok
137 | last_response.body.should == "signed cert"
138 | end
139 | it "issues a CSR with dNSNames" do
140 | csr = double("csr")
141 | @csr_factory.should_receive(:build).with(:csr => "csr").and_return(csr)
142 | #@validity_period_converter.should_receive(:convert).with("365").and_return({:not_before => 1, :not_after => 2})
143 | subject = R509::Subject.new [["CN", "domain.com"]]
144 | @subject_parser.should_receive(:parse).with(anything, "subject").and_return(subject)
145 | cert = double("cert")
146 | Dependo::Registry[:options_builders]["test_ca"].should_receive(:build_and_enforce).with(:csr => csr, :profile_name => "profile", :subject => subject, :extensions => kind_of(Array), :message_digest =>nil, :not_before=> kind_of(Time), :not_after => kind_of(Time) ).and_return(:csr => csr, :profile_name => "profile", :subject => subject, :message_digest => "SHA1")
147 | Dependo::Registry[:certificate_authorities]["test_ca"].should_receive(:sign).and_return(cert)
148 | cert.should_receive(:to_pem).and_return("signed cert")
149 |
150 | post "/1/certificate/issue", "ca" => "test_ca", "profile" => "profile", "subject" => "subject", "validityPeriod" => 365, "csr" => "csr", "extensions[dNSNames][]" => ["domain1.com","domain2.com"]
151 | last_response.should be_ok
152 | last_response.body.should == "signed cert"
153 | end
154 | it "issues a CSR with both SAN names and dNSNames provided (and ignore the dNSNames)" do
155 | csr = double("csr")
156 | @csr_factory.should_receive(:build).with(:csr => "csr").and_return(csr)
157 | #@validity_period_converter.should_receive(:convert).with("365").and_return({:not_before => 1, :not_after => 2})
158 | subject = R509::Subject.new [["CN", "domain.com"]]
159 | @subject_parser.should_receive(:parse).with(anything, "subject").and_return(subject)
160 | cert = double("cert")
161 | Dependo::Registry[:options_builders]["test_ca"].should_receive(:build_and_enforce).with(:csr => csr, :profile_name => "profile", :subject => subject, :extensions => kind_of(Array), :message_digest => nil, :not_before=> kind_of(Time), :not_after => kind_of(Time) ).and_return(:csr => csr)
162 | Dependo::Registry[:certificate_authorities]["test_ca"].should_receive(:sign).and_return(cert)
163 | cert.should_receive(:to_pem).and_return("signed cert")
164 |
165 | post "/1/certificate/issue", "ca" => "test_ca", "profile" => "profile", "subject" => "subject", "validityPeriod" => 365, "csr" => "csr", "extensions[subjectAlternativeName][]" => ["domain1.com","domain2.com"], "extensions[dNSNames][]" => ["domain3.com", "domain4.com"]
166 | last_response.should be_ok
167 | last_response.body.should == "signed cert"
168 | end
169 | it "issues an SPKI without SAN extensions" do
170 | #@validity_period_converter.should_receive(:convert).with("365").and_return({:not_before => 1, :not_after => 2})
171 | subject = R509::Subject.new [["CN", "domain.com"]]
172 | @subject_parser.should_receive(:parse).with(anything, "subject").and_return(subject)
173 | spki = double("spki")
174 | @spki_factory.should_receive(:build).with(:spki => "spki", :subject => subject).and_return(spki)
175 | cert = double("cert")
176 | Dependo::Registry[:options_builders]["test_ca"].should_receive(:build_and_enforce).with(:spki => spki, :profile_name => "profile", :extensions => [], :subject => subject, :message_digest => nil, :not_before=> kind_of(Time), :not_after => kind_of(Time) ).and_return(:spki => spki, :not_before=> kind_of(Time), :not_after => kind_of(Time) )
177 | Dependo::Registry[:certificate_authorities]["test_ca"].should_receive(:sign).and_return(cert)
178 | cert.should_receive(:to_pem).and_return("signed cert")
179 |
180 | post "/1/certificate/issue", "ca" => "test_ca", "profile" => "profile", "subject" => "subject", "validityPeriod" => 365, "spki" => "spki"
181 | last_response.should be_ok
182 | last_response.body.should == "signed cert"
183 | end
184 | it "issues an SPKI with SAN extensions" do
185 | #@validity_period_converter.should_receive(:convert).with("365").and_return({:not_before => 1, :not_after => 2})
186 | subject = R509::Subject.new [["CN", "domain.com"]]
187 | @subject_parser.should_receive(:parse).with(anything, "subject").and_return(subject)
188 | spki = double("spki")
189 | @spki_factory.should_receive(:build).with(:spki => "spki", :subject => subject).and_return(spki)
190 | cert = double("cert")
191 | Dependo::Registry[:options_builders]["test_ca"].should_receive(:build_and_enforce).with(:spki => spki, :profile_name => "profile", :extensions => kind_of(Array), :subject => subject, :message_digest => nil, :not_before=> kind_of(Time), :not_after => kind_of(Time) ).and_return(:spki => spki, :not_before=> kind_of(Time), :not_after => kind_of(Time) )
192 | Dependo::Registry[:certificate_authorities]["test_ca"].should_receive(:sign).and_return(cert)
193 | cert.should_receive(:to_pem).and_return("signed cert")
194 |
195 | post "/1/certificate/issue", "ca" => "test_ca", "profile" => "profile", "subject" => "subject", "validityPeriod" => 365, "spki" => "spki", "extensions[subjectAlternativeName][]" => ["domain1.com","domain2.com"]
196 | last_response.should be_ok
197 | last_response.body.should == "signed cert"
198 | end
199 | it "when there are empty SAN names" do
200 | csr = double("csr")
201 | @csr_factory.should_receive(:build).with(:csr => "csr").and_return(csr)
202 | #@validity_period_converter.should_receive(:convert).with("365").and_return({:not_before => 1, :not_after => 2})
203 | subject = R509::Subject.new [["CN", "domain.com"]]
204 | @subject_parser.should_receive(:parse).with(anything, "subject").and_return(subject)
205 | cert = double("cert")
206 | Dependo::Registry[:options_builders]["test_ca"].should_receive(:build_and_enforce).with(:csr => csr, :profile_name => "profile", :subject => subject, :extensions => kind_of(Array), :message_digest => nil, :not_before=> kind_of(Time), :not_after => kind_of(Time) ).and_return(:csr => csr, :not_before=> kind_of(Time), :not_after => kind_of(Time) )
207 | Dependo::Registry[:certificate_authorities]["test_ca"].should_receive(:sign).and_return(cert)
208 | cert.should_receive(:to_pem).and_return("signed cert")
209 |
210 | post "/1/certificate/issue", "ca" => "test_ca", "profile" => "profile", "subject" => "subject", "validityPeriod" => 365, "csr" => "csr", "extensions[subjectAlternativeName][]" => ["domain1.com","domain2.com","",""]
211 | last_response.should be_ok
212 | last_response.body.should == "signed cert"
213 | end
214 | end
215 |
216 | context "revoke certificate" do
217 | it "when no CA is given" do
218 | post "/1/certificate/revoke", "serial" => "foo"
219 | last_response.status.should == 500
220 | last_response.body.should == "#"
221 | end
222 | it "when CA is not found" do
223 | post "/1/certificate/revoke", "ca" => "bogus ca name", "serial" => "foo"
224 | last_response.status.should == 500
225 | last_response.body.should == "#"
226 | end
227 | it "when no serial is given" do
228 | post "/1/certificate/revoke", "ca" => "test_ca"
229 | last_response.should_not be_ok
230 | last_response.body.should == "#"
231 | end
232 | it "when serial is given but not reason" do
233 | Dependo::Registry[:crls]["test_ca"].should_receive(:revoke_cert).with("12345", nil).and_return(nil)
234 | crl_obj = double("crl-obj")
235 | Dependo::Registry[:crls]["test_ca"].should_receive(:generate_crl).and_return(crl_obj)
236 | crl_obj.should_receive(:to_pem).and_return("generated crl")
237 | post "/1/certificate/revoke", "ca" => "test_ca", "serial" => "12345"
238 | last_response.should be_ok
239 | last_response.body.should == "generated crl"
240 | end
241 | it "when serial and reason are given" do
242 | Dependo::Registry[:crls]["test_ca"].should_receive(:revoke_cert).with("12345", 1).and_return(nil)
243 | crl_obj = double("crl-obj")
244 | Dependo::Registry[:crls]["test_ca"].should_receive(:generate_crl).and_return(crl_obj)
245 | crl_obj.should_receive(:to_pem).and_return("generated crl")
246 | post "/1/certificate/revoke", "ca" => "test_ca", "serial" => "12345", "reason" => "1"
247 | last_response.should be_ok
248 | last_response.body.should == "generated crl"
249 | end
250 | it "when serial is not an integer" do
251 | Dependo::Registry[:crls]["test_ca"].should_receive(:revoke_cert).with("foo", nil).and_raise(R509::R509Error.new("some r509 error"))
252 | post "/1/certificate/revoke", "ca" => "test_ca", "serial" => "foo"
253 | last_response.should_not be_ok
254 | last_response.body.should == "#"
255 | end
256 | it "when reason is not an integer" do
257 | Dependo::Registry[:crls]["test_ca"].should_receive(:revoke_cert).with("12345", 0).and_return(nil)
258 | crl_obj = double("crl-obj")
259 | Dependo::Registry[:crls]["test_ca"].should_receive(:generate_crl).and_return(crl_obj)
260 | crl_obj.should_receive(:to_pem).and_return("generated crl")
261 | post "/1/certificate/revoke", "ca" => "test_ca", "serial" => "12345", "reason" => "foo"
262 | last_response.should be_ok
263 | last_response.body.should == "generated crl"
264 | end
265 | it "when reason is an empty string" do
266 | Dependo::Registry[:crls]["test_ca"].should_receive(:revoke_cert).with("12345", nil).and_return(nil)
267 | crl_obj = double("crl-obj")
268 | Dependo::Registry[:crls]["test_ca"].should_receive(:generate_crl).and_return(crl_obj)
269 | crl_obj.should_receive(:to_pem).and_return("generated crl")
270 | post "/1/certificate/revoke", "ca" => "test_ca", "serial" => "12345", "reason" => ""
271 | last_response.should be_ok
272 | last_response.body.should == "generated crl"
273 | end
274 | end
275 |
276 | context "unrevoke certificate" do
277 | it "when no CA is given" do
278 | post "/1/certificate/unrevoke", "serial" => "foo"
279 | last_response.status.should == 500
280 | last_response.body.should == "#"
281 | end
282 | it "when CA is not found" do
283 | post "/1/certificate/unrevoke", "ca" => "bogus ca", "serial" => "foo"
284 | last_response.status.should == 500
285 | last_response.body.should == "#"
286 | end
287 | it "when no serial is given" do
288 | post "/1/certificate/unrevoke", "ca" => "test_ca"
289 | last_response.should_not be_ok
290 | last_response.body.should == "#"
291 | end
292 | it "when serial is given" do
293 | Dependo::Registry[:crls]["test_ca"].should_receive(:unrevoke_cert).with(12345).and_return(nil)
294 | crl_obj = double("crl-obj")
295 | Dependo::Registry[:crls]["test_ca"].should_receive(:generate_crl).and_return(crl_obj)
296 | crl_obj.should_receive(:to_pem).and_return("generated crl")
297 | post "/1/certificate/unrevoke", "ca" => "test_ca", "serial" => "12345"
298 | last_response.should be_ok
299 | last_response.body.should == "generated crl"
300 | end
301 | end
302 |
303 | end
304 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'simplecov'
2 | SimpleCov.start
3 | begin
4 | require 'coveralls'
5 | Coveralls.wear!
6 | rescue LoadError
7 | end
8 |
9 | $:.unshift File.expand_path("../../lib", __FILE__)
10 | $:.unshift File.expand_path("../", __FILE__)
11 | require 'rubygems'
12 | #require 'fixtures'
13 | require 'rspec'
14 | require 'rack/test'
15 | require 'r509'
16 | require 'dependo'
17 | require 'logger'
18 |
19 | require 'r509/certificateauthority/http/server'
20 |
21 | RSpec.configure do |conf|
22 | conf.include Rack::Test::Methods
23 | end
24 |
--------------------------------------------------------------------------------
/spec/subject_parser_spec.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + '/spec_helper'
2 |
3 | describe R509::CertificateAuthority::HTTP::SubjectParser do
4 | before :all do
5 | @parser = R509::CertificateAuthority::HTTP::SubjectParser.new
6 | end
7 |
8 | it "when the query string is nil" do
9 | expect { @parser.parse(nil) }.to raise_error(ArgumentError, "Must provide a query string")
10 | end
11 | it "when the query string is empty" do
12 | subject = @parser.parse("")
13 | subject.empty?.should == true
14 | end
15 | it "when the query string doesn't contain any subject data" do
16 | subject = @parser.parse("validityPeriod=1095&data=blahblah")
17 | subject.empty?.should == true
18 | end
19 | it "when there is one subject component" do
20 | subject = @parser.parse("validityPeriod=1095&subject[CN]=domain.com&data=blahblah")
21 | subject.empty?.should == false
22 | subject["CN"].should == "domain.com"
23 | end
24 | it "when there are three subject components should maintain order" do
25 | subject = @parser.parse("validityPeriod=1095&subject[CN]=domain.com&subject[O]=org&subject[L]=locality&data=blahblah")
26 | subject.empty?.should == false
27 | subject["CN"].should == "domain.com"
28 | subject["O"].should == "org"
29 | subject["L"].should == "locality"
30 | subject.to_s.should == "/CN=domain.com/O=org/L=locality"
31 | end
32 | it "when one of the subject components has an unknown key" do
33 | expect { subject = @parser.parse("validityPeriod=1095&subject[CN]=domain.com&subject[NOTATHING]=org&subject[L]=locality&data=blahblah") }.to raise_error(OpenSSL::X509::NameError)
34 | end
35 | it "when one of the subject components is just an OID" do
36 | subject = @parser.parse("validityPeriod=1095&subject[CN]=domain.com&subject[1.3.6.1.4.1.311.60.2.1.300]=org&subject[L]=locality&data=blahblah")
37 | subject.empty?.should == false
38 | subject["CN"].should == "domain.com"
39 | subject["1.3.6.1.4.1.311.60.2.1.300"].should == "org"
40 | subject["L"].should == "locality"
41 | subject.to_s.should == "/CN=domain.com/1.3.6.1.4.1.311.60.2.1.300=org/L=locality"
42 | end
43 | it "when one of the subject components is an empty string" do
44 | subject = @parser.parse("validityPeriod=1095&subject[CN]=domain.com&subject[O]=&subject[L]=locality&data=blahblah")
45 | subject.empty?.should == false
46 | subject["CN"].should == "domain.com"
47 | subject["O"].should == nil
48 | subject["L"].should == "locality"
49 | subject.to_s.should == "/CN=domain.com/L=locality"
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/spec/validity_period_converter_spec.rb:
--------------------------------------------------------------------------------
1 | require File.dirname(__FILE__) + "/spec_helper"
2 |
3 | describe R509::CertificateAuthority::HTTP::ValidityPeriodConverter do
4 | before :all do
5 | @converter = R509::CertificateAuthority::HTTP::ValidityPeriodConverter.new
6 | end
7 |
8 | it "when validity period is nil" do
9 | expect { @converter.convert(nil) }.to raise_error(ArgumentError, "Must provide validity period")
10 | end
11 | it "when validity period is integer, negative" do
12 | expect { @converter.convert(-1) }.to raise_error(ArgumentError, "Validity period must be positive")
13 | end
14 | it "when validity period is string, negative" do
15 | expect { @converter.convert("-1") }.to raise_error(ArgumentError, "Validity period must be positive")
16 | end
17 | it "when validity period is integer, zero" do
18 | expect { @converter.convert(0) }.to raise_error(ArgumentError, "Validity period must be positive")
19 | end
20 | it "when validity period is string, zero" do
21 | expect { @converter.convert("0") }.to raise_error(ArgumentError, "Validity period must be positive")
22 | end
23 | it "when validity period is integer, 86400" do
24 | not_before = Time.now - 6*60*60
25 | not_after = Time.now + 1*24*60*60
26 | period = @converter.convert(86400)
27 | period[:not_before].to_i.should == not_before.to_i
28 | period[:not_after].to_i.should == not_after.to_i
29 | end
30 | it "when validity period is string, 86400" do
31 | not_before = Time.now - 6*60*60
32 | not_after = Time.now + 1*24*60*60
33 | period = @converter.convert("86400")
34 | period[:not_before].to_i.should == not_before.to_i
35 | period[:not_after].to_i.should == not_after.to_i
36 | end
37 | it "when validity period is integer, 31536000" do
38 | not_before = Time.now - 6*60*60
39 | not_after = Time.now + 365*24*60*60
40 | period = @converter.convert(31536000)
41 | period[:not_before].to_i.should == not_before.to_i
42 | period[:not_after].to_i.should == not_after.to_i
43 | end
44 | it "when validity period is string, 31536000" do
45 | not_before = Time.now - 6*60*60
46 | not_after = Time.now + 365*24*60*60
47 | period = @converter.convert("31536000")
48 | period[:not_before].to_i.should == not_before.to_i
49 | period[:not_after].to_i.should == not_after.to_i
50 | end
51 | it "when validity period is integer, 63072000" do
52 | not_before = Time.now - 6*60*60
53 | not_after = Time.now + 730*24*60*60
54 | period = @converter.convert(63072000)
55 | period[:not_before].to_i.should == not_before.to_i
56 | period[:not_after].to_i.should == not_after.to_i
57 | end
58 | it "when validity period is string, 63072000" do
59 | not_before = Time.now - 6*60*60
60 | not_after = Time.now + 730*24*60*60
61 | period = @converter.convert("63072000")
62 | period[:not_before].to_i.should == not_before.to_i
63 | period[:not_after].to_i.should == not_after.to_i
64 | end
65 | it "when validity period is integer, 94608000" do
66 | not_before = Time.now - 6*60*60
67 | not_after = Time.now + 1095*24*60*60
68 | period = @converter.convert(94608000)
69 | period[:not_before].to_i.should == not_before.to_i
70 | period[:not_after].to_i.should == not_after.to_i
71 | end
72 | it "when validity period is string, 94608000" do
73 | not_before = Time.now - 6*60*60
74 | not_after = Time.now + 1095*24*60*60
75 | period = @converter.convert("94608000")
76 | period[:not_before].to_i.should == not_before.to_i
77 | period[:not_after].to_i.should == not_after.to_i
78 | end
79 | end
80 |
--------------------------------------------------------------------------------