bullion 0.1.0 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.images/logo.png +0 -0
  4. data/.rubocop.yml +32 -0
  5. data/.travis.yml +12 -4
  6. data/Dockerfile +55 -0
  7. data/Gemfile +4 -2
  8. data/Gemfile.lock +148 -0
  9. data/LICENSE.txt +1 -1
  10. data/README.md +48 -16
  11. data/Rakefile +88 -3
  12. data/bin/console +4 -3
  13. data/bullion.gemspec +38 -15
  14. data/config.ru +22 -0
  15. data/config/puma.rb +3 -0
  16. data/db/migrate/20210104000000_create_accounts.rb +14 -0
  17. data/db/migrate/20210104060422_create_certificates.rb +18 -0
  18. data/db/migrate/20210105060406_create_orders.rb +19 -0
  19. data/db/migrate/20210106052306_create_authorizations.rb +16 -0
  20. data/db/migrate/20210106055421_create_challenges.rb +18 -0
  21. data/db/migrate/20210106060335_create_nonces.rb +12 -0
  22. data/db/schema.rb +92 -0
  23. data/lib/bullion.rb +93 -2
  24. data/lib/bullion/acme/error.rb +72 -0
  25. data/lib/bullion/challenge_client.rb +59 -0
  26. data/lib/bullion/challenge_clients/dns.rb +49 -0
  27. data/lib/bullion/challenge_clients/http.rb +33 -0
  28. data/lib/bullion/helpers/acme.rb +202 -0
  29. data/lib/bullion/helpers/service.rb +17 -0
  30. data/lib/bullion/helpers/ssl.rb +214 -0
  31. data/lib/bullion/models.rb +8 -0
  32. data/lib/bullion/models/account.rb +33 -0
  33. data/lib/bullion/models/authorization.rb +31 -0
  34. data/lib/bullion/models/certificate.rb +37 -0
  35. data/lib/bullion/models/challenge.rb +37 -0
  36. data/lib/bullion/models/nonce.rb +22 -0
  37. data/lib/bullion/models/order.rb +39 -0
  38. data/lib/bullion/service.rb +26 -0
  39. data/lib/bullion/services/ca.rb +370 -0
  40. data/lib/bullion/services/ping.rb +36 -0
  41. data/lib/bullion/version.rb +7 -1
  42. data/scripts/docker-entrypoint.sh +9 -0
  43. metadata +302 -17
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- require "bundler/setup"
4
- require "bullion"
4
+ require 'bundler/setup'
5
+ require 'bullion'
5
6
 
6
7
  # You can add fixtures and/or initialization code here to make experimenting
7
8
  # with your gem easier. You can also use a different console, if you like.
@@ -10,5 +11,5 @@ require "bullion"
10
11
  # require "pry"
11
12
  # Pry.start
12
13
 
13
- require "irb"
14
+ require 'irb'
14
15
  IRB.start(__FILE__)
@@ -1,28 +1,51 @@
1
+ # frozen_string_literal: true
1
2
 
