Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
Ian Eiloart
Sympl
Commits
ab2048b7
Commit
ab2048b7
authored
Apr 17, 2019
by
Paul Cammish
Browse files
Replace folded-in version of ruby acme-client with ~3 year old version
parent
fa16a1da
Changes
21
Show whitespace changes
Inline
Side-by-side
common/acme-client.zip
View file @
ab2048b7
No preview for this file type
common/lib/acme-client.rb
View file @
ab2048b7
module
Acme
;
class
Client
;
end
;
end
require
'faraday'
require
'json'
require
'json/jwt'
require
'openssl'
require
'digest'
require
'forwardable'
require
'acme/client/certificate'
require
'acme/client/certificate_request'
require
'acme/client/self_sign_certificate'
require
'acme/client/crypto'
require
'acme/client'
require
'acme/client/resources'
require
'acme/client/faraday_middleware'
require
'acme/client/error'
common/lib/acme/client.rb
View file @
ab2048b7
require
'acme-client'
# frozen_string_literal: true
require
'faraday'
require
'json'
require
'openssl'
require
'digest'
require
'forwardable'
require
'base64'
require
'time'
module
Acme
;
end
class
Acme::Client
;
end
require
'acme/client/version'
require
'acme/client/certificate'
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_ENDPOINT
=
'http://127.0.0.1:4000'
.
freeze
...
...
@@ -9,13 +30,23 @@ class Acme::Client
'revoke-cert'
=>
'/acme/revoke-cert'
}.
freeze
def
initialize
(
private_key
:,
endpoint:
DEFAULT_ENDPOINT
,
directory_uri:
nil
)
@endpoint
,
@private_key
,
@directory_uri
=
endpoint
,
private_key
,
directory_uri
def
initialize
(
jwk:
nil
,
private_key:
nil
,
endpoint:
DEFAULT_ENDPOINT
,
directory_uri:
nil
,
connection_options:
{})
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
@endpoint
,
@directory_uri
,
@connection_options
=
endpoint
,
directory_uri
,
connection_options
@nonces
||=
[]
load_directory!
end
attr_reader
:
private_key
,
:nonces
,
:operation_endpoints
attr_reader
:
jwk
,
:nonces
,
:endpoint
,
:directory_uri
,
:operation_endpoints
def
register
(
contact
:)
payload
=
{
...
...
@@ -36,7 +67,12 @@ class Acme::Client
}
response
=
connection
.
post
(
@operation_endpoints
.
fetch
(
'new-authz'
),
payload
)
::
Acme
::
Client
::
Resources
::
Authorization
.
new
(
self
,
response
)
::
Acme
::
Client
::
Resources
::
Authorization
.
new
(
self
,
response
.
headers
[
'Location'
],
response
)
end
def
fetch_authorization
(
uri
)
response
=
connection
.
get
(
uri
)
::
Acme
::
Client
::
Resources
::
Authorization
.
new
(
self
,
uri
,
response
)
end
def
new_certificate
(
csr
)
...
...
@@ -46,7 +82,7 @@ class Acme::Client
}
response
=
connection
.
post
(
@operation_endpoints
.
fetch
(
'new-cert'
),
payload
)
::
Acme
::
Client
::
Certificate
.
new
(
OpenSSL
::
X509
::
Certificate
.
new
(
response
.
body
),
fetch_chain
(
response
),
csr
)
::
Acme
::
Client
::
Certificate
.
new
(
OpenSSL
::
X509
::
Certificate
.
new
(
response
.
body
),
response
.
headers
[
'location'
],
fetch_chain
(
response
),
csr
)
end
def
revoke_certificate
(
certificate
)
...
...
@@ -62,23 +98,12 @@ class Acme::Client
end
def
connection
@connection
||=
Faraday
.
new
(
@endpoint
)
do
|
configuration
|
@connection
||=
Faraday
.
new
(
@endpoint
,
**
@connection_options
)
do
|
configuration
|
configuration
.
use
Acme
::
Client
::
FaradayMiddleware
,
client:
self
configuration
.
adapter
Faraday
.
default_adapter
end
end
def
challenge_from_hash
(
attributes
)
case
attributes
.
fetch
(
'type'
)
when
'http-01'
Acme
::
Client
::
Resources
::
Challenges
::
HTTP01
.
new
(
self
,
attributes
)
when
'dns-01'
Acme
::
Client
::
Resources
::
Challenges
::
DNS01
.
new
(
self
,
attributes
)
when
'tls-sni-01'
Acme
::
Client
::
Resources
::
Challenges
::
TLSSNI01
.
new
(
self
,
attributes
)
end
end
private
def
fetch_chain
(
response
,
limit
=
10
)
...
...
common/lib/acme/client/certificate.rb
View file @
ab2048b7
class
Acme::Client::Certificate
extend
Forwardable
attr_reader
:x509
,
:x509_chain
,
:request
,
:private_key
attr_reader
:x509
,
:x509_chain
,
:request
,
:private_key
,
:url
def_delegators
:x509
,
:to_pem
,
:to_der
def
initialize
(
certificate
,
chain
,
request
)
def
initialize
(
certificate
,
url
,
chain
,
request
)
@x509
=
certificate
@url
=
url
@x509_chain
=
chain
@request
=
request
end
...
...
common/lib/acme/client/certificate_request.rb
View file @
ab2048b7
...
...
@@ -79,8 +79,17 @@ class Acme::Client::CertificateRequest
def
generate
OpenSSL
::
X509
::
Request
.
new
.
tap
do
|
csr
|
csr
.
public_key
=
@private_key
.
public_key
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
...
...
@@ -108,3 +117,5 @@ class Acme::Client::CertificateRequest
)
end
end
require
'acme/client/certificate_request/ec_key_patch'
common/lib/acme/client/certificate_request/ec_key_patch.rb
0 → 100644
View file @
ab2048b7
# Class to handle bug #
class
Acme::Client::CertificateRequest::ECKeyPatch
<
OpenSSL
::
PKey
::
EC
alias
private
?
private_key?
alias
public
?
public_key?
end
common/lib/acme/client/crypto.rb
deleted
100644 → 0
View file @
fa16a1da
class
Acme::Client::Crypto
attr_reader
:private_key
def
initialize
(
private_key
)
@private_key
=
private_key
end
def
generate_signed_jws
(
header
:,
payload
:)
jwt
=
JSON
::
JWT
.
new
(
payload
||
{})
jwt
.
header
.
merge!
(
header
||
{})
jwt
.
header
[
:jwk
]
=
jwk
jwt
.
signature
=
jwt
.
sign
(
private_key
,
:RS256
).
signature
jwt
.
to_json
(
syntax: :flattened
)
end
def
thumbprint
jwk
.
thumbprint
end
def
digest
OpenSSL
::
Digest
::
SHA256
.
new
end
private
def
jwk
@jwk
||=
JSON
::
JWK
.
new
(
public_key
)
end
def
public_key
@public_key
||=
private_key
.
public_key
end
end
common/lib/acme/client/error.rb
View file @
ab2048b7
...
...
@@ -9,4 +9,8 @@ class Acme::Client::Error < StandardError
class
Acme::Tls
<
Acme
::
Client
::
Error
;
end
class
Unauthorized
<
Acme
::
Client
::
Error
;
end
class
UnknownHost
<
Acme
::
Client
::
Error
;
end
class
Timeout
<
Acme
::
Client
::
Error
;
end
class
RateLimited
<
Acme
::
Client
::
Error
;
end
class
RejectedIdentifier
<
Acme
::
Client
::
Error
;
end
class
UnsupportedIdentifier
<
Acme
::
Client
::
Error
;
end
end
common/lib/acme/client/faraday_middleware.rb
View file @
ab2048b7
# frozen_string_literal: true
class
Acme::Client::FaradayMiddleware
<
Faraday
::
Middleware
attr_reader
:env
,
:response
,
:client
repo_url
=
'https://github.com/unixcharles/acme-client'
USER_AGENT
=
"Acme::Client v
#{
Acme
::
Client
::
VERSION
}
(
#{
repo_url
}
)"
.
freeze
def
initialize
(
app
,
client
:)
super
(
app
)
@client
=
client
...
...
@@ -8,11 +13,11 @@ class Acme::Client::FaradayMiddleware < Faraday::Middleware
def
call
(
env
)
@env
=
env
unless
@env
.
body
.
nil?
@env
.
body
=
crypto
.
generate_signed_jws
(
header:
{
nonce:
pop_nonce
},
payload:
env
.
body
)
@env
.
request_headers
[
'Content-Type'
]
||=
'application/json'
end
@env
[
:request_headers
][
'User-Agent'
]
=
USER_AGENT
@env
.
body
=
client
.
jwk
.
jws
(
header:
{
nonce:
pop_nonce
},
payload:
env
.
body
)
@app
.
call
(
env
).
on_complete
{
|
response_env
|
on_complete
(
response_env
)
}
rescue
Faraday
::
TimeoutError
raise
Acme
::
Client
::
Error
::
Timeout
end
def
on_complete
(
env
)
...
...
@@ -47,14 +52,26 @@ class Acme::Client::FaradayMiddleware < Faraday::Middleware
end
def
error_class
error_name
=
env
.
body
[
'type'
].
gsub
(
'urn:acme:error:'
,
''
).
classify
if
Acme
::
Client
::
Error
.
qualified_const_defined?
(
error_name
)
"Acme::Client::Error::
#{
error_name
}
"
.
constantize
if
error_name
&&
!
error_name
.
empty?
&&
Acme
::
Client
::
Error
.
const_defined?
(
error_name
)
Object
.
const_get
(
"Acme::Client::Error::
#{
error_name
}
"
)
else
Acme
::
Client
::
Error
end
end
def
error_name
@error_name
||=
begin
return
unless
env
.
body
.
is_a?
(
Hash
)
return
unless
env
.
body
.
key?
(
'type'
)
error_type_to_klass
env
.
body
[
'type'
]
end
end
def
error_type_to_klass
(
type
)
type
.
gsub
(
'urn:acme:error:'
,
''
).
split
(
/[_-]/
).
map
{
|
type_part
|
type_part
[
0
].
upcase
+
type_part
[
1
..-
1
]
}.
join
end
def
decode_body
content_type
=
env
.
response_headers
[
'Content-Type'
]
...
...
@@ -92,19 +109,11 @@ class Acme::Client::FaradayMiddleware < Faraday::Middleware
end
def
get_nonce
response
=
Faraday
.
head
(
env
.
url
)
response
=
Faraday
.
head
(
env
.
url
,
nil
,
'User-Agent'
=>
USER_AGENT
)
response
.
headers
[
'replay-nonce'
]
end
def
nonces
client
.
nonces
end
def
private_key
client
.
private_key
end
def
crypto
@crypto
||=
Acme
::
Client
::
Crypto
.
new
(
private_key
)
end
end
common/lib/acme/client/jwk.rb
0 → 100644
View file @
ab2048b7
module
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'
common/lib/acme/client/jwk/base.rb
0 → 100644
View file @
ab2048b7
class
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
.
merge
(
header
)
encoded_header
=
Acme
::
Client
::
Util
.
urlsafe_base64
(
header
.
to_json
)
encoded_payload
=
Acme
::
Client
::
Util
.
urlsafe_base64
(
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
{
typ:
'JWT'
,
alg:
jwa_alg
,
jwk:
to_h
}
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
common/lib/acme/client/jwk/ecdsa.rb
0 → 100644
View file @
ab2048b7
class
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
# rubocop:disable Metrics/AbcSize
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
# rubocop:enable Metrics/AbcSize
def
public_key
@private_key
.
public_key
end
end
common/lib/acme/client/jwk/rsa.rb
0 → 100644
View file @
ab2048b7
class
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
common/lib/acme/client/resources/authorization.rb
View file @
ab2048b7
...
...
@@ -3,30 +3,42 @@ class Acme::Client::Resources::Authorization
DNS01
=
Acme
::
Client
::
Resources
::
Challenges
::
DNS01
TLSSNI01
=
Acme
::
Client
::
Resources
::
Challenges
::
TLSSNI01
attr_reader
:domain
,
:status
,
:expires
,
:http01
,
:dns01
,
:tls_sni01
attr_reader
:client
,
:uri
,
:domain
,
:status
,
:expires
,
:http01
,
:dns01
,
:tls_sni01
def
initialize
(
client
,
response
)
def
initialize
(
client
,
uri
,
response
)
@client
=
client
assign_challenges
(
response
.
body
[
'challenges'
])
@uri
=
uri
assign_attributes
(
response
.
body
)
end
private
def
verify_status
response
=
@client
.
connection
.
get
(
@uri
)
def
assign_challenges
(
challenges
)
challenges
.
each
do
|
attributes
|
case
attributes
.
fetch
(
'type'
)
when
'http-01'
then
@http01
=
HTTP01
.
new
(
@client
,
attributes
)
when
'dns-01'
then
@dns01
=
DNS01
.
new
(
@client
,
attributes
)
when
'tls-sni-01'
then
@tls_sni01
=
TLSSNI01
.
new
(
@client
,
attributes
)
# else no-op
end
end
assign_attributes
(
response
.
body
)
status
end
private
def
assign_attributes
(
body
)
@expires
=
Time
.
iso8601
(
body
[
'expires'
])
if
body
.
key?
'expires'
@domain
=
body
[
'identifier'
][
'value'
]
@status
=
body
[
'status'
]
assign_challenges
(
body
[
'challenges'
])
end
def
assign_challenges
(
challenges
)
challenges
.
each
do
|
attributes
|
challenge
=
case
attributes
.
fetch
(
'type'
)
when
'http-01'
@http01
||=
HTTP01
.
new
(
self
)
when
'dns-01'
@dns01
||=
DNS01
.
new
(
self
)
when
'tls-sni-01'
@tls_sni01
||=
TLSSNI01
.
new
(
self
)
end
challenge
.
assign_attributes
(
attributes
)
if
challenge
end