[
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2019 The Go Authors (https://golang.org/AUTHORS).\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google Inc. nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.md",
    "content": "# autocertdelegate\n\n## What\n\n[I wanted](https://twitter.com/bradfitz/status/1206058552357355520)\ninternal HTTPS servers to have valid TLS certs with minimal fuss.\n\nIn particular:\n\n* I didn't want to deal with being my own CA or configuring all my\n  devices to trust a new root.\n* I didn't want to use LetsEncrypt DNS challenges because there are\n  tons of DNS providers and I don't want API clients for tons of DNS\n  providers and I don't want to configure secrets (or anything)\n  anywhere.\n* I don't want to expose my internal services to the internet or deal\n  with updating firewall rules to only allow LetsEncrypt.\n\n## How\n\nSee https://godoc.org/github.com/bradfitz/autocertdelegate\n\nIt provides a client that plugs in to an http.Server to get certs & a\nserver handler for a public-facing server that does the LetsEncrypt\nALPN challenges. You then do split-horizon DNS to give out internal\nIPs to internal clients and a public IP (of the delegate server) to\neverybody else (namely LetsEncrypt doing the ALPN challenges).\n\nThen internal clients just ask the delegate server for the certs, and\nthe delegate server does a little challenge itself to test the\ninternal clients.\n\n## Is it secure?\n\nI built this for my own use on my home network.\nMaybe you'll find it useful, but maybe you'll find it insecure.\nBeauty is in the eye of the downloader.\n\n## Contributing\n\nI'm releasing as a Go project under the Go AUTHORs/LICENSEs, as it's\nrelated to golang.org/x/crypto/acme/autocert. As such, I'm not\naccepting any PRs unless you've contributed to Go or otherwise done\nthe Google CLA.\n"
  },
  {
    "path": "autocertdelegate.go",
    "content": "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Package autocertdelegate provides a mechanism to provision LetsEncrypt certs\n// for internal LAN TLS servers (that aren't reachable publicly) via a delegated\n// server that is.\n//\n// See also https://github.com/bradfitz/autocertdelegate.\npackage autocertdelegate\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/hmac\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"golang.org/x/crypto/acme\"\n\t\"golang.org/x/crypto/acme/autocert\"\n)\n\n// Server is an http.Handler that runs on the Internet-facing daemon\n// and gets the TLS certs from LetsEncrypt (using ALPN challenges) and\n// gives them out to internal clients.\n//\n// It will only give them out to internal clients whose DNS names\n// resolve to internal IP addresses and who can provide that they are\n// running code on that IP address. (This assumes that such hostnames\n// aren't multi-user systems with untrusted users.)\ntype Server struct {\n\tam  *autocert.Manager\n\tkey []byte\n}\n\n// NewServer returns a new server given an autocert.Manager\n// configuration.\nfunc NewServer(am *autocert.Manager) *Server {\n\tkey := make([]byte, 64)\n\tif _, err := rand.Read(key); err != nil {\n\t\tpanic(err)\n\t}\n\treturn &Server{\n\t\tam:  am,\n\t\tkey: key,\n\t}\n}\n\n// validDelegateServerName reports whether n is a valid name that we\n// can be a delegate cert fetcher for. It must be a bare DNS name (no\n// port, not an IP address).\nfunc validDelegateServerName(n string) bool {\n\tif n == \"\" {\n\t\treturn false\n\t}\n\tif !strings.Contains(n, \".\") {\n\t\treturn false\n\t}\n\tif strings.Contains(n, \":\") {\n\t\t// Contains port or is IPv6 literal.\n\t\treturn false\n\t}\n\tif net.ParseIP(n) != nil {\n\t\t// No IPs.\n\t\treturn false\n\t}\n\tif \"x://\"+n != (&url.URL{Scheme: \"x\", Host: n}).String() {\n\t\t// name must have contained invalid characters and caused escaping.\n\t\treturn false\n\t}\n\treturn true\n}\n\n// validChallengeAddr reports whether a is a valid IP address to serve\n// a delegated cert to.\nfunc validChallengeAddr(a string) bool {\n\t// TODO: flesh this out. parse a, make configurable, support\n\t// IPv6. Good enough for now.\n\treturn strings.HasPrefix(a, \"10.\") || strings.HasPrefix(a, \"192.168.\")\n}\n\n// badServerName says that something's wrong with the servername\n// parameter, without saying what, as this might be hit by the outside world.\nfunc badServerName(w http.ResponseWriter) {\n\thttp.Error(w, \"missing or invalid servername\", 403) // intentionally vague\n}\n\nfunc challengeAnswer(masterKey []byte, serverName string, t time.Time) string {\n\thm := hmac.New(sha256.New, masterKey)\n\tfmt.Fprintf(hm, \"%s-%d\", serverName, t.Unix())\n\treturn fmt.Sprintf(\"%x\", hm.Sum(nil))\n}\n\n// ServeHTTP is the HTTP handler to get challenges & certs for the Client.\n// The Handler only responds to GET requests over TLS. It can be installed\n// at any path, but the client only makes requests to the root. It's assumed\n// that any existing HTTP mux is routing based on the hostname.\nfunc (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tif r.TLS == nil {\n\t\thttp.Error(w, \"TLS required\", 403)\n\t\treturn\n\t}\n\tif r.Method != \"GET\" {\n\t\thttp.Error(w, \"wrong method; want GET\", 400)\n\t\treturn\n\t}\n\tserverName := r.FormValue(\"servername\")\n\tif !validDelegateServerName(serverName) {\n\t\tlog.Printf(\"autocertdelegate: invalid server name %q\", serverName)\n\t\tbadServerName(w)\n\t\treturn\n\t}\n\tif err := s.am.HostPolicy(r.Context(), serverName); err != nil {\n\t\tlog.Printf(\"autocertdelegate: %q denied by configured HostPolicy: %v\", serverName, err)\n\t\tbadServerName(w)\n\t\treturn\n\t}\n\n\tswitch r.FormValue(\"mode\") {\n\tdefault:\n\t\thttp.Error(w, \"unknown or missing mode argument\", 400)\n\t\treturn\n\tcase \"getchallenge\":\n\t\tt := time.Now()\n\t\tfmt.Fprintf(w, \"%s/%d/%s\\n\", serverName, t.Unix(), challengeAnswer(s.key, serverName, t))\n\t\treturn\n\tcase \"getcert\":\n\t}\n\n\t// Verify serverName resolves to a local IP.\n\tlookupCtx, cancel := context.WithTimeout(r.Context(), 5*time.Second)\n\tdefer cancel()\n\tvar resolver net.Resolver\n\tresolver.PreferGo = true\n\taddrs, err := resolver.LookupHost(lookupCtx, serverName)\n\tif err != nil {\n\t\tlog.Printf(\"autocertdelegate: lookup %q error: %v\", serverName, err)\n\t\tbadServerName(w)\n\t\treturn\n\t}\n\tif len(addrs) != 1 {\n\t\tlog.Printf(\"autocertDelegate: invalid server name %q; wrong number of resolved addrs. Want 1; got: %q\", serverName, addrs)\n\t\tbadServerName(w)\n\t\treturn\n\t}\n\tchallengeIP := addrs[0]\n\tif !validChallengeAddr(challengeIP) {\n\t\tlog.Printf(\"autocertDelegate: server name %q resolved to invalid challenge IP %q\", serverName, challengeIP)\n\t\tbadServerName(w)\n\t\treturn\n\t}\n\n\tchallengePort, err := strconv.Atoi(r.FormValue(\"challengeport\"))\n\tif err != nil || challengePort < 0 || challengePort > 64<<10 {\n\t\thttp.Error(w, \"invalid challengeport param\", 400)\n\t\treturn\n\t}\n\tchallengeScheme := r.FormValue(\"challengescheme\")\n\tswitch challengeScheme {\n\tcase \"http\", \"https\":\n\tcase \"\":\n\t\tchallengeScheme = \"http\"\n\tdefault:\n\t\thttp.Error(w, \"invalid challengescheme param\", 400)\n\t\treturn\n\t}\n\tchallengeURL := fmt.Sprintf(\"%s://%s:%d/.well-known/autocertdelegate-challenge\",\n\t\tchallengeScheme, challengeIP, challengePort)\n\n\tif err := s.verifyChallengeURL(r.Context(), challengeURL, serverName); err != nil {\n\t\tlog.Printf(\"autocertdelegate: failed challenge for %q: %v\", serverName, err)\n\t\tbadServerName(w)\n\t\treturn\n\t}\n\n\twantRSA, _ := strconv.ParseBool(r.FormValue(\"rsa\"))\n\n\tvar cipherSuites []uint16\n\tif !wantRSA {\n\t\tcipherSuites = append(cipherSuites, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256)\n\t}\n\t// Prime the cache:\n\tif _, err := s.am.GetCertificate(&tls.ClientHelloInfo{\n\t\tServerName:   r.FormValue(\"servername\"),\n\t\tCipherSuites: cipherSuites,\n\t}); err != nil {\n\t\thttp.Error(w, err.Error(), 500)\n\t\treturn\n\t}\n\tkey := serverName\n\tif wantRSA {\n\t\tkey += \"+rsa\"\n\t}\n\t// But what we really want is the on-disk PEM representation:\n\tpems, err := s.am.Cache.Get(r.Context(), key)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), 500)\n\t\treturn\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"text/plain; charset=utf-8\")\n\tw.Write(pems)\n}\n\nfunc (s *Server) verifyChallengeURL(ctx context.Context, challengeURL, serverName string) error {\n\tctx, cancel := context.WithTimeout(ctx, 5*time.Second)\n\tdefer cancel()\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", challengeURL, nil)\n\tif err != nil {\n\t\tlog.Printf(\"autocertdelegate: verifyChallengeURL: new request: %v\", err)\n\t\treturn err\n\t}\n\tres, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\tlog.Printf(\"autocertdelegate: fetch %v: %v\", challengeURL, err)\n\t\treturn err\n\t}\n\tdefer res.Body.Close()\n\tslurp, err := ioutil.ReadAll(io.LimitReader(res.Body, 4<<10))\n\tif err != nil {\n\t\treturn err\n\t}\n\tf := strings.SplitN(strings.TrimSpace(string(slurp)), \"/\", 3)\n\tif len(f) != 3 {\n\t\treturn errors.New(\"wrong number of parts\")\n\t}\n\tgotServerName, unixTimeStr, gotAnswer := f[0], f[1], f[2]\n\tif serverName != gotServerName {\n\t\treturn errors.New(\"wrong server name\")\n\t}\n\tunixTimeN, err := strconv.ParseInt(unixTimeStr, 10, 64)\n\tif err != nil {\n\t\treturn err\n\t}\n\tut := time.Unix(unixTimeN, 0)\n\tif ut.Before(time.Now().Add(-10 * time.Second)) {\n\t\treturn errors.New(\"too old\")\n\t}\n\twantAnswer := challengeAnswer(s.key, serverName, ut)\n\tif wantAnswer != gotAnswer {\n\t\treturn errors.New(\"wrong challenge answer\")\n\t}\n\treturn nil\n}\n\n// Client fetches certs from the Server.\n// Its GetCertificate method is suitable for use by an HTTP server's\n// TLSConfig.GetCertificate.\ntype Client struct {\n\tserver string\n\tam     *autocert.Manager\n}\n\n// NewClient returns a new client fetching from the provided server hostname.\n// The server must be a hostname only (without a scheme or path).\nfunc NewClient(server string) *Client {\n\tc := &Client{\n\t\tserver: server,\n\t}\n\tc.am = &autocert.Manager{\n\t\tCache:      &delegateCache{c},\n\t\tPrompt:     autocert.AcceptTOS,\n\t\tHostPolicy: func(ctx context.Context, host string) error { return nil },\n\t\tClient: &acme.Client{\n\t\t\tHTTPClient: &http.Client{\n\t\t\t\tTransport: failTransport{},\n\t\t\t},\n\t\t},\n\t}\n\treturn c\n}\n\n// GetCertificate fetches a certificate suitable for responding to the\n// provided hello. The signature of GetCertificate is suitable for\n// use by an HTTP server's TLSConfig.GetCertificate.\nfunc (c *Client) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {\n\treturn c.am.GetCertificate(hello)\n}\n\n// TODO: configuration knobs as needed.\nfunc (c *Client) httpClient() *http.Client      { return http.DefaultClient }\nfunc (c *Client) getCertTimeout() time.Duration { return 10 * time.Second }\n\ntype delegateCache struct{ c *Client }\n\nfunc (dc *delegateCache) Get(ctx context.Context, key string) ([]byte, error) {\n\trsa := strings.HasSuffix(key, \"+rsa\")\n\thost := strings.TrimSuffix(key, \"+rsa\")\n\n\tctx, cancel := context.WithTimeout(ctx, dc.c.getCertTimeout())\n\tdefer cancel()\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", fmt.Sprintf(\"https://%s/?servername=%s&mode=getchallenge\",\n\t\tdc.c.server, url.QueryEscape(host)), nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tres, err := dc.c.httpClient().Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get challenge for %s: %v\", host, err)\n\t}\n\tif res.StatusCode != 200 {\n\t\tres.Body.Close()\n\t\treturn nil, fmt.Errorf(\"failed to get challenge for %s: %v\", host, res.Status)\n\t}\n\tconst maxChalLen = 1 << 10\n\tchallenge, err := ioutil.ReadAll(io.LimitReader(res.Body, maxChalLen+1))\n\tres.Body.Close()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read challenge for %s: %v\", host, err)\n\t}\n\tif len(challenge) > maxChalLen || bytes.Count(challenge, []byte(\"\\n\")) > 1 {\n\t\treturn nil, fmt.Errorf(\"challenge for %s doesn't look like a challenge\", host)\n\t}\n\n\tln, err := net.Listen(\"tcp\", \":0\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer ln.Close()\n\tport := ln.Addr().(*net.TCPAddr).Port\n\n\tsrv := &http.Server{\n\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Write(challenge)\n\t\t}),\n\t}\n\tgo srv.Serve(ln)\n\n\treq, err = http.NewRequestWithContext(ctx, \"GET\", fmt.Sprintf(\"https://%s/?servername=%s&mode=getcert&rsa=%v&challengeport=%d&challengescheme=http\",\n\t\tdc.c.server, url.QueryEscape(host), rsa, port),\n\t\tnil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tres, err = dc.c.httpClient().Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get cert for %s: %v\", host, err)\n\t}\n\tif res.StatusCode != 200 {\n\t\tres.Body.Close()\n\t\treturn nil, fmt.Errorf(\"failed to get cert for %s: %v\", host, res.Status)\n\t}\n\tdefer res.Body.Close()\n\tslurp, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))\n\treturn slurp, err\n}\n\nfunc (c *delegateCache) Put(ctx context.Context, key string, data []byte) error { return nil }\n\nfunc (c *delegateCache) Delete(ctx context.Context, key string) error { return nil }\n\ntype failTransport struct{}\n\nfunc (failTransport) RoundTrip(r *http.Request) (*http.Response, error) {\n\tlog.Printf(\"Not doing ACME request: %s\", r.URL.String())\n\treturn nil, errors.New(\"network request denied\")\n}\n"
  },
  {
    "path": "autocertdelegate_test.go",
    "content": "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage autocertdelegate\n\nimport \"testing\"\n\nfunc TestValidChallengeAddr(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\twant bool\n\t}{\n\t\t{\"10.0.0.1\", true},\n\t\t{\"192.168.5.2\", true},\n\t\t{\"8.8.8.8\", false},\n\t\t{\"\", false},\n\t\t{\"::1\", false}, // yet\n\t}\n\tfor _, tt := range tests {\n\t\tgot := validChallengeAddr(tt.name)\n\t\tif got != tt.want {\n\t\t\tt.Errorf(\"validChallengeAddr(%q) = %v; want %v\", tt.name, got, tt.want)\n\t\t}\n\t}\n}\n\nfunc TestValidDelegateServerName(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\twant bool\n\t}{\n\t\t{\"\", false},\n\t\t{\"foo\", false},\n\t\t{\"::1\", false},\n\t\t{\"foo.com:123\", false},\n\t\t{\"8.8.8.8\", false},\n\t\t{\"cams.int.example.net\", true},\n\t\t{\"cams.int.example.net/foo\", false},\n\t}\n\tfor _, tt := range tests {\n\t\tgot := validDelegateServerName(tt.name)\n\t\tif got != tt.want {\n\t\t\tt.Errorf(\"validDelegateServerName(%q) = %v; want %v\", tt.name, got, tt.want)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/bradfitz/autocertdelegate\n\ngo 1.13\n\nrequire golang.org/x/crypto v0.0.0-20191219195013-becbf705a915\n"
  },
  {
    "path": "go.sum",
    "content": "golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191219195013-becbf705a915 h1:aJ0ex187qoXrJHPo8ZasVTASQB7llQP6YeNzgDALPRk=\ngolang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\n"
  }
]