bullion 0.11.0 → 0.11.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95d9b8437f39397499aaa73d1b9abf932930ab46a29ac5f08d5ab2533b0c0b83
4
- data.tar.gz: 78ca06dc463ffffd5e274bee516196d8cbaefe759b413fa5670fedc247c7c9a8
3
+ metadata.gz: 95ab410d446f9be5b65083d059c91636f5c4ee801b2973536387bb001623a0b2
4
+ data.tar.gz: 87f492721aaf01a88c746bed6e70d5709c19c7ba3192a7ae83d6c628e716a0dd
5
5
  SHA512:
6
- metadata.gz: '02358422a89125d538c4eb8b5c16d875121525658c11988209cc3bd8581c4cf8a7782d37a70ae2498a3d24e4e28015320d65e2d17285e14dce9f94ea09d69063'
7
- data.tar.gz: 6d927f92fdd653b044bff00f9683f57a026e1cbd504867dd89b1061cbe92219f0da11612af700d6044c90d99accb588ba19e517875547fe039173683245517e8
6
+ metadata.gz: 1ce61712fb1ee5ce6139bb6e105b242709c8f51eece706e116a4cff94f7361c3c7f9589598cc25373695f1df35f60b06866303f96872e1dc516748068fef338c
7
+ data.tar.gz: e69f33f2ee12e17572ad6ad2054a5123d7a1fe3f37471bd64432752b6b0df75892e1fba19004144349810cfae28bafb0ce23039d8cb53613f2ac9d66603eb8d1
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.11.0"
2
+ ".": "0.11.2"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.11.2](https://github.com/jgnagy/bullion/compare/bullion/v0.11.1...bullion/v0.11.2) (2026-02-16)
4
+
5
+
6
+ ### Features
7
+
8
+ * add EdDSA (Ed25519) JWT signature support ([bba2985](https://github.com/jgnagy/bullion/commit/bba29851e1c5644152f4a3e82d82c6c1d5c5ce5e))
9
+ * add EdDSA (Ed25519) JWT signature support ([77a2f5b](https://github.com/jgnagy/bullion/commit/77a2f5bfc30db3527ad3fe75136b210956279957)), closes [#3](https://github.com/jgnagy/bullion/issues/3)
10
+ * Add test coverage for Ed25519 certificate signing ([59f0f74](https://github.com/jgnagy/bullion/commit/59f0f74a039292a5d804c8e49a880f20ab5d0c1b))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * return proper badPublicKey error for Ed448 ([a21414f](https://github.com/jgnagy/bullion/commit/a21414f66075d47e2b5613d2034fa806a0a78602))
16
+
17
+ ## [0.11.1](https://github.com/jgnagy/bullion/compare/bullion/v0.11.0...bullion/v0.11.1) (2025-08-24)
18
+
19
+
20
+ ### Features
21
+
22
+ * add support for ECDSA CAs ([49b752e](https://github.com/jgnagy/bullion/commit/49b752ef6fde2b0543b59fb1c5977073f21b6731))
23
+
24
+
25
+ ### Bug Fixes
26
+
27
+ * improve detection of SANS for cert-manager ([605f80d](https://github.com/jgnagy/bullion/commit/605f80d97135727ab9a962d6c3078b2b4a74b533))
28
+ * loading required bigdecimal gem ([98d1668](https://github.com/jgnagy/bullion/commit/98d1668da600bba890dd0eb035af4a363fa79eef))
29
+
3
30
  ## [0.11.0](https://github.com/jgnagy/bullion/compare/bullion/v0.10.3...bullion/v0.11.0) (2025-08-23)
4
31
 
5
32
 
data/Gemfile.lock CHANGED
@@ -1,13 +1,14 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- bullion (0.11.0)
4
+ bullion (0.11.2)
5
5
  benchmark (~> 0.4)
6
6
  dry-configurable (~> 1.1)
7
7
  httparty (~> 0.21)
8
8
  itsi (~> 0.2)
9
9
  json (~> 2.6)
10
10
  jwt (~> 2.7)
11
+ jwt-eddsa (~> 0.9)
11
12
  openssl (~> 3.0)
12
13
  prometheus-client (~> 4.2)
13
14
  sinatra (~> 3.1)
@@ -46,7 +47,7 @@ GEM
46
47
  backport (1.2.0)
47
48
  base64 (0.3.0)
48
49
  benchmark (0.4.1)
49
- bigdecimal (3.2.2)
50
+ bigdecimal (4.0.1)
50
51
  byebug (11.1.3)
51
52
  concurrent-ruby (1.3.5)
52
53
  connection_pool (2.5.3)
@@ -61,15 +62,16 @@ GEM
61
62
  concurrent-ruby (~> 1.0)
62
63
  logger
63
64
  zeitwerk (~> 2.6)
64
- faraday (2.13.4)
65
+ ed25519 (1.4.0)
66
+ faraday (2.14.1)
65
67
  faraday-net_http (>= 2.0, < 3.5)
66
68
  json
67
69
  logger
68
- faraday-net_http (3.4.1)
69
- net-http (>= 0.5.0)
70
+ faraday-net_http (3.4.2)
71
+ net-http (~> 0.5)
70
72
  faraday-retry (2.3.2)
71
73
  faraday (~> 2.0)
72
- httparty (0.23.1)
74
+ httparty (0.24.0)
73
75
  csv
74
76
  mini_mime (>= 1.0.0)
75
77
  multi_xml (>= 0.5.2)
@@ -86,9 +88,13 @@ GEM
86
88
  rack (>= 1.6)
87
89
  rb_sys (~> 0.9.91)
88
90
  jaro_winkler (1.6.1)
89
- json (2.13.2)
91
+ json (2.18.1)
90
92
  jwt (2.10.2)
91
93
  base64
94
+ jwt-eddsa (0.9.0)
95
+ base64
96
+ ed25519
97
+ jwt (>= 2.9.0)
92
98
  kramdown (2.5.1)
93
99
  rexml (>= 3.3.9)
94
100
  kramdown-parser-gfm (1.1.0)
@@ -99,12 +105,12 @@ GEM
99
105
  mini_mime (1.1.5)
100
106
  minitest (5.25.5)
101
107
  multi_json (1.17.0)
102
- multi_xml (0.7.2)
103
- bigdecimal (~> 3.1)
108
+ multi_xml (0.8.1)
109
+ bigdecimal (>= 3.1, < 5)
104
110
  mustermann (3.0.4)
105
111
  ruby2_keywords (~> 0.0.1)
106
- net-http (0.6.0)
107
- uri
112
+ net-http (0.9.1)
113
+ uri (>= 0.11.1)
108
114
  nokogiri (1.18.9-aarch64-linux-gnu)
109
115
  racc (~> 1.4)
110
116
  nokogiri (1.18.9-aarch64-linux-musl)
@@ -132,7 +138,7 @@ GEM
132
138
  prometheus-client (4.2.5)
133
139
  base64
134
140
  racc (1.8.1)
135
- rack (2.2.17)
141
+ rack (2.2.20)
136
142
  rack-protection (3.2.0)
137
143
  base64 (>= 0.1.0)
138
144
  rack (~> 2.2, >= 2.2.4)
@@ -148,7 +154,7 @@ GEM
148
154
  regexp_parser (2.11.2)
149
155
  reverse_markdown (3.0.0)
150
156
  nokogiri
151
- rexml (3.4.1)
157
+ rexml (3.4.2)
152
158
  rspec (3.13.1)
153
159
  rspec-core (~> 3.13.0)
154
160
  rspec-expectations (~> 3.13.0)
@@ -245,7 +251,7 @@ GEM
245
251
  unicode-display_width (3.1.5)
246
252
  unicode-emoji (~> 4.0, >= 4.0.4)
247
253
  unicode-emoji (4.0.4)
248
- uri (1.0.3)
254
+ uri (1.1.1)
249
255
  yard (0.9.37)
250
256
  yard-solargraph (0.1.0)
251
257
  yard (~> 0.9)
data/Rakefile CHANGED
@@ -62,6 +62,7 @@ end
62
62
 
63
63
  require "openssl"
64
64
  require "sqlite3"
65
+ require "bigdecimal"
65
66
  require "trilogy"
66
67
  require "sinatra/activerecord/rake"
67
68
 
data/bullion.gemspec CHANGED
@@ -32,6 +32,7 @@ Gem::Specification.new do |spec|
32
32
  spec.add_dependency "itsi", "~> 0.2"
33
33
  spec.add_dependency "json", "~> 2.6"
34
34
  spec.add_dependency "jwt", "~> 2.7"
35
+ spec.add_dependency "jwt-eddsa", "~> 0.9"
35
36
  spec.add_dependency "openssl", "~> 3.0"
36
37
  spec.add_dependency "prometheus-client", "~> 4.2"
37
38
  spec.add_dependency "sinatra", "~> 3.1"
@@ -34,6 +34,11 @@ 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
+
37
42
  # ACME exception for invalid contacts in accounts
38
43
  class InvalidContact < Bullion::Acme::Error
39
44
  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
 
@@ -119,13 +120,13 @@ module Bullion
119
120
  csr_attrs = extract_csr_attrs(csr)
120
121
  csr_sans = extract_csr_sans(csr_attrs)
121
122
  csr_domains = extract_csr_domains(csr_sans)
122
- csr_cn = cn_from_csr(csr)
123
+ csr_cn = cn_from_csr(csr) || csr_domains.first
123
124
 
124
125
  # Make sure the CSR has a valid public key
125
126
  raise Bullion::Acme::Errors::BadCsr unless csr.verify(csr.public_key)
126
127
 
127
128
  return false unless order.ready_status?
128
- raise Bullion::Acme::Errors::BadCsr unless csr_domains.include?(csr_cn)
129
+ raise Bullion::Acme::Errors::BadCsr if csr_cn && !csr_domains.include?(csr_cn)
129
130
  raise Bullion::Acme::Errors::BadCsr unless csr_domains.sort == order.domains.sort
130
131
 
131
132
  true
@@ -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")
@@ -127,9 +151,7 @@ module Bullion
127
151
  )
128
152
 
129
153
  # Alternate Names
130
- cn = cn_from_csr(csr)
131
- existing_sans = filter_sans(csr_sans(csr))
132
- valid_alts = (["DNS:#{cn}"] + [*existing_sans]).uniq
154
+ valid_alts = build_valid_alt_names(csr)
133
155
 
134
156
  new_cert.add_extension(ef.create_extension("subjectAltName", valid_alts.join(",")))
135
157
 
@@ -137,21 +159,6 @@ module Bullion
137
159
  [new_cert, valid_alts]
138
160
  end
139
161
 
140
- def csr_sans(csr)
141
- raw_attributes = csr.attributes
142
- return [] unless raw_attributes
143
-
144
- seq = extract_csr_attrs(csr)
145
- return [] unless seq
146
-
147
- values = extract_san_values(seq)
148
- return [] unless values
149
-
150
- values = OpenSSL::ASN1.decode(values).value
151
-
152
- values.select { |v| v.tag == 2 }.map { |v| "DNS:#{v.value}" }
153
- end
154
-
155
162
  def extract_csr_attrs(csr)
156
163
  csr.attributes.select { |a| a.oid == "extReq" }.map { |a| a.value.map(&:value) }
157
164
  end
@@ -161,8 +168,9 @@ module Bullion
161
168
  end
162
169
 
163
170
  def extract_csr_domains(csr_sans)
164
- csr_decoded_sans = OpenSSL::ASN1.decode(csr_sans.first.value[1].value)
165
- csr_decoded_sans.select { |v| v.tag == 2 }.map(&:value)
171
+ subject_alt_names = csr_sans.first.value.find { |v| v.tag == 4 }
172
+ csr_decoded_sans = OpenSSL::ASN1.decode(subject_alt_names.value)
173
+ csr_decoded_sans.value.select { |v| v.tag == 2 }.map(&:value)
166
174
  end
167
175
 
168
176
  def extract_san_values(sequence)
@@ -184,13 +192,33 @@ module Bullion
184
192
  end
185
193
 
186
194
  def cn_from_csr(csr)
187
- if csr.subject.to_s
188
- cns = csr.subject.to_s.split("/").grep(/^CN=/)
195
+ return unless csr.subject.to_s
189
196
 
190
- return cns.first.split("=").last if cns && !cns.empty?
191
- end
197
+ cns = csr.subject.to_s.split("/").grep(/^CN=/)
198
+
199
+ cns.first.split("=").last if cns && !cns.empty?
200
+ end
201
+
202
+ def cn_or_first_san_from_csr(csr)
203
+ cn = cn_from_csr(csr)
204
+ return cn if cn
205
+
206
+ csr_attrs = extract_csr_attrs(csr)
207
+ csr_sans = extract_csr_sans(csr_attrs)
208
+ extract_csr_domains(csr_sans).first
209
+ end
210
+
211
+ def domains_from_csr(csr)
212
+ csr_attrs = extract_csr_attrs(csr)
213
+ csr_sans = extract_csr_sans(csr_attrs)
214
+ extract_csr_domains(csr_sans)
215
+ end
192
216
 
193
- csr_sans(csr).first.split(":").last
217
+ def build_valid_alt_names(csr)
218
+ cn = cn_or_first_san_from_csr(csr)
219
+ csr_domains = domains_from_csr(csr)
220
+ existing_sans = filter_sans(csr_domains).map { |d| "DNS:#{d}" }
221
+ (["DNS:#{cn}"] + [*existing_sans]).uniq
194
222
  end
195
223
 
196
224
  # Signs an ACME CSR
@@ -206,7 +234,7 @@ module Bullion
206
234
  csr_cert.not_after = csr_cert.not_before + (3 * 30 * 24 * 60 * 60)
207
235
 
208
236
  # Force a subject if the cert doesn't have one
209
- cert.subject = simple_subject(cn_from_csr(csr)) unless cert.subject
237
+ cert.subject = simple_subject(cn_or_first_san_from_csr(csr)) unless cert.subject
210
238
 
211
239
  csr_cert.subject = simple_subject(cert.subject.to_s)
212
240
 
@@ -227,5 +255,6 @@ module Bullion
227
255
  end
228
256
  # rubocop:enable Metrics/AbcSize
229
257
  end
258
+ # rubocop:enable Metrics/ModuleLength
230
259
  end
231
260
  end
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bullion
4
- VERSION = "0.11.0"
4
+ VERSION = "0.11.2"
5
5
  end
data/lib/bullion.rb CHANGED
@@ -7,6 +7,7 @@ require "securerandom"
7
7
  require "time"
8
8
  require "logger"
9
9
  require "openssl"
10
+ require "bigdecimal"
10
11
 
11
12
  # External requirements
12
13
  require "benchmark"
@@ -17,6 +18,8 @@ require "sinatra/custom_logger"
17
18
  require "trilogy"
18
19
  require "sinatra/activerecord"
19
20
  require "jwt"
21
+ require "jwt/eddsa"
22
+ require "ed25519"
20
23
  require "prometheus/client"
21
24
  require "httparty"
22
25
 
@@ -68,7 +71,11 @@ module Bullion
68
71
  MetricsRegistry = Prometheus::Client.registry
69
72
 
70
73
  def self.ca_key
71
- @ca_key ||= OpenSSL::PKey::RSA.new(File.read(config.ca.key_path), config.ca.secret)
74
+ @ca_key ||= begin
75
+ OpenSSL::PKey::RSA.new(File.read(config.ca.key_path), config.ca.secret)
76
+ rescue OpenSSL::PKey::RSAError
77
+ OpenSSL::PKey::EC.new(File.read(config.ca.key_path), config.ca.secret)
78
+ end
72
79
  end
73
80
 
74
81
  def self.ca_cert_file
@@ -1,5 +1,4 @@
1
1
  #!/bin/sh
2
2
 
3
3
  # Starts the server
4
- rake db:migrate
5
4
  itsi
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bullion
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.11.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Gnagy
@@ -93,6 +93,20 @@ dependencies:
93
93
  - - "~>"
94
94
  - !ruby/object:Gem::Version
95
95
  version: '2.7'
96
+ - !ruby/object:Gem::Dependency
97
+ name: jwt-eddsa
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '0.9'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '0.9'
96
110
  - !ruby/object:Gem::Dependency
97
111
  name: openssl
98
112
  requirement: !ruby/object:Gem::Requirement