Skip to content

Commit

Permalink
Initial working implementation. (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
jgiles authored May 8, 2018
1 parent 3e69441 commit 811f987
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
out/

# Binaries for programs and plugins
*.exe
*.dll
Expand Down
24 changes: 24 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
OS_TARGETS := linux darwin windows
NAME := gen-self-signed-cert
RELEASE_VERSION := $(shell cat release_version.txt)
BIN_DIRS := $(foreach os,$(OS_TARGETS),out/$(os))

.PHONY: all
all: $(BIN_DIRS)

out/linux: main.go
CGO_ENABLED=0 GOOS=linux go build -o out/linux/$(NAME)

out/darwin: main.go
CGO_ENABLED=0 GOOS=darwin go build -o out/darwin/$(NAME)

out/windows: main.go
CGO_ENABLED=0 GOOS=windows go build -o out/windows/$(NAME).exe

RELEASE_BASE := out/release/$(NAME)_v$(RELEASE_VERSION)
RELEASES := $(foreach os,$(OS_TARGETS),$(RELEASE_BASE)_$(os)_amd64.zip)
$(RELEASES): $(RELEASE_BASE)_%_amd64.zip : out/%
mkdir -p out/release && zip $@ out/$*/*

.PHONY: release
release: $(RELEASES)
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,36 @@
# gen-self-signed-cert
Dead-simple, portable generation of host self-signed x509 cert via temporary root CA
Dead-simple, portable generation of host self-signed x509 cert via temporary root CA.

## Purpose
This package provides simple, cross-platform generation of self-signed client and server TLS certs. It is intended for cases where it is infeasible to use a more complete internal CA.

## Installation

See the [Releases](https://github.com/paxos-bankchain/gen-self-signed-cert/releases) page. Download and extract the binary for your platform.

## Usage

Commands below are for OSX/Linux; for Windows use `gen-self-signed-cert.exe`.

1. Generate the CA and host certificate for your host (here, `myhost.example.com`):
- to create a plaintext key file
```bash
gen-self-signed-cert -host myhost.example.com
```
- to create a password-protected, AES-256 encrypted key file
```bash
gen-self-signed-cert -encrypt -host myhost.example.com
```
2. Send the `ca.crt` file to the system that needs to authenticate your host, and configure that system to trust your CA. For example:
- For a [HAProxy server performing client certificate authentication](http://www.loadbalancer.org/blog/client-certificate-authentication-with-haproxy/), this would be the `ca-file`.
- For a curl client authenticating a server, this would be the `--cacert` flag, as in:
```bash
curl --cacert ca.crt https://myhost.example.com
```
3. Configure your host to use the combination of `host.crt` and `host.key` to authenticate itself.
- For a curl client performing client certificate authentication, these would be the `--cert` and `--key` flags, as in:
```bash
curl --cert host.crt --key host.key https://some.server.com
```
- For a [HAProxy server terminating TLS](https://serversforhackers.com/c/using-ssl-certificates-with-haproxy), these would be combined into the `ssl crt /etc/ssl/xip.io/xip.io.pem` file.

199 changes: 199 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package main

import (
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"flag"
"fmt"
"golang.org/x/crypto/ssh/terminal"
"log"
"math/big"
"os"
"path/filepath"
"syscall"
"time"
)

const (
keyBits = 2048
)

var (
host = flag.String("host", "", "the DNS name to create a cert for")
outDir = flag.String("out", ".", "the directory to write cert files")
encrypt = flag.Bool("encrypt", false, "prompt for password and encrypt private key")
)

func main() {
flag.Parse()
if *host == "" {
log.Fatal("-host flag is required")
}
password := ""
if *encrypt {
fmt.Println("-encrypt set, will AES-256 encrypt private key with provided password")
fmt.Print("Enter password: ")
passwordBytes, err := terminal.ReadPassword(int(syscall.Stdin))
if err != nil {
log.Fatalf("failed to read password: %s", err)
}
fmt.Println()
password = string(passwordBytes)
if password == "" {
log.Fatalf("non-empty password must be set for encryption")
}
}
err := generateCerts(*host, *outDir, password)
if err != nil {
log.Fatal(err)
}
fmt.Println("CA cert and host certificate generated!")
fmt.Printf("CA certificate file: %s\n",
filepath.Join(*outDir, "ca.crt"))
fmt.Printf("Host certificate file: %s\n",
filepath.Join(*outDir, "host.crt"))
fmt.Printf("Host key file: %s\n",
filepath.Join(*outDir, "host.key"))
}

func generateCerts(host, outDir, keyPass string) error {
caCert, err := createCertificate(host, nil)
if err != nil {
return fmt.Errorf("error creating CA cert: %s", err)
}
hostCert, err := createCertificate(host, caCert)
if err != nil {
return fmt.Errorf("error creating host cert: %s", err)
}
if err := writeCertPem(outDir, "ca", caCert); err != nil {
return fmt.Errorf("error writing CA cert: %s", err)
}
if err := writeCertPem(outDir, "host", hostCert); err != nil {
return fmt.Errorf("error writing host cert: %s", err)
}
if err := writeKeyPem(outDir, "host", hostCert, keyPass); err != nil {
return fmt.Errorf("error writing host plaintextKey: %s", err)
}
return nil
}

type certData struct {
cert *x509.Certificate
certBytes []byte
privateKey *rsa.PrivateKey
}

func writeKeyPem(dir, basename string, data *certData, password string) (err error) {
bytes := x509.MarshalPKCS1PrivateKey(data.privateKey)
var pemBlock *pem.Block
if password != "" {
pemBlock, err = x509.EncryptPEMBlock(
rand.Reader,
"RSA PRIVATE KEY",
bytes,
[]byte(password),
x509.PEMCipherAES256)
if err != nil {
return err
}
} else {
pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: bytes}
}

file := filepath.Join(dir, basename+".key")

// TODO: Permissions on key file.
writer, err := os.Create(file)
if err != nil {
return err
}
err = pem.Encode(writer, pemBlock)
if err != nil {
return err
}
return writer.Close()
}

func writeCertPem(dir, basename string, data *certData) error {
file := filepath.Join(dir, basename+".crt")
writer, err := os.Create(file)
if err != nil {
return err
}
err = pem.Encode(writer, &pem.Block{Type: "CERTIFICATE", Bytes: data.certBytes})
if err != nil {
return err
}
return writer.Close()
}

func createCertificate(host string, parent *certData) (*certData, error) {
var err error
certTemplate := &x509.Certificate{
NotBefore: time.Now(),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
SignatureAlgorithm: x509.SHA256WithRSA,
BasicConstraintsValid: true,
}

certTemplate.SerialNumber, err = rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 159))
if err != nil {
return nil, fmt.Errorf("failed to generate serial number: %s", err)
}
privateKey, err := rsa.GenerateKey(rand.Reader, keyBits)
if err != nil {
return nil, fmt.Errorf("failed to generate RSA private plaintextKey: %s", err)
}
marshaledKey, err := x509.MarshalPKIXPublicKey(privateKey.Public())
if err != nil {
return nil, fmt.Errorf("failed to marshal pulic plaintextKey: %s", err)
}
subjectKeyID := sha1.Sum(marshaledKey)
certTemplate.SubjectKeyId = subjectKeyID[:]

var signingKey *rsa.PrivateKey
var parentCert *x509.Certificate
if parent == nil {
certTemplate.IsCA = true
// The CA is valid for 10 years and a day, to ensure it is valid longer than leaf.
certTemplate.NotAfter = certTemplate.NotBefore.AddDate(10, 0, 1)
certTemplate.KeyUsage |= x509.KeyUsageCertSign
certTemplate.Subject = pkix.Name{
CommonName: fmt.Sprintf("CA for %s", host),
}
certTemplate.AuthorityKeyId = certTemplate.SubjectKeyId
signingKey = privateKey
parentCert = certTemplate
} else {
// Valid for 10 years.
certTemplate.NotAfter = certTemplate.NotBefore.AddDate(10, 0, 0)
certTemplate.ExtKeyUsage = []x509.ExtKeyUsage{
x509.ExtKeyUsageServerAuth,
x509.ExtKeyUsageClientAuth,
}
certTemplate.Subject = pkix.Name{
CommonName: host,
}
certTemplate.DNSNames = []string{host}
signingKey = parent.privateKey
parentCert = parent.cert
}

certBytes, err := x509.CreateCertificate(rand.Reader, certTemplate, parentCert, privateKey.Public(), signingKey)
if err != nil {
return nil, fmt.Errorf("failed to create certificate: %s", err)
}
cert, err := x509.ParseCertificate(certBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %s", err)
}
return &certData{
cert: cert,
certBytes: certBytes,
privateKey: privateKey,
}, nil
}
1 change: 1 addition & 0 deletions release_version.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.1.0

0 comments on commit 811f987

Please sign in to comment.