Full Code of bradfitz/autocertdelegate for AI

master 0efddd2691dd cached
6 files
15.8 KB
4.7k tokens
21 symbols
1 requests
Download .txt
Repository: bradfitz/autocertdelegate
Branch: master
Commit: 0efddd2691dd
Files: 6
Total size: 15.8 KB

Directory structure:
gitextract__klnu8f7/

├── LICENSE
├── README.md
├── autocertdelegate.go
├── autocertdelegate_test.go
├── go.mod
└── go.sum

================================================
FILE CONTENTS
================================================

================================================
FILE: LICENSE
================================================
Copyright (c) 2019 The Go Authors (https://golang.org/AUTHORS).
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

   * Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
   * Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
   * Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


================================================
FILE: README.md
================================================
# autocertdelegate

## What

[I wanted](https://twitter.com/bradfitz/status/1206058552357355520)
internal HTTPS servers to have valid TLS certs with minimal fuss.

In particular:

* I didn't want to deal with being my own CA or configuring all my
  devices to trust a new root.
* I didn't want to use LetsEncrypt DNS challenges because there are
  tons of DNS providers and I don't want API clients for tons of DNS
  providers and I don't want to configure secrets (or anything)
  anywhere.
* I don't want to expose my internal services to the internet or deal
  with updating firewall rules to only allow LetsEncrypt.

## How

See https://godoc.org/github.com/bradfitz/autocertdelegate

It provides a client that plugs in to an http.Server to get certs & a
server handler for a public-facing server that does the LetsEncrypt
ALPN challenges. You then do split-horizon DNS to give out internal
IPs to internal clients and a public IP (of the delegate server) to
everybody else (namely LetsEncrypt doing the ALPN challenges).

Then internal clients just ask the delegate server for the certs, and
the delegate server does a little challenge itself to test the
internal clients.

## Is it secure?

I built this for my own use on my home network.
Maybe you'll find it useful, but maybe you'll find it insecure.
Beauty is in the eye of the downloader.

## Contributing

I'm releasing as a Go project under the Go AUTHORs/LICENSEs, as it's
related to golang.org/x/crypto/acme/autocert. As such, I'm not
accepting any PRs unless you've contributed to Go or otherwise done
the Google CLA.


================================================
FILE: autocertdelegate.go
================================================
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package autocertdelegate provides a mechanism to provision LetsEncrypt certs
// for internal LAN TLS servers (that aren't reachable publicly) via a delegated
// server that is.
//
// See also https://github.com/bradfitz/autocertdelegate.
package autocertdelegate

import (
	"bytes"
	"context"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"crypto/tls"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"golang.org/x/crypto/acme"
	"golang.org/x/crypto/acme/autocert"
)

// Server is an http.Handler that runs on the Internet-facing daemon
// and gets the TLS certs from LetsEncrypt (using ALPN challenges) and
// gives them out to internal clients.
//
// It will only give them out to internal clients whose DNS names
// resolve to internal IP addresses and who can provide that they are
// running code on that IP address. (This assumes that such hostnames
// aren't multi-user systems with untrusted users.)
type Server struct {
	am  *autocert.Manager
	key []byte
}

// NewServer returns a new server given an autocert.Manager
// configuration.
func NewServer(am *autocert.Manager) *Server {
	key := make([]byte, 64)
	if _, err := rand.Read(key); err != nil {
		panic(err)
	}
	return &Server{
		am:  am,
		key: key,
	}
}

// validDelegateServerName reports whether n is a valid name that we
// can be a delegate cert fetcher for. It must be a bare DNS name (no
// port, not an IP address).
func validDelegateServerName(n string) bool {
	if n == "" {
		return false
	}
	if !strings.Contains(n, ".") {
		return false
	}
	if strings.Contains(n, ":") {
		// Contains port or is IPv6 literal.
		return false
	}
	if net.ParseIP(n) != nil {
		// No IPs.
		return false
	}
	if "x://"+n != (&url.URL{Scheme: "x", Host: n}).String() {
		// name must have contained invalid characters and caused escaping.
		return false
	}
	return true
}

// validChallengeAddr reports whether a is a valid IP address to serve
// a delegated cert to.
func validChallengeAddr(a string) bool {
	// TODO: flesh this out. parse a, make configurable, support
	// IPv6. Good enough for now.
	return strings.HasPrefix(a, "10.") || strings.HasPrefix(a, "192.168.")
}

// badServerName says that something's wrong with the servername
// parameter, without saying what, as this might be hit by the outside world.
func badServerName(w http.ResponseWriter) {
	http.Error(w, "missing or invalid servername", 403) // intentionally vague
}

func challengeAnswer(masterKey []byte, serverName string, t time.Time) string {
	hm := hmac.New(sha256.New, masterKey)
	fmt.Fprintf(hm, "%s-%d", serverName, t.Unix())
	return fmt.Sprintf("%x", hm.Sum(nil))
}

// ServeHTTP is the HTTP handler to get challenges & certs for the Client.
// The Handler only responds to GET requests over TLS. It can be installed
// at any path, but the client only makes requests to the root. It's assumed
// that any existing HTTP mux is routing based on the hostname.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.TLS == nil {
		http.Error(w, "TLS required", 403)
		return
	}
	if r.Method != "GET" {
		http.Error(w, "wrong method; want GET", 400)
		return
	}
	serverName := r.FormValue("servername")
	if !validDelegateServerName(serverName) {
		log.Printf("autocertdelegate: invalid server name %q", serverName)
		badServerName(w)
		return
	}
	if err := s.am.HostPolicy(r.Context(), serverName); err != nil {
		log.Printf("autocertdelegate: %q denied by configured HostPolicy: %v", serverName, err)
		badServerName(w)
		return
	}

	switch r.FormValue("mode") {
	default:
		http.Error(w, "unknown or missing mode argument", 400)
		return
	case "getchallenge":
		t := time.Now()
		fmt.Fprintf(w, "%s/%d/%s\n", serverName, t.Unix(), challengeAnswer(s.key, serverName, t))
		return
	case "getcert":
	}

	// Verify serverName resolves to a local IP.
	lookupCtx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
	defer cancel()
	var resolver net.Resolver
	resolver.PreferGo = true
	addrs, err := resolver.LookupHost(lookupCtx, serverName)
	if err != nil {
		log.Printf("autocertdelegate: lookup %q error: %v", serverName, err)
		badServerName(w)
		return
	}
	if len(addrs) != 1 {
		log.Printf("autocertDelegate: invalid server name %q; wrong number of resolved addrs. Want 1; got: %q", serverName, addrs)
		badServerName(w)
		return
	}
	challengeIP := addrs[0]
	if !validChallengeAddr(challengeIP) {
		log.Printf("autocertDelegate: server name %q resolved to invalid challenge IP %q", serverName, challengeIP)
		badServerName(w)
		return
	}

	challengePort, err := strconv.Atoi(r.FormValue("challengeport"))
	if err != nil || challengePort < 0 || challengePort > 64<<10 {
		http.Error(w, "invalid challengeport param", 400)
		return
	}
	challengeScheme := r.FormValue("challengescheme")
	switch challengeScheme {
	case "http", "https":
	case "":
		challengeScheme = "http"
	default:
		http.Error(w, "invalid challengescheme param", 400)
		return
	}
	challengeURL := fmt.Sprintf("%s://%s:%d/.well-known/autocertdelegate-challenge",
		challengeScheme, challengeIP, challengePort)

	if err := s.verifyChallengeURL(r.Context(), challengeURL, serverName); err != nil {
		log.Printf("autocertdelegate: failed challenge for %q: %v", serverName, err)
		badServerName(w)
		return
	}

	wantRSA, _ := strconv.ParseBool(r.FormValue("rsa"))

	var cipherSuites []uint16
	if !wantRSA {
		cipherSuites = append(cipherSuites, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256)
	}
	// Prime the cache:
	if _, err := s.am.GetCertificate(&tls.ClientHelloInfo{
		ServerName:   r.FormValue("servername"),
		CipherSuites: cipherSuites,
	}); err != nil {
		http.Error(w, err.Error(), 500)
		return
	}
	key := serverName
	if wantRSA {
		key += "+rsa"
	}
	// But what we really want is the on-disk PEM representation:
	pems, err := s.am.Cache.Get(r.Context(), key)
	if err != nil {
		http.Error(w, err.Error(), 500)
		return
	}

	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
	w.Write(pems)
}

