bullion 0.11.1 → 0.11.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8821e563a9452dbd524f1bdfbd2f944ee4a3d0c071e57bab6d5f9b2c61e18555
4
- data.tar.gz: d31913ae560676de68a57ea6aa308126a6d3c92c7a3da69d4d882e032ed56196
3
+ metadata.gz: f1e03e0297625063a198db254192ac85e7389a46bd1ebd0be1161f1221300e4d
4
+ data.tar.gz: 2c5a0729d0521920a378fa235f928b6c7eb4c16cd3ee9a63bf74fa89eca7168c
5
5
  SHA512:
6
- metadata.gz: 4bcabcb78421e92a48fbd5485631de98bee3bf0d4452ba5d9c08eb04d34ac171934b073e0a441b5dc95aeeb353765a5445188ca7011b98d3e2f376d130aaf2d4
7
- data.tar.gz: 5541be539b018f62d1e466e51ada7985f5e37ee460eb98c4e344f79c443aa60fa3f8b2bf1d26d169a15c0b8efef38c7523c4023a416436e85654b585a5fcae6c
6
+ metadata.gz: 06e5aa78438a991e3e2cf9db8519a2c81bb35c3b872f1fc511ca7ea6d397088e3fce4642631d3dd7d77e9105e9fb4b808dfdc2b185fc121d3deebf8f3614e0d2
7
+ data.tar.gz: e3809f863bd43ff203a580b65a36e6d1f8f088cf12b1650ab7057cca3220019db00bfb231f7d85bbce983600d6488cd75f2748d3c164692c41646bbc9166c043
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.4.4
1
+ 3.4.10
data/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.11.5](https://github.com/jgnagy/bullion/compare/bullion/v0.11.4...bullion/v0.11.5) (2026-09-21)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **ci:** disable gem attestations to unblock release ([d850693](https://github.com/jgnagy/bullion/commit/d850693bc3ca6c58b15471cea773f0c58df1b0b1))
9
+ * **ci:** disable gem attestations to unblock release ([5bd3675](https://github.com/jgnagy/bullion/commit/5bd367593fc1bc4af1c71f27d2ed4bedbff2ad5c))
10
+
11
+ ## [0.11.4](https://github.com/jgnagy/bullion/compare/bullion/v0.11.3...bullion/v0.11.4) (2026-09-21)
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * package gem files without relying on git metadata ([ef70f0a](https://github.com/jgnagy/bullion/commit/ef70f0a42285aab8ae8bde1f8f0063e6a0ad38cb))
17
+ * package gem files without relying on git metadata ([8515841](https://github.com/jgnagy/bullion/commit/851584156440fef014503a9b3320049554a9083e))
18
+
19
+ ## [0.11.3](https://github.com/jgnagy/bullion/compare/bullion/v0.11.2...bullion/v0.11.3) (2026-07-16)
20
+
21
+
22
+ ### Features
23
+
24
+ * implement revokeCert endpoint (closes [#1](https://github.com/jgnagy/bullion/issues/1)) ([#107](https://github.com/jgnagy/bullion/issues/107)) ([b08c65a](https://github.com/jgnagy/bullion/commit/b08c65a7cbb62abef34d7a24fe7f89124fae4743))
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * replace undefined CERT_VALIDITY_DURATION constant with config call ([f858418](https://github.com/jgnagy/bullion/commit/f85841818f6d3511e436c010af86715433c57922))
30
+ * replace undefined CERT_VALIDITY_DURATION constant with config call ([8dc0ed0](https://github.com/jgnagy/bullion/commit/8dc0ed0dabdca4d50f720e22ce7d6825233042bb)), closes [#101](https://github.com/jgnagy/bullion/issues/101)
31
+
32
+ ## [0.11.2](https://github.com/jgnagy/bullion/compare/bullion/v0.11.1...bullion/v0.11.2) (2026-02-16)
33
+
34
+
35
+ ### Features
36
+
37
+ * add EdDSA (Ed25519) JWT signature support ([bba2985](https://github.com/jgnagy/bullion/commit/bba29851e1c5644152f4a3e82d82c6c1d5c5ce5e))
38
+ * add EdDSA (Ed25519) JWT signature support ([77a2f5b](https://github.com/jgnagy/bullion/commit/77a2f5bfc30db3527ad3fe75136b210956279957)), closes [#3](https://github.com/jgnagy/bullion/issues/3)
39
+ * Add test coverage for Ed25519 certificate signing ([59f0f74](https://github.com/jgnagy/bullion/commit/59f0f74a039292a5d804c8e49a880f20ab5d0c1b))
40
+
41
+
42
+ ### Bug Fixes
43
+
44
+ * return proper badPublicKey error for Ed448 ([a21414f](https://github.com/jgnagy/bullion/commit/a21414f66075d47e2b5613d2034fa806a0a78602))
45
+
3
46
  ## [0.11.1](https://github.com/jgnagy/bullion/compare/bullion/v0.11.0...bullion/v0.11.1) (2025-08-24)
4
47
 
5
48
 
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  Bullion is an [ACMEv2](https://tools.ietf.org/html/rfc8555)-compatible Certificate Authority (just like [Let's Encrypt](https://letsencrypt.org/)). Bullion makes it easy to leverage open standards for provisioning certificates for internal sites and services. Things like [cert-manager](https://cert-manager.io/), [certbot](https://certbot.eff.org/), and many other technologies work great with Let's Encrypt for _external_ hostnames, but for private domains and servers that aren't Internet accessible, there are few easy options.
6
6
 
7
- Bullion installs easily (both as a ruby gem or a Docker image) and will shortly come with Kubernetes examples and even a helm chart to make installing it that much easier. For its data, Bullion works with either [SQLite3](https://sqlite.org/index.html) for very small sites (where HA and high-throughput isn't a concern) or with [MariaDB](https://mariadb.org/)/[MySQL](https://www.mysql.com/).
7
+ Bullion installs easily (both as a ruby gem or a Docker image) and works with either [SQLite3](https://sqlite.org/index.html) for very small sites (where HA and high-throughput isn't a concern) or with [MariaDB](https://mariadb.org/)/[MySQL](https://www.mysql.com/) for production deployments.
8
8
 
9
9
  The goal of the Bullion project is to make running a scalable, internal ACMEv2 CA easy. Either make a custom root CA certificate or give Bullion a signed intermediary to use in simple, compatible PEM format and you're up and running.
10
10
 
@@ -20,9 +20,133 @@ The goal of the Bullion project is to make running a scalable, internal ACMEv2 C
20
20
 
21
21
  ## Usage
22
22
 
23
- ### Running
23
+ ### Prerequisites
24
24
 
25
- TODO: Write instructions for starting Bullion
25
+ Before running Bullion, you need a CA key pair. Bullion requires two PEM files:
26
+
27
+ - **`tls.key`** — An encrypted private key (RSA, ECDSA, or EdDSA) for signing certificates. Bullion supports encrypted PEM keys using AES-128-CBC.
28
+ - **`tls.crt`** — The corresponding public certificate. If Bullion is an intermediate CA, include the root CA's certificate in this file as well (certificate chain order: intermediate first, then root).
29
+
30
+ You can generate these with OpenSSL. For example, a self-signed root CA:
31
+
32
+ ```bash
33
+ # Generate an encrypted RSA private key
34
+ openssl genrsa -aes128 -out tls.key 4096
35
+
36
+ # Create a self-signed root certificate (5 years)
37
+ openssl req -x509 -new -key tls.key -sha256 -days 1825 \
38
+ -out tls.crt -subj "/DC=example/DC=com/CN=Bullion Root CA"
39
+ ```
40
+
41
+ For production, you'll typically want an intermediate CA signed by your organization's root CA. Provide the intermediate certificate and its private key to Bullion, and include the root CA certificate in `tls.crt` so clients receive the full chain.
42
+
43
+ ### Running with Docker
44
+
45
+ The quickest way to run Bullion is with the official Docker image:
46
+
47
+ ```bash
48
+ docker run -d \
49
+ --name bullion \
50
+ -p 9292:9292 \
51
+ -v /path/to/ssl:/ssl:ro \
52
+ -v /path/to/data:/data \
53
+ -e CA_DIR=/ssl \
54
+ -e CA_SECRET=your-key-password \
55
+ -e CA_DOMAINS=example.com \
56
+ -e DATABASE_URL=sqlite3:/data/bullion.db \
57
+ jgnagy/bullion:latest
58
+ ```
59
+
60
+ This starts Bullion on port 9292 with:
61
+
62
+ - CA key pair mounted from `/path/to/ssl` (containing `tls.key` and `tls.crt`)
63
+ - SQLite database persisted at `/path/to/data/bullion.db`
64
+ - Certificate signing restricted to `example.com` and its subdomains
65
+
66
+ Verify it's running:
67
+
68
+ ```bash
69
+ curl http://localhost:9292/ping
70
+ # {"status":"up"}
71
+ ```
72
+
73
+ For MySQL/MariaDB, use a `trilogy://` connection URL instead:
74
+
75
+ ```bash
76
+ -e DATABASE_URL=trilogy://user:password@mariadb:3306/bullion
77
+ ```
78
+
79
+ ### Running as a Gem
80
+
81
+ Install the gem:
82
+
83
+ ```bash
84
+ gem install bullion
85
+ ```
86
+
87
+ Bullion is a Rack application served by [Itsi](https://itsi.fyi/). After installing the gem, you'll need a `config.ru` and an `Itsi.rb` configuration file. The simplest approach is to create a working directory with your CA key pair and a minimal Rack config:
88
+
89
+ ```bash
90
+ mkdir bullion-server && cd bullion-server
91
+
92
+ # Place your CA key pair here
93
+ cp /path/to/tls.key .
94
+ cp /path/to/tls.crt
95
+
96
+ # Create a minimal config.ru
97
+ cat > config.ru <<'RUBY'
98
+ # frozen_string_literal: true
99
+
100
+ require "bullion"
101
+ Bullion.validate_config!
102
+
103
+ require "prometheus/middleware/collector"
104
+ require "prometheus/middleware/exporter"
105
+
106
+ use Rack::ShowExceptions
107
+ use Rack::Deflater
108
+ use Prometheus::Middleware::Collector
109
+ use Prometheus::Middleware::Exporter
110
+
111
+ mappings = {
112
+ "/ping" => Bullion::Services::Ping.new,
113
+ "/acme" => Bullion::Services::CA.new
114
+ }
115
+
116
+ run Rack::URLMap.new(mappings)
117
+ RUBY
118
+
119
+ # Initialize Itsi and run database migrations
120
+ itsi init
121
+ DATABASE_URL=sqlite3:./bullion.db bullion-exec rake db:migrate
122
+
123
+ # Start the server
124
+ CA_DIR=. CA_SECRET=your-key-password CA_DOMAINS=example.com \
125
+ DATABASE_URL=sqlite3:./bullion.db itsi
126
+ ```
127
+
128
+ ### Database Setup
129
+
130
+ Bullion uses ActiveRecord migrations to create its schema. The migrations are included with the gem. Run them before starting the server:
131
+
132
+ ```bash
133
+ DATABASE_URL=sqlite3:./bullion.db bundle exec rake db:migrate
134
+ ```
135
+
136
+ When using Docker, the included entrypoint handles this automatically on first start. If you're running the gem directly, you may need to run migrations manually after installing the gem.
137
+
138
+ ### Quick Local Demo
139
+
140
+ For testing or evaluation, the Rakefile includes a `local_demo` task that generates a temporary CA key pair, runs migrations, and starts the server in the foreground:
141
+
142
+ ```bash
143
+ git clone https://github.com/jgnagy/bullion.git
144
+ cd bullion
145
+ bundle install
146
+ bundle exec rake local_demo
147
+ ```
148
+
149
+ This creates a self-signed root CA and intermediate certificate in `tmp/`, uses an SQLite database in `tmp/db/`, and starts Bullion on port 9292. Press `Ctrl+C` to stop.
26
150
 
27
151
  ### Configuration Options
28
152
 
@@ -57,20 +181,10 @@ Bullion provides a `/ping` endpoint that should respond with `{ 'status': 'up' }
57
181
 
58
182
  Prometheus metrics are also scrapable at `/metrics`. This includes typical web request information as well as latencies related to the different Challenge types.
59
183
 
60
- ## Development
184
+ ## Development & Contributing
61
185
 
62
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
63
-
64
- To install this gem onto your local machine, run `bundle exec rake install`.
65
-
66
- ## Contributing
67
-
68
- Bug reports and pull requests are welcome on GitHub at https://github.com/jgnagy/bullion. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
186
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md) for development setup, testing, style guidelines, and pull request expectations. Guidelines for AI agents and automated tools are in [`AGENTS.md`](AGENTS.md).
69
187
 
70
188
  ## License
71
189
 
72
190
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
73
-
74
- ## Code of Conduct
75
-
76
- Everyone interacting in the Bullion project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/jgnagy/bullion/blob/master/CODE_OF_CONDUCT.md).
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adds revocation tracking columns to the certificates table
4
+ class AddRevocationToCertificates < ActiveRecord::Migration[8.0]
5
+ def change
6
+ add_column :certificates, :revoked, :boolean, default: false, null: false
7
+ add_column :certificates, :revoked_at, :datetime
8
+ add_column :certificates, :revocation_reason, :integer
9
+ end
10
+ end
data/db/schema.rb CHANGED
@@ -10,24 +10,24 @@
10
10
  #
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
13
- ActiveRecord::Schema[8.0].define(version: 2025_08_23_073342) do
13
+ ActiveRecord::Schema[8.1].define(version: 2025_08_23_073342) do
14
14
  create_table "accounts", force: :cascade do |t|
15
- t.boolean "tos_agreed", default: true, null: false
16
- t.text "public_key", null: false
17
15
  t.text "contacts", null: false
18
16
  t.datetime "created_at", null: false
19
- t.datetime "updated_at", null: false
17
+ t.text "public_key", null: false
20
18
  t.string "public_key_hash", limit: 44, null: false
19
+ t.boolean "tos_agreed", default: true, null: false
20
+ t.datetime "updated_at", null: false
21
21
  t.index ["public_key_hash"], name: "index_accounts_on_public_key_hash", unique: true
22
22
  t.index ["tos_agreed"], name: "index_accounts_on_tos_agreed"
23
23
  end
24
24
 
25
25
  create_table "authorizations", force: :cascade do |t|
26
- t.string "status", default: "pending", null: false
26
+ t.datetime "created_at", null: false
27
27
  t.datetime "expires", null: false
28
28
  t.text "identifier", null: false
29
29
  t.bigint "order_id"
30
- t.datetime "created_at", null: false
30
+ t.string "status", default: "pending", null: false
31
31
  t.datetime "updated_at", null: false
32
32
  t.index ["expires"], name: "index_authorizations_on_expires"
33
33
  t.index ["order_id"], name: "index_authorizations_on_order_id"
@@ -35,15 +35,15 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_23_073342) do
35
35
  end
36
36
 
37
37
  create_table "certificates", force: :cascade do |t|
38
- t.string "subject", null: false
38
+ t.text "alternate_names"
39
+ t.datetime "created_at", null: false
39
40
  t.string "csr_fingerprint", null: false
40
41
  t.text "data", null: false
41
- t.text "alternate_names"
42
42
  t.string "requester"
43
- t.boolean "validated", default: false, null: false
44
43
  t.bigint "serial", null: false
45
- t.datetime "created_at", null: false
44
+ t.string "subject", null: false
46
45
  t.datetime "updated_at", null: false
46
+ t.boolean "validated", default: false, null: false
47
47
  t.index ["csr_fingerprint"], name: "index_certificates_on_csr_fingerprint"
48
48
  t.index ["serial"], name: "index_certificates_on_serial", unique: true
49
49
  t.index ["subject"], name: "index_certificates_on_subject"
@@ -52,13 +52,13 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_23_073342) do
52
52
 
53
53
  create_table "challenges", force: :cascade do |t|
54
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
55
  t.bigint "authorization_id"
60
56
  t.datetime "created_at", null: false
57
+ t.datetime "expires", null: false
58
+ t.string "status", default: "pending", null: false
59
+ t.string "token", null: false
61
60
  t.datetime "updated_at", null: false
61
+ t.datetime "validated"
62
62
  t.index ["acme_type"], name: "index_challenges_on_acme_type"
63
63
  t.index ["authorization_id"], name: "index_challenges_on_authorization_id"
64
64
  t.index ["expires"], name: "index_challenges_on_expires"
@@ -66,21 +66,21 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_23_073342) do
66
66
  end
67
67
 
68
68
  create_table "nonces", force: :cascade do |t|
69
- t.string "token", null: false
70
69
  t.datetime "created_at", null: false
70
+ t.string "token", null: false
71
71
  t.datetime "updated_at", null: false
72
72
  t.index ["token"], name: "index_nonces_on_token", unique: true
73
73
  end
74
74
 
75
75
  create_table "orders", force: :cascade do |t|
76
- t.string "status", default: "pending", null: false
76
+ t.bigint "account_id"
77
+ t.bigint "certificate_id"
78
+ t.datetime "created_at", null: false
77
79
  t.datetime "expires", null: false
78
80
  t.text "identifiers", null: false
79
- t.datetime "not_before", null: false
80
81
  t.datetime "not_after", null: false
81
- t.bigint "certificate_id"
82
- t.bigint "account_id"
83
- t.datetime "created_at", null: false
82
+ t.datetime "not_before", null: false
83
+ t.string "status", default: "pending", null: false
84
84
  t.datetime "updated_at", null: false
85
85
  t.index ["account_id"], name: "index_orders_on_account_id"
86
86
  t.index ["certificate_id"], name: "index_orders_on_certificate_id"
@@ -34,6 +34,21 @@ module Bullion
34
34
  def acme_type = "badNonce"
35
35
  end
36
36
 
37
+ # ACME exception for bad/unsupported public keys
38
+ class BadPublicKey < Bullion::Acme::Error
39
+ def acme_type = "badPublicKey"
40
+ end
41
+
42
+ # ACME exception for bad certificates
43
+ class BadCertificate < Bullion::Acme::Error
44
+ def acme_type = "badCertificate"
45
+ end
46
+
47
+ # ACME exception for already-revoked certificates
48
+ class AlreadyRevoked < Bullion::Acme::Error
49
+ def acme_type = "alreadyRevoked"
50
+ end
51
+
37
52
  # ACME exception for invalid contacts in accounts
38
53
  class InvalidContact < Bullion::Acme::Error
39
54
  def acme_type = "invalidContact"
@@ -41,10 +41,11 @@ module Bullion
41
41
  JWT.decode(jwt_data, compat_public_key, true, { algorithm: @header_data["alg"] })
42
42
  else
43
43
  digest = digest_from_alg(@header_data["alg"])
44
+ alg = @header_data["alg"].downcase
44
45
 
45
- sig = if @header_data["alg"].downcase.start_with?("es")
46
+ sig = if alg.start_with?("es")
46
47
  ecdsa_sig_to_der(signature)
47
- elsif @header_data["alg"].downcase.start_with?("rs")
48
+ elsif alg.start_with?("rs") || alg == "eddsa"
48
49
  Base64.urlsafe_decode64(signature)
49
50
  end
50
51
 
@@ -173,7 +174,7 @@ module Bullion
173
174
  # don't allow nonsense certs
174
175
  raise Bullion::Acme::Errors::InvalidOrder unless nb < na
175
176
  # don't allow far-future certs
176
- if nb > Time.now + (7 * 86_400) || na > Time.now + CERT_VALIDITY_DURATION
177
+ if nb > Time.now + (7 * 86_400) || na > Time.now + Bullion.config.ca.cert_validity_duration
177
178
  raise Bullion::Acme::Errors::InvalidOrder
178
179
  end
179
180
 
@@ -3,6 +3,7 @@
3
3
  module Bullion
4
4
  module Helpers
5
5
  # SSL-related helper methods
6
+ # rubocop:disable Metrics/ModuleLength
6
7
  module Ssl
7
8
  # Converts the incoming key data to an OpenSSL public key usable to verify JWT signatures
8
9
  def openssl_compat(key_data)
@@ -11,6 +12,8 @@ module Bullion
11
12
  key_data_to_rsa(key_data)
12
13
  when "EC"
13
14
  key_data_to_ecdsa(key_data)
15
+ when "OKP"
16
+ key_data_to_eddsa(key_data)
14
17
  end
15
18
  end
16
19
 
@@ -58,6 +61,24 @@ module Bullion
58
61
  OpenSSL::PKey::EC.new(outer_sequence.to_der)
59
62
  end
60
63
 
64
+ def key_data_to_eddsa(key_data)
65
+ curve = key_data["crv"]
66
+ x = base64_to_octet(key_data["x"])
67
+
68
+ # For JWT verification with jwt-eddsa gem, we need Ed25519::VerifyKey
69
+ case curve
70
+ when "Ed25519"
71
+ # The raw public key bytes are exactly what Ed25519::VerifyKey expects
72
+ Ed25519::VerifyKey.new(x)
73
+ when "Ed448"
74
+ # Ed448 not currently supported by the ed25519 gem
75
+ raise Bullion::Acme::Errors::BadPublicKey,
76
+ "EdDSA with Ed448 is not supported; only Ed25519 is supported"
77
+ else
78
+ raise Bullion::Acme::Errors::BadPublicKey, "Unsupported EdDSA curve: #{curve}"
79
+ end
80
+ end
81
+
61
82
  def base64_to_long(data)
62
83
  Base64.urlsafe_decode64(data).to_s.unpack("C*").map do |byte|
63
84
  to_hex(byte)
@@ -69,6 +90,9 @@ module Bullion
69
90
  end
70
91
 
71
92
  def digest_from_alg(alg)
93
+ # EdDSA doesn't use a separate digest step
94
+ return nil if alg.downcase == "eddsa"
95
+
72
96
  if alg.end_with?("256")
73
97
  OpenSSL::Digest.new("SHA256")
74
98
  elsif alg.end_with?("384")
@@ -231,5 +255,6 @@ module Bullion
231
255
  end
232
256
  # rubocop:enable Metrics/AbcSize
233
257
  end
258
+ # rubocop:enable Metrics/ModuleLength
234
259
  end
235
260
  end
@@ -24,6 +24,11 @@ module Bullion
24
24
  subject.split("/").grep(/^CN=/).first.split("=").last
25
25
  end
26
26
 
27
+ # Returns true if the certificate has been revoked
28
+ def revoked?
29
+ revoked == true
30
+ end
31
+
27
32
  def self.from_csr(csr)
28
33
  subjt = csr.subject if csr.subject && !csr.subject.to_s.empty?
29
34
 
@@ -50,7 +50,17 @@ module Bullion
50
50
 
51
51
  def lexicographically_ordered_public_key
52
52
  jwk = authorization.order.account.public_key
53
- [["e", jwk["e"]], ["kty", jwk["kty"]], ["n", jwk["n"]]].to_h
53
+ case jwk["kty"]
54
+ when "RSA"
55
+ [["e", jwk["e"]], ["kty", jwk["kty"]], ["n", jwk["n"]]].to_h
56
+ when "EC"
57
+ [["crv", jwk["crv"]], ["kty", jwk["kty"]], ["x", jwk["x"]], ["y", jwk["y"]]].to_h
58
+ when "OKP"
59
+ [["crv", jwk["crv"]], ["kty", jwk["kty"]], ["x", jwk["x"]]].to_h
60
+ else
61
+ # Fallback for unknown types
62
+ jwk.sort.to_h
63
+ end
54
64
  end
55
65
  end
56
66
  end
@@ -8,6 +8,7 @@ module Bullion
8
8
 
9
9
  configure do
10
10
  set :protection, except: :http_origin
11
+ set :host_authorization, permitted_hosts: []
11
12
  set :logging, true
12
13
  set :logger, Bullion::LOGGER
13
14
  set :database, Bullion.config.db_url
@@ -66,6 +66,10 @@ module Bullion
66
66
  halt 200
67
67
  end
68
68
 
69
+ options "/revokecert" do
70
+ halt 200
71
+ end
72
+
69
73
  # Non-standard endpoint that returns the CA bundle for Bullion
70
74
  # Trusting this bundle should be sufficient to trust all Bullion-issued certs
71
75
  options "/cabundle" do
@@ -384,6 +388,44 @@ module Bullion
384
388
  halt(422, { error: "Order not valid" }.to_json)
385
389
  end
386
390
  end
391
+
392
+ # Revokes a certificate
393
+ # @see https://tools.ietf.org/html/rfc8555#section-7.6
394
+ post "/revokecert" do
395
+ parse_acme_jwt
396
+ add_acme_headers @new_nonce
397
+
398
+ # The certificate is base64url-encoded DER
399
+ cert_der = Base64.urlsafe_decode64(@payload_data["certificate"])
400
+ cert = OpenSSL::X509::Certificate.new(cert_der)
401
+
402
+ # Find the certificate in the database by matching the PEM data
403
+ record = Models::Certificate.find_by(serial: cert.serial.to_i)
404
+
405
+ unless record
406
+ content_type "application/problem+json"
407
+ halt 400, {
408
+ type: Bullion::Acme::Errors::BadCertificate.new.acme_error,
409
+ detail: "Certificate not found"
410
+ }.to_json
411
+ end
412
+
413
+ if record.revoked?
414
+ content_type "application/problem+json"
415
+ halt 400, {
416
+ type: Bullion::Acme::Errors::AlreadyRevoked.new.acme_error,
417
+ detail: "Certificate has already been revoked"
418
+ }.to_json
419
+ end
420
+
421
+ reason = @payload_data["reason"]
422
+ record.update!(revoked: true, revoked_at: Time.now, revocation_reason: reason)
423
+
424
+ halt 200
425
+ rescue Bullion::Acme::Error => e
426
+ content_type "application/problem+json"
427
+ halt 400, { type: e.acme_error, detail: e.message }.to_json
428
+ end
387
429
  end
388
430
  end
389
431
  end
@@ -5,6 +5,7 @@ module Bullion
5
5
  # Healthcheck service
6
6
  class Ping < Sinatra::Application
7
7
  configure do
8
+ set :host_authorization, permitted_hosts: []
8
9
  set :logging, true
9
10
  set :logger, Bullion::LOGGER
10
11
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bullion
4
- VERSION = "0.11.1"
4
+ VERSION = "0.11.5"
5
5
  end
data/lib/bullion.rb CHANGED
@@ -18,6 +18,8 @@ require "sinatra/custom_logger"
18
18
  require "trilogy"
19
19
  require "sinatra/activerecord"
20
20
  require "jwt"
21
+ require "jwt/eddsa"
22
+ require "ed25519"
21
23
  require "prometheus/client"
22
24
  require "httparty"
23
25
 
@@ -105,11 +107,11 @@ module Bullion
105
107
  raise ConfigError,
106
108
  "Invalid Cert Path: #{config.ca.cert_path}"
107
109
  end
108
- if 60 * 60 * 24 * 397 < config.ca.cert_validity_duration
110
+ if config.ca.cert_validity_duration > 60 * 60 * 24 * 397
109
111
  raise ConfigError,
110
112
  "Cert Validity Too Long"
111
113
  end
112
- if 60 * 60 * 24 * 2 > config.ca.cert_validity_duration
114
+ if config.ca.cert_validity_duration < 60 * 60 * 24 * 2
113
115
  raise ConfigError,
114
116
  "Cert Validity Too Short"
115
117
  end