Commit b2be470f authored by ale's avatar ale

Refactoring

Introduces improvements such as:
- better command-line interface using init/sign commands
- all commands are idempotent
- support dry-run --check option for automation
parent ee9c51f9
x509ca
======
A very simple command-line tool to generate arbitrary X509 certificates.
A very simple command-line tool to generate arbitrary X509
certificates. Uses ECDSA and P256 curves.
## Usage
To initialize a Certification Authority:
$ x509 init --ca-cert ca.pem --ca-key ca.key --subject O=MyCA
To sign a new certificate:
$ x509 sign --ca-cert ca.pem --ca-key ca.key \
--cert cert.pem --key key.pem --subject CN=myserver --server
Various options are available to customize the resulting certificate,
including setting subjectAltNames and IP addresses. See `x509ca sign
--help` for the full list.
If the certificate and private key referenced by the `--cert` and
`--key` options already exist, they will be regenerated only if they
are about to expire, or the certificate parameters have changed.
## Exit status
The exit status of *x509ca* is always 0 on success, 1 on error.
Both commands support the `--check` option, which will prevent the
program from writing anything to the filesystem, and will cause it to
exit with a status of 0 if the certificate already exists and would be
unchanged, 1 otherwise (certificate missing or expired).
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"flag"
"fmt"
"log"
"math/big"
"net"
"time"
"github.com/google/subcommands"
"golang.org/x/net/context"
)
// CA holds the private key for our certification authority.
type CA struct {
pkey *ecdsa.PrivateKey
cert *x509.Certificate
}
func loadCA(certPath, keyPath string) (*CA, error) {
pkey, err := loadPrivateKey(keyPath)
if err != nil {
return nil, err
}
cert, err := loadCertificate(certPath)
if err != nil {
return nil, err
}
return &CA{pkey: pkey, cert: cert}, nil
}
func renewCA(ca *CA, certPath string, subj *pkix.Name) (*CA, error) {
return signCA(ca.pkey, certPath, subj)
}
func newCA(certPath, keyPath string, subj *pkix.Name) (*CA, error) {
pkey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, fmt.Errorf("failed to generate ECDSA private key: %v", err)
}
if err := savePrivateKey(pkey, keyPath); err != nil {
return nil, err
}
return signCA(pkey, certPath, subj)
}
func signCA(pkey *ecdsa.PrivateKey, certPath string, subj *pkix.Name) (*CA, error) {
now := time.Now().UTC()
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: *subj,
NotBefore: now.Add(-5 * time.Minute),
NotAfter: now.AddDate(5, 0, 0), // 5 years.
SignatureAlgorithm: x509.ECDSAWithSHA256,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 1,
}
der, err := x509.CreateCertificate(rand.Reader, &template, &template, pkey.Public(), pkey)
if err != nil {
return nil, fmt.Errorf("could not self-sign CA certificate: %v", err)
}
cert, err := x509.ParseCertificate(der)
if err != nil {
// This would be weird.
return nil, err
}
// Save the new files.
if err := saveCertificate(cert, certPath); err != nil {
return nil, err
}
return &CA{pkey: pkey, cert: cert}, nil
}
func (ca *CA) newCertificate(certPath, keyPath string, subj *pkix.Name, altNames []string, ipAddrs []net.IP, isClient, isServer bool, validity time.Duration) (*ecdsa.PrivateKey, *x509.Certificate, error) {
pkey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, fmt.Errorf("failed to generate ECDSA key: %v", err)
}
if err := savePrivateKey(pkey, keyPath); err != nil {
return nil, nil, err
}
cert, err := ca.signCertificate(pkey, certPath, subj, altNames, ipAddrs, isClient, isServer, validity)
if err != nil {
return nil, nil, err
}
return pkey, cert, nil
}
// Create a new private key and signed certificate.
func (ca *CA) signCertificate(pkey *ecdsa.PrivateKey, certPath string, subj *pkix.Name, altNames []string, ipAddrs []net.IP, isClient, isServer bool, validity time.Duration) (*x509.Certificate, error) {
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return nil, fmt.Errorf("failed to generate serial number: %v", err)
}
var extUsage []x509.ExtKeyUsage
if isServer {
extUsage = append(extUsage, x509.ExtKeyUsageServerAuth)
}
if isClient {
extUsage = append(extUsage, x509.ExtKeyUsageClientAuth)
}
now := time.Now().UTC()
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: *subj,
IPAddresses: ipAddrs,
DNSNames: altNames,
NotBefore: now.Add(-5 * time.Minute),
NotAfter: now.Add(validity),
SignatureAlgorithm: x509.ECDSAWithSHA256,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: extUsage,
PublicKey: pkey.PublicKey,
BasicConstraintsValid: true,
}
der, err := x509.CreateCertificate(rand.Reader, &template, ca.cert, pkey.Public(), ca.pkey)
if err != nil {
return nil, err
}
cert, err := x509.ParseCertificate(der)
if err != nil {
// This would be weird.
return nil, err
}
if err := saveCertificate(cert, certPath); err != nil {
return nil, err
}
return cert, nil
}
type initCmd struct {
subject pkixNameFlag
caCertPath string
caKeyPath string
checkOnly bool
}
func (c *initCmd) Name() string { return "init" }
func (c *initCmd) Synopsis() string { return "Initialize the CA" }
func (c *initCmd) Usage() string {
return `init [<options>]
Initialize the CA. If called more than once, it will check
for CA validity and eventually renew its public certificate
before it expires.
`
}
func (c *initCmd) SetFlags(f *flag.FlagSet) {
f.Var(&c.subject, "subject", "CA subject (in CN=.../OU=.../etc format)")
f.StringVar(&c.caCertPath, "ca-cert", "", "CA certificate path")
f.StringVar(&c.caKeyPath, "ca-key", "", "CA private key path")
f.BoolVar(&c.checkOnly, "check", false, "Only check if initialization or renewal should be performed")
}
func (c *initCmd) Execute(ctx context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
if c.caCertPath == "" || c.caKeyPath == "" {
log.Printf("ERROR: --ca-cert and --ca-key must be specified")
return subcommands.ExitFailure
}
if c.subject.Name == nil {
log.Printf("ERROR: --subject must be specified")
return subcommands.ExitFailure
}
ca, err := loadCA(c.caCertPath, c.caKeyPath)
if err != nil {
if c.checkOnly {
return subcommands.ExitFailure
}
log.Printf("generating new CA certificate for %s", pkixNameToString(*c.subject.Name))
ca, err = newCA(c.caCertPath, c.caKeyPath, c.subject.Name)
if err != nil {
log.Printf("ERROR: could not create new CA: %v", err)
return subcommands.ExitFailure
}
}
if aboutToExpire(ca.cert) {
if c.checkOnly {
return subcommands.ExitFailure
}
log.Printf("renewing CA certificate for %s", pkixNameToString(*c.subject.Name))
ca, err = renewCA(ca, c.caCertPath, c.subject.Name)
if err != nil {
log.Printf("ERROR: could not renew CA certificate: %v", err)
return subcommands.ExitFailure
}
}
return subcommands.ExitSuccess
}
func init() {
subcommands.Register(&initCmd{}, "")
}
package main
import (
"crypto/x509"
"crypto/x509/pkix"
"flag"
"fmt"
"log"
"net"
"reflect"
"sort"
"strings"
"time"
"github.com/google/subcommands"
"golang.org/x/net/context"
)
type ipListFlag []net.IP
func (l ipListFlag) String() string {
var out []string
for _, ip := range l {
out = append(out, ip.String())
}
return strings.Join(out, ",")
}
func (l ipListFlag) Contains(ip net.IP) bool {
for _, elem := range l {
if ip.Equal(elem) {
return true
}
}
return false
}
func (l *ipListFlag) Set(s string) error {
ip := net.ParseIP(s)
if ip == nil {
return fmt.Errorf("could not parse IP: %s", s)
}
if !l.Contains(ip) {
*l = append(*l, ip)
}
return nil
}
type sortableIPList []net.IP
func (l sortableIPList) Len() int { return len(l) }
func (l sortableIPList) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
func (l sortableIPList) Less(i, j int) bool {
return l[i].String() < l[j].String()
}
type stringListFlag []string
func (l *stringListFlag) String() string {
return strings.Join(*l, ",")
}
func (l *stringListFlag) Contains(s string) bool {
for _, elem := range *l {
if elem == s {
return true
}
}
return false
}
func (l *stringListFlag) Set(s string) error {
if !l.Contains(s) {
*l = append(*l, s)
}
return nil
}
type sortableStringList []string
func (l sortableStringList) Len() int { return len(l) }
func (l sortableStringList) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
func (l sortableStringList) Less(i, j int) bool {
return l[i] < l[j]
}
func compareStringList(a, b []string) bool {
sort.Sort(sortableStringList(a))
sort.Sort(sortableStringList(b))
return reflect.DeepEqual(a, b)
}
func compareIPList(a, b []net.IP) bool {
sort.Sort(sortableIPList(a))
sort.Sort(sortableIPList(b))
return reflect.DeepEqual(a, b)
}
func pkixEqual(a, b pkix.Name) bool {
return (a.CommonName == b.CommonName &&
compareStringList(a.Country, b.Country) &&
compareStringList(a.Organization, b.Organization) &&
compareStringList(a.OrganizationalUnit, b.OrganizationalUnit) &&
compareStringList(a.Locality, b.Locality) &&
compareStringList(a.Province, b.Province))
}
func hasExtUsage(cert *x509.Certificate, usage x509.ExtKeyUsage) bool {
for _, u := range cert.ExtKeyUsage {
if u == usage {
return true
}
}
return false
}
func certificateMetadataEqual(cert *x509.Certificate, subject *pkix.Name, sanList []string, ipList []net.IP, isClient, isServer bool) bool {
return (pkixEqual(cert.Subject, *subject) &&
compareStringList(cert.DNSNames, sanList) &&
compareIPList(cert.IPAddresses, ipList) &&
(hasExtUsage(cert, x509.ExtKeyUsageClientAuth) == isClient) &&
(hasExtUsage(cert, x509.ExtKeyUsageServerAuth) == isServer))
}
type signCmd struct {
subject pkixNameFlag
certPath string
keyPath string
caCertPath string
caKeyPath string
checkOnly bool
sanList stringListFlag
ipList ipListFlag
isClient bool
isServer bool
validityDays int
}
func (c *signCmd) Name() string { return "sign" }
func (c *signCmd) Synopsis() string { return "Sign a certificate" }
func (c *signCmd) Usage() string {
return `sign [<options>]
Sign a certificate. If called more than once, it will check for
certificate validity (including changes in subjectAltName or IP lists)
and eventually renew the certificate before it expires.
`
}
func (c *signCmd) SetFlags(f *flag.FlagSet) {
f.Var(&c.subject, "subject", "Certificate subject (in CN=.../OU=.../etc format)")
f.StringVar(&c.certPath, "cert", "", "Certificate path")
f.StringVar(&c.keyPath, "key", "", "Private key path")
f.StringVar(&c.caCertPath, "ca-cert", "", "CA certificate path")
f.StringVar(&c.caKeyPath, "ca-key", "", "CA private key path")
f.BoolVar(&c.checkOnly, "check", false, "Only check if certificate creation or renewal should be performed")
f.Var(&c.sanList, "alt", "Add subjectAltName to the certificate")
f.Var(&c.ipList, "ip", "Add an IP to the certificate")
f.BoolVar(&c.isClient, "client", false, "Enable client mode for the certificate")
f.BoolVar(&c.isServer, "server", false, "Enable server mode for the certificate")
f.IntVar(&c.validityDays, "validity", 365, "Validity of the new certificate (days)")
}
func (c *signCmd) Execute(ctx context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
if c.certPath == "" || c.keyPath == "" {
log.Printf("ERROR: --cert and --key must be specified")
return subcommands.ExitFailure
}
if c.caCertPath == "" || c.caKeyPath == "" {
log.Printf("ERROR: --ca-cert and --ca-key must be specified")
return subcommands.ExitFailure
}
if c.subject.Name == nil {
log.Printf("ERROR: --subject must be specified")
return subcommands.ExitFailure
}
if !c.isClient && !c.isServer {
log.Printf("ERROR: --client and/or --server must be specified")
return subcommands.ExitFailure
}
// The subjectAltNames must include the CN.
c.sanList.Set(c.subject.Name.CommonName)
var ca *CA
if !c.checkOnly {
var err error
ca, err = loadCA(c.caCertPath, c.caKeyPath)
if err != nil {
log.Printf("ERROR: could not load CA: %v", err)
return subcommands.ExitFailure
}
}
pkey, cert, err := loadCertificateAndPrivateKey(c.certPath, c.keyPath)
if err != nil {
if c.checkOnly {
return subcommands.ExitFailure
}
log.Printf("generating new certificate for %s", pkixNameToString(*c.subject.Name))
pkey, cert, err = ca.newCertificate(c.certPath, c.keyPath, c.subject.Name, c.sanList, c.ipList, c.isClient, c.isServer, time.Duration(c.validityDays)*oneDay)
if err != nil {
log.Printf("ERROR: could not sign certificate: %v", err)
return subcommands.ExitFailure
}
}
if aboutToExpire(cert) || !certificateMetadataEqual(cert, c.subject.Name, c.sanList, c.ipList, c.isClient, c.isServer) {
if c.checkOnly {
return subcommands.ExitFailure
}
log.Printf("renewing certificate for %s", pkixNameToString(*c.subject.Name))
cert, err = ca.signCertificate(pkey, c.certPath, c.subject.Name, c.sanList, c.ipList, c.isClient, c.isServer, time.Duration(c.validityDays)*oneDay)
if err != nil {
log.Printf("ERROR: could not renew certificate: %v", err)
}
}
return subcommands.ExitSuccess
}
func init() {
subcommands.Register(&signCmd{}, "")
}
Want to contribute? Great! First, read this page (including the small print at the end).
### Before you contribute
Before we can use your code, you must sign the
[Google Individual Contributor License Agreement]
(https://cla.developers.google.com/about/google-individual)
(CLA), which you can do online. The CLA is necessary mainly because you own the
copyright to your changes, even after your contribution becomes part of our
codebase, so we need your permission to use and distribute your code. We also
need to be sure of various other things—for instance that you'll tell us if you
know that your code infringes on other people's patents. You don't have to sign
the CLA until after you've submitted your code for review and a member has
approved it, but you must do it before we can put your code into our codebase.
Before you start working on a larger contribution, you should get in touch with
us first through the issue tracker with your idea so that we can help out and
possibly guide you. Coordinating up front makes it much easier to avoid
frustration later on.
### Code reviews
All submissions, including submissions by project members, require review. We
use Github pull requests for this purpose.
### The small print
Contributions made by corporations are covered by a different agreement than
the one above, the
[Software Grant and Corporate Contributor License Agreement]
(https://cla.developers.google.com/about/google-corporate).
This diff is collapsed.
# subcommands #
Subcommands is a Go package that implements a simple way for a single command to
have many subcommands, each of which takes arguments and so forth.
This is not an official Google product.
## Usage ##
Set up a 'print' subcommand:
```go
import (
"flag"
"fmt"
"os"
"strings"
"github.com/google/subcommands"
"golang.org/x/net/context"
)
type printCmd struct {
capitalize bool
}
func (*printCmd) Name() string { return "print" }
func (*printCmd) Synopsis() string { return "Print args to stdout." }
func (*printCmd) Usage() string {
return `print [-capitalize] <some text>:
Print args to stdout.
`
}
func (p *printCmd) SetFlags(f *flag.FlagSet) {
f.BoolVar(&p.capitalize, "capitalize", false, "capitalize output")
}
func (p *printCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
for _, arg := range f.Args() {
if p.capitalize {
arg = strings.ToUpper(arg)
}
fmt.Printf("%s ", arg)
}
fmt.Println()
return subcommands.ExitSuccess
}
```
Register using the default Commander, also use some built in subcommands,
finally run Execute using ExitStatus as the exit code:
```go
func main() {
subcommands.Register(subcommands.HelpCommand(), "")
subcommands.Register(subcommands.FlagsCommand(), "")
subcommands.Register(subcommands.CommandsCommand(), "")
subcommands.Register(&printCmd{}, "")
flag.Parse()
ctx := context.Background()
os.Exit(int(subcommands.Execute(ctx)))
}
```
This diff is collapsed.
Copyright (c) 2009 The Go 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.
Additional IP Rights Grant (Patents)
"This implementation" means the copyrightable works distributed by
Google as part of the Go project.
Google hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable (except as stated in this section)
patent license to make, have made, use, offer to sell, sell, import,
transfer and otherwise run, modify and propagate the contents of this
implementation of Go, where such license applies only to those patent
claims, both currently owned or controlled by Google and acquired in
the future, licensable by Google that are necessarily infringed by this
implementation of Go. This grant does not include claims that would be
infringed only as a consequence of further modification of this
implementation. If you or your agent or exclusive licensee institute or
order or agree to the institution of patent litigation against any
entity (including a cross-claim or counterclaim in a lawsuit) alleging
that this implementation of Go or any code incorporated within this
implementation of Go constitutes direct or contributory patent
infringement, or inducement of patent infringement, then any patent
rights granted to you under this License for this implementation of Go
shall terminate as of the date such litigation is filed.
// Copyright 2014 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 context defines the Context type, which carries deadlines,
// cancelation signals, and other request-scoped values across API boundaries
// and between processes.
//
// Incoming requests to a server should create a Context, and outgoing calls to
// servers should accept a Context. The chain of function calls between must
// propagate the Context, optionally replacing it with a modified copy created
// using WithDeadline, WithTimeout, WithCancel, or WithValue.
//
// Programs that use Contexts should follow these rules to keep interfaces
// consistent across packages and enable static analysis tools to check context
// propagation:
//
// Do not store Contexts inside a struct type; instead, pass a Context
// explicitly to each function that needs it. The Context should be the first
// parameter, typically named ctx:
//
// func DoSomething(ctx context.Context, arg Arg) error {
// // ... use ctx ...
// }
//
// Do not pass a nil Context, even if a function permits it. Pass context.TODO
// if you are unsure about which Context to use.
//
// Use context Values only for request-scoped data that transits processes and
// APIs, not for passing optional parameters to functions.
//
// The same Context may be passed to functions running in different goroutines;
// Contexts are safe for simultaneous use by multiple goroutines.
//
// See http://blog.golang.org/context for example code for a server that uses
// Contexts.
package context // import "golang.org/x/net/context"
import "time"
// A Context carries a deadline, a cancelation signal, and other values across
// API boundaries.
//
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
// Deadline returns the time when work done on behalf of this context
// should be canceled. Deadline returns ok==false when no deadline is
// set. Successive calls to Deadline return the same results.
Deadline() (deadline time.Time, ok bool)
// Done returns a channel that's closed when work done on behalf of this
// context should be canceled. Done may return nil if this context can
// never be canceled. Successive calls to Done return the same value.
//
// WithCancel arranges for Done to be closed when cancel is called;
// WithDeadline arranges for Done to be closed when the deadline
// expires; WithTimeout arranges for Done to be closed when the timeout
// elapses.
//
// Done is provided for use in select statements:
//
// // Stream generates values with DoSomething and sends them to out
// // until DoSomething returns an error or ctx.Done is closed.
// func Stream(ctx context.Context, out chan<- Value) error {
// for {
// v, err := DoSomething(ctx)
// if err != nil {
// return err
// }
// select {
// case <-ctx.Done():
// return ctx.Err()
// case out <- v:
// }
// }
// }
//
// See http://blog.golang.org/pipelines for more examples of how to use
// a Done channel for cancelation.
Done() <-chan struct{}
// Err returns a non-nil error value after Done is closed. Err returns
// Canceled if the context was canceled or DeadlineExceeded if the
// context's deadline passed. No other values for Err are defined.
// After Done is closed, successive calls to Err return the same value.
Err() error
// Value returns the value associated with this context for key, or nil
// if no value is associated with key. Successive calls to Value with
// the same key returns the same result.
//
// Use context values only for request-scoped data that transits
// processes and API boundaries, not for passing optional parameters to
// functions.
//
// A key identifies a specific value in a Context. Functions that wish
// to store values in Context typically allocate a key in a global
// variable then use that key as the argument to context.WithValue and
// Context.Value. A key can be any type that supports equality;
// packages should define keys as an unexported type to avoid
// collisions.
//
// Packages that define a Context key should provide type-safe accessors
// for the values stores using that key:
//
// // Package user defines a User type that's stored in Contexts.
// package user
//
// import "golang.org/x/net/context"
//
// // User is the type of value stored in the Contexts.
// type User struct {...}
//
// // key is an unexported type for keys defined in this package.
// // This prevents collisions with keys defined in other packages.