func (s *Server) verifyChallengeURL(ctx context.Context, challengeURL, serverName string) error {
	ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()
	req, err := http.NewRequestWithContext(ctx, "GET", challengeURL, nil)
	if err != nil {
		log.Printf("autocertdelegate: verifyChallengeURL: new request: %v", err)
		return err
	}
	res, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Printf("autocertdelegate: fetch %v: %v", challengeURL, err)
		return err
	}
	defer res.Body.Close()
	slurp, err := ioutil.ReadAll(io.LimitReader(res.Body, 4<<10))
	if err != nil {
		return err
	}
	f := strings.SplitN(strings.TrimSpace(string(slurp)), "/", 3)
	if len(f) != 3 {
		return errors.New("wrong number of parts")
	}
	gotServerName, unixTimeStr, gotAnswer := f[0], f[1], f[2]
	if serverName != gotServerName {
		return errors.New("wrong server name")
	}
	unixTimeN, err := strconv.ParseInt(unixTimeStr, 10, 64)
	if err != nil {
		return err
	}
	ut := time.Unix(unixTimeN, 0)
	if ut.Before(time.Now().Add(-10 * time.Second)) {
		return errors.New("too old")
	}
	wantAnswer := challengeAnswer(s.key, serverName, ut)
	if wantAnswer != gotAnswer {
		return errors.New("wrong challenge answer")
	}
	return nil
}

