Commit e87c2278 authored by Paul Cammish's avatar Paul Cammish
Browse files

Update core/lib/acme/client.rb, core/lib/acme/client/chain_identifier.rb,...

Update core/lib/acme/client.rb, core/lib/acme/client/chain_identifier.rb, core/lib/acme/client/error.rb, core/lib/acme/client/faraday_middleware.rb, core/lib/acme/client/util.rb, core/lib/acme/client/version.rb, core/lib/acme/client/jwk/base.rb, core/lib/acme/client/jwk/ecdsa.rb, core/lib/acme/client/resources/account.rb, core/lib/acme/client/resources/authorization.rb, core/lib/acme/client/resources/challenges.rb, core/lib/acme/client/resources/directory.rb, core/lib/acme/client/resources/order.rb, core/lib/acme/client/resources/challenges/base.rb, core/lib/acme/client/resources/challenges/unsupported_challenge.rb, core/debian/changelog, CHANGELOG files
parent 3d376df3
CHANGELOG
---------
2021-10-01
sympl-core
* Updated acme-client library to 2.0.9
2021-09-21
sympl-mail:
sympl-mail
* Deal with aliases correctly when no mailboxes directory exists
2021-08-19
all:
all packages
* Debian Bullseye Release
sympl-core:
sympl-core
* Updates to MOTD and banners
2021-08-18
sympl-core:
sympl-core
* Check htdocs/stats for AWFFull rather than Webalizer
sympl-web
* Update Webalizer references to AWFFull
......@@ -21,7 +25,7 @@ CHANGELOG
* Further fixes for Exim 4.94 in Debian Bullseye
2021-08-13
sympl-mail:
sympl-mail
* use systemd socket activation for sympl-mail-poppassd
2021-04-09
......
sympl-core (11.20211001.0) stable; urgency=medium
* Updated acme-client library to 2.0.9
-- Paul Cammish <sympl@kelduum.net> Fri, 01 Oct 2021 19:15:00 +0100
sympl-core (11.20210819.0) stable; urgency=medium
* Fixes for missing MOTD
......
......@@ -20,6 +20,7 @@ require 'acme/client/faraday_middleware'
require 'acme/client/jwk'
require 'acme/client/error'
require 'acme/client/util'
require 'acme/client/chain_identifier'
class Acme::Client
DEFAULT_DIRECTORY = 'http://127.0.0.1:4000/directory'.freeze
......@@ -84,13 +85,35 @@ class Acme::Client
Acme::Client::Resources::Account.new(self, url: kid, **arguments)
end
def account_key_change(new_private_key: nil, new_jwk: nil)
if new_private_key.nil? && new_jwk.nil?
raise ArgumentError, 'must specify new_jwk or new_private_key'
end
old_jwk = jwk
new_jwk ||= Acme::Client::JWK.from_private_key(new_private_key)
inner_payload_header = {
url: endpoint_for(:key_change)
}
inner_payload = {
account: kid,
oldKey: old_jwk.to_h
}
payload = JSON.parse(new_jwk.jws(header: inner_payload_header, payload: inner_payload))
response = post(endpoint_for(:key_change), payload: payload, mode: :kid)
arguments = attributes_from_account_response(response)
@jwk = new_jwk
Acme::Client::Resources::Account.new(self, url: kid, **arguments)
end
def account
@kid ||= begin
response = post(endpoint_for(:new_account), payload: { onlyReturnExisting: true }, mode: :jwk)
response.headers.fetch(:location)
end
response = post(@kid)
response = post_as_get(@kid)
arguments = attributes_from_account_response(response)
Acme::Client::Resources::Account.new(self, url: @kid, **arguments)
end
......@@ -101,13 +124,7 @@ class Acme::Client
def new_order(identifiers:, not_before: nil, not_after: nil)
payload = {}
payload['identifiers'] = if identifiers.is_a?(Hash)
identifiers
else
Array(identifiers).map do |identifier|
{ type: 'dns', value: identifier }
end
end
payload['identifiers'] = prepare_order_identifiers(identifiers)
payload['notBefore'] = not_before if not_before
payload['notAfter'] = not_after if not_after
......@@ -117,7 +134,7 @@ class Acme::Client
end
def order(url:)
response = get(url)
response = post_as_get(url)
arguments = attributes_from_order_response(response)
Acme::Client::Resources::Order.new(self, **arguments.merge(url: url))
end
......@@ -133,13 +150,28 @@ class Acme::Client
Acme::Client::Resources::Order.new(self, **arguments)
end
def certificate(url:)
def certificate(url:, force_chain: nil)
response = download(url, format: :pem)
response.body
pem = response.body
return pem if force_chain.nil?
return pem if ChainIdentifier.new(pem).match_name?(force_chain)
alternative_urls = Array(response.headers.dig('link', 'alternate'))
alternative_urls.each do |alternate_url|
response = download(alternate_url, format: :pem)
pem = response.body
if ChainIdentifier.new(pem).match_name?(force_chain)
return pem
end
end
raise Acme::Client::Error::ForcedChainNotFound, "Could not find any matching chain for `#{force_chain}`"
end
def authorization(url:)
response = get(url)
response = post_as_get(url)
arguments = attributes_from_authorization_response(response)
Acme::Client::Resources::Authorization.new(self, url: url, **arguments)
end
......@@ -151,13 +183,13 @@ class Acme::Client
end
def challenge(url:)
response = get(url)
response = post_as_get(url)
arguments = attributes_from_challenge_response(response)
Acme::Client::Resources::Challenges.new(self, **arguments)
end
def request_challenge_validation(url:, key_authorization:)
response = post(url, payload: { keyAuthorization: key_authorization })
def request_challenge_validation(url:, key_authorization: nil)
response = post(url, payload: {})
arguments = attributes_from_challenge_response(response)
Acme::Client::Resources::Challenges.new(self, **arguments)
end
......@@ -206,6 +238,20 @@ class Acme::Client
private
def prepare_order_identifiers(identifiers)
if identifiers.is_a?(Hash)
[identifiers]
else
Array(identifiers).map do |identifier|
if identifier.is_a?(String)
{ type: 'dns', value: identifier }
else
identifier
end
end
end
end
def attributes_from_account_response(response)
extract_attributes(
response.body,
......@@ -252,14 +298,19 @@ class Acme::Client
connection.post(url, payload)
end
def post_as_get(url, mode: :kid)
connection = connection_for(url: url, mode: mode)
connection.post(url, nil)
end
def get(url, mode: :kid)
connection = connection_for(url: url, mode: mode)
connection.get(url)
end
def download(url, format:)
connection = connection_for(url: url, mode: :download)
connection.get do |request|
connection = connection_for(url: url, mode: :kid)
connection.post do |request|
request.url(url)
request.headers['Accept'] = CONTENT_TYPES.fetch(format)
end
......
class Acme::Client
class ChainIdentifier
def initialize(pem_certificate_chain)
@pem_certificate_chain = pem_certificate_chain
end
def match_name?(name)
issuers.any? do |issuer|
issuer.include?(name)
end
end
private
def issuers
x509_certificates.map(&:issuer).map(&:to_s)
end
def x509_certificates
@x509_certificates ||= splitted_pem_certificates.map { |pem| OpenSSL::X509::Certificate.new(pem) }
end
def splitted_pem_certificates
@pem_certificate_chain.each_line.slice_after(/END CERTIFICATE/).map(&:join)
end
end
end
......@@ -7,6 +7,7 @@ class Acme::Client::Error < StandardError
class UnsupportedChallengeType < ClientError; end
class NotFound < ClientError; end
class CertificateNotReady < ClientError; end
class ForcedChainNotFound < ClientError; end
class ServerError < Acme::Client::Error; end
class BadCSR < ServerError; end
......
......@@ -5,10 +5,10 @@ class Acme::Client::FaradayMiddleware < Faraday::Middleware
CONTENT_TYPE = 'application/jose+json'
def initialize(app, client:, mode:)
def initialize(app, options)
super(app)
@client = client
@mode = mode
@client = options.fetch(:client)
@mode = options.fetch(:mode)
end
def call(env)
......@@ -82,18 +82,10 @@ class Acme::Client::FaradayMiddleware < Faraday::Middleware
end
end
LINK_MATCH = /<(.*?)>;rel="([\w-]+)"/
def decode_link_headers
return unless env.response_headers.key?('Link')
link_header = env.response_headers['Link']
links = link_header.split(', ').map { |entry|
_, link, name = *entry.match(LINK_MATCH)
[name, link]
}
Hash[*links.flatten]
Acme::Client::Util.decode_link_headers(link_header)
end
def store_nonce
......
......@@ -14,10 +14,10 @@ class Acme::Client::JWK::Base
# payload - A Hash of payload data.
#
# Returns a JSON String.
def jws(header: {}, payload: {})
def jws(header: {}, payload:)
header = jws_header(header)
encoded_header = Acme::Client::Util.urlsafe_base64(header.to_json)
encoded_payload = Acme::Client::Util.urlsafe_base64(payload.to_json)
encoded_payload = Acme::Client::Util.urlsafe_base64(payload.nil? ? '' : payload.to_json)
signature_data = "#{encoded_header}.#{encoded_payload}"
signature = sign(signature_data)
......
......@@ -73,8 +73,10 @@ class Acme::Client::JWK::ECDSA < Acme::Client::JWK::Base
# BigNumbers
bns = ints.map(&:value)
byte_size = (@private_key.group.degree + 7) / 8
# Binary R/S values
r, s = bns.map { |bn| [bn.to_s(16)].pack('H*') }
r, s = bns.map { |bn| bn.to_s(2).rjust(byte_size, "\x00") }
# JWS wants raw R/S concatenated.
[r, s].join
......
......@@ -5,7 +5,7 @@ class Acme::Client::Resources::Account
def initialize(client, **arguments)
@client = client
assign_attributes(arguments)
assign_attributes(**arguments)
end
def kid
......
......@@ -5,7 +5,7 @@ class Acme::Client::Resources::Authorization
def initialize(client, **arguments)
@client = client
assign_attributes(arguments)
assign_attributes(**arguments)
end
def deactivate
......
......@@ -4,6 +4,7 @@ module Acme::Client::Resources::Challenges
require 'acme/client/resources/challenges/base'
require 'acme/client/resources/challenges/http01'
require 'acme/client/resources/challenges/dns01'
require 'acme/client/resources/challenges/unsupported_challenge'
CHALLENGE_TYPES = {
'http-01' => Acme::Client::Resources::Challenges::HTTP01,
......@@ -11,11 +12,6 @@ module Acme::Client::Resources::Challenges
}
def self.new(client, type:, **arguments)
klass = CHALLENGE_TYPES[type]
if klass
klass.new(client, **arguments)
else
{ type: type }.merge(arguments)
end
CHALLENGE_TYPES.fetch(type, Unsupported).new(client, **arguments)
end
end
......@@ -5,7 +5,7 @@ class Acme::Client::Resources::Challenges::Base
def initialize(client, **arguments)
@client = client
assign_attributes(arguments)
assign_attributes(**arguments)
end
def challenge_type
......@@ -21,17 +21,9 @@ class Acme::Client::Resources::Challenges::Base
true
end
def send_challenge_vallidation(url:, key_authorization:)
@client.request_challenge_validation(
url: url,
key_authorization: key_authorization
).to_h
end
def request_validation
assign_attributes(**send_challenge_vallidation(
url: url,
key_authorization: key_authorization
assign_attributes(**send_challenge_validation(
url: url
))
true
end
......@@ -42,6 +34,12 @@ class Acme::Client::Resources::Challenges::Base
private
def send_challenge_validation(url:)
@client.request_challenge_validation(
url: url
).to_h
end
def assign_attributes(status:, url:, token:, error: nil)
@status = status
@url = url
......
class Acme::Client::Resources::Challenges::Unsupported < Acme::Client::Resources::Challenges::Base
end
......@@ -68,9 +68,13 @@ class Acme::Client::Resources::Directory
end
def fetch_directory
connection = Faraday.new(url: @directory, **@connection_options)
connection = Faraday.new(url: @directory, **@connection_options) do |configuration|
configuration.use Acme::Client::FaradayMiddleware, client: nil, mode: nil
configuration.adapter Faraday.default_adapter
end
connection.headers[:user_agent] = Acme::Client::USER_AGENT
response = connection.get(@url)
JSON.parse(response.body)
response.body
end
end
......@@ -5,7 +5,7 @@ class Acme::Client::Resources::Order
def initialize(client, **arguments)
@client = client
assign_attributes(arguments)
assign_attributes(**arguments)
end
def reload
......@@ -24,9 +24,9 @@ class Acme::Client::Resources::Order
true
end
def certificate
def certificate(force_chain: nil)
if certificate_url
@client.certificate(url: certificate_url)
@client.certificate(url: certificate_url, force_chain: force_chain)
else
raise Acme::Client::Error::CertificateNotReady, 'No certificate_url to collect the order'
end
......
......@@ -3,6 +3,17 @@ module Acme::Client::Util
Base64.urlsafe_encode64(data).sub(/[\s=]*\z/, '')
end
LINK_MATCH = /<(.*?)>\s?;\s?rel="([\w-]+)"/
# See RFC 8288 - https://tools.ietf.org/html/rfc8288#section-3
def decode_link_headers(link_header)
link_header.split(',').each_with_object({}) { |entry, hash|
_, link, name = *entry.match(LINK_MATCH)
hash[name] ||= []
hash[name].push(link)
}
end
# Sets public key on CSR or cert.
#
# obj - An OpenSSL::X509::Certificate or OpenSSL::X509::Request instance.
......
......@@ -2,6 +2,6 @@
module Acme
class Client
VERSION = '2.0.4'.freeze
VERSION = '2.0.9'.freeze
end
end
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment