Skip to content

Commit 29840e6

Browse files
jgnagyclaude
andcommitted
refactor: modernize ACME error handling and add order submission tests
- Convert ACME error classes to use modern Ruby syntax (def method = value) - Add AccountDoesNotExist error class for proper ACME compliance - Refactor CA service error handling for better flow - Add comprehensive test coverage for order submission - Update RuboCop config to accommodate longer test examples - Clean up test helper methods 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent d2fdd75 commit 29840e6

File tree

5 files changed

+75
-32
lines changed

5 files changed

+75
-32
lines changed

.rubocop.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ RSpec/MultipleExpectations:
7373
Max: 13
7474

7575
RSpec/ExampleLength:
76-
Max: 34
76+
Max: 40
7777

7878
Gemspec/DevelopmentDependencies:
7979
Enabled: false

lib/bullion/acme/error.rb

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,53 +19,44 @@ def acme_error
1919
end
2020

2121
module Errors
22+
# ACME exception for nonexistent accounts
23+
class AccountDoesNotExist < Bullion::Acme::Error
24+
def acme_type = "accountDoesNotExist"
25+
end
26+
2227
# ACME exception for bad CSRs
2328
class BadCsr < Bullion::Acme::Error
24-
def acme_type
25-
"badCSR"
26-
end
29+
def acme_type = "badCSR"
2730
end
2831

2932
# ACME exception for bad Nonces
3033
class BadNonce < Bullion::Acme::Error
31-
def acme_type
32-
"badNonce"
33-
end
34+
def acme_type = "badNonce"
3435
end
3536

3637
# ACME exception for invalid contacts in accounts
3738
class InvalidContact < Bullion::Acme::Error
38-
def acme_type
39-
"invalidContact"
40-
end
39+
def acme_type = "invalidContact"
4140
end
4241

4342
# ACME exception for invalid orders
4443
class InvalidOrder < Bullion::Acme::Error
45-
def acme_type
46-
"invalidOrder"
47-
end
44+
def acme_type = "invalidOrder"
4845
end
4946

5047
# ACME exception for malformed requests
5148
class Malformed < Bullion::Acme::Error
52-
def acme_type
53-
"malformed"
54-
end
49+
def acme_type = "malformed"
5550
end
5651

5752
# ACME exception for unsupported contacts in accounts
5853
class UnsupportedContact < Bullion::Acme::Error
59-
def acme_type
60-
"unsupportedContact"
61-
end
54+
def acme_type = "unsupportedContact"
6255
end
6356

6457
# Non-standard exception for unsupported challenge types
6558
class UnsupportedChallengeType < Bullion::Acme::Error
66-
def acme_error
67-
"urn:ietf:params:bullion:error:unsupportedChallengeType"
68-
end
59+
def acme_error = "urn:ietf:params:bullion:error:unsupportedChallengeType"
6960
end
7061
end
7162
end

lib/bullion/services/ca.rb

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,22 +118,20 @@ class CA < Service
118118
# @see https://tools.ietf.org/html/rfc8555#section-7.3
119119
post "/accounts" do
120120
header_data = JSON.parse(Base64.decode64(@json_body[:protected]))
121-
begin
122-
parse_acme_jwt(header_data["jwk"], validate_nonce: false)
121+
parse_acme_jwt(header_data["jwk"], validate_nonce: false)
123122

124-
account_data_valid?(@payload_data)
125-
rescue Bullion::Acme::Error => e
126-
content_type "application/problem+json"
127-
halt 400, { type: e.acme_error, detail: e.message }.to_json
128-
end
123+
account_data_valid?(@payload_data)
129124

130125
user = Models::Account.where(
131126
public_key: header_data["jwk"]
132127
).first
133128

134129
if @payload_data["onlyReturnExisting"]
135130
content_type "application/problem+json"
136-
halt 400, { type: "urn:ietf:params:acme:error:accountDoesNotExist" }.to_json unless user
131+
unless user
132+
raise Bullion::Acme::Error::AccountDoesNotExist,
133+
"onlyReturnExisting requested and account does not exist"
134+
end
137135
end
138136

139137
user ||= Models::Account.new(public_key: header_data["jwk"])
@@ -149,6 +147,9 @@ class CA < Service
149147
contact: user.contacts,
150148
orders: uri("/accounts/#{user.id}/orders")
151149
}.to_json
150+
rescue Bullion::Acme::Error => e
151+
content_type "application/problem+json"
152+
halt 400, { type: e.acme_error, detail: e.message }.to_json
152153
end
153154

154155
# Endpoint for updating accounts

spec/bullion/services/ca_spec.rb

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,5 +422,57 @@ def app
422422
expect(parsed_body["contact"]).to eq(["mailto:#{account_email}"])
423423
expect(parsed_body["orders"]).to match(%r{^http://.+/accounts/[0-9]+/orders$})
424424
end
425+
426+
it "allows submitting new orders" do
427+
account_key = ecdsa_key
428+
429+
# Create an account so we can use it
430+
Bullion::Models::Account.create(
431+
tos_agreed: true,
432+
contacts: [account_email],
433+
public_key: ecdsa_public_key_hash(account_key)
434+
)
435+
436+
get "/nonces"
437+
438+
nonce = last_response.headers["Replay-Nonce"]
439+
440+
jwk = {
441+
typ: "JWT",
442+
alg: "ES256",
443+
jwk: ecdsa_public_key_hash(account_key),
444+
nonce:,
445+
url: "/orders"
446+
}.to_json
447+
448+
encoded_jwk = acme_base64(jwk)
449+
payload = { identifiers: }.to_json
450+
encoded_payload = acme_base64(payload)
451+
signature_data = "#{encoded_jwk}.#{encoded_payload}"
452+
453+
body = {
454+
protected: encoded_jwk,
455+
payload: encoded_payload,
456+
signature: acme_sign(account_key, signature_data)
457+
}.to_json
458+
459+
post "/orders", body, { "CONTENT_TYPE" => "application/jose+json" }
460+
461+
expect(last_response).to be_created
462+
expect(last_response.headers["Content-Type"]).to eq("application/json")
463+
expect(last_response.headers["Location"]).to match(%r{^http://.+/orders/[0-9]+$})
464+
expect(last_response.body).to be_a(String)
465+
466+
parsed_body = JSON.parse(last_response.body)
467+
expect(parsed_body["status"]).to eq("pending")
468+
expect(parsed_body).to include("expires")
469+
expect(parsed_body).to include("notBefore")
470+
expect(parsed_body).to include("notAfter")
471+
expect(parsed_body).to include("authorizations")
472+
expect(parsed_body["authorizations"]).to be_an(Array)
473+
expect(parsed_body["authorizations"].size).to eq(2)
474+
expect(parsed_body["identifiers"]).to eq(identifiers)
475+
expect(parsed_body["finalize"]).to match(%r{http://.+/orders/[0-9]+/finalize})
476+
end
425477
end
426478
end

spec/spec_helper.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,7 @@ def ecsda_crv_to_openssl(crv)
5757
end
5858

5959
def ecdsa_key(crv = "P-256")
60-
key = OpenSSL::PKey::EC.generate(ecsda_crv_to_openssl(crv))
61-
@ecdsa_key ||= key
60+
@ecdsa_key ||= OpenSSL::PKey::EC.generate(ecsda_crv_to_openssl(crv))
6261
end
6362

6463
def ecdsa_public_key_hash(key, crv = "P-256")

0 commit comments

Comments
 (0)