Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
Menu
Open sidebar
Ian Eiloart
Sympl
Commits
fbaafda0
Commit
fbaafda0
authored
Apr 03, 2019
by
Paul Cammish
Browse files
Folded in ruby-acme-client from
https://github.com/BytemarkHosting/acme-client
parent
ba9c1815
Changes
19
Hide whitespace changes
Inline
Side-by-side
common/acme-client.zip
0 → 100644
View file @
fbaafda0
File added
common/debian/control
View file @
fbaafda0
...
...
@@ -10,7 +10,7 @@ XS-Ruby-Versions: all
Package: symbiosis-common
Architecture: all
XB-Ruby-Versions: ${ruby:Versions}
Depends: ruby | ruby-interpreter,
ruby-acme-client (>= 0.3.5),
ruby-password, ruby-diffy, ruby-erubis, ruby-mocha, ruby-webmock, ruby-test-unit, gnutls-bin, openssl, sudo, adduser, ssl-cert, ${misc:Depends}
Depends: ruby | ruby-interpreter, ruby-password, ruby-diffy, ruby-erubis, ruby-mocha, ruby-webmock, ruby-test-unit, gnutls-bin, openssl, sudo, adduser, ssl-cert, ${misc:Depends}
Replaces: symbiosis-firewall (<< 2011:1214), symbiosis-range, symbiosis-test, bytemark-vhost-range, bytemark-vhost-test, symbiosis-crack
Breaks: symbiosis-firewall (<< 2011:1214), symbiosis-email (<< 2012:0215)
Conflicts: symbiosis-range, symbiosis-test, symbiosis-crack, bytemark-vhost-range, bytemark-vhost-test, symbiosis-email (<< 2012:0215)
...
...
common/lib/acme-client.rb
0 → 100644
View file @
fbaafda0
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
0 → 100644
View file @
fbaafda0
require
'acme-client'
class
Acme::Client
DEFAULT_ENDPOINT
=
'http://127.0.0.1:4000'
.
freeze
DIRECTORY_DEFAULT
=
{
'new-authz'
=>
'/acme/new-authz'
,
'new-cert'
=>
'/acme/new-cert'
,
'new-reg'
=>
'/acme/new-reg'
,
'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
@nonces
||=
[]
load_directory!
end
attr_reader
:private_key
,
:nonces
,
:operation_endpoints
def
register
(
contact
:)
payload
=
{
resource:
'new-reg'
,
contact:
Array
(
contact
)
}
response
=
connection
.
post
(
@operation_endpoints
.
fetch
(
'new-reg'
),
payload
)
::
Acme
::
Client
::
Resources
::
Registration
.
new
(
self
,
response
)
end
def
authorize
(
domain
:)
payload
=
{
resource:
'new-authz'
,
identifier:
{
type:
'dns'
,
value:
domain
}
}
response
=
connection
.
post
(
@operation_endpoints
.
fetch
(
'new-authz'
),
payload
)
::
Acme
::
Client
::
Resources
::
Authorization
.
new
(
self
,
response
)
end
def
new_certificate
(
csr
)
payload
=
{
resource:
'new-cert'
,
csr:
Base64
.
urlsafe_encode64
(
csr
.
to_der
)
}
response
=
connection
.
post
(
@operation_endpoints
.
fetch
(
'new-cert'
),
payload
)
::
Acme
::
Client
::
Certificate
.
new
(
OpenSSL
::
X509
::
Certificate
.
new
(
response
.
body
),
fetch_chain
(
response
),
csr
)
end
def
revoke_certificate
(
certificate
)
payload
=
{
resource:
'revoke-cert'
,
certificate:
Base64
.
urlsafe_encode64
(
certificate
.
to_der
)
}
endpoint
=
@operation_endpoints
.
fetch
(
'revoke-cert'
)
response
=
connection
.
post
(
endpoint
,
payload
)
response
.
success?
end
def
self
.
revoke_certificate
(
certificate
,
*
arguments
)
client
=
new
(
*
arguments
)
client
.
revoke_certificate
(
certificate
)
end
def
connection
@connection
||=
Faraday
.
new
(
@endpoint
)
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
)
links
=
response
.
headers
[
'link'
]
if
limit
.
zero?
||
links
.
nil?
||
links
[
'up'
].
nil?
[]
else
issuer
=
connection
.
get
(
links
[
'up'
])
[
OpenSSL
::
X509
::
Certificate
.
new
(
issuer
.
body
),
*
fetch_chain
(
issuer
,
limit
-
1
)]
end
end
def
load_directory!
@operation_endpoints
=
if
@directory_uri
response
=
connection
.
get
(
@directory_uri
)
body
=
response
.
body
{
'new-reg'
=>
body
.
fetch
(
'new-reg'
),
'new-authz'
=>
body
.
fetch
(
'new-authz'
),
'new-cert'
=>
body
.
fetch
(
'new-cert'
),
'revoke-cert'
=>
body
.
fetch
(
'revoke-cert'
),
}
else
DIRECTORY_DEFAULT
end
end
end
common/lib/acme/client/certificate.rb
0 → 100644
View file @
fbaafda0
class
Acme::Client::Certificate
extend
Forwardable
attr_reader
:x509
,
:x509_chain
,
:request
,
:private_key
def_delegators
:x509
,
:to_pem
,
:to_der
def
initialize
(
certificate
,
chain
,
request
)
@x509
=
certificate
@x509_chain
=
chain
@request
=
request
end
def
chain_to_pem
x509_chain
.
map
(
&
:to_pem
).
join
end
def
x509_fullchain
[
x509
,
*
x509_chain
]
end
def
fullchain_to_pem
x509_fullchain
.
map
(
&
:to_pem
).
join
end
def
common_name
x509
.
subject
.
to_a
.
find
{
|
name
,
_
,
_
|
name
==
'CN'
}[
1
]
end
end
common/lib/acme/client/certificate_request.rb
0 → 100644
View file @
fbaafda0
class
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
|
csr
.
public_key
=
@private_key
.
public_key
csr
.
subject
=
generate_subject
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
)
return
if
@names
.
size
<=
1
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
common/lib/acme/client/crypto.rb
0 → 100644
View file @
fbaafda0
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
0 → 100644
View file @
fbaafda0
class
Acme::Client::Error
<
StandardError
class
NotFound
<
Acme
::
Client
::
Error
;
end
class
BadCSR
<
Acme
::
Client
::
Error
;
end
class
BadNonce
<
Acme
::
Client
::
Error
;
end
class
Connection
<
Acme
::
Client
::
Error
;
end
class
Dnssec
<
Acme
::
Client
::
Error
;
end
class
Malformed
<
Acme
::
Client
::
Error
;
end
class
ServerInternal
<
Acme
::
Client
::
Error
;
end
class
Acme::Tls
<
Acme
::
Client
::
Error
;
end
class
Unauthorized
<
Acme
::
Client
::
Error
;
end
class
UnknownHost
<
Acme
::
Client
::
Error
;
end
end
common/lib/acme/client/faraday_middleware.rb
0 → 100644
View file @
fbaafda0
class
Acme::Client::FaradayMiddleware
<
Faraday
::
Middleware
attr_reader
:env
,
:response
,
:client
def
initialize
(
app
,
client
:)
super
(
app
)
@client
=
client
end
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
@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
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
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
else
Acme
::
Client
::
Error
end
end
def
decode_body
content_type
=
env
.
response_headers
[
'Content-Type'
]
if
content_type
==
'application/json'
||
content_type
==
'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
nonces
<<
env
.
response_headers
[
'replay-nonce'
]
end
def
pop_nonce
if
nonces
.
empty?
get_nonce
else
nonces
.
pop
end
end
def
get_nonce
response
=
Faraday
.
head
(
env
.
url
)
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/resources.rb
0 → 100644
View file @
fbaafda0
module
Acme::Client::Resources
;
end
require
'acme/client/resources/registration'
require
'acme/client/resources/challenges'
require
'acme/client/resources/authorization'
common/lib/acme/client/resources/authorization.rb
0 → 100644
View file @
fbaafda0
class
Acme::Client::Resources::Authorization
HTTP01
=
Acme
::
Client
::
Resources
::
Challenges
::
HTTP01
DNS01
=
Acme
::
Client
::
Resources
::
Challenges
::
DNS01
TLSSNI01
=
Acme
::
Client
::
Resources
::
Challenges
::
TLSSNI01
attr_reader
:domain
,
:status
,
:expires
,
:http01
,
:dns01
,
:tls_sni01
def
initialize
(
client
,
response
)
@client
=
client
assign_challenges
(
response
.
body
[
'challenges'
])
assign_attributes
(
response
.
body
)
end
private
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
end
def
assign_attributes
(
body
)
@expires
=
Time
.
iso8601
(
body
[
'expires'
])
if
body
.
key?
'expires'
@domain
=
body
[
'identifier'
][
'value'
]
@status
=
body
[
'status'
]
end
end
common/lib/acme/client/resources/challenges.rb
0 → 100644
View file @
fbaafda0
module
Acme::Client::Resources::Challenges
;
end
require
'acme/client/resources/challenges/base'
require
'acme/client/resources/challenges/http01'
require
'acme/client/resources/challenges/dns01'
require
'acme/client/resources/challenges/tls_sni01'
common/lib/acme/client/resources/challenges/base.rb
0 → 100644
View file @
fbaafda0
class
Acme::Client::Resources::Challenges::Base
attr_reader
:client
,
:status
,
:uri
,
:token
,
:error
def
initialize
(
client
,
attributes
)
@client
=
client
assign_attributes
(
attributes
)
end
def
verify_status
response
=
@client
.
connection
.
get
(
@uri
)
assign_attributes
(
response
.
body
)
@error
=
response
.
body
[
'error'
]
status
end
def
request_verification
response
=
@client
.
connection
.
post
(
@uri
,
resource:
'challenge'
,
type:
challenge_type
,
keyAuthorization:
authorization_key
)
response
.
success?
end
def
to_h
{
'token'
=>
token
,
'uri'
=>
uri
,
'type'
=>
challenge_type
}
end
private
def
challenge_type
self
.
class
::
CHALLENGE_TYPE
end
def
authorization_key
"
#{
token
}
.
#{
crypto
.
thumbprint
}
"
end
def
assign_attributes
(
attributes
)
@status
=
attributes
.
fetch
(
'status'
,
'pending'
)
@uri
=
attributes
.
fetch
(
'uri'
)
@token
=
attributes
.
fetch
(
'token'
)
end
def
crypto
@crypto
||=
Acme
::
Client
::
Crypto
.
new
(
@client
.
private_key
)
end
end
common/lib/acme/client/resources/challenges/dns01.rb
0 → 100644
View file @
fbaafda0
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
def
record_name
RECORD_NAME
end
def
record_type
RECORD_TYPE
end
def
record_content
Base64
.
urlsafe_encode64
(
crypto
.
digest
.
digest
(
authorization_key
)).
sub
(
/[\s=]*\z/
,
''
)
end
end
common/lib/acme/client/resources/challenges/http01.rb
0 → 100644
View file @
fbaafda0
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
authorization_key
end
def
filename
".well-known/acme-challenge/
#{
token
}
"
end
end
common/lib/acme/client/resources/challenges/tls_sni01.rb
0 → 100644
View file @
fbaafda0
class
Acme::Client::Resources::Challenges::TLSSNI01
<
Acme
::
Client
::
Resources
::
Challenges
::
Base
CHALLENGE_TYPE
=
'tls-sni-01'
.
freeze
def
hostname
digest
=
crypto
.
digest
.
hexdigest
(
authorization_key
)
"
#{
digest
[
0
..
31
]
}
.
#{
digest
[
32
..
64
]
}
.acme.invalid"
end
def
certificate
self_sign_certificate
.
certificate
end
def
private_key
self_sign_certificate
.
private_key
end
private
def
self_sign_certificate
@self_sign_certificate
||=
Acme
::
Client
::
SelfSignCertificate
.
new
(
subject_alt_names:
[
hostname
])
end
end
common/lib/acme/client/resources/registration.rb
0 → 100644
View file @
fbaafda0
class
Acme::Client::Resources::Registration
attr_reader
:id
,
:key
,
:contact
,
:uri
,
:next_uri
,
:recover_uri
,
:term_of_service_uri
def
initialize
(
client
,
response
)
@client
=
client
@uri
=
response
.
headers
[
'location'
]
assign_links
(
response
.
headers
[
'Link'
])
assign_attributes
(
response
.
body
)