bullion 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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