Skip to content

Commit

Permalink
Optionally sign generated certs with a mkcert root CA
Browse files Browse the repository at this point in the history
  • Loading branch information
ydnar committed Jun 29, 2021
1 parent f4c60b3 commit f6e49a2
Show file tree
Hide file tree
Showing 5 changed files with 220 additions and 4 deletions.
27 changes: 27 additions & 0 deletions LICENSE-mkcert
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Copyright (c) 2018 The mkcert 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.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

Generate deterministic [TLS certificates](https://golang.org/pkg/crypto/tls/) for local [Go](https://golang.org/) development servers. The certificates use a [P-256 ECDSA private key](https://csrc.nist.gov/csrc/media/events/workshop-on-elliptic-curve-cryptography-standards/documents/papers/session6-adalier-mehmet.pdf) generated with a total lack of randomness.

Optionally, the certificates generated by this package will be signed by your local [mkcert](https://github.com/FiloSottile/mkcert) root CA. See the [mkcert docs](https://github.com/FiloSottile/mkcert) for more information.

## Why?

So your browser can trust a single certificate from your development servers, and dev/test with TLS. **Do not use in production.**
Expand All @@ -12,6 +14,10 @@ So your browser can trust a single certificate from your development servers, an

`go get github.com/alta/insecure`

### Local CA

This package works with [mkcert](https://github.com/FiloSottile/mkcert) to generate certificates that are signed by your machine’s local certificate authority (CA). To use this feature, run `mkcert -install` on your development machine before generating a certificate.

## Usage

Get a TLS certificate suitable for `localhost`, `127.0.0.1`, etc:
Expand Down Expand Up @@ -39,3 +45,5 @@ Seriously, do not use this in production.
## Author

Originally developed by [@cee-dub](https://github.com/cee-dub) for Alta Software LLC.

This package includes functions adapted from [mkcert](https://github.com/FiloSottile/mkcert). Neither the authors of mkcert nor Google, Inc. have promoted or endorsed this project.
110 changes: 110 additions & 0 deletions ca.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package insecure

import (
"crypto"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
)

// This file borrows heavily from the mkcert project:
// https://github.com/FiloSottile/mkcert
//
// This package will attempt to sign generated certs with your local mkcert CA, if present.

const (
rootName = "rootCA.pem"
rootKeyName = "rootCA-key.pem"
)

// CA returns the mkcert CA certificate and key if found.
// Returns an error if either fail to load or parse.
func CA() (cert *x509.Certificate, key crypto.PrivateKey, err error) {
certPEMBlock, keyPEMBlock, err := CAPEM()
if err != nil {
return nil, nil, err
}

certDERBlock, _ := pem.Decode(certPEMBlock)
if certDERBlock == nil || certDERBlock.Type != "CERTIFICATE" {
return nil, nil, errors.New("failed to read the CA certificate: unexpected content")
}
cert, err = x509.ParseCertificate(certDERBlock.Bytes)
if err != nil {
return nil, nil, err
}

keyDERBlock, _ := pem.Decode(keyPEMBlock)
if keyDERBlock == nil || keyDERBlock.Type != "PRIVATE KEY" {
return nil, nil, errors.New("ERROR: failed to read the CA key: unexpected content")
}
key, err = x509.ParsePKCS8PrivateKey(keyDERBlock.Bytes)
if err != nil {
return nil, nil, err
}

return
}

// CAPEM returns the raw PEM mkcert CA certificate and key if found.
// Returns an error if either doesn’t exist or fails to load.
func CAPEM() (cert []byte, key []byte, err error) {
caRoot := getCARoot()

caPath := filepath.Join(caRoot, rootName)
if !pathExists(caPath) {
return nil, nil, fmt.Errorf("no CA certificate located at: %s", caPath)
}
cert, err = ioutil.ReadFile(caPath)
if err != nil {
return nil, nil, err
}

keyPath := filepath.Join(caRoot, rootKeyName)
if !pathExists(keyPath) {
return nil, nil, fmt.Errorf("no CA key located at: %s", keyPath)
}
key, err = ioutil.ReadFile(keyPath)
if err != nil {
return nil, nil, err
}

return
}

func pathExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}

func getCARoot() string {
if env := os.Getenv("CAROOT"); env != "" {
return env
}

var dir string
switch {
case runtime.GOOS == "windows":
dir = os.Getenv("LocalAppData")
case os.Getenv("XDG_DATA_HOME") != "":
dir = os.Getenv("XDG_DATA_HOME")
case runtime.GOOS == "darwin":
dir = os.Getenv("HOME")
if dir == "" {
return ""
}
dir = filepath.Join(dir, "Library", "Application Support")
default: // Unix
dir = os.Getenv("HOME")
if dir == "" {
return ""
}
dir = filepath.Join(dir, ".local", "share")
}
return filepath.Join(dir, "mkcert")
}
15 changes: 12 additions & 3 deletions cert.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package insecure

import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/sha256"
Expand Down Expand Up @@ -54,7 +55,7 @@ func PEM(sans ...string) (cert []byte, key []byte, err error) {

notBefore, notAfter := notBeforeOrAfter(time.Now())

template := x509.Certificate{
template := &x509.Certificate{
SerialNumber: big.NewInt(SerialNumber),
Subject: pkix.Name{
Organization: []string{Organization},
Expand Down Expand Up @@ -83,8 +84,16 @@ func PEM(sans ...string) (cert []byte, key []byte, err error) {

template.SerialNumber = big.NewInt(0).SetBytes(hash.Sum(nil))

// For deterministic output. Do NOT do this for any real server.
b, err := x509.CreateCertificate(zeroes{}, &template, &template, priv.Public(), priv)
// Get CA, if present.
parent := template
signKey := crypto.PrivateKey(priv)
caCert, caKey, err := CA()
if err == nil && caCert != nil && caKey != nil {
parent = caCert
signKey = caKey
}

b, err := x509.CreateCertificate(zeroes{}, template, parent, priv.Public(), signKey)
if err != nil {
return nil, nil, fmt.Errorf("failed to create certificate: %s", err)
}
Expand Down
64 changes: 63 additions & 1 deletion cert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,19 @@ import (
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"testing"
"time"
)

func TestPEM(t *testing.T) {
func TestUnsigned(t *testing.T) {
caRoot := os.Getenv("CAROOT")
os.Setenv("CAROOT", ".")
defer func() {
os.Setenv("CAROOT", caRoot)
}()

tests := []struct {
name string
sans []string
Expand Down Expand Up @@ -68,6 +76,60 @@ func TestPEM(t *testing.T) {
}
}

func TestSigned(t *testing.T) {
caCert, _, err := CA()
if err != nil {
cmd := exec.Command("mkcert", "-install")
err := cmd.Run()
if err != nil {
t.Fatal(err)
}
}

roots := x509.NewCertPool()
roots.AddCert(caCert)

tests := []struct {
name string
sans []string
wantNames []string
wantErr bool
}{
{"computer.local", []string{"computer.local"}, []string{"computer.local"}, false},
{"local SANs + computer.local", append(LocalSANs(), "computer.local"), append(LocalSANs(), "computer.local"), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
certPEM, _, err := PEM(tt.sans...)
if (err != nil) != tt.wantErr {
t.Errorf("PEM() error = %v, wantErr %v", err, tt.wantErr)
return
}

block, _ := pem.Decode(certPEM)
if block == nil {
t.Fatal("failed to parse certificate PEM")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("failed to parse certificate: " + err.Error())
}

// Verify certificate is valid for all expected names
for _, name := range tt.wantNames {
opts := x509.VerifyOptions{
DNSName: name,
Roots: roots,
}

if _, err := cert.Verify(opts); err != nil {
t.Errorf("failed to verify certificate: " + err.Error())
}
}
})
}
}

func TestServeCert(t *testing.T) {
// Configure server
cert, err := Cert()
Expand Down

0 comments on commit f6e49a2

Please sign in to comment.