// Client fetches certs from the Server.
// Its GetCertificate method is suitable for use by an HTTP server's
// TLSConfig.GetCertificate.
type Client struct {
	server string
	am     *autocert.Manager
}

// NewClient returns a new client fetching from the provided server hostname.
// The server must be a hostname only (without a scheme or path).
func NewClient(server string) *Client {
	c := &Client{
		server: server,
	}
	c.am = &autocert.Manager{
		Cache:      &delegateCache{c},
		Prompt:     autocert.AcceptTOS,
		HostPolicy: func(ctx context.Context, host string) error { return nil },
		Client: &acme.Client{
			HTTPClient: &http.Client{
				Transport: failTransport{},
			},
		},
	}
	return c
}

// GetCertificate fetches a certificate suitable for responding to the
// provided hello. The signature of GetCertificate is suitable for
// use by an HTTP server's TLSConfig.GetCertificate.
func (c *Client) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
	return c.am.GetCertificate(hello)
}

// TODO: configuration knobs as needed.
func (c *Client) httpClient() *http.Client      { return http.DefaultClient }
func (c *Client) getCertTimeout() time.Duration { return 10 * time.Second }

type delegateCache struct{ c *Client }

func (dc *delegateCache) Get(ctx context.Context, key string) ([]byte, error) {
	rsa := strings.HasSuffix(key, "+rsa")
	host := strings.TrimSuffix(key, "+rsa")

	ctx, cancel := context.WithTimeout(ctx, dc.c.getCertTimeout())
	defer cancel()
	req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://%s/?servername=%s&mode=getchallenge",
		dc.c.server, url.QueryEscape(host)), nil)
	if err != nil {
		return nil, err
	}
	res, err := dc.c.httpClient().Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to get challenge for %s: %v", host, err)
	}
	if res.StatusCode != 200 {
		res.Body.Close()
		return nil, fmt.Errorf("failed to get challenge for %s: %v", host, res.Status)
	}
	const maxChalLen = 1 << 10
	challenge, err := ioutil.ReadAll(io.LimitReader(res.Body, maxChalLen+1))
	res.Body.Close()
	if err != nil {
		return nil, fmt.Errorf("failed to read challenge for %s: %v", host, err)
	}
	if len(challenge) > maxChalLen || bytes.Count(challenge, []byte("\n")) > 1 {
		return nil, fmt.Errorf("challenge for %s doesn't look like a challenge", host)
	}

	ln, err := net.Listen("tcp", ":0")
	if err != nil {
		return nil, err
	}
	defer ln.Close()
	port := ln.Addr().(*net.TCPAddr).Port

	srv := &http.Server{
		Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			w.Write(challenge)
		}),
	}
	go srv.Serve(ln)

	req, err = http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("https://%s/?servername=%s&mode=getcert&rsa=%v&challengeport=%d&challengescheme=http",
		dc.c.server, url.QueryEscape(host), rsa, port),
		nil)
	if err != nil {
		return nil, err
	}
	res, err = dc.c.httpClient().Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to get cert for %s: %v", host, err)
	}
	if res.StatusCode != 200 {
		res.Body.Close()
		return nil, fmt.Errorf("failed to get cert for %s: %v", host, res.Status)
	}
	defer res.Body.Close()
	slurp, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
	return slurp, err
}

func (c *delegateCache) Put(ctx context.Context, key string, data []byte) error { return nil }

func (c *delegateCache) Delete(ctx context.Context, key string) error { return nil }

type failTransport struct{}

func (failTransport) RoundTrip(r *http.Request) (*http.Response, error) {
	log.Printf("Not doing ACME request: %s", r.URL.String())
	return nil, errors.New("network request denied")
}


================================================
FILE: autocertdelegate_test.go
================================================
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package autocertdelegate

import "testing"

