Projects
home:rottame:rubygems
rubygem-acme
Log In
Username
Password
We truncated the diff of some files because they were too big. If you want to see the full diff for every file,
click here
.
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
Expand all
Collapse all
Changes of Revision 2
View file
rubygem-acme-client.changes
Changed
@@ -1,4 +1,9 @@ ------------------------------------------------------------------- +Mon May 18 12:03:13 UTC 2026 - Angelo Grossini <rottame@intercom.it> + +- update to 2.0.31 + +------------------------------------------------------------------- Mon Nov 27 14:37:38 UTC 2017 - jweberhofer@weberhofer.at - Initial release 1.0.1
View file
rubygem-acme-client.spec
Changed
@@ -1,7 +1,7 @@ %define mod_name acme-client %define mod_full_name %{mod_name}-%{version} Name: rubygem-acme-client -Version: 2.0.3 +Version: 2.0.31 Release: 0 Summary: Client for the ACME protocol License: Apache-2.0
View file
acme-client-2.0.3.gem/data/.gitignore
Deleted
@@ -1,11 +0,0 @@ -/.bundle/ -/.yardoc -/Gemfile.lock -/_yardoc/ -/coverage/ -/doc/ -/pkg/ -/spec/reports/ -/tmp/ -/vendor/bundle -/.idea/
View file
acme-client-2.0.3.gem/data/.rspec
Deleted
@@ -1,3 +0,0 @@ ---format documentation ---color ---order rand
View file
acme-client-2.0.3.gem/data/.rubocop.yml
Deleted
@@ -1,139 +0,0 @@ -AllCops: - TargetRubyVersion: 2.1 - Exclude: - - 'bin/*' - - 'vendor/**/*' - -Rails: - Enabled: false - -Style/FileName: - Exclude: - - 'lib/acme-client.rb' - -Lint/AssignmentInCondition: - Enabled: false - -Style/ClassAndModuleChildren: - Enabled: false - -Style/Documentation: - Enabled: false - -Layout/MultilineOperationIndentation: - Enabled: false - -Style/SignalException: - EnforcedStyle: only_raise - -Layout/AlignParameters: - EnforcedStyle: with_fixed_indentation - -Layout/ElseAlignment: - Enabled: false - -Style/MultipleComparison: - Enabled: false - -Layout/IndentationWidth: - Enabled: false - -Style/SymbolArray: - Enabled: false - -Layout/FirstParameterIndentation: - EnforcedStyle: consistent - -Style/TrailingCommaInArguments: - Enabled: false - -Style/PercentLiteralDelimiters: - Enabled: false - -Metrics/BlockLength: - Enabled: false - -Layout/SpaceInsideBlockBraces: - Enabled: false - -Style/StringLiterals: - Enabled: single_quotes - -Metrics/LineLength: - Max: 140 - -Metrics/ParameterLists: - Max: 5 - CountKeywordArgs: false - -Lint/EndAlignment: - Enabled: false - -Style/ParallelAssignment: - Enabled: false - -Style/ModuleFunction: - Enabled: false - -Style/TrivialAccessors: - AllowPredicates: true - -Lint/UnusedMethodArgument: - AllowUnusedKeywordArguments: true - -Metrics/MethodLength: - Max: 15 - -Style/DoubleNegation: - Enabled: false - -Style/IfUnlessModifier: - Enabled: false - -Style/MultilineBlockChain: - Enabled: false - -Style/BlockDelimiters: - EnforcedStyle: semantic - -Style/Lambda: - Enabled: false - -Style/GuardClause: - Enabled: false - -Style/Alias: - Enabled: false - -Lint/AmbiguousOperator: - Enabled: false - -Metrics/MethodLength: - Enabled: false - -Metrics/PerceivedComplexity: - Enabled: false - -Metrics/CyclomaticComplexity: - Enabled: false - -Metrics/AbcSize: - Enabled: false - -Metrics/ClassLength: - Enabled: false - -Style/MutableConstant: - Enabled: false - -Style/GlobalVars: - Enabled: false - -Style/ExpandPathArguments: - Enabled: false - -Security/JSONLoad: - Enabled: false - -Style/AccessorMethodName: - Enabled: false
View file
acme-client-2.0.3.gem/data/.travis.yml
Deleted
@@ -1,8 +0,0 @@ -language: ruby -cache: bundler -rvm: - - 2.1 - - 2.2 - - 2.3.3 - - 2.4.0 - - 2.6.1
View file
acme-client-2.0.3.gem/data/lib/acme/client/faraday_middleware.rb
Deleted
@@ -1,119 +0,0 @@ -# frozen_string_literal: true - -class Acme::Client::FaradayMiddleware < Faraday::Middleware - attr_reader :env, :response, :client - - CONTENT_TYPE = 'application/jose+json' - - def initialize(app, client:, mode:) - super(app) - @client = client - @mode = mode - end - - def call(env) - @env = env - @env:request_headers'User-Agent' = Acme::Client::USER_AGENT - @env:request_headers'Content-Type' = CONTENT_TYPE - - if @env.method != :get - @env.body = client.jwk.jws(header: jws_header, payload: env.body) - end - - @app.call(env).on_complete { |response_env| on_complete(response_env) } - rescue Faraday::TimeoutError, Faraday::ConnectionFailed - raise Acme::Client::Error::Timeout - end - - def on_complete(env) - @env = env - - raise_on_not_found! - store_nonce - env.body = decode_body - env.response_headers'Link' = decode_link_headers - - return if env.success? - - raise_on_error! - end - - private - - def jws_header - headers = { nonce: pop_nonce, url: env.url.to_s } - headers:kid = client.kid if @mode == :kid - headers - end - - def raise_on_not_found! - raise Acme::Client::Error::NotFound, env.url.to_s if env.status == 404 - end - - def raise_on_error! - raise error_class, error_message - end - - def error_message - if env.body.is_a? Hash - env.body'detail' - else - "Error message: #{env.body}" - end - end - - def error_class - Acme::Client::Error::ACME_ERRORS.fetch(error_name, Acme::Client::Error) - end - - def error_name - return unless env.body.is_a?(Hash) - return unless env.body.key?('type') - env.body'type' - end - - def decode_body - content_type = env.response_headers'Content-Type'.to_s - - if content_type.start_with?('application/json', 'application/problem+json') - JSON.load(env.body) - else - env.body - 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 - end - - def store_nonce - nonce = env.response_headers'replay-nonce' - nonces << nonce if nonce - end - - def pop_nonce - if nonces.empty? - get_nonce - end - - nonces.pop - end - - def get_nonce - client.get_nonce - end - - def nonces - client.nonces - end -end
View file
acme-client-2.0.3.gem/checksums.yaml.gz -> acme-client-2.0.31.gem/checksums.yaml.gz
Changed
@@ -1,7 +1,7 @@ --- -SHA1: - metadata.gz: c5cf05983ff0530ff6a437895b70d686deb7ffd8 - data.tar.gz: 351f8408667c94ff7107f95b48215b589736db34 +SHA256: + metadata.gz: 55d60fbf24893ea26c59535b2ffe6041916e678d4bbea5982666c5f9eb0f2bae + data.tar.gz: b62818cd2dc3ca8b9676e7ec355dfcec40eaf90b3f9b7abc26b2dddea0cc4473 SHA512: - metadata.gz: 419dde577de805c570f1b502ec660eb7734aba59f8243f7e7febc46a2eacc86e5e93d922bca0085dc5eb3cd783e1ccd1927a8b2844c55ed8d12a717d66603f44 - data.tar.gz: d0a6e1682e3d3bbe04f645e1faf5b6ef11675e24cece7475ec42c134a9aa7c15c85c61e1518c5670793b4dfa42a3aad83234627aa249c7e4c74cba3d44669b22 + metadata.gz: b3311579f5f990f433e2ede868ce5baf6ce910eb38b19889107436a0dea91f7d96c36619e6039e0e4b4382402e8bc81dce939fee0f915c850b068b80ced67c1d + data.tar.gz: 45b7de2260bad0703cfa5f865a33e5367789cde176e0a906224fc7e7ad29ba5eeb9f756ce79b29719460c03264c26c5808702737be912d2f5df9cce38003b2f9
View file
acme-client-2.0.3.gem/data/CHANGELOG.md -> acme-client-2.0.31.gem/data/CHANGELOG.md
Changed
@@ -1,3 +1,129 @@ +## `2.0.31` + +* Expose Retry-After header on all +* ARI improvement +* Expose full error message on Error#acme_error_body +* Expose error subproblems (RFC7807) on Error#subproblems + +## `2.0.30` + +* Add a default message to RateLimited error + +This fix avoid argument error on RateLimited object when stubbing without passing arguments. + +## `2.0.29` + +* IP support to the CertificateRequest helper + +## `2.0.28` + +* Make Retry-After(https://datatracker.ietf.org/doc/html/rfc8555/#section-6.6) accessible from RateLimited#retry_after exceptions + +## `2.0.27` + +* Add support for Renewal Information (ARI) (RFC 9773) + +## `2.0.26` + +* Add support for dns-account-01 challenge (RFC draft-ietf-acme-dns-account-label-01) + +## `2.0.25` + +* Add support for profiles extension + +## `2.0.24` + +* Add support for account orders url attribute. + +## `2.0.23` + +* Allow Order to be create without url. Location is not always required in the specification. + +## `2.0.22` + +* Loosen base64 dependency constraint + +## `2.0.21` + +* Add validated attribute to challenges + +## `2.0.20` + +* Add OrderNotReady exception + +## `2.0.19` + +* Fix an issue CSR generation. Version should be set to zero according to the spec. It's causing issue with some ACME server implementation. + +## `2.0.18` + +* Fix an issue public key encoding. `OpenSSL::BN` cause keys with leading zero to fail. + +## `2.0.17` + +* Fix bug where depending on call order `jws` get generated with the wrong `kid` + +## `2.0.16` + +* Refactor Directory +* Fix an issue where the client would crash when ACME provider return nonce for directory endpoint + +## `2.0.15` + +* Also pass connection_options to Faraday for Client#get_nonce + + +## `2.0.14` + +* Fix Faraday HTTP exceptions leaking out, always raise `Acme::Client::Error` instead + +## `2.0.13` + +* Add support for External Account Binding + +## `2.0.12` + +* Update test matrix to current Ruby versions (2.7 to 3.2) +* Support for Faraday retry 2.x + +## `2.0.11` + +* Add support for error code `AlreadyRevoked` and `BadPublicKey` + +## `2.0.10` + +* Support for Faraday 1.0 / 2.0 + +## `2.0.9` + +* Support for Ruby 3.0 and Faraday 0.17.x +* Raise when directory is rate limited + +## `2.0.8` + +* Add support for the keyChange endpoint + +https://tools.ietf.org/html/rfc8555#section-7.3.5 + + +## `2.0.7` + +* Add support for alternate certificate chain +* Change `Link` headers parsing to return array of value. This add support multiple entries at the same `rel` + +## `2.0.6` + +* Allow Faraday up to `< 2.0` + +## `2.0.5` + +* Use post-as-get +* Remove deprecated keyAuthorization + +## `2.0.4` + +* Add an option to retry bad nonce errors + ## `2.0.3` * Do not try to set the body on GET request
View file
acme-client-2.0.3.gem/data/Gemfile -> acme-client-2.0.31.gem/data/Gemfile
Changed
@@ -1,12 +1,12 @@ source 'https://rubygems.org' + gemspec +if faraday_version = ENV'FARADAY_VERSION' + gem 'faraday', faraday_version +end + group :development, :test do gem 'pry' - gem 'rubocop', '~> 0.49.0' gem 'ruby-prof', require: false - - if Gem::Version.new(RUBY_VERSION) <= Gem::Version.new('2.2.2') - gem 'activesupport', '~> 4.2.6' - end end
View file
acme-client-2.0.3.gem/data/README.md -> acme-client-2.0.31.gem/data/README.md
Changed
@@ -1,15 +1,11 @@ # Acme::Client -!Build Status(https://travis-ci.org/unixcharles/acme-client.svg?branch=master)(https://travis-ci.org/unixcharles/acme-client) - -`acme-client` is a client implementation of the ACMEv2(https://github.com/ietf-wg-acme/acme) protocol in Ruby. +`acme-client` is a client implementation of the ACME / RFC 8555(https://tools.ietf.org/html/rfc8555) protocol in Ruby. You can find the ACME reference implementations of the server(https://github.com/letsencrypt/boulder) in Go and the client(https://github.com/certbot/certbot) in Python. ACME is part of the Letsencrypt(https://letsencrypt.org/) project, which goal is to provide free SSL/TLS certificates with automation of the acquiring and renewal process. -You can find ACMEv1 compatible client in the acme-v1(https://github.com/unixcharles/acme-client/tree/acme-v1) branch. - ## Installation Via RubyGems: @@ -23,17 +19,26 @@ ``` ## Usage -* Setting up a client(#setting-up-a-client) -* Account management(#account-management) -* Obtaining a certificate(#obtaining-a-certificate) - * Ordering a certificate(#ordering-a-certificate) - * Completing an HTTP challenge(#preparing-for-http-challenge) - * Completing an DNS challenge(#preparing-for-dns-challenge) - * Requesting a challenge verification(#requesting-a-challenge-verification) - * Downloading a certificate(#downloading-a-certificate) -* Extra(#extra) - * Certificate revokation(#certificate-revokation) - * Certificate renewal(#certificate-renewal) +- Acme::Client(#acmeclient) + - Installation(#installation) + - Usage(#usage) + - Setting up a client(#setting-up-a-client) + - Account management(#account-management) + - Obtaining a certificate(#obtaining-a-certificate) + - Ordering a certificate(#ordering-a-certificate) + - Preparing for HTTP challenge(#preparing-for-http-challenge) + - Preparing for DNS challenge(#preparing-for-dns-challenge) + - Requesting a challenge verification(#requesting-a-challenge-verification) + - Downloading a certificate(#downloading-a-certificate) + - Ordering an alternative certificate(#ordering-an-alternative-certificate) + - Extra(#extra) + - Certificate revokation(#certificate-revokation) + - Certificate renewal(#certificate-renewal) + - Not implemented(#not-implemented) + - Requirements(#requirements) + - Development(#development) + - Pull request?(#pull-request) + - License(#license) ## Setting up a client @@ -91,7 +96,7 @@ If you already have an existing account (for example one created in ACME v1) please note that unless the `kid` is provided at initialization, the client will lazy load the `kid` by doing a `POST` to `newAccount` whenever the `kid` is required. Therefore, you can easily get your `kid` for an existing account and (if needed) store it for reuse: -``` +```ruby client = Acme::Client.new(private_key: private_key, directory: 'https://acme-staging-v02.api.letsencrypt.org/directory') # kid is not set, therefore a call to newAccount is made to lazy-initialize the kid @@ -99,6 +104,15 @@ => "https://acme-staging-v02.api.letsencrypt.org/acme/acct/000000" ``` +## External Account Binding support + +You can use External Account Binding by providing a `external_account_binding` with a `kid` and `hmac_key`. + +```ruby +client = Acme::Client.new(private_key: private_key, directory: 'https://acme.zerossl.com/v2/DV90') +account = client.new_account(contact: 'mailto:info@example.com', terms_of_service_agreed: true, external_account_binding: { kid: "your kid", hmac_key: "your hmac key"}) +``` + ## Obtaining a certificate ### Ordering a certificate @@ -106,9 +120,11 @@ The returned order will contain a list of `Authorization` that need to be completed in other to finalize the order, generally one per identifier. -Each authorization contains multiple challenges, typically a `dns-01` and a `http-01` challenge. The applicant is only required to complete one of the challenges. +Each authorization contains multiple challenges, typically a `dns-01`, `dns-account-01`, and a `http-01` challenge. The applicant is only required to complete one of the challenges. -You can access the challenge you wish to complete using the `#dns` or `#http` method. +The `dns-account-01` challenge prepends an account-specific label before `_acme-challenge`, producing a record name of the form `_<label>._acme-challenge` so different clients can validate the same domain concurrently. + +You can access the challenge you wish to complete using the `#dns`, `#dns_account`, or `#http` methods. ```ruby order = client.new_order(identifiers: 'example.com') @@ -151,6 +167,25 @@ dns_challenge.record_content # => 'HRV3PS5sRDyV-ous4HJk4z24s5JjmUTjcCaUjFt28-8' ``` +### Preparing for DNS-Account-01 challenge + +To complete the DNS-Account-01 challenge, you must set a DNS TXT record using an account-specific name. This allows multiple ACME clients to validate the same domain concurrently without conflicts. + +The DNSAccount01 object has utility methods to generate the required DNS record: + +```ruby +dns_account_challenge = authorization.dns_account + +dns_account_challenge.record_name # => '_ujmmovf2vn55tgye._acme-challenge' +dns_account_challenge.record_type # => 'TXT' +dns_account_challenge.record_content # => 'HRV3PS5sRDyV-ous4HJk4z24s5JjmUTjcCaUjFt28-8' +``` + +The record name includes an account-specific label derived from your account URL, ensuring different clients can validate simultaneously: + +- **DNS-01**: `_acme-challenge.example.com` (shared) +- **DNS-Account-01**: `_ujmmovf2vn55tgye._acme-challenge.example.com` (account-specific) + ### Requesting a challenge verification Once you are ready to complete the challenge, you can request the server perform the verification. @@ -182,10 +217,29 @@ ```ruby csr = Acme::Client::CertificateRequest.new(private_key: a_different_private_key, subject: { common_name: 'example.com' }) order.finalize(csr: csr) -sleep(1) while order.status == 'processing' +while order.status == 'processing' + sleep(1) + order.reload +end order.certificate # => PEM-formatted certificate ``` +### Ordering an alternative certificate + +The provider may provide alternate certificate with different certificate chain. You can specify the required chain and the client will automatically download alternate certificate and match the chain by name. + +```ruby +begin + order.certificate(force_chain: 'DST Root CA X3') +rescue Acme::Client::Error::ForcedChainNotFound + order.certificate +end +``` + +Note: if the specified forced chain doesn't match an existing alternative certificate the method will raise an `Acme::Client::Error::ForcedChainNotFound` error. + +Learn more about the original Github issue for this client here(https://github.com/unixcharles/acme-client/issues/186), information from Let's Encrypt here(https://letsencrypt.org/2019/04/15/transitioning-to-isrg-root.html), and cross-signing here(https://letsencrypt.org/certificates/#cross-signing). + ## Extra ### Certificate revokation @@ -198,16 +252,38 @@ ### Certificate renewal -The is no renewal process, just create a new order. +There is no renewal process, just create a new order. -## Not implemented +### Account Key Roll-over + +To change the key used for an account you can call `#account_key_change` with the new private key or jwk. + +```ruby +require 'openssl' +new_private_key = OpenSSL::PKey::RSA.new(4096) +client.account_key_change(new_private_key: new_private_key) +``` -- Account Key Roll-over. +### Profile Extension + +Provide a CA profile when creating a new order: + +```ruby +order = client.new_order(identifiers: 'example.com', profile: 'shortlived') +``` + +ACME servers may list supported profiles in the directory endpoint: + +```ruby +client.profiles => {"classic": "https://example.com/docs/classic", "shortlived": "https://example.com/docs/shortlived"} +``` + +See the RFC draft of certificate profiles(https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/) for more info. ## Requirements -Ruby >= 2.1 +Ruby >= 3.0 ## Development @@ -224,4 +300,3 @@ ## License MIT License(http://opensource.org/licenses/MIT) -
View file
acme-client-2.0.3.gem/data/Rakefile -> acme-client-2.0.31.gem/data/Rakefile
Changed
@@ -3,7 +3,4 @@ require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) -require 'rubocop/rake_task' -RuboCop::RakeTask.new - -task default: :spec, :rubocop +task default: :spec
View file
acme-client-2.0.3.gem/data/acme-client.gemspec -> acme-client-2.0.31.gem/data/acme-client.gemspec
Changed
@@ -11,16 +11,19 @@ spec.homepage = 'http://github.com/unixcharles/acme-client' spec.license = 'MIT' - spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) || f.start_with?('.') } spec.require_paths = 'lib' - spec.required_ruby_version = '>= 2.1.0' + spec.required_ruby_version = '>= 2.3.0' - spec.add_development_dependency 'bundler', '~> 1.6', '>= 1.6.9' - spec.add_development_dependency 'rake', '~> 10.0' - spec.add_development_dependency 'rspec', '~> 3.3', '>= 3.3.0' - spec.add_development_dependency 'vcr', '~> 2.9', '>= 2.9.3' - spec.add_development_dependency 'webmock', '~> 1.21', '>= 1.21.0' + spec.add_development_dependency 'rake', '~> 13.0' + spec.add_development_dependency 'rspec', '~> 3.9' + spec.add_development_dependency 'vcr', '~> 6.0' + spec.add_development_dependency 'bigdecimal' + spec.add_development_dependency 'webmock', '~> 3.8' + spec.add_development_dependency 'webrick', '~> 1.7' - spec.add_runtime_dependency 'faraday', '~> 0.9', '>= 0.9.1' + spec.add_runtime_dependency 'base64', '~> 0.2' + spec.add_runtime_dependency 'faraday', '>= 1.0', '< 3.0.0' + spec.add_runtime_dependency 'faraday-retry', '>= 1.0', '< 3.0.0' end
View file
acme-client-2.0.31.gem/data/bin/generate_keystash
Added
@@ -0,0 +1,9 @@ +#!/usr/bin/env ruby + +require 'bundler/setup' +require 'acme-client' + +require File.join(File.dirname(__FILE__), '../spec/support/ssl_helper') + + +SSLHelper::KEYSTASH.generate_keystash!(size: 200)
View file
acme-client-2.0.3.gem/data/lib/acme/client.rb -> acme-client-2.0.31.gem/data/lib/acme/client.rb
Changed
@@ -1,6 +1,7 @@ # frozen_string_literal: true require 'faraday' +require 'faraday/retry' require 'json' require 'openssl' require 'digest' @@ -13,13 +14,15 @@ class Acme::Client; end require 'acme/client/version' +require 'acme/client/http_client' require 'acme/client/certificate_request' require 'acme/client/self_sign_certificate' require 'acme/client/resources' -require 'acme/client/faraday_middleware' require 'acme/client/jwk' require 'acme/client/error' +require 'acme/client/error/rate_limited' require 'acme/client/util' +require 'acme/client/chain_identifier' class Acme::Client DEFAULT_DIRECTORY = 'http://127.0.0.1:4000/directory'.freeze @@ -29,7 +32,7 @@ pem: 'application/pem-certificate-chain' } - def initialize(jwk: nil, kid: nil, private_key: nil, directory: DEFAULT_DIRECTORY, connection_options: {}) + def initialize(jwk: nil, kid: nil, private_key: nil, directory: DEFAULT_DIRECTORY, connection_options: {}, bad_nonce_retry: 0) if jwk.nil? && private_key.nil? raise ArgumentError, 'must specify jwk or private_key' end @@ -41,13 +44,15 @@ end @kid, @connection_options = kid, connection_options - @directory = Acme::Client::Resources::Directory.new(URI(directory), @connection_options) + @bad_nonce_retry = bad_nonce_retry + @directory_url = URI(directory) @nonces ||= end attr_reader :jwk, :nonces - def new_account(contact:, terms_of_service_agreed: nil) + def new_account(contact:, terms_of_service_agreed: nil, external_account_binding: nil) + new_account_endpoint = endpoint_for(:new_account) payload = { contact: Array(contact) } @@ -56,7 +61,18 @@ payload:termsOfServiceAgreed = terms_of_service_agreed end - response = post(endpoint_for(:new_account), payload: payload, mode: :jws) + if external_account_binding + kid, hmac_key = external_account_binding.values_at(:kid, :hmac_key) + if kid.nil? || hmac_key.nil? + raise ArgumentError, 'must specify kid and hmac_key key for external_account_binding' + end + + hmac = Acme::Client::JWK::HMAC.new(Base64.urlsafe_decode64(hmac_key)) + external_account_payload = hmac.jws(header: { kid: kid, url: new_account_endpoint }, payload: @jwk) + payload:externalAccountBinding = JSON.parse(external_account_payload) + end + + response = post(new_account_endpoint, payload: payload, mode: :jws) @kid = response.headers.fetch(:location) if response.body.nil? || response.body.empty? @@ -83,13 +99,35 @@ 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 @@ -98,17 +136,13 @@ @kid ||= account.kid end - def new_order(identifiers:, not_before: nil, not_after: nil) + def new_order(identifiers:, not_before: nil, not_after: nil, profile: nil, replaces: 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 + payload'profile' = profile if profile + payload'replaces' = replaces if replaces response = post(endpoint_for(:new_order), payload: payload) arguments = attributes_from_order_response(response) @@ -116,7 +150,7 @@ 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 @@ -132,13 +166,28 @@ 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 @@ -150,13 +199,13 @@ 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 @@ -176,40 +225,89 @@ response.success? end + def renewal_info(certificate: nil, ari_id: nil) + if certificate.nil? && ari_id.nil? + raise ArgumentError, 'must specify certificate or ari_id' + end + + ari_id ||= Acme::Client::Util.ari_certificate_identifier(certificate) +
View file
acme-client-2.0.3.gem/data/lib/acme/client/certificate_request.rb -> acme-client-2.0.31.gem/data/lib/acme/client/certificate_request.rb
Changed
@@ -89,7 +89,7 @@ end csr.public_key = @private_key csr.subject = generate_subject - csr.version = 2 + csr.version = 0 add_extension(csr) csr.sign @private_key, @digest end @@ -104,8 +104,15 @@ end def add_extension(csr) + san = @names.map do |name| + if valid_ip_address?(name) + "IP:#{name}" + else + "DNS:#{name}" + end + end extension = OpenSSL::X509::ExtensionFactory.new.create_extension( - 'subjectAltName', @names.map { |name| "DNS:#{name}" }.join(', '), false + 'subjectAltName', san.join(', '), false ) csr.add_attribute( OpenSSL::X509::Attribute.new( @@ -116,4 +123,14 @@ end end +def valid_ip_address?(address) + require 'ipaddr' + begin + ip = IPAddr.new(address) + true + rescue IPAddr::InvalidAddressError, IPAddr::AddressFamilyError + false + end +end + require 'acme/client/certificate_request/ec_key_patch'
View file
acme-client-2.0.31.gem/data/lib/acme/client/chain_identifier.rb
Added
@@ -0,0 +1,27 @@ +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
View file
acme-client-2.0.3.gem/data/lib/acme/client/error.rb -> acme-client-2.0.31.gem/data/lib/acme/client/error.rb
Changed
@@ -1,4 +1,36 @@ class Acme::Client::Error < StandardError + attr_reader :retry_after, :retry_after_time, :subproblems, :acme_error_body + + Subproblem = Struct.new(:type, :detail, :identifier, keyword_init: true) do + def to_h + { type: type, detail: detail, identifier: identifier } + end + end + + def initialize(message = nil, retry_after: nil, acme_error_body: nil, subproblems: nil) + super(message) + @retry_after_time = Acme::Client::Util.parse_retry_after(retry_after) + @retry_after = @retry_after_time ? (@retry_after_time - Time.now).ceil, 0.max : nil + @acme_error_body = acme_error_body + @subproblems = parse_subproblems(subproblems) + end + + private + + def parse_subproblems(raw) + return if raw.nil? || !raw.is_a?(Array) + + raw.map do |sp| + Subproblem.new( + type: sp'type', + detail: sp'detail', + identifier: sp'identifier' + ) + end + end + + public + class Timeout < Acme::Client::Error; end class ClientError < Acme::Client::Error; end @@ -7,10 +39,16 @@ class UnsupportedChallengeType < ClientError; end class NotFound < ClientError; end class CertificateNotReady < ClientError; end + class ForcedChainNotFound < ClientError; end + class OrderNotReady < ClientError; end + class OrderUrlNil < ClientError; end class ServerError < Acme::Client::Error; end + class AlreadyReplaced < ServerError; end + class AlreadyRevoked < ServerError; end class BadCSR < ServerError; end class BadNonce < ServerError; end + class BadPublicKey < ServerError; end class BadSignatureAlgorithm < ServerError; end class InvalidContact < ServerError; end class UnsupportedContact < ServerError; end @@ -31,14 +69,18 @@ class IncorrectResponse < ServerError; end ACME_ERRORS = { + 'urn:ietf:params:acme:error:alreadyReplaced' => AlreadyReplaced, + 'urn:ietf:params:acme:error:alreadyRevoked' => AlreadyRevoked, 'urn:ietf:params:acme:error:badCSR' => BadCSR, 'urn:ietf:params:acme:error:badNonce' => BadNonce, + 'urn:ietf:params:acme:error:badPublicKey' => BadPublicKey, 'urn:ietf:params:acme:error:badSignatureAlgorithm' => BadSignatureAlgorithm, 'urn:ietf:params:acme:error:invalidContact' => InvalidContact, 'urn:ietf:params:acme:error:unsupportedContact' => UnsupportedContact, 'urn:ietf:params:acme:error:externalAccountRequired' => ExternalAccountRequired, 'urn:ietf:params:acme:error:accountDoesNotExist' => AccountDoesNotExist, 'urn:ietf:params:acme:error:malformed' => Malformed, + 'urn:ietf:params:acme:error:orderNotReady' => OrderNotReady, 'urn:ietf:params:acme:error:rateLimited' => RateLimited, 'urn:ietf:params:acme:error:rejectedIdentifier' => RejectedIdentifier, 'urn:ietf:params:acme:error:serverInternal' => ServerInternal,
View file
acme-client-2.0.31.gem/data/lib/acme/client/error/rate_limited.rb
Added
@@ -0,0 +1,15 @@ +class Acme::Client::Error::RateLimited < Acme::Client::Error::ServerError + DEFAULT_MESSAGE = 'Error message: urn:ietf:params:acme:error:rateLimited' + DEFAULT_RETRY_SECONDS = 10 + + def initialize(message = DEFAULT_MESSAGE, retry_after = nil, acme_error_body: nil, subproblems: nil) + retry_after_time = case retry_after + when Time then retry_after + when nil then Time.now + DEFAULT_RETRY_SECONDS + else Acme::Client::Util.parse_retry_after(retry_after) || Time.now + DEFAULT_RETRY_SECONDS + end + int_retry_after = retry_after.nil? ? DEFAULT_RETRY_SECONDS : (retry_after_time - Time.now).ceil, 0.max + super(message, retry_after: int_retry_after, acme_error_body: acme_error_body, subproblems: subproblems) + @retry_after_time = retry_after_time + end +end
View file
acme-client-2.0.31.gem/data/lib/acme/client/http_client.rb
Added
@@ -0,0 +1,173 @@ +# frozen_string_literal: true + +module Acme::Client::HTTPClient + # Creates and returns a new HTTP client, with default settings. + # + # @param url URI:HTTPS + # @param options Hash + # @return Faraday::Connection + def self.new_connection(url:, options: {}) + Faraday.new(url, options) do |configuration| + configuration.use Acme::Client::HTTPClient::ErrorMiddleware + + yield(configuration) if block_given? + + configuration.headers:user_agent = Acme::Client::USER_AGENT + configuration.adapter Faraday.default_adapter + end + end + + # Creates and returns a new HTTP client designed for the Acme-protocol, with default settings. + # + # @param url URI:HTTPS + # @param client Acme::Client + # @param mode Symbol + # @param options Hash + # @param bad_nonce_retry Integer + # @return Faraday::Connection + def self.new_acme_connection(url:, client:, mode:, options: {}, bad_nonce_retry: 0) + new_connection(url: url, options: options) do |configuration| + if bad_nonce_retry > 0 + configuration.request(:retry, + max: bad_nonce_retry, + methods: Faraday::Connection::METHODS, + exceptions: Acme::Client::Error::BadNonce) + end + + configuration.use Acme::Client::HTTPClient::AcmeMiddleware, client: client, mode: mode + + yield(configuration) if block_given? + end + end + + # ErrorMiddleware ensures the HTTP Client would not raise exceptions outside the Acme namespace. + # + # Exceptions are rescued and re-packaged as Acme exceptions. + class ErrorMiddleware < Faraday::Middleware + # Implements the Rack-alike Faraday::Middleware interface. + def call(env) + @app.call(env) + rescue Faraday::TimeoutError, Faraday::ConnectionFailed + raise Acme::Client::Error::Timeout + end + end + + # AcmeMiddleware implements the Acme-protocol requirements for JWK requests. + class AcmeMiddleware < Faraday::Middleware + attr_reader :env, :response, :client + + CONTENT_TYPE = 'application/jose+json' + + def initialize(app, options) + super(app) + @client = options.fetch(:client) + @mode = options.fetch(:mode) + end + + def call(env) + @env = env + @env:request_headers'Content-Type' = CONTENT_TYPE + + if @env.method != :get + @env.body = client.jwk.jws(header: jws_header, payload: env.body) + end + + @app.call(env).on_complete { |response_env| on_complete(response_env) } + end + + def on_complete(env) + @env = env + + raise_on_not_found! + store_nonce + env.body = decode_body + env.response_headers'Link' = decode_link_headers + + return if env.success? + + raise_on_error! + end + + private + + def jws_header + headers = { nonce: pop_nonce, url: env.url.to_s } + headers:kid = client.kid if @mode == :kid + headers + end + + def raise_on_not_found! + raise Acme::Client::Error::NotFound, env.url.to_s if env.status == 404 + end + + def raise_on_error! + retry_after = env.response_headers'retry-after' + body = env.body.is_a?(Hash) ? env.body : nil + subproblems = error_subproblems + if error_class == Acme::Client::Error::RateLimited + raise error_class.new(error_message, retry_after, acme_error_body: body, subproblems: subproblems) + end + raise error_class.new(error_message, retry_after: retry_after, acme_error_body: body, subproblems: subproblems) + end + + def error_message + if env.body.is_a? Hash + env.body'detail' + else + "Error message: #{env.body}" + end + end + + def error_class + Acme::Client::Error::ACME_ERRORS.fetch(error_name, Acme::Client::Error) + end + + def error_name + return unless env.body.is_a?(Hash) + return unless env.body.key?('type') + env.body'type' + end + + def error_subproblems + return unless env.body.is_a?(Hash) + env.body'subproblems' + end + + def decode_body + content_type = env.response_headers'Content-Type'.to_s + + if content_type.start_with?('application/json', 'application/problem+json') + JSON.load(env.body) + else + env.body + end + end + + def decode_link_headers + return unless env.response_headers.key?('Link') + link_header = env.response_headers'Link' + Acme::Client::Util.decode_link_headers(link_header) + end + + def store_nonce + nonce = env.response_headers'replay-nonce' + nonces << nonce if nonce + end + + def pop_nonce + if nonces.empty? + get_nonce + end + + nonces.pop + end + + def get_nonce + client.get_nonce + end + + def nonces + client.nonces + end + end +end
View file
acme-client-2.0.3.gem/data/lib/acme/client/jwk.rb -> acme-client-2.0.31.gem/data/lib/acme/client/jwk.rb
Changed
@@ -19,3 +19,4 @@ require 'acme/client/jwk/base' require 'acme/client/jwk/rsa' require 'acme/client/jwk/ecdsa' +require 'acme/client/jwk/hmac'
View file
acme-client-2.0.3.gem/data/lib/acme/client/jwk/base.rb -> acme-client-2.0.31.gem/data/lib/acme/client/jwk/base.rb
Changed
@@ -14,10 +14,10 @@ # 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)
View file
acme-client-2.0.3.gem/data/lib/acme/client/jwk/ecdsa.rb -> acme-client-2.0.31.gem/data/lib/acme/client/jwk/ecdsa.rb
Changed
@@ -50,8 +50,8 @@ { crv: @curve_params:jwa_crv, kty: 'EC', - x: Acme::Client::Util.urlsafe_base64(coordinates:x.to_s(2)), - y: Acme::Client::Util.urlsafe_base64(coordinates:y.to_s(2)) + x: Acme::Client::Util.urlsafe_base64(coordinates:x), + y: Acme::Client::Util.urlsafe_base64(coordinates:y) } end @@ -73,8 +73,10 @@ # 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 @@ -90,8 +92,8 @@ hex_y = hex2 + data_len / 2, data_len / 2 { - x: OpenSSL::BN.new(hex_x.pack('H*'), 2), - y: OpenSSL::BN.new(hex_y.pack('H*'), 2) + x: hex_x.pack('H*'), + y: hex_y.pack('H*') } end end
View file
acme-client-2.0.31.gem/data/lib/acme/client/jwk/hmac.rb
Added
@@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class Acme::Client::JWK::HMAC < Acme::Client::JWK::Base + # Instantiate a new HMAC JWS. + # + # key - A string. + # + # Returns nothing. + def initialize(key) + @key = key + end + + # Sign a message with the private key. + # + # message - A String message to sign. + # + # Returns a String signature. + def sign(message) + OpenSSL::HMAC.digest('SHA256', @key, message) + end + + # The name of the algorithm as needed for the `alg` member of a JWS object. + # + # Returns a String. + def jwa_alg + # https://tools.ietf.org/html/rfc7518#section-3.1 + # HMAC using SHA-256 + 'HS256' + end +end
View file
acme-client-2.0.3.gem/data/lib/acme/client/resources.rb -> acme-client-2.0.31.gem/data/lib/acme/client/resources.rb
Changed
@@ -5,3 +5,4 @@ require 'acme/client/resources/order' require 'acme/client/resources/authorization' require 'acme/client/resources/challenges' +require 'acme/client/resources/renewal_info'
View file
acme-client-2.0.3.gem/data/lib/acme/client/resources/account.rb -> acme-client-2.0.31.gem/data/lib/acme/client/resources/account.rb
Changed
@@ -5,7 +5,7 @@ def initialize(client, **arguments) @client = client - assign_attributes(arguments) + assign_attributes(**arguments) end def kid @@ -34,16 +34,18 @@ url: url, term_of_service: term_of_service, status: status, - contact: contact + contact: contact, + orders: orders_url } end private - def assign_attributes(url:, term_of_service:, status:, contact:) + def assign_attributes(url:, term_of_service:, status:, contact:, orders: nil) @url = url @term_of_service = term_of_service @status = status @contact = Array(contact) + @orders_url = orders end end
View file
acme-client-2.0.3.gem/data/lib/acme/client/resources/authorization.rb -> acme-client-2.0.31.gem/data/lib/acme/client/resources/authorization.rb
Changed
@@ -1,11 +1,11 @@ # frozen_string_literal: true class Acme::Client::Resources::Authorization - attr_reader :url, :identifier, :domain, :expires, :status, :wildcard + attr_reader :url, :identifier, :domain, :expires, :status, :wildcard, :retry_after, :retry_after_time def initialize(client, **arguments) @client = client - assign_attributes(arguments) + assign_attributes(**arguments) end def deactivate @@ -38,6 +38,13 @@ end alias_method :dns, :dns01 + def dns_account_01 + @dns_account_01 ||= challenges.find { |challenge| + challenge.is_a?(Acme::Client::Resources::Challenges::DNSAccount01) + } + end + alias_method :dns_account, :dns_account_01 + def to_h { url: url, @@ -45,7 +52,8 @@ status: status, expires: expires, challenges: @challenges, - wildcard: wildcard + wildcard: wildcard, + retry_after: retry_after } end @@ -56,13 +64,13 @@ type: attributes.fetch('type'), status: attributes.fetch('status'), url: attributes.fetch('url'), - token: attributes.fetch('token'), + token: attributes.fetch('token', nil), error: attributes'error' } Acme::Client::Resources::Challenges.new(@client, **arguments) end - def assign_attributes(url:, status:, expires:, challenges:, identifier:, wildcard: false) + def assign_attributes(url:, status:, expires:, challenges:, identifier:, wildcard: false, retry_after: nil) @url = url @identifier = identifier @domain = identifier.fetch('value') @@ -70,5 +78,7 @@ @expires = expires @challenges = challenges @wildcard = wildcard + @retry_after = retry_after + @retry_after_time = Acme::Client::Util.parse_retry_after(retry_after) end end
View file
acme-client-2.0.3.gem/data/lib/acme/client/resources/challenges.rb -> acme-client-2.0.31.gem/data/lib/acme/client/resources/challenges.rb
Changed
@@ -4,18 +4,16 @@ require 'acme/client/resources/challenges/base' require 'acme/client/resources/challenges/http01' require 'acme/client/resources/challenges/dns01' + require 'acme/client/resources/challenges/dns_account01' + require 'acme/client/resources/challenges/unsupported_challenge' CHALLENGE_TYPES = { 'http-01' => Acme::Client::Resources::Challenges::HTTP01, - 'dns-01' => Acme::Client::Resources::Challenges::DNS01 + 'dns-01' => Acme::Client::Resources::Challenges::DNS01, + 'dns-account-01' => Acme::Client::Resources::Challenges::DNSAccount01 } def self.new(client, type:, **arguments) - klass = CHALLENGE_TYPEStype - if klass - klass.new(client, **arguments) - else - { type: type }.merge(arguments) - end + CHALLENGE_TYPES.fetch(type, Unsupported).new(client, **arguments) end end
View file
acme-client-2.0.3.gem/data/lib/acme/client/resources/challenges/base.rb -> acme-client-2.0.31.gem/data/lib/acme/client/resources/challenges/base.rb
Changed
@@ -1,11 +1,11 @@ # frozen_string_literal: true class Acme::Client::Resources::Challenges::Base - attr_reader :status, :url, :token, :error + attr_reader :status, :url, :token, :error, :validated, :retry_after, :retry_after_time def initialize(client, **arguments) @client = client - assign_attributes(arguments) + assign_attributes(**arguments) end def challenge_type @@ -21,31 +21,41 @@ 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 + def typed_error + return nil unless error + + error_type = error'type' + error_detail = error'detail' || 'Unknown error' + error_class = Acme::Client::Error::ACME_ERRORS.fetch(error_type, Acme::Client::Error) + error_class.new(error_detail) + end + def to_h - { status: status, url: url, token: token, error: error } + { status: status, url: url, token: token, error: error, validated: validated, retry_after: retry_after } end private - def assign_attributes(status:, url:, token:, error: nil) + def send_challenge_validation(url:) + @client.request_challenge_validation( + url: url + ).to_h + end + + def assign_attributes(status:, url:, token:, error: nil, validated: nil, retry_after: nil) @status = status @url = url @token = token @error = error + @validated = validated + @retry_after = retry_after + @retry_after_time = Acme::Client::Util.parse_retry_after(retry_after) end end
View file
acme-client-2.0.31.gem/data/lib/acme/client/resources/challenges/dns_account01.rb
Added
@@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# DNS-Account-01 challenge following draft-ietf-acme-dns-account-label-01 +# Enables multiple ACME clients to validate the same domain concurrently +class Acme::Client::Resources::Challenges::DNSAccount01 < Acme::Client::Resources::Challenges::Base + CHALLENGE_TYPE = 'dns-account-01'.freeze + RECORD_PREFIX = '_'.freeze + RECORD_SUFFIX = '._acme-challenge'.freeze + RECORD_TYPE = 'TXT'.freeze + DIGEST = OpenSSL::Digest::SHA256 + BASE32_ALPHABET = 'abcdefghijklmnopqrstuvwxyz234567'.freeze # RFC 4648 lowercase alphabet + + # Generates account-specific DNS record name using SHA256(account_url) + Base32 + # Format: _<base32_label>._acme-challenge + def record_name + digest = DIGEST.digest(@client.kid)0, 10 # First 10 octets for label + bits = digest.unpack1('B*') + label = bits.scan(/.{5}/).map { |chunk| BASE32_ALPHABETchunk.to_i(2) }.join + "#{RECORD_PREFIX}#{label}#{RECORD_SUFFIX}" + end + + def record_type + RECORD_TYPE + end + + def record_content + Acme::Client::Util.urlsafe_base64(DIGEST.digest(key_authorization)) + end +end +
View file
acme-client-2.0.31.gem/data/lib/acme/client/resources/challenges/unsupported_challenge.rb
Added
@@ -0,0 +1,2 @@ +class Acme::Client::Resources::Challenges::Unsupported < Acme::Client::Resources::Challenges::Base +end
View file
acme-client-2.0.3.gem/data/lib/acme/client/resources/directory.rb -> acme-client-2.0.31.gem/data/lib/acme/client/resources/directory.rb
Changed
@@ -7,22 +7,25 @@ new_order: 'newOrder', new_authz: 'newAuthz', revoke_certificate: 'revokeCert', - key_change: 'keyChange' + key_change: 'keyChange', + renewal_info: 'renewalInfo' } DIRECTORY_META = { terms_of_service: 'termsOfService', website: 'website', caa_identities: 'caaIdentities', - external_account_required: 'externalAccountRequired' + external_account_required: 'externalAccountRequired', + profiles: 'profiles' } - def initialize(url, connection_options) - @url, @connection_options = url, connection_options + def initialize(client, **arguments) + @client = client + assign_attributes(**arguments) end def endpoint_for(key) - directory.fetch(key) do |missing_key| + @directory.fetch(key) do |missing_key| raise Acme::Client::Error::UnsupportedOperation, "Directory at #{@url} does not include `#{missing_key}`" end @@ -44,33 +47,21 @@ metaDIRECTORY_META:external_account_required end + def profiles + metaDIRECTORY_META:profiles + end + def meta - directory:meta + @directory:meta end private - def directory - @directory ||= load_directory - end - - def load_directory - body = fetch_directory - result = {} - result:meta = body.delete('meta') + def assign_attributes(directory:) + @directory = {} + @directory:meta = directory.delete('meta') DIRECTORY_RESOURCES.each do |key, entry| - resultkey = URI(bodyentry) if bodyentry + @directorykey = URI(directoryentry) if directoryentry end - result - rescue JSON::ParserError => exception - raise Acme::Client::Error::InvalidDirectory, - "Invalid directory url\n#{@directory} did not return a valid directory\n#{exception.inspect}" - end - - def fetch_directory - connection = Faraday.new(url: @directory, **@connection_options) - connection.headers:user_agent = Acme::Client::USER_AGENT - response = connection.get(@url) - JSON.parse(response.body) end end
View file
acme-client-2.0.3.gem/data/lib/acme/client/resources/order.rb -> acme-client-2.0.31.gem/data/lib/acme/client/resources/order.rb
Changed
@@ -1,14 +1,16 @@ # frozen_string_literal: true class Acme::Client::Resources::Order - attr_reader :url, :status, :contact, :finalize_url, :identifiers, :authorization_urls, :expires, :certificate_url + attr_reader :url, :status, :contact, :finalize_url, :identifiers, :authorization_urls, :expires, :certificate_url, :profile, :replaces, :retry_after, :retry_after_time def initialize(client, **arguments) @client = client - assign_attributes(arguments) + assign_attributes(**arguments) end def reload + raise Acme::Client::Error::OrderUrlNil, 'Cannot reload order with nil url.' if url.nil? + assign_attributes(**@client.order(url: url).to_h) true end @@ -24,14 +26,26 @@ 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 end + def renew(replaces: nil, **arguments) + replaces ||= renewal_info.ari_id + + @client.new_order(replaces: replaces, **to_h.slice(:identifiers, :profile).merge(arguments)) + end + + def renewal_info(certificate: nil, ari_id: nil) + certificate ||= self.certificate if ari_id.nil? + + @client.renewal_info(certificate:, ari_id:) + end + def to_h { url: url, @@ -40,19 +54,26 @@ finalize_url: finalize_url, authorization_urls: authorization_urls, identifiers: identifiers, - certificate_url: certificate_url + certificate_url: certificate_url, + profile: profile, + replaces: replaces, + retry_after: retry_after } end private - def assign_attributes(url:, status:, expires:, finalize_url:, authorization_urls:, identifiers:, certificate_url: nil) - @url = url + def assign_attributes(url: nil, status:, expires:, finalize_url:, authorization_urls:, identifiers:, certificate_url: nil, profile: nil, replaces: nil, retry_after: nil) # rubocop:disable Layout/LineLength,Metrics/ParameterLists + @url = url unless url.nil? @status = status @expires = expires @finalize_url = finalize_url @authorization_urls = authorization_urls @identifiers = identifiers @certificate_url = certificate_url + @profile = profile + @replaces = replaces + @retry_after = retry_after + @retry_after_time = Acme::Client::Util.parse_retry_after(retry_after) end end
View file
acme-client-2.0.31.gem/data/lib/acme/client/resources/renewal_info.rb
Added
@@ -0,0 +1,54 @@ +# frozen_string_literal: true + +class Acme::Client::Resources::RenewalInfo + attr_reader :ari_id, :suggested_window, :explanation_url, :retry_after, :retry_after_time + + def initialize(client, **arguments) + @client = client + assign_attributes(**arguments) + end + + def reload + assign_attributes(**@client.renewal_info(ari_id: ari_id).to_h) + end + + def suggested_window_start + suggested_window&.fetch('start', nil) + end + + def suggested_window_end + suggested_window&.fetch('end', nil) + end + + def suggested_renewal_time + return nil unless suggested_window_start && suggested_window_end + + start_time = DateTime.rfc3339(suggested_window_start).to_time + end_time = DateTime.rfc3339(suggested_window_end).to_time + window_duration = end_time - start_time + + random_offset = rand(0.0..window_duration) + selected_time = start_time + random_offset + + selected_time > Time.now ? selected_time : Time.now + end + + def to_h + { + ari_id: ari_id, + suggested_window: suggested_window, + explanation_url: explanation_url, + retry_after: retry_after + } + end + + private + + def assign_attributes(ari_id:, suggested_window:, explanation_url: nil, retry_after: nil) + @ari_id = ari_id + @suggested_window = suggested_window + @explanation_url = explanation_url + @retry_after = retry_after + @retry_after_time = Acme::Client::Util.parse_retry_after(retry_after) + end +end
View file
acme-client-2.0.3.gem/data/lib/acme/client/util.rb -> acme-client-2.0.31.gem/data/lib/acme/client/util.rb
Changed
@@ -1,8 +1,39 @@ +require 'time' + module Acme::Client::Util + extend self + + # Parses a Retry-After header value into a Time. + # RFC 7231 ยง7.1.3: the value is either delay-seconds or an HTTP-date. + # Returns a Time, or nil if the value is nil or unparseable. + def parse_retry_after(value) + return nil if value.nil? + + value = value.to_s + Integer(value, 10).then { |seconds| Time.now + seconds } + rescue ArgumentError, RangeError + begin + Time.httpdate(value) + rescue ArgumentError + nil + end + end + def urlsafe_base64(data) 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) + hashname ||= + hashname.push(link) + } + end + # Sets public key on CSR or cert. # # obj - An OpenSSL::X509::Certificate or OpenSSL::X509::Request instance. @@ -20,5 +51,40 @@ end end - extend self + # Generates a certificate identifier for ACME Renewal Information (ARI) as per RFC 9773. + # The identifier is constructed by extracting the Authority Key Identifier (AKI) from + # the certificate extension, and the DER-encoded serial number (without tag and length bytes). + # Both values are base64url-encoded and concatenated with a period separator. + # + # certificate - An OpenSSL::X509::Certificate instance or PEM string. + # + # Returns a string in the format: base64url(AKI).base64url(serial) + def ari_certificate_identifier(certificate) + cert = if certificate.is_a?(OpenSSL::X509::Certificate) + certificate + else + OpenSSL::X509::Certificate.new(certificate) + end + + aki_ext = cert.extensions.find { |ext| ext.oid == 'authorityKeyIdentifier' } + raise ArgumentError, 'Certificate does not have an Authority Key Identifier extension' unless aki_ext + + aki_value = aki_ext.value + hex_string = if aki_value =~ /keyid:(0-9A-Fa-f:+)/ + $1 + elsif aki_value =~ /^0-9A-Fa-f:+$/ + aki_value + else + raise ArgumentError, 'Could not parse Authority Key Identifier' + end + + key_identifier = hex_string.split(':').map { |hex| hex.to_i(16).chr }.join + serial_der = OpenSSL::ASN1::Integer.new(cert.serial).to_der + serial_value = OpenSSL::ASN1.decode(serial_der).value.to_s(2) + + aki_b64 = urlsafe_base64(key_identifier) + serial_b64 = urlsafe_base64(serial_value) + + "#{aki_b64}.#{serial_b64}" + end end
View file
acme-client-2.0.3.gem/data/lib/acme/client/version.rb -> acme-client-2.0.31.gem/data/lib/acme/client/version.rb
Changed
@@ -2,6 +2,6 @@ module Acme class Client - VERSION = '2.0.3'.freeze + VERSION = '2.0.31'.freeze end end
View file
acme-client-2.0.3.gem/metadata.gz -> acme-client-2.0.31.gem/metadata.gz
Changed
@@ -1,140 +1,158 @@ --- !ruby/object:Gem::Specification name: acme-client version: !ruby/object:Gem::Version - version: 2.0.3 + version: 2.0.31 platform: ruby authors: - Charles Barbier -autorequire: bindir: bin cert_chain: -date: 2019-04-24 00:00:00.000000000 Z +date: 1980-01-02 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency - name: bundler + name: rake requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version - version: '1.6' - - - ">=" - - !ruby/object:Gem::Version - version: 1.6.9 + version: '13.0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version - version: '1.6' - - - ">=" - - !ruby/object:Gem::Version - version: 1.6.9 + version: '13.0' - !ruby/object:Gem::Dependency - name: rake + name: rspec requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version - version: '10.0' + version: '3.9' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version - version: '10.0' + version: '3.9' - !ruby/object:Gem::Dependency - name: rspec + name: vcr requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version - version: '3.3' - - - ">=" - - !ruby/object:Gem::Version - version: 3.3.0 + version: '6.0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version - version: '3.3' - - - ">=" - - !ruby/object:Gem::Version - version: 3.3.0 + version: '6.0' - !ruby/object:Gem::Dependency - name: vcr + name: bigdecimal requirement: !ruby/object:Gem::Requirement requirements: - - - "~>" - - !ruby/object:Gem::Version - version: '2.9' - - ">=" - !ruby/object:Gem::Version - version: 2.9.3 + version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - - "~>" - - !ruby/object:Gem::Version - version: '2.9' - - ">=" - !ruby/object:Gem::Version - version: 2.9.3 + version: '0' - !ruby/object:Gem::Dependency name: webmock requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version - version: '1.21' - - - ">=" - - !ruby/object:Gem::Version - version: 1.21.0 + version: '3.8' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version - version: '1.21' - - - ">=" + version: '3.8' +- !ruby/object:Gem::Dependency + name: webrick + requirement: !ruby/object:Gem::Requirement + requirements: + - - "~>" - !ruby/object:Gem::Version - version: 1.21.0 + version: '1.7' + type: :development + prerelease: false + version_requirements: !ruby/object:Gem::Requirement + requirements: + - - "~>" + - !ruby/object:Gem::Version + version: '1.7' - !ruby/object:Gem::Dependency - name: faraday + name: base64 requirement: !ruby/object:Gem::Requirement requirements: - - "~>" - !ruby/object:Gem::Version - version: '0.9' + version: '0.2' + type: :runtime + prerelease: false + version_requirements: !ruby/object:Gem::Requirement + requirements: + - - "~>" + - !ruby/object:Gem::Version + version: '0.2' +- !ruby/object:Gem::Dependency + name: faraday + requirement: !ruby/object:Gem::Requirement + requirements: - - ">=" - !ruby/object:Gem::Version - version: 0.9.1 + version: '1.0' + - - "<" + - !ruby/object:Gem::Version + version: 3.0.0 type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - - "~>" + - - ">=" + - !ruby/object:Gem::Version + version: '1.0' + - - "<" + - !ruby/object:Gem::Version + version: 3.0.0 +- !ruby/object:Gem::Dependency + name: faraday-retry + requirement: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: '1.0' + - - "<" - !ruby/object:Gem::Version - version: '0.9' + version: 3.0.0 + type: :runtime + prerelease: false + version_requirements: !ruby/object:Gem::Requirement + requirements: - - ">=" - !ruby/object:Gem::Version - version: 0.9.1 -description: + version: '1.0' + - - "<" + - !ruby/object:Gem::Version + version: 3.0.0 email: - unixcharles@gmail.com executables:
Locations
Projects
Search
Status Monitor
Help
Open Build Service
OBS Manuals
API Documentation
OBS Portal
Reporting a Bug
Contact
Mailing List
Forums
Chat (IRC)
Twitter
Open Build Service (OBS)
is an
openSUSE project
.