-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathletsencrypt_this.rb
152 lines (129 loc) · 5.12 KB
/
letsencrypt_this.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
#!/usr/bin/env ruby
require 'acme-client'
require 'openssl'
require 'uri'
require 'net/http'
# require 'byebug'
opts = Hash[ARGV.each_slice(2).to_a]
if opts['-h'] || !opts['-d']
puts 'options:'
puts "-d domain"
puts "-m mode (optional. test or live. default is live)"
puts "-e email (optional. default is info@<domain>)"
puts "-k key (optional. path to private key)"
puts "-c challenge (optional. path to stored challenge file)"
exit
end
endpoint = if ['test', 'staging'].include?(opts['-m'])
puts 'test/staging mode'
'https://acme-staging.api.letsencrypt.org/'
else
puts 'live mode'
'https://acme-v01.api.letsencrypt.org/'
end
domain = opts['-d']
raise 'specify domain via -d option' unless domain
email = opts['-e'] || "info@#{domain}"
base_dir = File.expand_path("./#{domain}")
FileUtils.mkdir_p(base_dir)
stored_challange = opts['-c'] || File.join(base_dir, './letsencrypt_challenge.json')
key_file = opts['-k'] || File.join(base_dir, './id_rsa')
priv_key = if File.exist?(key_file)
puts 'using existing private key'
OpenSSL::PKey::RSA.new(IO.read(File.expand_path(key_file)))
else
puts 'creating new private key'
OpenSSL::PKey::RSA.new(4096).tap do |key|
File.write(File.join(base_dir, './id_rsa'), key.to_s)
File.write(File.join(base_dir, './id_rsa.pub'), key.public_key.to_s)
end
end
########################################################################################################################
# Patch some shit
class Acme::Client::Crypto
def generate_signed_jws(header:, payload:)
jwt = JSON::JWT.new(payload || {})
jwt.header.merge!(header || {})
jwt.header[:jwk] = jwk
jws = jwt.sign(private_key, :RS256)
jws.to_json(syntax: :flattened)
end
end
########################################################################################################################
# Create client, register it if it hasn't been registered yet
client = Acme::Client.new(private_key: priv_key,
endpoint: endpoint,
connection_options: { request: { open_timeout: 10, timeout: 10 } })
begin
print 'Trying to register client.'
registration = client.register(contact: "mailto:#{email}")
registration.agree_terms
puts 'done.'
rescue Acme::Client::Error::Malformed
puts 'already registered.'
end
########################################################################################################################
# Prove that you're the owner of said domain(s)
authorization = client.authorize(domain: domain)
if stored_challange && File.exist?(stored_challange)
puts 'using existing challenge'
challenge = client.challenge_from_hash(JSON.parse(File.read(stored_challange)))
else
puts 'creating challenge'
challenge = authorization.http01
challenge_dir = File.join(base_dir, File.dirname(challenge.filename))
challenge_file = File.basename(challenge.filename)
FileUtils.mkdir_p(challenge_dir)
File.write(File.join(challenge_dir, challenge_file), challenge.file_content)
File.write((stored_challange || 'letsencrypt_challenge.json'), challenge.to_h.to_json)
puts "challenge created in: #{File.join(base_dir, challenge.filename)}"
puts
end
puts "challenge content is: #{challenge.file_content}"
puts "make it accessible at: #{domain}/#{challenge.filename}"
puts
print 'checking from local '
loop do
response = `curl -skL '#{domain}/#{challenge.filename}'` # TODO: exchange with pure ruby solution
result = response.strip == challenge.file_content.strip
if result
puts 'FOUND!'
break
end
print '.'
sleep 5
end
puts 'attempting letsencrypt challenge verification ...'
challenge.request_verification
print 'waiting for challenge verification '
loop do
if challenge.verify_status != 'pending'
puts 'VERIFIED!'
break
end
sleep 2
print '.'
end
raise "Challenge could not be verified: #{challenge.verify_status}." if challenge.verify_status != 'valid'
########################################################################################################################
# Create certificate files for your domain(s)
csr = Acme::Client::CertificateRequest.new(names: [domain])
certificate = client.new_certificate(csr)
def dhparam(opts = {})
opts[:bits] ||= 4096
if opts[:generate]
puts 'generating dhparam. this may take a while.'
OpenSSL::PKey::DH.new(opts[:bits]).to_s
else
puts 'using precomputed dhparam. has nobody got time to wait.'
Net::HTTP.get(URI("https://2ton.com.au/dhparam/#{opts[:bits]}/#{rand(0..127)}"))
end
end
puts 'writing pem files.'
# Save the certificate and the private key to files
File.write(File.join(base_dir, 'privkey.pem'), certificate.request.private_key.to_pem)
File.write(File.join(base_dir, 'cert.pem'), certificate.to_pem)
File.write(File.join(base_dir, 'chain.pem'), certificate.chain_to_pem)
File.write(File.join(base_dir, 'fullchain.pem'), certificate.fullchain_to_pem)
File.write(File.join(base_dir, 'dhparam.pem'), dhparam(bits: 4096, generate: false)) # TODO: make parameterized
puts 'all done!'