Commit a8335c18 authored by ale's avatar ale

Split signature and key generation

parent 5d0618f5
stages:
- build_src
- build_pkg
- upload
build:src:
stage: build_src
image: "ai/build:stretch"
script: "build-dsc"
artifacts:
paths:
- build-deb/
only:
- master
build:pkg:
stage: build_pkg
image: "ai/build:stretch"
script: "build-deb"
dependencies:
- build:src
artifacts:
paths:
- output-deb/
only:
- master
upload:pkg:
stage: upload
image: "ai/pkg:base"
script: "upload-packages -r ai3"
dependencies:
- build:pkg
only:
- master
......@@ -10,7 +10,6 @@ import (
"fmt"
"log"
"math/big"
"net"
"time"
"github.com/google/subcommands"
......@@ -82,25 +81,24 @@ func signCA(pkey *ecdsa.PrivateKey, certPath string, subj *pkix.Name) (*CA, erro
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
func (ca *CA) signCSR(csr *x509.CertificateRequest, isClient, isServer bool, validity time.Duration) (*x509.Certificate, error) {
if err := csr.CheckSignature(); err != nil {
return nil, err
}
cert, err := ca.signCertificate(pkey, certPath, subj, altNames, ipAddrs, isClient, isServer, validity)
tpl, err := templateFromCSR(csr, isClient, isServer, validity)
if err != nil {
return nil, nil, err
return nil, err
}
return pkey, cert, nil
der, err := x509.CreateCertificate(rand.Reader, tpl, ca.cert, csr.PublicKey, ca.pkey)
if err != nil {
return nil, err
}
return x509.ParseCertificate(der)
}
// 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) {
func templateFromCSR(csr *x509.CertificateRequest, 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 {
......@@ -114,36 +112,22 @@ func (ca *CA) signCertificate(pkey *ecdsa.PrivateKey, certPath string, subj *pki
if isClient {
extUsage = append(extUsage, x509.ExtKeyUsageClientAuth)
}
now := time.Now().UTC()
template := x509.Certificate{
return &x509.Certificate{
SerialNumber: serialNumber,
Subject: *subj,
IPAddresses: ipAddrs,
DNSNames: altNames,
Subject: csr.Subject,
IPAddresses: csr.IPAddresses,
DNSNames: csr.DNSNames,
EmailAddresses: csr.EmailAddresses,
NotBefore: now.Add(-5 * time.Minute),
NotAfter: now.Add(validity),
SignatureAlgorithm: x509.ECDSAWithSHA256,
SignatureAlgorithm: csr.SignatureAlgorithm,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: extUsage,
PublicKey: pkey.PublicKey,
PublicKey: csr.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
}, nil
}
type initCmd struct {
......@@ -151,6 +135,7 @@ type initCmd struct {
caCertPath string
caKeyPath string
checkOnly bool
renewDays int
}
func (c *initCmd) Name() string { return "init" }
......@@ -167,9 +152,10 @@ 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.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")
f.IntVar(&c.renewDays, "renew-days", 60, "How many `days` to warn in advance that a certificate is about to expire")
}
func (c *initCmd) Execute(ctx context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
......@@ -196,12 +182,12 @@ func (c *initCmd) Execute(ctx context.Context, _ *flag.FlagSet, _ ...interface{}
}
changed = true
}
if aboutToExpire(ca.cert) {
if aboutToExpire(ca.cert, c.renewDays) {
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)
_, err = renewCA(ca, c.caCertPath, c.subject.Name)
if err != nil {
log.Printf("ERROR: could not renew CA certificate: %v", err)
return subcommands.ExitFailure
......
package main
import (
"crypto/x509"
"crypto/x509/pkix"
"flag"
"fmt"
"log"
"net"
"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 {
if len(a) != len(b) {
return false
}
sort.Sort(sortableStringList(a))
sort.Sort(sortableStringList(b))
for i := 0; i < len(a); i++ {
if a[i] != b[i] {
return false
}
}
return true
}
func compareIPList(a, b []net.IP) bool {
if len(a) != len(b) {
return false
}
sort.Sort(sortableIPList(a))
sort.Sort(sortableIPList(b))
for i := 0; i < len(a); i++ {
if !a[i].Equal(b[i]) {
return false
}
}
return true
}
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
}
}
changed := false
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
}
changed = true
}
if aboutToExpire(cert) || !certificateMetadataEqual(cert, c.subject.Name, c.sanList, c.ipList, c.isClient, c.isServer) {
if c.checkOnly {
return subcommands.ExitFailure
}
if aboutToExpire(cert) {
log.Printf("renewing certificate for %s", pkixNameToString(*c.subject.Name))
} else {
log.Printf("updating 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)
}
changed = true
}
return exitStatus(changed, c.checkOnly)
}
func init() {
subcommands.Register(&signCmd{}, "")
}
package main
import (
"crypto/x509"
"crypto/x509/pkix"
"flag"
"fmt"
"log"
"net"
"time"
"github.com/google/subcommands"
"golang.org/x/net/context"
)
type checkCmd struct {
subject pkixNameFlag
certPath string
sanList stringListFlag
ipList ipListFlag
isClient bool
isServer bool
renewDays int
}
func (c *checkCmd) Name() string { return "check" }
func (c *checkCmd) Synopsis() string { return "Check if a certificate needs to be updated" }
func (c *checkCmd) Usage() string {
return `check [<options>]
Check if a certificate needs to be updated. It will check for
certificate metadata correctness (including changes in subjectAltName
or IP lists) and expiration time.
`
}
func (c *checkCmd) SetFlags(f *flag.FlagSet) {
f.StringVar(&c.certPath, "cert", "", "Certificate `path`")
f.Var(&c.subject, "subject", "Certificate subject (in CN=.../OU=.../etc format)")
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.renewDays, "renew-days", 60, "How many `days` to warn in advance that a certificate is about to expire")
}
func (c *checkCmd) Execute(ctx context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
if c.certPath == "" {
log.Printf("ERROR: --cert must be specified")
return subcommands.ExitUsageError
}
cert, err := loadCertificate(c.certPath)
if err != nil {
log.Printf("ERROR: could not load certificate: %v", err)
return subcommands.ExitFailure
}
switch {
case aboutToExpire(cert, c.renewDays):
fmt.Printf("certificate must be renewed (about to expire)\n")
case !certificateMetadataEqual(cert, c.subject.Name, c.sanList, c.ipList, c.isClient, c.isServer):
fmt.Printf("certificate must be renewed (metadata change)\n")
default:
return subcommands.ExitSuccess
}
return subcommands.ExitFailure
}
func init() {
subcommands.Register(&checkCmd{}, "")
}
var oneDay = 24 * time.Hour
func aboutToExpire(cert *x509.Certificate, renewDays int) bool {
remaining := int(cert.NotAfter.Sub(time.Now().UTC()) / oneDay)
return remaining < renewDays
}
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))
}
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 compareStringList(a, b []string) bool {
tmp := make(map[string]struct{})
for _, elem := range a {
tmp[elem] = struct{}{}
}
for _, elem := range b {
if _, ok := tmp[elem]; !ok {
return false
}
delete(tmp, elem)
}
return len(tmp) == 0
}
func compareIPList(a, b []net.IP) bool {
tmp := make(map[string]struct{})
for _, elem := range a {
tmp[elem.String()] = struct{}{}
}
for _, elem := range b {
s := elem.String()
if _, ok := tmp[s]; !ok {
return false
}
delete(tmp, s)
}
return len(tmp) == 0
}
package main
import (
"crypto/ecdsa"
"crypto/rand"
"crypto/x509"
"flag"
"fmt"
"log"
"github.com/google/subcommands"
"golang.org/x/net/context"
)
type csrCmd struct {
subject pkixNameFlag
csrPath string
keyPath string
sanList stringListFlag
ipList ipListFlag
}
func (c *csrCmd) Name() string { return "csr" }
func (c *csrCmd) Synopsis() string { return "Generate a CSR" }
func (c *csrCmd) Usage() string {
return `csr [<options>]
Generate a certificate signing request.
`
}
func (c *csrCmd) SetFlags(f *flag.FlagSet) {
f.Var(&c.subject, "subject", "Certificate subject (in CN=.../OU=.../etc format)")
f.StringVar(&c.csrPath, "csr", "", "Output CSR `path`")
f.StringVar(&c.keyPath, "key", "private_key.pem", "Private key `path`")
f.Var(&c.sanList, "alt", "Add `subjectAltName` to the certificate")
f.Var(&c.ipList, "ip", "Add an `IP` to the certificate")
}
func (c *csrCmd) Execute(ctx context.Context, _ *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus {
if c.subject.Name == nil {
log.Printf("ERROR: --subject must be specified")
return subcommands.ExitUsageError
}
if c.subject.Name.CommonName == "" {
log.Printf("ERROR: --subject must include a CN")
return subcommands.ExitUsageError
}
priv, err := loadPrivateKey(c.keyPath)
if err != nil {
log.Printf("ERROR: could not load private key: %v", err)
return subcommands.ExitFailure
}
c.sanList.Set(c.subject.Name.CommonName)
csr, err := c.makeCertificateRequest(priv)
if err != nil {
log.Printf("ERROR: could not create certificate signing request: %v", err)
return subcommands.ExitFailure
}
data := MarshalCertificateRequest(csr)
fmt.Printf("%s\n", data)
return subcommands.ExitSuccess
}
func init() {
subcommands.Register(&csrCmd{}, "")
}
func (c *csrCmd) makeCertificateRequest(priv *ecdsa.PrivateKey) (*x509.CertificateRequest, error) {
b, err := x509.CreateCertificateRequest(
rand.Reader,
&x509.CertificateRequest{
Subject: c.subject.PKIXName(),
DNSNames: c.sanList,
IPAddresses: c.ipList,
SignatureAlgorithm: x509.ECDSAWithSHA256,
},
priv,
)
if err != nil {
return nil, err
}
return x509.ParseCertificateRequest(b)
}
x509ca (0.2) unstable; urgency=medium
* Initial packaged release.
-- ale <ale@incal.net> Sun, 14 Jan 2018 17:15:13 +0000
Source: x509ca
Section: admin
Priority: optional
Maintainer: Autistici/Inventati <debian@autistici.org>
Build-Depends: debhelper (>=9), golang-go, dh-golang
Standards-Version: 3.9.6
Package: x509ca
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}
Description: Simple tool to manipulate X509 certificates
Simple tool to manipulate X509 certificates.
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: x509ca
Source: <https://git.autistici.org/ale/x509ca>
Files: *
Copyright: 2017 ale <ale@incal.net>
License: GPL-3.0+
License: GPL-3.0+
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
.
This package is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
.
On Debian systems, the complete text of the GNU General
Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".
#!/usr/bin/make -f
export DH_GOPKG = git.autistici.org/ale/x509ca
export DH_GOLANG_EXCLUDES = vendor
%:
dh $@ --with golang --buildsystem golang
override_dh_install:
rm -fr $(CURDIR)/debian/x509ca/usr/share/gocode
dh_install
package main
import (
"crypto/x509/pkix"
"fmt"
"net"
"strings"
)
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)