acme-client-2.0.5/0000755000004100000410000000000013625017253013733 5ustar www-datawww-dataacme-client-2.0.5/.travis.yml0000644000004100000410000000012113625017253016036 0ustar www-datawww-datalanguage: ruby cache: bundler rvm: - 2.1 - 2.2 - 2.3.3 - 2.4.0 - 2.6.1 acme-client-2.0.5/.rspec0000644000004100000410000000005413625017253015047 0ustar www-datawww-data--format documentation --color --order rand acme-client-2.0.5/README.md0000644000004100000410000001725213625017253015221 0ustar www-datawww-data# 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 / [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: $ gem install acme-client Or add it to a Gemfile: ```ruby gem 'acme-client' ``` ## 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) ## Setting up a client The client is initialized with a private key and the directory of your ACME provider. LetsEncrypt's `directory` is `https://acme-v02.api.letsencrypt.org/directory`. They also have a staging endpoint at `https://acme-staging-v02.api.letsencrypt.org/directory`. `acme-ruby` expects `OpenSSL::PKey::RSA` or `OpenSSL::PKey::EC` You can generate one in Ruby using OpenSSL. ```ruby require 'openssl' private_key = OpenSSL::PKey::RSA.new(4096) ``` Or load one from a PEM file ```ruby require 'openssl' OpenSSL::PKey::RSA.new(File.read('/path/to/private_key.pem')) ``` See [RSA](https://ruby.github.io/openssl/OpenSSL/PKey/RSA.html) and [EC](https://ruby.github.io/openssl/OpenSSL/PKey/EC.html) for documentation. ```ruby client = Acme::Client.new(private_key: private_key, directory: 'https://acme-staging-v02.api.letsencrypt.org/directory') ``` If your account is already registered, you can save some API calls by passing your key ID directly. This will avoid an unnecessary API call to retrieve it from your private key. ```ruby client = Acme::Client.new(private_key: private_key, directory: 'https://acme-staging-v02.api.letsencrypt.org/directory', kid: 'https://example.com/acme/acct/1') ``` ## Account management Accounts are tied to a private key. Before being allowed to create orders, the account must be registered and the ToS accepted using the private key. The account will be assigned a key ID. ```ruby client = Acme::Client.new(private_key: private_key, directory: 'https://acme-staging-v02.api.letsencrypt.org/directory') account = client.new_account(contact: 'mailto:info@example.com', terms_of_service_agreed: true) ``` After the registration you can retrieve the account key indentifier (kid). ```ruby client = Acme::Client.new(private_key: private_key, directory: 'https://acme-staging-v02.api.letsencrypt.org/directory') account = client.new_account(contact: 'mailto:info@example.com', terms_of_service_agreed: true) account.kid # => ``` 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: ``` 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 client.kid => "https://acme-staging-v02.api.letsencrypt.org/acme/acct/000000" ``` ## Obtaining a certificate ### Ordering a certificate To order a new certificate, the client must provide a list of identifiers. 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. You can access the challenge you wish to complete using the `#dns` or `#http` method. ```ruby order = client.new_order(identifiers: ['example.com']) authorization = order.authorizations.first challenge = authorization.http ``` ### Preparing for HTTP challenge To complete the HTTP challenge, you must return a file using HTTP. The path follows the following format: > .well-known/acme-challenge/#{token} And the file content is the key authorization. The HTTP01 object has utility methods to generate them. ```ruby > http_challenge.content_type # => 'text/plain' > http_challenge.file_content # => example_token.TO1xJ0UDgfQ8WY5zT3txynup87UU3PhcDEIcuPyw4QU > http_challenge.filename # => '.well-known/acme-challenge/example_token' > http_challenge.token # => 'example_token' ``` For test purposes you can just save the challenge file and use Ruby to serve it: ```bash ruby -run -e httpd public -p 8080 --bind-address 0.0.0.0 ``` ### Preparing for DNS challenge To complete the DNS challenge, you must set a DNS record to prove that you control the domain. The DNS01 object has utility methods to generate them. ```ruby dns_challenge.record_name # => '_acme-challenge' dns_challenge.record_type # => 'TXT' dns_challenge.record_content # => 'HRV3PS5sRDyV-ous4HJk4z24s5JjmUTjcCaUjFt28-8' ``` ### Requesting a challenge verification Once you are ready to complete the challenge, you can request the server perform the verification. ```ruby challenge.request_validation ``` The validation is performed asynchronously and can take some time to be performed by the server. You can poll until its status changes. ```ruby while challenge.status == 'pending' sleep(2) challenge.reload end challenge.status # => 'valid' ``` ### Downloading a certificate Once all required authorizations have been validated through challenges, the order can be finalized using a CSR ([Certificate Signing Request](https://en.wikipedia.org/wiki/Certificate_signing_request)). A CSR can be slightly tricky to generate using OpenSSL from Ruby standard library. `acme-client` provide a utility class `CertificateRequest` to help with that. You'll need to use a different private key for the certificate request than the one you use for your `Acme::Client` account. Certificate generation happens asynchronously. You may need to poll. ```ruby csr = Acme::Client::CertificateRequest.new(private_key: a_different_private_key, subject: { common_name: 'example.com' }) order.finalize(csr: csr) while order.status == 'processing' sleep(1) challenge.reload end order.certificate # => PEM-formatted certificate ``` ## Extra ### Certificate revokation To revoke a certificate you can call `#revoke` with the certificate. ```ruby client.revoke(certificate: certificate) ``` ### Certificate renewal The is no renewal process, just create a new order. ## Not implemented - Account Key Roll-over. ## Requirements Ruby >= 2.1 ## Development All the tests use VCR to mock the interaction with the server. If you need to record new interaction you can specify the directory URL with the `ACME_DIRECTORY_URL` environment variable. ``` ACME_DIRECTORY_URL=https://acme-staging-v02.api.letsencrypt.org/directory rspec ``` ## Pull request? Yes. ## License [MIT License](http://opensource.org/licenses/MIT) acme-client-2.0.5/acme-client.gemspec0000644000004100000410000000201213625017253017454 0ustar www-datawww-datalib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'acme/client/version' Gem::Specification.new do |spec| spec.name = 'acme-client' spec.version = Acme::Client::VERSION spec.authors = ['Charles Barbier'] spec.email = ['unixcharles@gmail.com'] spec.summary = 'Client for the ACME protocol.' 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.require_paths = ['lib'] spec.required_ruby_version = '>= 2.1.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', '~> 3.3' spec.add_runtime_dependency 'faraday', '~> 0.9', '>= 0.9.1' end acme-client-2.0.5/bin/0000755000004100000410000000000013625017253014503 5ustar www-datawww-dataacme-client-2.0.5/bin/release0000755000004100000410000000076013625017253016054 0ustar www-datawww-data#!/usr/bin/env ruby require 'bundler/setup' require 'acme-client' version = Acme::Client::VERSION def `(command) puts super(command) if $?.exitstatus > 0 fail("failed at: #{command}") end end `git add CHANGELOG.md` `git add lib/acme/client/version.rb` `git commit -m "bump to #{version}"` `git pull --rebase origin master` `git tag 'v#{version}'` `git push --tags origin master` `gem build acme-client.gemspec` `gem push acme-client-#{version}.gem` `rm acme-client-#{version}.gem` acme-client-2.0.5/bin/console0000755000004100000410000000013413625017253016071 0ustar www-datawww-data#!/usr/bin/env ruby require 'bundler/setup' require 'acme-client' require 'pry' Pry.start acme-client-2.0.5/bin/setup0000755000004100000410000000007213625017253015570 0ustar www-datawww-data#!/bin/bash set -euo pipefail IFS=$'\n\t' bundle install acme-client-2.0.5/CHANGELOG.md0000644000004100000410000000677313625017253015561 0ustar www-datawww-data## `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 ## `2.0.2` * Fix constant lookup on InvalidDirectory * Forward connection options when fetching nonce * Fix splats without parenthesis warning ## `2.0.1` * Properly require URI ## `2.0.0` * Release of the `ACMEv2` branch ## `1.0.0` * Development for `ACMEv1` moved into `1.0.x` ## `0.6.3` * Handle Faraday::ConnectionFailed errors as Timeout error. ## `0.6.2` * Do not cache error type ## `0.6.1` * Fix typo in ECDSA curves ## `0.6.0` * Support external account keys ## `0.5.5` * Release script fixes. ## `0.5.4` * Enable ECDSA certificates ## `0.5.3` * Build release script ## `0.5.2` * Fix acme error names * ASN1 parsing improvements ## `0.5.1` * Set serial number of self-signed certificate ## `0.5.0` * Allow access to `Acme::Client#endpoint` and `Acme::Client#directory_uri` * Add `Acme::Client#fetch_authorization` * Setup cyclic dependency between challenges and their authorization for easier access of either with the other. * Drop `Acme::Client#challenge_from_hash` and `Acme::Client::Resources::Challenges::Base#to_h` in favor of the new API. * Delegate `Acme::Client::Resources::Challenges::Base#verify_status` to `Acme::Client::Resources::Authorization#verify_status` and make it update existing challenge objects. This makes it so that whichever is called, the correct status is reflected everywhere. * Add `Authorization#verify_status` - Recent versions of boulder will no longer process a challenge if the associated authorization is already valid, that is another challenge was previously solved. This means we need to allow people to poll on the authorizations status rather than the challenge status so they don't have to poll on the status of all challenges of an authorization all the time. See https://community.letsencrypt.org/t/upcoming-change-valid-authz-reuse/16982 and https://github.com/letsencrypt/boulder/issues/2057 ## `0.4.1` * Set the X509 version of the self-signed certificate * Fix requiring of time standard library ## `0.4.0` * Drop json-jwt dependency, implement JWS on our own * Drop ActiveSupport dependency ## `0.3.7` * Simplify internal `require` statements * Fix usage of json-jwt return value * Remove usage of deprecated `qualified_const_defined?` * Add user agent to upstream calls * Fix gem requiring * Set CSR version ## `0.3.6` * Handle non-json errors better ## `0.3.5` * Handle non protocol related server error ## `0.3.4` * Make `Acme::Client#challenge_from_hash` more strict with the arguments it receives ## `0.3.3` * Add new `unsupportedIdentifier` error from acme protocol ## `0.3.2` * Adds `rejectedIdentifier` error * Adds `RateLimited` error class * Clean up gem loading * Make client connection options configurable * Add URL to certificate ## `0.3.1` * Add ability to serialize challenges ## `0.3.0` * Use ISO8601 format for time parsing * Expose the authorization expiration timestamp. The ACME server returns an optional timestamp that signifies the expiration date of the domain authorization challenge. The time format is RFC3339 and can be parsed by Time#parse. See: https://letsencrypt.github.io/acme-spec/ Section 5.3 - expires * Update dns-01 record content to comply with ACME spec * Fix `SelfSignCertificate#default_not_before` ## `0.2.4` * Support tls-sni-01 ## `0.2.3` * Support certificate revocation * Move everything under the `Acme::Client` namespace * Improved errors acme-client-2.0.5/.rubocop.yml0000644000004100000410000000374213625017253016213 0ustar www-datawww-dataAllCops: 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 acme-client-2.0.5/.gitignore0000644000004100000410000000017413625017253015725 0ustar www-datawww-data/.bundle/ /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ /vendor/bundle /.idea/ .tool-versionsacme-client-2.0.5/Rakefile0000644000004100000410000000026113625017253015377 0ustar www-datawww-datarequire 'bundler/gem_tasks' require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) require 'rubocop/rake_task' RuboCop::RakeTask.new task default: [:spec, :rubocop] acme-client-2.0.5/lib/0000755000004100000410000000000013625017253014501 5ustar www-datawww-dataacme-client-2.0.5/lib/acme/0000755000004100000410000000000013625017253015406 5ustar www-datawww-dataacme-client-2.0.5/lib/acme/client/0000755000004100000410000000000013625017253016664 5ustar www-datawww-dataacme-client-2.0.5/lib/acme/client/resources.rb0000644000004100000410000000036613625017253021230 0ustar www-datawww-datamodule Acme::Client::Resources; end require 'acme/client/resources/directory' require 'acme/client/resources/account' require 'acme/client/resources/order' require 'acme/client/resources/authorization' require 'acme/client/resources/challenges' acme-client-2.0.5/lib/acme/client/version.rb0000644000004100000410000000014113625017253020672 0ustar www-datawww-data# frozen_string_literal: true module Acme class Client VERSION = '2.0.5'.freeze end end acme-client-2.0.5/lib/acme/client/faraday_middleware.rb0000644000004100000410000000460013625017253023015 0ustar www-datawww-data# 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 acme-client-2.0.5/lib/acme/client/certificate_request.rb0000644000004100000410000000663513625017253023255 0ustar www-datawww-dataclass Acme::Client::CertificateRequest extend Forwardable DEFAULT_KEY_LENGTH = 2048 DEFAULT_DIGEST = OpenSSL::Digest::SHA256 SUBJECT_KEYS = { common_name: 'CN', country_name: 'C', organization_name: 'O', organizational_unit: 'OU', state_or_province: 'ST', locality_name: 'L' }.freeze SUBJECT_TYPES = { 'CN' => OpenSSL::ASN1::UTF8STRING, 'C' => OpenSSL::ASN1::UTF8STRING, 'O' => OpenSSL::ASN1::UTF8STRING, 'OU' => OpenSSL::ASN1::UTF8STRING, 'ST' => OpenSSL::ASN1::UTF8STRING, 'L' => OpenSSL::ASN1::UTF8STRING }.freeze attr_reader :private_key, :common_name, :names, :subject def_delegators :csr, :to_pem, :to_der def initialize(common_name: nil, names: [], private_key: generate_private_key, subject: {}, digest: DEFAULT_DIGEST.new) @digest = digest @private_key = private_key @subject = normalize_subject(subject) @common_name = common_name || @subject[SUBJECT_KEYS[:common_name]] || @subject[:common_name] @names = names.to_a.dup normalize_names @subject[SUBJECT_KEYS[:common_name]] ||= @common_name validate_subject end def csr @csr ||= generate end private def generate_private_key OpenSSL::PKey::RSA.new(DEFAULT_KEY_LENGTH) end def normalize_subject(subject) @subject = subject.each_with_object({}) do |(key, value), hash| hash[SUBJECT_KEYS.fetch(key, key)] = value.to_s end end def normalize_names if @common_name @names.unshift(@common_name) unless @names.include?(@common_name) else raise ArgumentError, 'No common name and no list of names given' if @names.empty? @common_name = @names.first end end def validate_subject validate_subject_attributes validate_subject_common_name end def validate_subject_attributes extra_keys = @subject.keys - SUBJECT_KEYS.keys - SUBJECT_KEYS.values return if extra_keys.empty? raise ArgumentError, "Unexpected subject attributes given: #{extra_keys.inspect}" end def validate_subject_common_name return if @common_name == @subject[SUBJECT_KEYS[:common_name]] raise ArgumentError, 'Conflicting common name given in arguments and subject' end def generate OpenSSL::X509::Request.new.tap do |csr| if @private_key.is_a?(OpenSSL::PKey::EC) && RbConfig::CONFIG['MAJOR'] == '2' && RbConfig::CONFIG['MINOR'].to_i < 4 # OpenSSL::PKey::EC does not respect classic PKey interface (as defined by # PKey::RSA and PKey::DSA) until ruby 2.4. # Supporting this interface needs monkey patching of OpenSSL:PKey::EC, or # subclassing it. Here, use a subclass. @private_key = ECKeyPatch.new(@private_key) end csr.public_key = @private_key csr.subject = generate_subject csr.version = 2 add_extension(csr) csr.sign @private_key, @digest end end def generate_subject OpenSSL::X509::Name.new( @subject.map {|name, value| [name, value, SUBJECT_TYPES[name]] } ) end def add_extension(csr) extension = OpenSSL::X509::ExtensionFactory.new.create_extension( 'subjectAltName', @names.map { |name| "DNS:#{name}" }.join(', '), false ) csr.add_attribute( OpenSSL::X509::Attribute.new( 'extReq', OpenSSL::ASN1::Set.new([OpenSSL::ASN1::Sequence.new([extension])]) ) ) end end require 'acme/client/certificate_request/ec_key_patch' acme-client-2.0.5/lib/acme/client/resources/0000755000004100000410000000000013625017253020676 5ustar www-datawww-dataacme-client-2.0.5/lib/acme/client/resources/account.rb0000644000004100000410000000166713625017253022671 0ustar www-datawww-data# frozen_string_literal: true class Acme::Client::Resources::Account attr_reader :url, :status, :contact, :term_of_service, :orders_url def initialize(client, **arguments) @client = client assign_attributes(arguments) end def kid url end def update(contact: nil, terms_of_service_agreed: nil) assign_attributes(**@client.account_update( contact: contact, terms_of_service_agreed: term_of_service ).to_h) true end def deactivate assign_attributes(**@client.account_deactivate.to_h) true end def reload assign_attributes(**@client.account.to_h) true end def to_h { url: url, term_of_service: term_of_service, status: status, contact: contact } end private def assign_attributes(url:, term_of_service:, status:, contact:) @url = url @term_of_service = term_of_service @status = status @contact = Array(contact) end end acme-client-2.0.5/lib/acme/client/resources/challenges.rb0000644000004100000410000000106613625017253023333 0ustar www-datawww-data# frozen_string_literal: true 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, 'dns-01' => Acme::Client::Resources::Challenges::DNS01 } def self.new(client, type:, **arguments) CHALLENGE_TYPES.fetch(type, Unsupported).new(client, **arguments) end end acme-client-2.0.5/lib/acme/client/resources/order.rb0000644000004100000410000000261413625017253022341 0ustar www-datawww-data# frozen_string_literal: true class Acme::Client::Resources::Order attr_reader :url, :status, :contact, :finalize_url, :identifiers, :authorization_urls, :expires, :certificate_url def initialize(client, **arguments) @client = client assign_attributes(arguments) end def reload assign_attributes(**@client.order(url: url).to_h) true end def authorizations @authorization_urls.map do |authorization_url| @client.authorization(url: authorization_url) end end def finalize(csr:) assign_attributes(**@client.finalize(url: finalize_url, csr: csr).to_h) true end def certificate if certificate_url @client.certificate(url: certificate_url) else raise Acme::Client::Error::CertificateNotReady, 'No certificate_url to collect the order' end end def to_h { url: url, status: status, expires: expires, finalize_url: finalize_url, authorization_urls: authorization_urls, identifiers: identifiers, certificate_url: certificate_url } end private def assign_attributes(url:, status:, expires:, finalize_url:, authorization_urls:, identifiers:, certificate_url: nil) @url = url @status = status @expires = expires @finalize_url = finalize_url @authorization_urls = authorization_urls @identifiers = identifiers @certificate_url = certificate_url end end acme-client-2.0.5/lib/acme/client/resources/authorization.rb0000644000004100000410000000322013625017253024120 0ustar www-datawww-data# frozen_string_literal: true class Acme::Client::Resources::Authorization attr_reader :url, :identifier, :domain, :expires, :status, :wildcard def initialize(client, **arguments) @client = client assign_attributes(arguments) end def deactivate assign_attributes(**@client.deactivate_authorization(url: url).to_h) true end def reload assign_attributes(**@client.authorization(url: url).to_h) true end def challenges @challenges.map do |challenge| initialize_challenge(challenge) end end def http01 @http01 ||= challenges.find { |challenge| challenge.is_a?(Acme::Client::Resources::Challenges::HTTP01) } end alias_method :http, :http01 def dns01 @dns01 ||= challenges.find { |challenge| challenge.is_a?(Acme::Client::Resources::Challenges::DNS01) } end alias_method :dns, :dns01 def to_h { url: url, identifier: identifier, status: status, expires: expires, challenges: @challenges, wildcard: wildcard } end private def initialize_challenge(attributes) arguments = { type: attributes.fetch('type'), status: attributes.fetch('status'), url: attributes.fetch('url'), token: attributes.fetch('token'), error: attributes['error'] } Acme::Client::Resources::Challenges.new(@client, **arguments) end def assign_attributes(url:, status:, expires:, challenges:, identifier:, wildcard: false) @url = url @identifier = identifier @domain = identifier.fetch('value') @status = status @expires = expires @challenges = challenges @wildcard = wildcard end end acme-client-2.0.5/lib/acme/client/resources/challenges/0000755000004100000410000000000013625017253023003 5ustar www-datawww-dataacme-client-2.0.5/lib/acme/client/resources/challenges/base.rb0000644000004100000410000000160513625017253024244 0ustar www-datawww-data# frozen_string_literal: true class Acme::Client::Resources::Challenges::Base attr_reader :status, :url, :token, :error def initialize(client, **arguments) @client = client assign_attributes(arguments) end def challenge_type self.class::CHALLENGE_TYPE end def key_authorization "#{token}.#{@client.jwk.thumbprint}" end def reload assign_attributes(**@client.challenge(url: url).to_h) true end def request_validation assign_attributes(**send_challenge_validation( url: url )) true end def to_h { status: status, url: url, token: token, error: error } end 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 @token = token @error = error end end acme-client-2.0.5/lib/acme/client/resources/challenges/unsupported_challenge.rb0000644000004100000410000000014713625017253027724 0ustar www-datawww-dataclass Acme::Client::Resources::Challenges::Unsupported < Acme::Client::Resources::Challenges::Base end acme-client-2.0.5/lib/acme/client/resources/challenges/dns01.rb0000644000004100000410000000070213625017253024254 0ustar www-datawww-data# frozen_string_literal: true class Acme::Client::Resources::Challenges::DNS01 < Acme::Client::Resources::Challenges::Base CHALLENGE_TYPE = 'dns-01'.freeze RECORD_NAME = '_acme-challenge'.freeze RECORD_TYPE = 'TXT'.freeze DIGEST = OpenSSL::Digest::SHA256 def record_name RECORD_NAME end def record_type RECORD_TYPE end def record_content Acme::Client::Util.urlsafe_base64(DIGEST.digest(key_authorization)) end end acme-client-2.0.5/lib/acme/client/resources/challenges/http01.rb0000644000004100000410000000054513625017253024454 0ustar www-datawww-data# frozen_string_literal: true class Acme::Client::Resources::Challenges::HTTP01 < Acme::Client::Resources::Challenges::Base CHALLENGE_TYPE = 'http-01'.freeze CONTENT_TYPE = 'text/plain'.freeze def content_type CONTENT_TYPE end def file_content key_authorization end def filename ".well-known/acme-challenge/#{token}" end end acme-client-2.0.5/lib/acme/client/resources/directory.rb0000644000004100000410000000343013625017253023227 0ustar www-datawww-data# frozen_string_literal: true class Acme::Client::Resources::Directory DIRECTORY_RESOURCES = { new_nonce: 'newNonce', new_account: 'newAccount', new_order: 'newOrder', new_authz: 'newAuthz', revoke_certificate: 'revokeCert', key_change: 'keyChange' } DIRECTORY_META = { terms_of_service: 'termsOfService', website: 'website', caa_identities: 'caaIdentities', external_account_required: 'externalAccountRequired' } def initialize(url, connection_options) @url, @connection_options = url, connection_options end def endpoint_for(key) directory.fetch(key) do |missing_key| raise Acme::Client::Error::UnsupportedOperation, "Directory at #{@url} does not include `#{missing_key}`" end end def terms_of_service meta[DIRECTORY_META[:terms_of_service]] end def website meta[DIRECTORY_META[:website]] end def caa_identities meta[DIRECTORY_META[:caa_identities]] end def external_account_required meta[DIRECTORY_META[:external_account_required]] end def meta directory[:meta] end private def directory @directory ||= load_directory end def load_directory body = fetch_directory result = {} result[:meta] = body.delete('meta') DIRECTORY_RESOURCES.each do |key, entry| result[key] = URI(body[entry]) if body[entry] 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 acme-client-2.0.5/lib/acme/client/error.rb0000644000004100000410000000501713625017253020345 0ustar www-datawww-dataclass Acme::Client::Error < StandardError class Timeout < Acme::Client::Error; end class ClientError < Acme::Client::Error; end class InvalidDirectory < ClientError; end class UnsupportedOperation < ClientError; end class UnsupportedChallengeType < ClientError; end class NotFound < ClientError; end class CertificateNotReady < ClientError; end class ServerError < Acme::Client::Error; end class BadCSR < ServerError; end class BadNonce < ServerError; end class BadSignatureAlgorithm < ServerError; end class InvalidContact < ServerError; end class UnsupportedContact < ServerError; end class ExternalAccountRequired < ServerError; end class AccountDoesNotExist < ServerError; end class Malformed < ServerError; end class RateLimited < ServerError; end class RejectedIdentifier < ServerError; end class ServerInternal < ServerError; end class Unauthorized < ServerError; end class UnsupportedIdentifier < ServerError; end class UserActionRequired < ServerError; end class BadRevocationReason < ServerError; end class Caa < ServerError; end class Dns < ServerError; end class Connection < ServerError; end class Tls < ServerError; end class IncorrectResponse < ServerError; end ACME_ERRORS = { 'urn:ietf:params:acme:error:badCSR' => BadCSR, 'urn:ietf:params:acme:error:badNonce' => BadNonce, '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:rateLimited' => RateLimited, 'urn:ietf:params:acme:error:rejectedIdentifier' => RejectedIdentifier, 'urn:ietf:params:acme:error:serverInternal' => ServerInternal, 'urn:ietf:params:acme:error:unauthorized' => Unauthorized, 'urn:ietf:params:acme:error:unsupportedIdentifier' => UnsupportedIdentifier, 'urn:ietf:params:acme:error:userActionRequired' => UserActionRequired, 'urn:ietf:params:acme:error:badRevocationReason' => BadRevocationReason, 'urn:ietf:params:acme:error:caa' => Caa, 'urn:ietf:params:acme:error:dns' => Dns, 'urn:ietf:params:acme:error:connection' => Connection, 'urn:ietf:params:acme:error:tls' => Tls, 'urn:ietf:params:acme:error:incorrectResponse' => IncorrectResponse } end acme-client-2.0.5/lib/acme/client/jwk/0000755000004100000410000000000013625017253017457 5ustar www-datawww-dataacme-client-2.0.5/lib/acme/client/jwk/ecdsa.rb0000644000004100000410000000445713625017253021075 0ustar www-datawww-dataclass Acme::Client::JWK::ECDSA < Acme::Client::JWK::Base # JWA parameters for supported OpenSSL curves. # https://tools.ietf.org/html/rfc7518#section-3.1 KNOWN_CURVES = { 'prime256v1' => { jwa_crv: 'P-256', jwa_alg: 'ES256', digest: OpenSSL::Digest::SHA256 }.freeze, 'secp384r1' => { jwa_crv: 'P-384', jwa_alg: 'ES384', digest: OpenSSL::Digest::SHA384 }.freeze, 'secp521r1' => { jwa_crv: 'P-521', jwa_alg: 'ES512', digest: OpenSSL::Digest::SHA512 }.freeze }.freeze # Instantiate a new ECDSA JWK. # # private_key - A OpenSSL::PKey::EC instance. # # Returns nothing. def initialize(private_key) unless private_key.is_a?(OpenSSL::PKey::EC) raise ArgumentError, 'private_key must be a OpenSSL::PKey::EC' end unless @curve_params = KNOWN_CURVES[private_key.group.curve_name] raise ArgumentError, 'Unknown EC curve' end @private_key = private_key end # The name of the algorithm as needed for the `alg` member of a JWS object. # # Returns a String. def jwa_alg @curve_params[:jwa_alg] end # Get this JWK as a Hash for JSON serialization. # # Returns a Hash. def to_h { 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)) } end # Sign a message with the private key. # # message - A String message to sign. # # Returns a String signature. def sign(message) # DER encoded ASN.1 signature der = @private_key.sign(@curve_params[:digest].new, message) # ASN.1 SEQUENCE seq = OpenSSL::ASN1.decode(der) # ASN.1 INTs ints = seq.value # BigNumbers bns = ints.map(&:value) # Binary R/S values r, s = bns.map { |bn| [bn.to_s(16)].pack('H*') } # JWS wants raw R/S concatenated. [r, s].join end private def coordinates @coordinates ||= begin hex = public_key.to_bn.to_s(16) data_len = hex.length - 2 hex_x = hex[2, data_len / 2] hex_y = hex[2 + data_len / 2, data_len / 2] { x: OpenSSL::BN.new([hex_x].pack('H*'), 2), y: OpenSSL::BN.new([hex_y].pack('H*'), 2) } end end def public_key @private_key.public_key end end acme-client-2.0.5/lib/acme/client/jwk/base.rb0000644000004100000410000000366513625017253020730 0ustar www-datawww-dataclass Acme::Client::JWK::Base THUMBPRINT_DIGEST = OpenSSL::Digest::SHA256 # Initialize a new JWK. # # Returns nothing. def initialize raise NotImplementedError end # Generate a JWS JSON web signature. # # header - A Hash of extra header fields to include. # payload - A Hash of payload data. # # Returns a JSON String. 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.nil? ? '' : payload.to_json) signature_data = "#{encoded_header}.#{encoded_payload}" signature = sign(signature_data) encoded_signature = Acme::Client::Util.urlsafe_base64(signature) { protected: encoded_header, payload: encoded_payload, signature: encoded_signature }.to_json end # Serialize this JWK as JSON. # # Returns a JSON string. def to_json to_h.to_json end # Get this JWK as a Hash for JSON serialization. # # Returns a Hash. def to_h raise NotImplementedError end # JWK thumbprint as used for key authorization. # # Returns a String. def thumbprint Acme::Client::Util.urlsafe_base64(THUMBPRINT_DIGEST.digest(to_json)) end # Header fields for a JSON web signature. # # typ: - Value for the `typ` field. Default 'JWT'. # # Returns a Hash. def jws_header(header) jws = { typ: 'JWT', alg: jwa_alg }.merge(header) jws[:jwk] = to_h if header[:kid].nil? jws end # The name of the algorithm as needed for the `alg` member of a JWS object. # # Returns a String. def jwa_alg raise NotImplementedError end # Sign a message with the private key. # # message - A String message to sign. # # Returns a String signature. # rubocop:disable Lint/UnusedMethodArgument def sign(message) raise NotImplementedError end # rubocop:enable Lint/UnusedMethodArgument end acme-client-2.0.5/lib/acme/client/jwk/rsa.rb0000644000004100000410000000223213625017253020570 0ustar www-datawww-dataclass Acme::Client::JWK::RSA < Acme::Client::JWK::Base # Digest algorithm to use when signing. DIGEST = OpenSSL::Digest::SHA256 # Instantiate a new RSA JWK. # # private_key - A OpenSSL::PKey::RSA instance. # # Returns nothing. def initialize(private_key) unless private_key.is_a?(OpenSSL::PKey::RSA) raise ArgumentError, 'private_key must be a OpenSSL::PKey::RSA' end @private_key = private_key end # Get this JWK as a Hash for JSON serialization. # # Returns a Hash. def to_h { e: Acme::Client::Util.urlsafe_base64(public_key.e.to_s(2)), kty: 'RSA', n: Acme::Client::Util.urlsafe_base64(public_key.n.to_s(2)) } end # Sign a message with the private key. # # message - A String message to sign. # # Returns a String signature. def sign(message) @private_key.sign(DIGEST.new, 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 # RSASSA-PKCS1-v1_5 using SHA-256 'RS256' end private def public_key @private_key.public_key end end acme-client-2.0.5/lib/acme/client/self_sign_certificate.rb0000644000004100000410000000325613625017253023532 0ustar www-datawww-dataclass Acme::Client::SelfSignCertificate attr_reader :private_key, :subject_alt_names, :not_before, :not_after extend Forwardable def_delegators :certificate, :to_pem, :to_der def initialize(subject_alt_names:, not_before: default_not_before, not_after: default_not_after, private_key: generate_private_key) @private_key = private_key @subject_alt_names = subject_alt_names @not_before = not_before @not_after = not_after end def certificate @certificate ||= begin certificate = generate_certificate extension_factory = generate_extension_factory(certificate) subject_alt_name_entry = subject_alt_names.map { |d| "DNS: #{d}" }.join(',') subject_alt_name_extension = extension_factory.create_extension('subjectAltName', subject_alt_name_entry) certificate.add_extension(subject_alt_name_extension) certificate.sign(private_key, digest) end end private def generate_private_key OpenSSL::PKey::RSA.new(2048) end def default_not_before Time.now - 3600 end def default_not_after Time.now + 30 * 24 * 3600 end def digest OpenSSL::Digest::SHA256.new end def generate_certificate certificate = OpenSSL::X509::Certificate.new certificate.not_before = not_before certificate.not_after = not_after Acme::Client::Util.set_public_key(certificate, private_key) certificate.version = 2 certificate.serial = 1 certificate end def generate_extension_factory(certificate) extension_factory = OpenSSL::X509::ExtensionFactory.new extension_factory.subject_certificate = certificate extension_factory.issuer_certificate = certificate extension_factory end end acme-client-2.0.5/lib/acme/client/certificate_request/0000755000004100000410000000000013625017253022716 5ustar www-datawww-dataacme-client-2.0.5/lib/acme/client/certificate_request/ec_key_patch.rb0000644000004100000410000000023513625017253025661 0ustar www-datawww-data# Class to handle bug # class Acme::Client::CertificateRequest::ECKeyPatch < OpenSSL::PKey::EC alias private? private_key? alias public? public_key? end acme-client-2.0.5/lib/acme/client/util.rb0000644000004100000410000000110613625017253020164 0ustar www-datawww-datamodule Acme::Client::Util def urlsafe_base64(data) Base64.urlsafe_encode64(data).sub(/[\s=]*\z/, '') end # Sets public key on CSR or cert. # # obj - An OpenSSL::X509::Certificate or OpenSSL::X509::Request instance. # priv - An OpenSSL::PKey::EC or OpenSSL::PKey::RSA instance. # # Returns nothing. def set_public_key(obj, priv) case priv when OpenSSL::PKey::EC obj.public_key = priv when OpenSSL::PKey::RSA obj.public_key = priv.public_key else raise ArgumentError, 'priv must be EC or RSA' end end extend self end acme-client-2.0.5/lib/acme/client/jwk.rb0000644000004100000410000000107513625017253020007 0ustar www-datawww-datamodule Acme::Client::JWK # Make a JWK from a private key. # # private_key - An OpenSSL::PKey::EC or OpenSSL::PKey::RSA instance. # # Returns a JWK::Base subclass instance. def self.from_private_key(private_key) case private_key when OpenSSL::PKey::RSA Acme::Client::JWK::RSA.new(private_key) when OpenSSL::PKey::EC Acme::Client::JWK::ECDSA.new(private_key) else raise ArgumentError, 'private_key must be EC or RSA' end end end require 'acme/client/jwk/base' require 'acme/client/jwk/rsa' require 'acme/client/jwk/ecdsa' acme-client-2.0.5/lib/acme/client.rb0000644000004100000410000002160413625017253017214 0ustar www-datawww-data# frozen_string_literal: true require 'faraday' require 'json' require 'openssl' require 'digest' require 'forwardable' require 'base64' require 'time' require 'uri' module Acme; end class Acme::Client; end require 'acme/client/version' 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/util' class Acme::Client DEFAULT_DIRECTORY = 'http://127.0.0.1:4000/directory'.freeze repo_url = 'https://github.com/unixcharles/acme-client' USER_AGENT = "Acme::Client v#{Acme::Client::VERSION} (#{repo_url})".freeze CONTENT_TYPES = { pem: 'application/pem-certificate-chain' } 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 @jwk = if jwk jwk else Acme::Client::JWK.from_private_key(private_key) end @kid, @connection_options = kid, connection_options @bad_nonce_retry = bad_nonce_retry @directory = Acme::Client::Resources::Directory.new(URI(directory), @connection_options) @nonces ||= [] end attr_reader :jwk, :nonces def new_account(contact:, terms_of_service_agreed: nil) payload = { contact: Array(contact) } if terms_of_service_agreed payload[:termsOfServiceAgreed] = terms_of_service_agreed end response = post(endpoint_for(:new_account), payload: payload, mode: :jws) @kid = response.headers.fetch(:location) if response.body.nil? || response.body.empty? account else arguments = attributes_from_account_response(response) Acme::Client::Resources::Account.new(self, url: @kid, **arguments) end end def account_update(contact: nil, terms_of_service_agreed: nil) payload = {} payload[:contact] = Array(contact) if contact payload[:termsOfServiceAgreed] = terms_of_service_agreed if terms_of_service_agreed response = post(kid, payload: payload) arguments = attributes_from_account_response(response) Acme::Client::Resources::Account.new(self, url: kid, **arguments) end def account_deactivate response = post(kid, payload: { status: 'deactivated' }) arguments = attributes_from_account_response(response) 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_as_get(@kid) arguments = attributes_from_account_response(response) Acme::Client::Resources::Account.new(self, url: @kid, **arguments) end def kid @kid ||= account.kid end def new_order(identifiers:, not_before: nil, not_after: nil) payload = {} payload['identifiers'] = prepare_order_identifiers(identifiers) payload['notBefore'] = not_before if not_before payload['notAfter'] = not_after if not_after response = post(endpoint_for(:new_order), payload: payload) arguments = attributes_from_order_response(response) Acme::Client::Resources::Order.new(self, **arguments) end def order(url:) response = post_as_get(url) arguments = attributes_from_order_response(response) Acme::Client::Resources::Order.new(self, **arguments.merge(url: url)) end def finalize(url:, csr:) unless csr.respond_to?(:to_der) raise ArgumentError, 'csr must respond to `#to_der`' end base64_der_csr = Acme::Client::Util.urlsafe_base64(csr.to_der) response = post(url, payload: { csr: base64_der_csr }) arguments = attributes_from_order_response(response) Acme::Client::Resources::Order.new(self, **arguments) end def certificate(url:) response = download(url, format: :pem) response.body end def authorization(url:) response = post_as_get(url) arguments = attributes_from_authorization_response(response) Acme::Client::Resources::Authorization.new(self, url: url, **arguments) end def deactivate_authorization(url:) response = post(url, payload: { status: 'deactivated' }) arguments = attributes_from_authorization_response(response) Acme::Client::Resources::Authorization.new(self, url: url, **arguments) end def challenge(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: nil) response = post(url, payload: {}) arguments = attributes_from_challenge_response(response) Acme::Client::Resources::Challenges.new(self, **arguments) end def revoke(certificate:, reason: nil) der_certificate = if certificate.respond_to?(:to_der) certificate.to_der else OpenSSL::X509::Certificate.new(certificate).to_der end base64_der_certificate = Acme::Client::Util.urlsafe_base64(der_certificate) payload = { certificate: base64_der_certificate } payload[:reason] = reason unless reason.nil? response = post(endpoint_for(:revoke_certificate), payload: payload) response.success? end def get_nonce connection = new_connection(endpoint: endpoint_for(:new_nonce)) response = connection.head(nil, nil, 'User-Agent' => USER_AGENT) nonces << response.headers['replay-nonce'] true end def meta @directory.meta end def terms_of_service @directory.terms_of_service end def website @directory.website end def caa_identities @directory.caa_identities end def external_account_required @directory.external_account_required end 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, :status, [:term_of_service, 'termsOfServiceAgreed'], :contact ) end def attributes_from_order_response(response) attributes = extract_attributes( response.body, :status, :expires, [:finalize_url, 'finalize'], [:authorization_urls, 'authorizations'], [:certificate_url, 'certificate'], :identifiers ) attributes[:url] = response.headers[:location] if response.headers[:location] attributes end def attributes_from_authorization_response(response) extract_attributes(response.body, :identifier, :status, :expires, :challenges, :wildcard) end def attributes_from_challenge_response(response) extract_attributes(response.body, :status, :url, :token, :type, :error) end def extract_attributes(input, *attributes) attributes .map {|fields| Array(fields) } .each_with_object({}) { |(key, field), hash| field ||= key.to_s hash[key] = input[field] } end def post(url, payload: {}, mode: :kid) connection = connection_for(url: url, mode: mode) 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: :kid) connection.post do |request| request.url(url) request.headers['Accept'] = CONTENT_TYPES.fetch(format) end end def connection_for(url:, mode:) uri = URI(url) endpoint = "#{uri.scheme}://#{uri.hostname}:#{uri.port}" @connections ||= {} @connections[mode] ||= {} @connections[mode][endpoint] ||= new_acme_connection(endpoint: endpoint, mode: mode) end def new_acme_connection(endpoint:, mode:) new_connection(endpoint: endpoint) do |configuration| configuration.use Acme::Client::FaradayMiddleware, client: self, mode: mode end end def new_connection(endpoint:) Faraday.new(endpoint, **@connection_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 yield(configuration) if block_given? configuration.adapter Faraday.default_adapter end end def fetch_chain(response, limit = 10) links = response.headers['link'] if limit.zero? || links.nil? || links['up'].nil? [] else issuer = get(links['up']) [OpenSSL::X509::Certificate.new(issuer.body), *fetch_chain(issuer, limit - 1)] end end def endpoint_for(key) @directory.endpoint_for(key) end end acme-client-2.0.5/lib/acme-client.rb0000644000004100000410000000002613625017253017205 0ustar www-datawww-datarequire 'acme/client' acme-client-2.0.5/Gemfile0000644000004100000410000000037713625017253015235 0ustar www-datawww-datasource 'https://rubygems.org' gemspec 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 acme-client-2.0.5/LICENSE.txt0000644000004100000410000000207213625017253015557 0ustar www-datawww-dataThe MIT License (MIT) Copyright (c) 2015 Charles Barbier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.