-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathgo-acme-nsupdate.go
391 lines (344 loc) · 10.5 KB
/
go-acme-nsupdate.go
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
package main
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"flag"
"fmt"
"io/ioutil"
"log"
"math/big"
"os"
"os/exec"
"strings"
"time"
"github.com/patrickhaller/acme"
)
var (
domainList []string
contactsList string
accountFile string
nsKeyFile string
nameServer string
isDebug bool
isTesting bool
isWildcard bool
useRSA bool
useECDSA bool
rsaLength int
)
var directoryURL = acme.LetsEncryptProduction
var certFileFmt = "%s.pem"
var keyFileFmt = "%s.key"
var certFile = ""
var keyFile = ""
type acmeAccountFile struct {
PrivateKey *ecdsa.PrivateKey `json:"privateKey"`
URL string `json:"url"`
}
var usageFmt = `USAGE:
%s [OPTIONS] HOSTNAME [HOSTNAME ...]
for wildcard certs set HOSTNAME to the domainname
`
func parseCmdLineFlags() {
flag.IntVar(&rsaLength, "bits", 2048,
"the bit-length of the RSA private key")
flag.BoolVar(&useRSA, "rsa", false,
"use RSA")
flag.BoolVar(&useECDSA, "ecdsa", false,
"use ECDSA")
flag.BoolVar(&isWildcard, "wild", false,
"make a wildcard cert")
flag.BoolVar(&isDebug, "v", false,
"\nenable verbose output / debugging")
flag.BoolVar(&isTesting, "test", false,
"run against LetsEncrypt staging, not production servers")
flag.StringVar(&contactsList, "contact", "",
"comma separated contact emails to use for new accounts")
flag.StringVar(&accountFile, "accountfile", "account.json",
"file of account data -- will be auto-created if unset)")
flag.StringVar(&nameServer, "ns", "",
"Secondary DNS server to query for the status of the nsupdate")
flag.StringVar(&nsKeyFile, "nskey", "nsupdate.key",
"file for the nsupdate key")
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), usageFmt, os.Args[0])
flag.PrintDefaults()
}
flag.Parse()
if useRSA == false && useECDSA == false {
useECDSA = true
}
domainList = flag.Args()
if len(domainList) == 0 {
log.Fatal("No domains provided")
}
certFile = fmt.Sprintf(certFileFmt, domainList[0])
keyFile = fmt.Sprintf(keyFileFmt, domainList[0])
if isWildcard {
for i, domain := range domainList {
domainList[i] = fmt.Sprintf("*.%s", domain)
}
}
}
func main() {
parseCmdLineFlags()
log.SetFlags(0)
if isTesting {
directoryURL = acme.LetsEncryptStaging
}
client, err := acme.NewClient(directoryURL)
if err != nil {
log.Fatalf("Error connecting to acme directory(%s): %v", directoryURL, err)
}
var account acme.Account
if _, err := os.Stat(accountFile); err == nil {
if account, err = loadAccount(client); err != nil {
log.Fatalf("Error loading existing account: %v", err)
}
} else {
logD("Creating new account")
if account, err = createAccount(client); err != nil {
log.Fatalf("Error creating new account: %v", err)
}
}
logD("Account url: %s", account.URL)
var acmeIDs []acme.Identifier
for _, domain := range domainList {
acmeIDs = append(acmeIDs, acme.Identifier{Type: "dns", Value: domain})
}
order, err := client.NewOrder(account, acmeIDs)
if err != nil {
log.Fatalf("Error creating new order for domain `%v': %v", domainList, err)
}
logD("Order created: %s", order.URL)
for _, authURL := range order.Authorizations {
logD("Fetching authorization: %s", authURL)
auth, err := client.FetchAuthorization(account, authURL)
if err != nil {
log.Fatalf("Error fetching authorization url %q: %v", authURL, err)
}
logD("Fetched authorization: %s", auth.Identifier.Value)
chal, ok := auth.ChallengeMap[acme.ChallengeTypeDNS01]
if !ok {
log.Fatalf("Unable to find dns challenge for auth %s", auth.Identifier.Value)
}
nsDomain := auth.Identifier.Value
if strings.HasPrefix(auth.Identifier.Value, "*.") {
idx := strings.Index(auth.Identifier.Value, ".")
nsDomain = auth.Identifier.Value[idx+1:]
}
rr := fmt.Sprintf("_acme-challenge.%s.", nsDomain)
logD("Using nsupdate domain `%s'", nsDomain)
logD("Sending nsupdate request")
err = nsUpdate(rr, acme.EncodeDNS01KeyAuthorization(chal.KeyAuthorization), "add")
if err != nil {
log.Fatalf("Error nsupdating authorization %s challenge: %v", auth.Identifier.Value, err)
}
logD("Updating challenge")
chal, err = client.UpdateChallenge(account, chal)
if err != nil {
log.Fatalf("Error updating authorization %s challenge url `%s': %v", auth.Identifier.Value, chal.URL, err)
}
}
logD("Generating certificate private key")
var certKey interface{}
var tpl *x509.CertificateRequest
if useECDSA {
certKey, tpl = mkCertECDSA()
} else if useRSA {
certKey, tpl = mkCertECDSA()
} else {
log.Fatalf("No valid certificate algorithm")
}
csrDer, err := x509.CreateCertificateRequest(rand.Reader, tpl, certKey)
if err != nil {
log.Fatalf("Error creating certificate request: %v", err)
}
csr, err := x509.ParseCertificateRequest(csrDer)
if err != nil {
log.Fatalf("Error parsing certificate request: %v", err)
}
logD("Finalising order: %s", order.URL)
order, err = client.FinalizeOrder(account, order, csr)
if err != nil {
log.Fatalf("Error finalizing order: %v", err)
}
logD("Fetching certificate: %s", order.Certificate)
certs, err := client.FetchCertificates(account, order.Certificate)
if err != nil {
log.Fatalf("Error fetching order certificates: %v", err)
}
logD("Saving certificate to: %s", certFile)
var pemData []string
for _, c := range certs {
pemData = append(pemData, strings.TrimSpace(string(pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: c.Raw,
}))))
}
if err := ioutil.WriteFile(certFile, []byte(strings.Join(pemData, "\n")), 0600); err != nil {
log.Fatalf("Error writing certificate file %q: %v", certFile, err)
}
for _, domain := range domainList {
if err := nsUpdate(domain, "", "delete"); err != nil {
log.Fatalf("error deleting nsupdate record: `%v'", err)
}
}
logD("Done.")
}
func nsUpdate(rr string, challenge string, addDelete string) error {
var input string
if addDelete == "add" {
input = fmt.Sprintf("update add %s 1 TXT %s", rr, challenge)
} else {
input = fmt.Sprintf("update delete %s TXT", rr)
}
logD("Sending nsupdate: `%v'", input)
cmd := exec.Command("nsupdate", "-v", "-k", nsKeyFile)
cmd.Stdin = strings.NewReader(fmt.Sprintf("%s\nsend\n", input))
err := cmd.Run()
if err != nil || nameServer == "" {
return err
}
for {
cmd = exec.Command("dig", "TXT", rr, fmt.Sprintf("@%s", nameServer))
out, _ := cmd.Output()
logD("dig output: %s", out)
if strings.Contains(string(out), challenge) {
return nil
}
logD("waiting on nameserver `%s` to see the nsupdate", nameServer)
time.Sleep(1 * time.Second)
}
return fmt.Errorf("too many retries looking for nsupdates")
}
func logD(fmt string, args ...interface{}) {
if isDebug == true {
log.Printf(fmt, args...)
}
}
/* elliptic Curve cannot be unmarshal'ed, so we fake it */
type fakeCurve struct {
P, N, B, Gx, Gy *big.Int
BitSize int
Name string
}
type fakePrivateKey struct {
D, X, Y *big.Int
Curve *fakeCurve
}
type fakeAccountFile struct {
URL string `json:"url"`
PrivateKey fakePrivateKey `json:"privateKey"`
}
func loadAccount(client acme.Client) (acme.Account, error) {
if _, err := os.Stat(accountFile); err != nil {
return acme.Account{}, err
}
raw, err := ioutil.ReadFile(accountFile)
if err != nil {
return acme.Account{}, err
}
var pp bytes.Buffer
json.Indent(&pp, raw, " ", " ")
logD("accountFile contents =\n%s", pp.String())
var faf fakeAccountFile
if err := json.Unmarshal(raw, &faf); err != nil {
return acme.Account{}, fmt.Errorf("error reading account file: %v", err)
}
var apkey ecdsa.PrivateKey
apkey.D = faf.PrivateKey.D
apkey.X = faf.PrivateKey.X
apkey.Y = faf.PrivateKey.Y
apkey.Curve = elliptic.P256()
acct := acme.Account{PrivateKey: &apkey, URL: faf.URL}
account, err := client.UpdateAccount(acct, true, getContacts()...)
if err != nil {
return acme.Account{}, fmt.Errorf("error updating existing account: %v", err)
}
return account, nil
}
func createAccount(client acme.Client) (acme.Account, error) {
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return acme.Account{}, fmt.Errorf("error creating private key: %v", err)
}
account, err := client.NewAccount(privKey, false, true, getContacts()...)
if err != nil {
return acme.Account{}, fmt.Errorf("error creating new account: %v", err)
}
raw, err := json.Marshal(acmeAccountFile{PrivateKey: privKey, URL: account.URL})
if err != nil {
return acme.Account{}, fmt.Errorf("error parsing new account: %v", err)
}
if err := ioutil.WriteFile(accountFile, raw, 0600); err != nil {
return acme.Account{}, fmt.Errorf("error creating account file: %v", err)
}
return account, nil
}
func getContacts() []string {
var contacts []string
if contactsList != "" {
contacts = strings.Split(contactsList, ",")
for i := 0; i < len(contacts); i++ {
contacts[i] = "mailto:" + contacts[i]
}
}
return contacts
}
func mkCertECDSA() (*ecdsa.PrivateKey, *x509.CertificateRequest) {
certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
log.Fatalf("Error generating certificate key: %v", err)
}
certKeyEnc, err := x509.MarshalECPrivateKey(certKey)
if err != nil {
log.Fatalf("Error encoding certificate key file: %v", err)
}
logD("Writing key file: %s", keyFile)
if err := ioutil.WriteFile(keyFile, pem.EncodeToMemory(&pem.Block{
Type: "EC PRIVATE KEY",
Bytes: certKeyEnc,
}), 0600); err != nil {
log.Fatalf("Error writing key file %q: %v", keyFile, err)
}
logD("Creating csr")
tpl := &x509.CertificateRequest{
SignatureAlgorithm: x509.ECDSAWithSHA256,
PublicKeyAlgorithm: x509.ECDSA,
PublicKey: certKey.Public(),
Subject: pkix.Name{CommonName: domainList[0]},
DNSNames: domainList,
}
return certKey, tpl
}
func mkCertRSA() (*rsa.PrivateKey, *x509.CertificateRequest) {
certKey, err := rsa.GenerateKey(rand.Reader, rsaLength)
if err != nil {
log.Fatalf("Error generating certificate key: %v", err)
}
certKeyEnc := x509.MarshalPKCS1PrivateKey(certKey)
logD("Writing key file: %s", keyFile)
if err := ioutil.WriteFile(keyFile, pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: certKeyEnc,
}), 0600); err != nil {
log.Fatalf("Error writing key file %q: %v", keyFile, err)
}
logD("Creating csr")
tpl := &x509.CertificateRequest{
SignatureAlgorithm: x509.SHA256WithRSA,
PublicKeyAlgorithm: x509.RSA,
PublicKey: certKey.Public(),
Subject: pkix.Name{CommonName: domainList[0]},
DNSNames: domainList,
}
return certKey, tpl
}