├── .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 [![Build Status](https://secure.travis-ci.org/r509/r509-ca-http.png)](http://travis-ci.org/r509/r509-ca-http) [![Coverage Status](https://coveralls.io/repos/r509/r509-ca-http/badge.png)](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 |
10 |

11 | CA: 12 |
13 | 14 |

15 |

16 | Profile: 17 |
18 | 19 |

20 |

21 | Validity Period (in seconds): 22 |
23 | 24 |

25 |

26 | C: 27 |
28 | 29 |

30 |

31 | ST: 32 |
33 | 34 |

35 |

36 | L: 37 |
38 | 39 |

40 |

41 | O: 42 |
43 | 44 |

45 |

46 | OU: 47 |
48 | 49 |

50 |

51 | CN: 52 |
53 | 54 |

55 |

56 | emailAddress: 57 |
58 | 59 |

60 |

61 | SAN: 62 |
63 | 64 |
65 | 66 |
67 | 68 |
69 | 70 |
71 | 72 |
73 |

74 |

75 | CSR: 76 |
77 | 78 |

79 |

80 | 81 |

82 |
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 |
10 |

11 | CA: 12 |
13 | 14 |

15 |

16 | Serial: 17 |
18 | 19 |

20 |

21 | Reason (integer or nil): 22 |
23 | 24 |

25 |

26 | 27 |

28 |
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 |
10 |

11 | CA: 12 |
13 | 14 |

15 |

16 | Serial: 17 |
18 | 19 |

20 |

21 | 22 |

23 |
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 | --------------------------------------------------------------------------------