2
- lib = File.expand_path("../lib", __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require "bullion/version"
3
+ require_relative 'lib/bullion/version'
5
4
 
6
5
  Gem::Specification.new do |spec|
7
- spec.name = "bullion"
6
+ spec.name = 'bullion'
8
7
  spec.version = Bullion::VERSION
9
- spec.authors = ["Jonathan Gnagy"]
10
- spec.email = ["jonathan.gnagy@gmail.com"]
8
+ spec.authors = ['Jonathan Gnagy']
9
+ spec.email = ['jonathan.gnagy@gmail.com']
11
10
 
12
- spec.summary = 'Ruby ACME v2 Certificate Issuer'
13
- spec.homepage = "https://github.com/jgnagy/bullion"
14
- spec.license = "MIT"
11
+ spec.summary = 'Ruby ACME v2 Certificate Authority'
12
+ spec.homepage = 'https://github.com/jgnagy/bullion'
13
+ spec.license = 'MIT'
14
+
15
+ spec.metadata['homepage_uri'] = spec.homepage
16
+ spec.metadata['source_code_uri'] = 'https://github.com/jgnagy/bullion'
15
17
 
16
18
  # Specify which files should be added to the gem when it is released.
17
19
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
18
- spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
20
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
19
21
  `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
22
  end
21
- spec.bindir = "exe"
23
+ spec.bindir = 'exe'
22
24
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
- spec.require_paths = ["lib"]
25
+ spec.require_paths = ['lib']
26
+
27
+ spec.required_ruby_version = '~> 2.6'
28
+
29
+ spec.add_runtime_dependency 'httparty', '~> 0.18'
30
+ spec.add_runtime_dependency 'json', '~> 2.5'
31
+ spec.add_runtime_dependency 'jwt', '~> 1.5'
32
+ spec.add_runtime_dependency 'mysql2', '~> 0.5'
33
+ spec.add_runtime_dependency 'openssl', '~> 2.2'
34
+ spec.add_runtime_dependency 'prometheus-client', '~> 2.1'
35
+ spec.add_runtime_dependency 'puma', '~> 3.12'
36
+ spec.add_runtime_dependency 'sinatra', '~> 2.1'
37
+ spec.add_runtime_dependency 'sinatra-activerecord', '~> 2.0'
38
+ spec.add_runtime_dependency 'sinatra-contrib', '~> 2.1'
39
+ spec.add_runtime_dependency 'sqlite3', '~> 1.4'
24
40
 
25
- spec.add_development_dependency "bundler", "~> 1.17"
26
- spec.add_development_dependency "rake", "~> 10.0"
27
- spec.add_development_dependency "rspec", "~> 3.0"
41
+ spec.add_development_dependency 'acme-client', '~> 2.0'
42
+ spec.add_development_dependency 'bundler', '~> 2.0'
43
+ spec.add_development_dependency 'byebug', '~> 9'
44
+ spec.add_development_dependency 'rack-test', '~> 0.8'
45
+ spec.add_development_dependency 'rake', '~> 12.3'
46
+ spec.add_development_dependency 'rspec', '~> 3.10'
47
+ spec.add_development_dependency 'rubocop', '~> 0.93'
48
+ spec.add_development_dependency 'simplecov', '~> 0.20'
49
+ spec.add_development_dependency 'simplecov-cobertura', '~> 1.4'
50
+ spec.add_development_dependency 'yard', '~> 0.9'
28
51
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # \ -s puma
4
+
5
+ require 'bullion'
6
+ Bullion.validate_config!
7
+
8
+ require 'prometheus/middleware/collector'
9
+ require 'prometheus/middleware/exporter'
10
+
11
+ use Rack::ShowExceptions
12
+ use Rack::Deflater
13
+ use Prometheus::Middleware::Collector
14
+ use Prometheus::Middleware::Exporter
15
+
16
+ # Prometheus metrics are on /metrics
17
+ mappings = {
18
+ '/ping' => Bullion::Services::Ping.new,
19
+ '/acme' => Bullion::Services::CA.new
20
+ }
21
+
22
+ run Rack::URLMap.new(mappings)
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ threads 2, Integer(ENV.fetch('MAX_THREADS', 32))
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Creates the accounts table
4
+ class CreateAccounts < ActiveRecord::Migration[6.1]
5
+ def change
6
+ create_table :accounts do |t|
7
+ t.boolean :tos_agreed, null: false, default: true, index: true
8
+ t.text :public_key, null: false, index: { unique: true }
9
+ t.text :contacts, null: false
10
+
11
+ t.timestamps
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Creates the certificates table
4
+ class CreateCertificates < ActiveRecord::Migration[6.1]
5
+ def change
6
+ create_table :certificates do |t|
7
+ t.string :subject, null: false, index: true
8
+ t.string :csr_fingerprint, null: false, index: true
9
+ t.text :data, null: false
10
+ t.text :alternate_names
11
+ t.string :requester
12
+ t.boolean :validated, null: false, default: false, index: true
13
+ t.integer :serial, null: false, index: { unique: true }
14
+
15
+ t.timestamps
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Creates the orders table
4
+ class CreateOrders < ActiveRecord::Migration[6.1]
5
+ def change
6
+ create_table :orders do |t|
7
+ t.string :status, null: false, default: 'pending', index: true
8
+ t.timestamp :expires, null: false, index: true
9
+ t.text :identifiers, null: false
10
+ t.timestamp :not_before, null: false
11
+ t.timestamp :not_after, null: false
12
+ t.references :certificate, foreign_key: true
13
+
14
+ t.belongs_to :account
15
+
16
+ t.timestamps
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Creates the authorizations table
4
+ class CreateAuthorizations < ActiveRecord::Migration[6.1]
5
+ def change
6
+ create_table :authorizations do |t|
7
+ t.string :status, null: false, default: 'pending', index: true
8
+ t.timestamp :expires, null: false, index: true
9
+ t.text :identifier, null: false
10
+
11
+ t.belongs_to :order
12
+
13
+ t.timestamps
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Creates the challenges table
4
+ class CreateChallenges < ActiveRecord::Migration[6.1]
5
+ def change
6
+ create_table :challenges do |t|
7
+ t.string :acme_type, null: false, index: true
8
+ t.string :status, null: false, default: :pending, index: true
9
+ t.timestamp :expires, null: false, index: true
10
+ t.string :token, null: false
11
+ t.timestamp :validated
12
+
13
+ t.belongs_to :authorization
14
+
15
+ t.timestamps
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Creates the nonces table
4
+ class CreateNonces < ActiveRecord::Migration[6.1]
5
+ def change
6
+ create_table :nonces do |t|
7
+ t.string :token, null: false, index: { unique: true }
8
+
9
+ t.timestamps
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,92 @@
1
+ # This file is auto-generated from the current state of the database. Instead
2
+ # of editing this file, please use the migrations feature of Active Record to
3
+ # incrementally modify your database, and then regenerate this schema definition.
4
+ #
5
+ # This file is the source Rails uses to define your schema when running `bin/rails
6
+ # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7
+ # be faster and is potentially less error prone than running all of your
8
+ # migrations from scratch. Old migrations may fail to apply correctly if those
9
+ # migrations use external dependencies or application code.
10
+ #
11
+ # It's strongly recommended that you check this file into your version control system.
12
+
13
+ ActiveRecord::Schema.define(version: 2021_01_06_060335) do
14
+
15
+ create_table "accounts", force: :cascade do |t|
16
+ t.boolean "tos_agreed", default: true, null: false
17
+ t.text "public_key", null: false
18
+ t.text "contacts", null: false
19
+ t.datetime "created_at", precision: 6, null: false
20
+ t.datetime "updated_at", precision: 6, null: false
21
+ t.index ["public_key"], name: "index_accounts_on_public_key", unique: true
22
+ t.index ["tos_agreed"], name: "index_accounts_on_tos_agreed"
23
+ end
24
+
25
+ create_table "authorizations", force: :cascade do |t|
26
+ t.string "status", default: "pending", null: false
27
+ t.datetime "expires", null: false
28
+ t.text "identifier", null: false
29
+ t.integer "order_id"
30
+ t.datetime "created_at", precision: 6, null: false
31
+ t.datetime "updated_at", precision: 6, null: false
32
+ t.index ["expires"], name: "index_authorizations_on_expires"
33
+ t.index ["order_id"], name: "index_authorizations_on_order_id"
34
+ t.index ["status"], name: "index_authorizations_on_status"
35
+ end
36
+
37
+ create_table "certificates", force: :cascade do |t|
38
+ t.string "subject", null: false
39
+ t.string "csr_fingerprint", null: false
40
+ t.text "data", null: false
41
+ t.text "alternate_names"
42
+ t.string "requester"
43
+ t.boolean "validated", default: false, null: false
44
+ t.integer "serial", null: false
45
+ t.datetime "created_at", precision: 6, null: false
46
+ t.datetime "updated_at", precision: 6, null: false
47
+ t.index ["csr_fingerprint"], name: "index_certificates_on_csr_fingerprint"
48
+ t.index ["serial"], name: "index_certificates_on_serial", unique: true
49
+ t.index ["subject"], name: "index_certificates_on_subject"
50
+ t.index ["validated"], name: "index_certificates_on_validated"
51
+ end
52
+
53
+ create_table "challenges", force: :cascade do |t|
54
+ t.string "acme_type", null: false
55
+ t.string "status", default: "pending", null: false
56
+ t.datetime "expires", null: false
57
+ t.string "token", null: false
58
+ t.datetime "validated"
59
+ t.integer "authorization_id"
60
+ t.datetime "created_at", precision: 6, null: false
61
+ t.datetime "updated_at", precision: 6, null: false
62
+ t.index ["acme_type"], name: "index_challenges_on_acme_type"
63
+ t.index ["authorization_id"], name: "index_challenges_on_authorization_id"
64
+ t.index ["expires"], name: "index_challenges_on_expires"
65
+ t.index ["status"], name: "index_challenges_on_status"
66
+ end
67
+
68
+ create_table "nonces", force: :cascade do |t|
69
+ t.string "token", null: false
70
+ t.datetime "created_at", precision: 6, null: false
71
+ t.datetime "updated_at", precision: 6, null: false
72
+ t.index ["token"], name: "index_nonces_on_token", unique: true
73
+ end
74
+
75
+ create_table "orders", force: :cascade do |t|
76
+ t.string "status", default: "pending", null: false
77
+ t.datetime "expires", null: false
78
+ t.text "identifiers", null: false
79
+ t.datetime "not_before", null: false
80
+ t.datetime "not_after", null: false
81
+ t.integer "certificate_id"
82
+ t.integer "account_id"
83
+ t.datetime "created_at", precision: 6, null: false
84
+ t.datetime "updated_at", precision: 6, null: false
85
+ t.index ["account_id"], name: "index_orders_on_account_id"
86
+ t.index ["certificate_id"], name: "index_orders_on_certificate_id"
87
+ t.index ["expires"], name: "index_orders_on_expires"
88
+ t.index ["status"], name: "index_orders_on_status"
89
+ end
90
+
91
+ add_foreign_key "orders", "certificates"
92
+ end
@@ -1,6 +1,97 @@
1
- require "bullion/version"
1
+ # frozen_string_literal: true
2
2
 
3
+ # Standard Library requirements
4
+ require 'base64'
5
+ require 'resolv'
6
+ require 'securerandom'
7
+ require 'time'
8
+ require 'logger'
9
+ require 'openssl'
10
+
11
+ # External requirements
12
+ require 'sinatra/base'
13
+ require 'sinatra/custom_logger'
14
+ require 'mysql2'
15
+ require 'sinatra/activerecord'
16
+ require 'jwt'
17
+ require 'prometheus/client'
18
+ require 'httparty'
19
+
20
+ # The top-level module for Bullion
3
21
  module Bullion
4
22
  class Error < StandardError; end
5
- # Your code goes here...
23
+ class ConfigError < Error; end
24
+
25
+ LOGGER = Logger.new($stdout)
26
+
27
+ # Config through environment variables
28
+ CA_DIR = File.expand_path ENV.fetch('CA_DIR', 'tmp')
29
+ CA_SECRET = ENV.fetch('CA_SECRET', 'SomeS3cret')
30
+ CA_KEY_PATH = ENV.fetch('CA_KEY_PATH') { File.join(CA_DIR, 'tls.key') }
31
+ CA_CERT_PATH = ENV.fetch('CA_CERT_PATH') { File.join(CA_DIR, 'tls.crt') }
32
+ CA_DOMAINS = ENV.fetch('CA_DOMAINS', 'example.com').split(',')
33
+
34
+ # Set up log level
35
+ LOGGER.level = ENV.fetch('LOG_LEVEL', :warn)
36
+
37
+ # 90 days cert expiration
38
+ CERT_VALIDITY_DURATION = Integer(
39
+ ENV.fetch('CERT_VALIDITY_DURATION', 60 * 60 * 24 * 30 * 3)
40
+ )
41
+
42
+ DB_CONNECTION_SETTINGS =
43
+ ENV['DATABASE_URL'] || {
44
+ adapter: 'mysql2',
45
+ database: ENV.fetch('DB_NAME', 'bullion'),
46
+ encoding: ENV.fetch('DB_ENCODING', 'utf8mb4'),
47
+ pool: Integer(ENV.fetch('MAX_THREADS', 32)),
48
+ username: ENV.fetch('DB_USERNAME', 'root'),
49
+ password: ENV['DB_PASSWORD'],
50
+ host: ENV.fetch('DB_HOST', 'localhost')
51
+ }
52
+ DB_CONNECTION_SETTINGS.freeze
53
+
54
+ NAMESERVERS = ENV.fetch('DNS01_NAMESERVERS', '8.8.8.8').split(',')
55
+
56
+ MetricsRegistry = Prometheus::Client.registry
57
+
58
+ def self.ca_key
59
+ @ca_key ||= OpenSSL::PKey::RSA.new(File.read(CA_KEY_PATH), CA_SECRET)
60
+ end
61
+
62
+ def self.ca_cert
63
+ @ca_cert ||= OpenSSL::X509::Certificate.new(File.read(CA_CERT_PATH))
64
+ end
65
+
66
+ def self.rotate_keys!
67
+ @ca_key = nil
68
+ @ca_cert = nil
69
+ ca_key
70
+ ca_cert
71
+ true
72
+ end
73
+
74
+ # Ensures configuration settings are valid
75
+ # @see https://support.apple.com/en-us/HT211025
76
+ def self.validate_config!
77
+ raise ConfigError, 'Invalid Key Passphrase' unless CA_SECRET.is_a?(String)
78
+ raise ConfigError, "Invalid Key Path: #{CA_KEY_PATH}" unless File.readable?(CA_KEY_PATH)
79
+ raise ConfigError, "Invalid Cert Path: #{CA_CERT_PATH}" unless File.readable?(CA_CERT_PATH)
80
+ raise ConfigError, 'Cert Validity Too Long' if CERT_VALIDITY_DURATION > 60 * 60 * 24 * 397
81
+ raise ConfigError, 'Cert Validity Too Short' if CERT_VALIDITY_DURATION < 60 * 60 * 24 * 2
82
+ end
6
83
  end
84
+
85
+ # Internal requirements
86
+ require 'bullion/version'
87
+ require 'bullion/acme/error'
88
+ require 'bullion/helpers/acme'
89
+ require 'bullion/helpers/service'
90
+ require 'bullion/helpers/ssl'
91
+ require 'bullion/models'
92
+ require 'bullion/service'
93
+ require 'bullion/services/ping'
94
+ require 'bullion/services/ca'
95
+ require 'bullion/challenge_client'
96
+ require 'bullion/challenge_clients/dns'
97
+ require 'bullion/challenge_clients/http'
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bullion
4
+ module Acme
5
+ # ACME protocol errors super class
6
+ class Error < Bullion::Error
7
+ # @see https://tools.ietf.org/html/rfc8555#section-6.7
8
+ def acme_type
9
+ 'genericError'
10
+ end
11
+
12
+ def acme_preface
13
+ 'urn:ietf:params:acme:error:'
14
+ end
15
+
16
+ def acme_error
17
+ acme_preface + acme_type
18
+ end
19
+ end
20
+
21
+ module Errors
22
+ # ACME exception for bad CSRs
23
+ class BadCsr < Bullion::Acme::Error
24
+ def acme_type
25
+ 'badCSR'
26
+ end
27
+ end
28
+
29
+ # ACME exception for bad Nonces
30
+ class BadNonce < Bullion::Acme::Error
31
+ def acme_type
32
+ 'badNonce'
33
+ end
34
+ end
35
+
36
+ # ACME exception for invalid contacts in accounts
37
+ class InvalidContact < Bullion::Acme::Error
38
+ def acme_type
39
+ 'invalidContact'
40
+ end
41
+ end
42
+
43
+ # ACME exception for invalid orders
44
+ class InvalidOrder < Bullion::Acme::Error
45
+ def acme_type
46
+ 'invalidOrder'
47
+ end
48
+ end
49
+
50
+ # ACME exception for malformed requests
51
+ class Malformed < Bullion::Acme::Error
52
+ def acme_type
53
+ 'malformed'
54
+ end
55
+ end
56
+
57
+ # ACME exception for unsupported contacts in accounts
58
+ class UnsupportedContact < Bullion::Acme::Error
59
+ def acme_type
60
+ 'unsupportedContact'
61
+ end
62
+ end
63
+
64
+ # Non-standard exception for unsupported challenge types
65
+ class UnsupportedChallengeType < Bullion::Acme::Error
66
+ def acme_error
67
+ 'urn:ietf:params:bullion:error:unsupportedChallengeType'
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end