bullion 0.11.2 → 0.11.6

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: 95ab410d446f9be5b65083d059c91636f5c4ee801b2973536387bb001623a0b2
4
- data.tar.gz: 87f492721aaf01a88c746bed6e70d5709c19c7ba3192a7ae83d6c628e716a0dd
3
+ metadata.gz: 44ad24a873adf3bab2e535688588282797d477c7f973835094b52d00cb9c6592
4
+ data.tar.gz: ee339845aaaca5001f5a33f21c75448c8d59b19893191bbc91055d41b13edc66
5
5
  SHA512:
6
- metadata.gz: 1ce61712fb1ee5ce6139bb6e105b242709c8f51eece706e116a4cff94f7361c3c7f9589598cc25373695f1df35f60b06866303f96872e1dc516748068fef338c
7
- data.tar.gz: e69f33f2ee12e17572ad6ad2054a5123d7a1fe3f37471bd64432752b6b0df75892e1fba19004144349810cfae28bafb0ce23039d8cb53613f2ac9d66603eb8d1
6
+ metadata.gz: 2f663b5f7aedb812648c9065fd324ab0ab70a49b4ecb58c2b1160e12c84c93a5e340512be14812ec1e9d6790cce41344182df20e942d4ade2383ff81cf686dae
7
+ data.tar.gz: 7145c2c82d0e710de29bfc97dbe481464696d74a5381ac4dd55e56bd06605dc225dc6bd2a0593df459413d962c9583f5b3e6d133757b1e5ed75654af880b271f
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.4.4
1
+ 3.4.10
data/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.11.6](https://github.com/jgnagy/bullion/compare/bullion/v0.11.5...bullion/v0.11.6) (2026-10-02)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * give each request fiber its own database connection ([c47a277](https://github.com/jgnagy/bullion/commit/c47a2779ab6b115bbb0c9af75559c17010353962))
9
+ * give each request fiber its own database connection ([cca3b31](https://github.com/jgnagy/bullion/commit/cca3b313a6adf04ef9680698ca64f53027b58d3b))
10
+ * return orderNotReady instead of badCSR for non-ready finalize ([2d5fc6f](https://github.com/jgnagy/bullion/commit/2d5fc6f15f98ba39253b294868674073d56e5f81))
11
+
12
+ ## [0.11.5](https://github.com/jgnagy/bullion/compare/bullion/v0.11.4...bullion/v0.11.5) (2026-09-21)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * **ci:** disable gem attestations to unblock release ([d850693](https://github.com/jgnagy/bullion/commit/d850693bc3ca6c58b15471cea773f0c58df1b0b1))
18
+ * **ci:** disable gem attestations to unblock release ([5bd3675](https://github.com/jgnagy/bullion/commit/5bd367593fc1bc4af1c71f27d2ed4bedbff2ad5c))
19
+
20
+ ## [0.11.4](https://github.com/jgnagy/bullion/compare/bullion/v0.11.3...bullion/v0.11.4) (2026-09-21)
21
+
22
+
23
+ ### Bug Fixes
24
+
25
+ * package gem files without relying on git metadata ([ef70f0a](https://github.com/jgnagy/bullion/commit/ef70f0a42285aab8ae8bde1f8f0063e6a0ad38cb))
26
+ * package gem files without relying on git metadata ([8515841](https://github.com/jgnagy/bullion/commit/851584156440fef014503a9b3320049554a9083e))
27
+
28
+ ## [0.11.3](https://github.com/jgnagy/bullion/compare/bullion/v0.11.2...bullion/v0.11.3) (2026-07-16)
29
+
30
+
31
+ ### Features
32
+
33
+ * 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))
34
+
35
+
36
+ ### Bug Fixes
37
+
38
+ * replace undefined CERT_VALIDITY_DURATION constant with config call ([f858418](https://github.com/jgnagy/bullion/commit/f85841818f6d3511e436c010af86715433c57922))
39
+ * 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)
40
+
3
41
  ## [0.11.2](https://github.com/jgnagy/bullion/compare/bullion/v0.11.1...bullion/v0.11.2) (2026-02-16)
4
42
 
5
43
 
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
 
@@ -36,7 +160,7 @@ Whether run locally or via Docker, the following environment variables configure
36
160
  | `CA_CERT_PATH` | `$CA_DIR/tls.crt` | Public cert for Bullion. If Bullion is an intermediate CA, you'll want to include the root CA's public cert in this file as well the signed cert for Bullion. |
37
161
  | `CA_DOMAINS` | `example.com` | A comma-delimited list of domains for which Bullion will sign certificate requests. Subdomains are automatically allowed. Certificates containing other domains will be rejected. |
38
162
  | `CERT_VALIDITY_DURATION` | `7776000` | How long should issued certs be valid (in seconds)? Default is 90 days. |
39
- | `DATABASE_URL` | _None_ | **(Required)** A shorthand for telling Bullion how to connect to a database. Acceptable URLs will either begin with `sqlite3:` or [`trilogy://`](https://github.com/trilogy-libraries/trilogy/tree/main/contrib/ruby). |
163
+ | `DATABASE_URL` | _None_ | **(Required)** A shorthand for telling Bullion how to connect to a database. Acceptable URLs will either begin with `sqlite3:` or [`trilogy://`](https://github.com/trilogy-libraries/trilogy/tree/main/contrib/ruby). ActiveRecord connection options may be passed as query parameters, e.g. `trilogy://user:pass@host:3306/bullion?max_connections=25&checkout_timeout=5`. |
40
164
  | `DNS01_NAMESERVERS` | _None_ | A comma-delimited list of nameservers to use for resolving [DNS-01](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge) challenges. Usually you'll want this to be set to your _internal_ nameservers so internal names resolve correctly. When not set, it'll use the host's DNS. |
41
165
  | `LOG_LEVEL` | `warn` | Log level for Bullion. Supported levels (starting with the noisiest) are debug, info, warn, error, and fatal. |
42
166
  | `BULLION_PORT` | `9292` | TCP port Bullion will listen on. |
@@ -45,6 +169,8 @@ Whether run locally or via Docker, the following environment variables configure
45
169
  | `WORKER_MEMORY_LIMIT` | `1024**3` | [Itsi worker memory limit](https://itsi.fyi/options/worker_memory_limit/) for each worker process (in bytes). Default is 1GiB. |
46
170
  | `RACK_ENV` | `production`* | When run via Docker, the default is `production`, when run via `rake local_demo` it is `development`. Used to tell Bullion if it is run in development mode or for testing. |
47
171
 
172
+ Because Bullion serves requests through Itsi's [fiber scheduler](https://itsi.fyi/options/fiber_scheduler/), each in-flight request leases its own database connection. ActiveRecord's connection pool defaults to 5 connections, so raise `max_connections` on `DATABASE_URL` if you expect more concurrent requests. `checkout_timeout` (default 5 seconds) controls how long a request waits for a connection before failing.
173
+
48
174
  ### Integrating
49
175
 
50
176
  Any client that speaks the [ACMEv2](https://tools.ietf.org/html/rfc8555) protocol can be pointed at `/acme/directory` and everything else can be auto-discovered.
@@ -57,20 +183,10 @@ Bullion provides a `/ping` endpoint that should respond with `{ 'status': 'up' }
57
183
 
58
184
  Prometheus metrics are also scrapable at `/metrics`. This includes typical web request information as well as latencies related to the different Challenge types.
59
185
 
60
- ## Development
186
+ ## Development & Contributing
61
187
 
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.
188
+ 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
189
 
70
190
  ## License
71
191
 
72
192
  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: 2026_07_16_000000) 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,18 @@ 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
43
+ t.integer "revocation_reason"
44
+ t.boolean "revoked", default: false, null: false
45
+ t.datetime "revoked_at"
44
46
  t.bigint "serial", null: false
45
- t.datetime "created_at", null: false
47
+ t.string "subject", null: false
46
48
  t.datetime "updated_at", null: false
49
+ t.boolean "validated", default: false, null: false
47
50
  t.index ["csr_fingerprint"], name: "index_certificates_on_csr_fingerprint"
48
51
  t.index ["serial"], name: "index_certificates_on_serial", unique: true
49
52
  t.index ["subject"], name: "index_certificates_on_subject"
@@ -52,13 +55,13 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_23_073342) do
52
55
 
53
56
  create_table "challenges", force: :cascade do |t|
54
57
  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
58
  t.bigint "authorization_id"
60
59
  t.datetime "created_at", null: false
60
+ t.datetime "expires", null: false
61
+ t.string "status", default: "pending", null: false
62
+ t.string "token", null: false
61
63
  t.datetime "updated_at", null: false
64
+ t.datetime "validated"
62
65
  t.index ["acme_type"], name: "index_challenges_on_acme_type"
63
66
  t.index ["authorization_id"], name: "index_challenges_on_authorization_id"
64
67
  t.index ["expires"], name: "index_challenges_on_expires"
@@ -66,21 +69,21 @@ ActiveRecord::Schema[8.0].define(version: 2025_08_23_073342) do
66
69
  end
67
70
 
68
71
  create_table "nonces", force: :cascade do |t|
69
- t.string "token", null: false
70
72
  t.datetime "created_at", null: false
73
+ t.string "token", null: false
71
74
  t.datetime "updated_at", null: false
72
75
  t.index ["token"], name: "index_nonces_on_token", unique: true
73
76
  end
74
77
 
75
78
  create_table "orders", force: :cascade do |t|
76
- t.string "status", default: "pending", null: false
79
+ t.bigint "account_id"
80
+ t.bigint "certificate_id"
81
+ t.datetime "created_at", null: false
77
82
  t.datetime "expires", null: false
78
83
  t.text "identifiers", null: false
79
- t.datetime "not_before", null: false
80
84
  t.datetime "not_after", null: false
81
- t.bigint "certificate_id"
82
- t.bigint "account_id"
83
- t.datetime "created_at", null: false
85
+ t.datetime "not_before", null: false
86
+ t.string "status", default: "pending", null: false
84
87
  t.datetime "updated_at", null: false
85
88
  t.index ["account_id"], name: "index_orders_on_account_id"
86
89
  t.index ["certificate_id"], name: "index_orders_on_certificate_id"
@@ -39,6 +39,16 @@ module Bullion
39
39
  def acme_type = "badPublicKey"
40
40
  end
41
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
+
42
52
  # ACME exception for invalid contacts in accounts
43
53
  class InvalidContact < Bullion::Acme::Error
44
54
  def acme_type = "invalidContact"
@@ -54,6 +64,11 @@ module Bullion
54
64
  def acme_type = "malformed"
55
65
  end
56
66
 
67
+ # ACME exception for orders that aren't ready to be finalized
68
+ class OrderNotReady < Bullion::Acme::Error
69
+ def acme_type = "orderNotReady"
70
+ end
71
+
57
72
  # ACME exception for unsupported contacts in accounts
58
73
  class UnsupportedContact < Bullion::Acme::Error
59
74
  def acme_type = "unsupportedContact"
@@ -125,7 +125,6 @@ module Bullion
125
125
  # Make sure the CSR has a valid public key
126
126
  raise Bullion::Acme::Errors::BadCsr unless csr.verify(csr.public_key)
127
127
 
128
- return false unless order.ready_status?
129
128
  raise Bullion::Acme::Errors::BadCsr if csr_cn && !csr_domains.include?(csr_cn)
130
129
  raise Bullion::Acme::Errors::BadCsr unless csr_domains.sort == order.domains.sort
131
130
 
@@ -174,7 +173,7 @@ module Bullion
174
173
  # don't allow nonsense certs
175
174
  raise Bullion::Acme::Errors::InvalidOrder unless nb < na
176
175
  # don't allow far-future certs
177
- if nb > Time.now + (7 * 86_400) || na > Time.now + CERT_VALIDITY_DURATION
176
+ if nb > Time.now + (7 * 86_400) || na > Time.now + Bullion.config.ca.cert_validity_duration
178
177
  raise Bullion::Acme::Errors::InvalidOrder
179
178
  end
180
179
 
@@ -192,6 +191,24 @@ module Bullion
192
191
  Bullion.config.ca.domains.none? { domain["value"].end_with?(it) }
193
192
  end
194
193
  end
194
+
195
+ # Builds the ACMEv2 order resource representation
196
+ # @see https://tools.ietf.org/html/rfc8555#section-7.1.3
197
+ def order_data(order)
198
+ data = {
199
+ status: order.status,
200
+ expires: order.expires,
201
+ notBefore: order.not_before,
202
+ notAfter: order.not_after,
203
+ identifiers: order.identifiers,
204
+ authorizations: order.authorizations.map { uri("/authorizations/#{it.id}") },
205
+ finalize: uri("/orders/#{order.id}/finalize")
206
+ }
207
+
208
+ data[:certificate] = uri("/certificates/#{order.certificate.id}") if order.valid_status?
209
+
210
+ data
211
+ end
195
212
  end
196
213
  end
197
214
  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
 
@@ -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
@@ -210,15 +214,7 @@ module Bullion
210
214
  content_type "application/json"
211
215
  add_acme_headers @new_nonce, additional: { "Location" => uri("/orders/#{order.id}") }
212
216
 
213
- halt 201, {
214
- status: order.status,
215
- expires: order.expires,
216
- notBefore: order.not_before,
217
- notAfter: order.not_after,
218
- identifiers: order.identifiers,
219
- authorizations: order.authorizations.map { uri("/authorizations/#{it.id}") },
220
- finalize: uri("/orders/#{order.id}/finalize")
221
- }.to_json
217
+ halt 201, order_data(order).to_json
222
218
  rescue Bullion::Acme::Error => e
223
219
  content_type "application/problem+json"
224
220
  halt 400, { type: e.acme_error, detail: e.message }.to_json
@@ -233,19 +229,7 @@ module Bullion
233
229
 
234
230
  order = Models::Order.find(params[:id])
235
231
 
236
- data = {
237
- status: order.status,
238
- expires: order.expires,
239
- notBefore: order.not_before,
240
- notAfter: order.not_after,
241
- identifiers: order.identifiers,
242
- authorizations: order.authorizations.map { uri("/authorizations/#{it.id}") },
243
- finalize: uri("/orders/#{order.id}/finalize")
244
- }
245
-
246
- data[:certificate] = uri("/certificates/#{order.certificate.id}") if order.valid_status?
247
-
248
- data.to_json
232
+ order_data(order).to_json
249
233
  rescue Bullion::Acme::Error => e
250
234
  content_type "application/problem+json"
251
235
  halt 400, { type: e.acme_error, detail: e.message }.to_json
@@ -263,6 +247,17 @@ module Bullion
263
247
 
264
248
  order_csr = Models::OrderCsr.from_acme_request(order, @payload_data["csr"])
265
249
 
250
+ unless order.ready_status?
251
+ # Finalize is idempotent for orders that have already been finalized
252
+ halt 200, order_data(order).to_json if %w[processing valid].include?(order.status)
253
+
254
+ content_type "application/problem+json"
255
+ halt 403, {
256
+ type: Bullion::Acme::Errors::OrderNotReady.new.acme_error,
257
+ detail: "Order is not ready to be finalized"
258
+ }.to_json
259
+ end
260
+
266
261
  unless acme_csr_valid?(order_csr)
267
262
  content_type "application/problem+json"
268
263
  halt 400, {
@@ -277,19 +272,7 @@ module Bullion
277
272
  order.status = "valid"
278
273
  order.save
279
274
 
280
- data = {
281
- status: order.status,
282
- expires: order.expires,
283
- notBefore: order.not_before,
284
- notAfter: order.not_after,
285
- identifiers: order.identifiers,
286
- authorizations: order.authorizations.map { uri("/authorizations/#{it.id}") },
287
- finalize: uri("/orders/#{order.id}/finalize")
288
- }
289
-
290
- data[:certificate] = uri("/certificates/#{order.certificate.id}") if order.valid_status?
291
-
292
- data.to_json
275
+ order_data(order).to_json
293
276
  rescue Bullion::Acme::Error => e
294
277
  content_type "application/problem+json"
295
278
  halt 422, { type: e.acme_error, detail: e.message }.to_json
@@ -384,6 +367,44 @@ module Bullion
384
367
  halt(422, { error: "Order not valid" }.to_json)
385
368
  end
386
369
  end
370
+
371
+ # Revokes a certificate
372
+ # @see https://tools.ietf.org/html/rfc8555#section-7.6
373
+ post "/revokecert" do
374
+ parse_acme_jwt
375
+ add_acme_headers @new_nonce
376
+
377
+ # The certificate is base64url-encoded DER
378
+ cert_der = Base64.urlsafe_decode64(@payload_data["certificate"])
379
+ cert = OpenSSL::X509::Certificate.new(cert_der)
380
+
381
+ # Find the certificate in the database by matching the PEM data
382
+ record = Models::Certificate.find_by(serial: cert.serial.to_i)
383
+
384
+ unless record
385
+ content_type "application/problem+json"
386
+ halt 400, {
387
+ type: Bullion::Acme::Errors::BadCertificate.new.acme_error,
388
+ detail: "Certificate not found"
389
+ }.to_json
390
+ end
391
+
392
+ if record.revoked?
393
+ content_type "application/problem+json"
394
+ halt 400, {
395
+ type: Bullion::Acme::Errors::AlreadyRevoked.new.acme_error,
396
+ detail: "Certificate has already been revoked"
397
+ }.to_json
398
+ end
399
+
400
+ reason = @payload_data["reason"]
401
+ record.update!(revoked: true, revoked_at: Time.now, revocation_reason: reason)
402
+
403
+ halt 200
404
+ rescue Bullion::Acme::Error => e
405
+ content_type "application/problem+json"
406
+ halt 400, { type: e.acme_error, detail: e.message }.to_json
407
+ end
387
408
  end
388
409
  end
389
410
  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.2"
4
+ VERSION = "0.11.6"
5
5
  end
data/lib/bullion.rb CHANGED
@@ -107,11 +107,11 @@ module Bullion
107
107
  raise ConfigError,
108
108
  "Invalid Cert Path: #{config.ca.cert_path}"
109
109
  end
110
- if 60 * 60 * 24 * 397 < config.ca.cert_validity_duration
110
+ if config.ca.cert_validity_duration > 60 * 60 * 24 * 397
111
111
  raise ConfigError,
112
112
  "Cert Validity Too Long"
113
113
  end
114
- if 60 * 60 * 24 * 2 > config.ca.cert_validity_duration
114
+ if config.ca.cert_validity_duration < 60 * 60 * 24 * 2
115
115
  raise ConfigError,
116
116
  "Cert Validity Too Short"
117
117
  end
@@ -140,6 +140,19 @@ Bullion.configure do |config|
140
140
  config.nameservers = nameservers if nameservers
141
141
  end
142
142
 
143
+ # Itsi processes each request inside its own Fiber, but all fibers on a thread
144
+ # share an ActiveSupport execution context by default. ActiveRecord leases a
145
+ # database connection per execution context, so without fiber isolation every
146
+ # concurrent request fiber on a thread shares (and races on) the same
147
+ # connection. Trilogy detects this and raises Trilogy::SynchronizationError.
148
+ # Mirror Itsi's own scheduler_mode hook (which itsi-server 0.2.27 never loads)
149
+ # and the Rails 8.1+ default by opting into fiber isolation, honoring Itsi's
150
+ # opt-out.
151
+ # @see https://github.com/jgnagy/bullion/issues/116
152
+ unless ENV["ITSI_DISABLE_AS_AUTO_FIBER_ISOLATION_LEVEL"]
153
+ ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
154
+ end
155
+
143
156
  # Internal requirements
144
157
  require "bullion/version"
145
158
  require "bullion/acme/error"