func TestValidChallengeAddr(t *testing.T) {
	tests := []struct {
		name string
		want bool
	}{
		{"10.0.0.1", true},
		{"192.168.5.2", true},
		{"8.8.8.8", false},
		{"", false},
		{"::1", false}, // yet
	}
	for _, tt := range tests {
		got := validChallengeAddr(tt.name)
		if got != tt.want {
			t.Errorf("validChallengeAddr(%q) = %v; want %v", tt.name, got, tt.want)
		}
	}
}

func TestValidDelegateServerName(t *testing.T) {
	tests := []struct {
		name string
		want bool
	}{
		{"", false},
		{"foo", false},
		{"::1", false},
		{"foo.com:123", false},
		{"8.8.8.8", false},
		{"cams.int.example.net", true},
		{"cams.int.example.net/foo", false},
	}
	for _, tt := range tests {
		got := validDelegateServerName(tt.name)
		if got != tt.want {
			t.Errorf("validDelegateServerName(%q) = %v; want %v", tt.name, got, tt.want)
		}
	}
}


================================================
FILE: go.mod
================================================
module github.com/bradfitz/autocertdelegate

go 1.13

require golang.org/x/crypto v0.0.0-20191219195013-becbf705a915


================================================
FILE: go.sum
================================================
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915 h1:aJ0ex187qoXrJHPo8ZasVTASQB7llQP6YeNzgDALPRk=
golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
Download .txt
gitextract__klnu8f7/

├── LICENSE
├── README.md
├── autocertdelegate.go
├── autocertdelegate_test.go
├── go.mod
└── go.sum
Download .txt
SYMBOL INDEX (21 symbols across 2 files)

FILE: autocertdelegate.go
  type Server (line 43) | type Server struct
    method ServeHTTP (line 110) | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    method verifyChallengeURL (line 217) | func (s *Server) verifyChallengeURL(ctx context.Context, challengeURL,...
  function NewServer (line 50) | func NewServer(am *autocert.Manager) *Server {
  function validDelegateServerName (line 64) | func validDelegateServerName(n string) bool {
  function validChallengeAddr (line 88) | func validChallengeAddr(a string) bool {
  function badServerName (line 96) | func badServerName(w http.ResponseWriter) {
  function challengeAnswer (line 100) | func challengeAnswer(masterKey []byte, serverName string, t time.Time) s...
  type Client (line 261) | type Client struct
    method GetCertificate (line 288) | func (c *Client) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Cert...
    method httpClient (line 293) | func (c *Client) httpClient() *http.Client      { return http.DefaultC...
    method getCertTimeout (line 294) | func (c *Client) getCertTimeout() time.Duration { return 10 * time.Sec...
  function NewClient (line 268) | func NewClient(server string) *Client {
  type delegateCache (line 296) | type delegateCache struct
    method Get (line 298) | func (dc *delegateCache) Get(ctx context.Context, key string) ([]byte,...
    method Put (line 360) | func (c *delegateCache) Put(ctx context.Context, key string, data []by...
    method Delete (line 362) | func (c *delegateCache) Delete(ctx context.Context, key string) error ...
  type failTransport (line 364) | type failTransport struct
    method RoundTrip (line 366) | func (failTransport) RoundTrip(r *http.Request) (*http.Response, error) {

FILE: autocertdelegate_test.go
  function TestValidChallengeAddr (line 9) | func TestValidChallengeAddr(t *testing.T) {
  function TestValidDelegateServerName (line 28) | func TestValidDelegateServerName(t *testing.T) {
Condensed preview — 6 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (18K chars).
[
  {
    "path": "LICENSE",
    "chars": 1508,
    "preview": "Copyright (c) 2019 The Go Authors (https://golang.org/AUTHORS).\nAll rights reserved.\n\nRedistribution and use in source a"
  },
  {
    "path": "README.md",
    "chars": 1582,
    "preview": "# autocertdelegate\n\n## What\n\n[I wanted](https://twitter.com/bradfitz/status/1206058552357355520)\ninternal HTTPS servers "
  },
  {
    "path": "autocertdelegate.go",
    "chars": 11002,
    "preview": "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license "
  },
  {
    "path": "autocertdelegate_test.go",
    "chars": 1039,
    "preview": "// Copyright 2019 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license "
  },
  {
    "path": "go.mod",
    "chars": 117,
    "preview": "module github.com/bradfitz/autocertdelegate\n\ngo 1.13\n\nrequire golang.org/x/crypto v0.0.0-20191219195013-becbf705a915\n"
  },
  {
    "path": "go.sum",
    "chars": 897,
    "preview": "golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org"
  }
]

About this extraction

This page contains the full source code of the bradfitz/autocertdelegate GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 6 files (15.8 KB), approximately 4.7k tokens, and a symbol index with 21 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!