From d6bc349e734cb1fa46bf69b4d5d1be13a139ee3c Mon Sep 17 00:00:00 2001 From: m5r Date: Sun, 26 Feb 2023 11:02:30 +0100 Subject: [PATCH] first iteration to automate certificate generation --- .dockerignore | 5 + .gitignore | 1 + .../admin@local-ip.sh/account.json | 12 ++ Dockerfile | 1 + certs/account.go | 114 ++++++++++++++++++ certs/certs.go | 43 +++++++ certs/config.go | 18 +++ certs/provider.go | 29 +++++ go.mod | 5 + go.sum | 19 +++ main.go | 14 ++- xip/xip.go | 35 +++++- 12 files changed, 291 insertions(+), 5 deletions(-) create mode 100644 .dockerignore create mode 100644 .lego/accounts/acme-staging-v02.api.letsencrypt.org/admin@local-ip.sh/account.json create mode 100644 certs/account.go create mode 100644 certs/certs.go create mode 100644 certs/config.go create mode 100644 certs/provider.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..04dcbec --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +ddd +old +loadtest +.lego.new +.lego.bak \ No newline at end of file diff --git a/.gitignore b/.gitignore index 18c5545..bc2fcbf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .lego/accounts/acme-v02.api.letsencrypt.org/*/keys +.lego/accounts/acme-staging-v02.api.letsencrypt.org/*/keys .lego/certificates \ No newline at end of file diff --git a/.lego/accounts/acme-staging-v02.api.letsencrypt.org/admin@local-ip.sh/account.json b/.lego/accounts/acme-staging-v02.api.letsencrypt.org/admin@local-ip.sh/account.json new file mode 100644 index 0000000..5b55562 --- /dev/null +++ b/.lego/accounts/acme-staging-v02.api.letsencrypt.org/admin@local-ip.sh/account.json @@ -0,0 +1,12 @@ +{ + "Email": "admin@local-ip.sh", + "Registration": { + "body": { + "status": "valid", + "contact": [ + "mailto:admin@local-ip.sh" + ] + }, + "uri": "https://acme-staging-v02.api.letsencrypt.org/acme/acct/90246504" + } +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index a034c3a..0a7f76d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,7 @@ ENV PORT 53 WORKDIR / COPY --from=build /app/local-ip / +COPY ./.lego /.lego EXPOSE $PORT USER root diff --git a/certs/account.go b/certs/account.go new file mode 100644 index 0000000..40af6c6 --- /dev/null +++ b/certs/account.go @@ -0,0 +1,114 @@ +package certs + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "encoding/json" + "encoding/pem" + "log" + "os" + "path/filepath" + "strings" + + "github.com/go-acme/lego/v4/lego" + "github.com/go-acme/lego/v4/registration" +) + +type Account struct { + Email string + Registration *registration.Resource + key *ecdsa.PrivateKey +} + +func (u *Account) GetEmail() string { + return u.Email +} +func (u *Account) GetRegistration() *registration.Resource { + return u.Registration +} +func (u *Account) GetPrivateKey() crypto.PrivateKey { + return u.key +} + +func LoadAccount() *Account { + jsonBytes, err := os.ReadFile(accountFilePath) + if err != nil { + if strings.Contains(err.Error(), "no such file or directory") { + RegisterAccount() + return LoadAccount() + } + log.Fatal(err) + } + account := &Account{} + err = json.Unmarshal(jsonBytes, account) + if err != nil { + log.Fatal(err) + } + + privKey, err := os.ReadFile(keyFilePath) + if err != nil { + log.Fatal(err) + } + privateKey := decode(string(privKey)) + + account.key = privateKey + return account +} + +func RegisterAccount() { + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + log.Fatal(err) + } + + account := &Account{ + Email: email, + key: privateKey, + } + config := lego.NewConfig(account) + config.CADirURL = caDirUrl + legoClient, err := lego.NewClient(config) + + reg, err := legoClient.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) + if reg.Body.Status != "valid" { + log.Fatalf("registration failed with status %s", reg.Body.Status) + } + log.Println(reg.Body.TermsOfServiceAgreed) + account.Registration = reg + + os.MkdirAll(filepath.Dir(keyFilePath), os.ModePerm) + privKey := encode(privateKey) + err = os.WriteFile(keyFilePath, []byte(privKey), 0o644) + if err != nil { + log.Fatal(err) + } + + jsonBytes, err := json.MarshalIndent(account, "", "\t") + if err != nil { + log.Fatal(err) + } + + os.MkdirAll(filepath.Dir(accountFilePath), os.ModePerm) + err = os.WriteFile(accountFilePath, jsonBytes, 0o600) + if err != nil { + log.Fatal(err) + } +} + +func encode(privateKey *ecdsa.PrivateKey) string { + x509Encoded, _ := x509.MarshalECPrivateKey(privateKey) + pemEncoded := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: x509Encoded}) + + return string(pemEncoded) +} + +func decode(pemEncoded string) *ecdsa.PrivateKey { + block, _ := pem.Decode([]byte(pemEncoded)) + x509Encoded := block.Bytes + privateKey, _ := x509.ParseECPrivateKey(x509Encoded) + + return privateKey +} diff --git a/certs/certs.go b/certs/certs.go new file mode 100644 index 0000000..446a426 --- /dev/null +++ b/certs/certs.go @@ -0,0 +1,43 @@ +package certs + +import ( + "fmt" + "log" + + "github.com/go-acme/lego/v4/certificate" + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/lego" + "local-ip.sh/xip" +) + +type certsClient struct { + legoClient *lego.Client +} + +func (c *certsClient) RequestCertificate() { + certificates, err := c.legoClient.Certificate.Obtain(certificate.ObtainRequest{ + Domains: []string{"*.local-ip.sh"}, + Bundle: true, + }) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("%#v\n", certificates) +} + +func NewCertsClient(xip *xip.Xip, user *Account) *certsClient { + config := lego.NewConfig(user) + config.CADirURL = caDirUrl + legoClient, err := lego.NewClient(config) + if err != nil { + log.Fatal(err) + } + + provider := newProviderLocalIp(xip) + legoClient.Challenge.SetDNS01Provider(provider, dns01.AddRecursiveNameservers([]string{"1.1.1.1:53", "8.8.8.8:53"})) + + return &certsClient{ + legoClient, + } +} diff --git a/certs/config.go b/certs/config.go new file mode 100644 index 0000000..2425334 --- /dev/null +++ b/certs/config.go @@ -0,0 +1,18 @@ +package certs + +import ( + "fmt" + "net/url" + + "github.com/go-acme/lego/v4/lego" +) + +const ( + email = "admin@local-ip.sh" + caDirUrl = lego.LEDirectoryStaging +) + +var parsedCaDirUrl, _ = url.Parse(caDirUrl) +var caDirHostname = parsedCaDirUrl.Hostname() +var accountFilePath = fmt.Sprintf("./.lego/accounts/%s/%s/account.json", caDirHostname, email) +var keyFilePath = fmt.Sprintf("./.lego/accounts/%s/%s/keys/%s.key", caDirHostname, email, email) diff --git a/certs/provider.go b/certs/provider.go new file mode 100644 index 0000000..b7a0796 --- /dev/null +++ b/certs/provider.go @@ -0,0 +1,29 @@ +package certs + +import ( + "github.com/go-acme/lego/v4/challenge/dns01" + + "local-ip.sh/xip" +) + +type DNSProviderLocalIp struct { + xip *xip.Xip +} + +func (d *DNSProviderLocalIp) Present(domain, token, keyAuth string) error { + fqdn, value := dns01.GetRecord(domain, keyAuth) + d.xip.SetTXTRecord(fqdn, value) + return nil +} + +func (d *DNSProviderLocalIp) CleanUp(domain, token, keyAuth string) error { + fqdn, _ := dns01.GetRecord(domain, keyAuth) + d.xip.UnsetTXTRecord(fqdn) + return nil +} + +func newProviderLocalIp(xip *xip.Xip) *DNSProviderLocalIp { + return &DNSProviderLocalIp{ + xip, + } +} diff --git a/go.mod b/go.mod index f7d0a75..3a023eb 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,14 @@ go 1.19 require github.com/miekg/dns v1.1.50 require ( + github.com/cenkalti/backoff/v4 v4.2.0 // indirect + github.com/go-acme/lego/v4 v4.10.1 // indirect + github.com/go-jose/go-jose/v3 v3.0.0 // indirect + golang.org/x/crypto v0.5.0 // indirect golang.org/x/mod v0.8.0 // indirect golang.org/x/net v0.7.0 // indirect golang.org/x/sys v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect golang.org/x/tools v0.6.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect ) diff --git a/go.sum b/go.sum index 9c59168..c241300 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,22 @@ +github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= +github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-acme/lego/v4 v4.10.1 h1:MiJvoBXNdmAwEK/SImyhwZ8ZL4IR0jtWDD1wST+N138= +github.com/go-acme/lego/v4 v4.10.1/go.mod h1:EMbf0Jmqwv94nJ5WL9qWnSXIBZnvsS9gNypansHGc6U= +github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= +github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= @@ -30,6 +44,8 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 h1:BonxutuHCTL0rBDnZlKjpGIQFTjyUVTexFOdWkB6Fg0= @@ -40,7 +56,10 @@ golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 144382a..34b89ad 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,7 @@ import ( "flag" "strings" - xip "local-ip.sh/xip" + "local-ip.sh/xip" ) const ( @@ -17,5 +17,17 @@ func main() { flag.Parse() n := xip.NewXip(zone, strings.Split(nameservers, ","), *port) + + // not functional yet + /* go func() { + account := certs.LoadAccount() + log.Println(account.Registration.Body.Contact) + ddd := certs.NewCertsClient(n, account) + + time.Sleep(5 * time.Second) + fmt.Println("requesting certs") + ddd.RequestCertificate() + }() */ + n.StartServer() } diff --git a/xip/xip.go b/xip/xip.go index 1762465..500483b 100644 --- a/xip/xip.go +++ b/xip/xip.go @@ -99,6 +99,34 @@ var ( } ) +func (xip *Xip) SetTXTRecord(fqdn string, value string) { + log.Printf("trying to set TXT record \"%s\" for fqdn \"%s\"", value, fqdn) + if fqdn != "_acme-challenge.local-ip.sh." { + log.Println("not allowed, abort") + return + } + + if records, ok := hardcodedRecords[fqdn]; ok { + records.TXT = &dns.TXT{ + Txt: []string{value}, + } + hardcodedRecords["_acme-challenge.local-ip.sh."] = records + } +} + +func (xip *Xip) UnsetTXTRecord(fqdn string) { + log.Printf("trying to unset TXT record for fqdn \"%s\"", fqdn) + if fqdn != "_acme-challenge.local-ip.sh." { + log.Println("not allowed, abort") + return + } + + if records, ok := hardcodedRecords[fqdn]; ok { + records.TXT = nil + hardcodedRecords["_acme-challenge.local-ip.sh."] = records + } +} + func (xip *Xip) fqdnToA(fqdn string) []*dns.A { if hardcodedRecords[strings.ToLower(fqdn)].A != nil { var records []*dns.A @@ -310,10 +338,9 @@ func (xip *Xip) handleQuery(message *dns.Msg) { log.Printf("class: %d\n", question.Qclass) log.Printf("type: %d\n", question.Qtype) - // if fly - /* if strings.HasPrefix(strings.ToLower(question.Name), "_acme-challenge.") { + if strings.HasPrefix(strings.ToLower(question.Name), "_acme-challenge.") { message.Authoritative = false - } */ + } switch question.Qtype { case dns.TypeA: @@ -372,7 +399,7 @@ func NewXip(zone string, nameservers []string, port int) (xip *Xip) { } xip.server = dns.Server{ - Addr: ":" + strconv.Itoa(port), + Addr: "0.0.0.0:" + strconv.Itoa(port), Net: "udp", }