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 +4 -4
- data/.ruby-version +1 -1
- data/CHANGELOG.md +43 -0
- data/README.md +129 -15
- data/db/migrate/20260716000000_add_revocation_to_certificates.rb +10 -0
- data/db/schema.rb +20 -20
- data/lib/bullion/acme/error.rb +15 -0
- data/lib/bullion/helpers/acme.rb +4 -3
- data/lib/bullion/helpers/ssl.rb +25 -0
- data/lib/bullion/models/certificate.rb +5 -0
- data/lib/bullion/models/challenge.rb +11 -1
- data/lib/bullion/service.rb +1 -0
- data/lib/bullion/services/ca.rb +42 -0
- data/lib/bullion/services/ping.rb +1 -0
- data/lib/bullion/version.rb +1 -1
- data/lib/bullion.rb +4 -2
- metadata +65 -32
- data/.github/workflows/ci.yml +0 -54
- data/.github/workflows/release.yml +0 -56
- data/.gitignore +0 -15
- data/.images/logo.png +0 -0
- data/.release-please-manifest.json +0 -3
- data/.rspec +0 -2
- data/.rubocop.yml +0 -79
- data/CODE_OF_CONDUCT.md +0 -74
- data/Dockerfile +0 -50
- data/Gemfile +0 -8
- data/Gemfile.lock +0 -284
- data/bullion.gemspec +0 -56
- data/release-please-config.json +0 -15
- data/scripts/build.sh +0 -3
- data/scripts/docker-entrypoint.sh +0 -4
- data/scripts/publish.sh +0 -7
- data/scripts/release.sh +0 -9
- data/scripts/test.sh +0 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f1e03e0297625063a198db254192ac85e7389a46bd1ebd0be1161f1221300e4d
|
|
4
|
+
data.tar.gz: 2c5a0729d0521920a378fa235f928b6c7eb4c16cd3ee9a63bf74fa89eca7168c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 06e5aa78438a991e3e2cf9db8519a2c81bb35c3b872f1fc511ca7ea6d397088e3fce4642631d3dd7d77e9105e9fb4b808dfdc2b185fc121d3deebf8f3614e0d2
|
|
7
|
+
data.tar.gz: e3809f863bd43ff203a580b65a36e6d1f8f088cf12b1650ab7057cca3220019db00bfb231f7d85bbce983600d6488cd75f2748d3c164692c41646bbc9166c043
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.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
|
|
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
|
-
###
|
|
23
|
+
### Prerequisites
|
|
24
24
|
|
|
25
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
82
|
-
t.
|
|
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"
|
data/lib/bullion/acme/error.rb
CHANGED
|
@@ -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"
|
data/lib/bullion/helpers/acme.rb
CHANGED
|
@@ -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
|
|
46
|
+
sig = if alg.start_with?("es")
|
|
46
47
|
ecdsa_sig_to_der(signature)
|
|
47
|
-
elsif
|
|
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 +
|
|
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
|
|
data/lib/bullion/helpers/ssl.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/bullion/service.rb
CHANGED
data/lib/bullion/services/ca.rb
CHANGED
|
@@ -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
|
data/lib/bullion/version.rb
CHANGED
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
|
|
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
|
|
114
|
+
if config.ca.cert_validity_duration < 60 * 60 * 24 * 2
|
|
113
115
|
raise ConfigError,
|
|
114
116
|
"Cert Validity Too Short"
|
|
115
117
|
end
|