Added pki which is a fork of https://github.com/mastahyeti/fakeca needed for testing.
This commit is contained in:
parent
6604a9bc8c
commit
d4389bbcc5
368
pki/pki.go
Normal file
368
pki/pki.go
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
//Package pki can create ca's intermediates and certificates
|
||||||
|
package pki
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"math/big"
|
||||||
|
"os/exec"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type configuration struct {
|
||||||
|
subject *pkix.Name
|
||||||
|
issuer *Identity
|
||||||
|
nextSN *int64
|
||||||
|
priv *crypto.Signer
|
||||||
|
isCA bool
|
||||||
|
notBefore *time.Time
|
||||||
|
notAfter *time.Time
|
||||||
|
issuingCertificateURL []string
|
||||||
|
ocspServer []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *configuration) generate() *Identity {
|
||||||
|
templ := &x509.Certificate{
|
||||||
|
Subject: c.getSubject(),
|
||||||
|
IsCA: c.isCA,
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
NotAfter: c.getNotAfter(),
|
||||||
|
NotBefore: c.getNotBefore(),
|
||||||
|
IssuingCertificateURL: c.issuingCertificateURL,
|
||||||
|
OCSPServer: c.ocspServer,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
parent *x509.Certificate
|
||||||
|
thisPriv = c.getPrivateKey()
|
||||||
|
priv crypto.Signer
|
||||||
|
)
|
||||||
|
|
||||||
|
if c.issuer != nil {
|
||||||
|
parent = c.issuer.Certificate
|
||||||
|
templ.SerialNumber = big.NewInt(c.issuer.IncrementSN())
|
||||||
|
priv = c.issuer.PrivateKey
|
||||||
|
} else {
|
||||||
|
parent = templ
|
||||||
|
templ.SerialNumber = randSN()
|
||||||
|
priv = thisPriv
|
||||||
|
}
|
||||||
|
|
||||||
|
der, err := x509.CreateCertificate(rand.Reader, templ, parent, thisPriv.Public(), priv)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := x509.ParseCertificate(der)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Identity{
|
||||||
|
Certificate: cert,
|
||||||
|
PrivateKey: thisPriv,
|
||||||
|
Issuer: c.issuer,
|
||||||
|
NextSN: c.getNextSN(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// DefaultCountry is the default subject Country.
|
||||||
|
DefaultCountry = []string{"US"}
|
||||||
|
|
||||||
|
// DefaultProvince is the default subject Province.
|
||||||
|
DefaultProvince = []string{"CA"}
|
||||||
|
|
||||||
|
// DefaultLocality is the default subject Locality.
|
||||||
|
DefaultLocality = []string{"San Francisco"}
|
||||||
|
|
||||||
|
// DefaultStreetAddress is the default subject StreetAddress.
|
||||||
|
DefaultStreetAddress = []string(nil)
|
||||||
|
|
||||||
|
// DefaultPostalCode is the default subject PostalCode.
|
||||||
|
DefaultPostalCode = []string(nil)
|
||||||
|
|
||||||
|
// DefaultCommonName is the default subject CommonName.
|
||||||
|
DefaultCommonName = "fakeca"
|
||||||
|
|
||||||
|
cnCounter int64
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *configuration) getSubject() pkix.Name {
|
||||||
|
if c.subject != nil {
|
||||||
|
return *c.subject
|
||||||
|
}
|
||||||
|
|
||||||
|
var cn string
|
||||||
|
if cnCounter == 0 {
|
||||||
|
cn = DefaultCommonName
|
||||||
|
} else {
|
||||||
|
cn = fmt.Sprintf("%s #%d", DefaultCommonName, cnCounter)
|
||||||
|
}
|
||||||
|
cnCounter++
|
||||||
|
|
||||||
|
return pkix.Name{
|
||||||
|
Country: DefaultCountry,
|
||||||
|
Province: DefaultProvince,
|
||||||
|
Locality: DefaultLocality,
|
||||||
|
StreetAddress: DefaultStreetAddress,
|
||||||
|
PostalCode: DefaultPostalCode,
|
||||||
|
CommonName: cn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *configuration) getNextSN() int64 {
|
||||||
|
if c.nextSN == nil {
|
||||||
|
sn := randSN().Int64()
|
||||||
|
c.nextSN = &sn
|
||||||
|
}
|
||||||
|
|
||||||
|
return *c.nextSN
|
||||||
|
}
|
||||||
|
|
||||||
|
func randSN() *big.Int {
|
||||||
|
i, err := rand.Int(rand.Reader, big.NewInt(int64(math.MaxInt64)))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *configuration) getPrivateKey() crypto.Signer {
|
||||||
|
if c.priv == nil {
|
||||||
|
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signer := crypto.Signer(priv)
|
||||||
|
|
||||||
|
c.priv = &signer
|
||||||
|
}
|
||||||
|
|
||||||
|
return *c.priv
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *configuration) getNotBefore() time.Time {
|
||||||
|
if c.notBefore == nil {
|
||||||
|
return time.Unix(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return *c.notBefore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *configuration) getNotAfter() time.Time {
|
||||||
|
if c.notAfter == nil {
|
||||||
|
return time.Now().Add(time.Hour * 24 * 365 * 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
return *c.notAfter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option is an option that can be passed to New().
|
||||||
|
type Option option
|
||||||
|
type option func(c *configuration)
|
||||||
|
|
||||||
|
// Subject is an Option that sets a identity's subject field.
|
||||||
|
func Subject(value pkix.Name) Option {
|
||||||
|
return func(c *configuration) {
|
||||||
|
c.subject = &value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextSerialNumber is an Option that determines the SN of the next issued
|
||||||
|
// certificate.
|
||||||
|
func NextSerialNumber(value int64) Option {
|
||||||
|
return func(c *configuration) {
|
||||||
|
c.nextSN = &value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrivateKey is an Option for setting the identity's private key.
|
||||||
|
func PrivateKey(value crypto.Signer) Option {
|
||||||
|
return func(c *configuration) {
|
||||||
|
c.priv = &value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issuer is an Option for setting the identity's issuer.
|
||||||
|
func Issuer(value *Identity) Option {
|
||||||
|
return func(c *configuration) {
|
||||||
|
c.issuer = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotBefore is an Option for setting the identity's certificate's NotBefore.
|
||||||
|
func NotBefore(value time.Time) Option {
|
||||||
|
return func(c *configuration) {
|
||||||
|
c.notBefore = &value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotAfter is an Option for setting the identity's certificate's NotAfter.
|
||||||
|
func NotAfter(value time.Time) Option {
|
||||||
|
return func(c *configuration) {
|
||||||
|
c.notAfter = &value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IssuingCertificateURL is an Option for setting the identity's certificate's
|
||||||
|
// IssuingCertificateURL.
|
||||||
|
func IssuingCertificateURL(value ...string) Option {
|
||||||
|
return func(c *configuration) {
|
||||||
|
c.issuingCertificateURL = append(c.issuingCertificateURL, value...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OCSPServer is an Option for setting the identity's certificate's OCSPServer.
|
||||||
|
func OCSPServer(value ...string) Option {
|
||||||
|
return func(c *configuration) {
|
||||||
|
c.ocspServer = append(c.ocspServer, value...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCA is an Option for making an identity a certificate authority.
|
||||||
|
var IsCA Option = func(c *configuration) {
|
||||||
|
c.isCA = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identity is a certificate and private key.
|
||||||
|
type Identity struct {
|
||||||
|
Issuer *Identity
|
||||||
|
PrivateKey crypto.Signer
|
||||||
|
Certificate *x509.Certificate
|
||||||
|
NextSN int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new CA.
|
||||||
|
func New(opts ...Option) *Identity {
|
||||||
|
c := &configuration{}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
option(opt)(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.generate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issue issues a new Identity with this one as its parent.
|
||||||
|
func (id *Identity) Issue(opts ...Option) *Identity {
|
||||||
|
opts = append(opts, Issuer(id))
|
||||||
|
return New(opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PFX wraps the certificate and private key in an encrypted PKCS#12 packet. The
|
||||||
|
// provided password must be alphanumeric.
|
||||||
|
func (id *Identity) PFX(password string) []byte {
|
||||||
|
return toPFX(id.Certificate, id.PrivateKey, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chain builds a slice of *x509.Certificate from this CA and its issuers.
|
||||||
|
func (id *Identity) Chain() []*x509.Certificate {
|
||||||
|
chain := []*x509.Certificate{}
|
||||||
|
for this := id; this != nil; this = this.Issuer {
|
||||||
|
chain = append(chain, this.Certificate)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chain
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChainPool builds an *x509.CertPool from this CA and its issuers.
|
||||||
|
func (id *Identity) ChainPool() *x509.CertPool {
|
||||||
|
chain := x509.NewCertPool()
|
||||||
|
for this := id; this != nil; this = this.Issuer {
|
||||||
|
chain.AddCert(this.Certificate)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chain
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrementSN returns the next serial number.
|
||||||
|
func (id *Identity) IncrementSN() int64 {
|
||||||
|
defer func() {
|
||||||
|
id.NextSN++
|
||||||
|
}()
|
||||||
|
|
||||||
|
return id.NextSN
|
||||||
|
}
|
||||||
|
|
||||||
|
func toPFX(cert *x509.Certificate, priv interface{}, password string) []byte {
|
||||||
|
// only allow alphanumeric passwords
|
||||||
|
for _, c := range password {
|
||||||
|
switch {
|
||||||
|
case c >= 'a' && c <= 'z':
|
||||||
|
case c >= 'A' && c <= 'Z':
|
||||||
|
case c >= '0' && c <= '9':
|
||||||
|
default:
|
||||||
|
panic("password must be alphanumeric")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
passout := fmt.Sprintf("pass:%s", password)
|
||||||
|
cmd := exec.Command("openssl", "pkcs12", "-export", "-passout", passout)
|
||||||
|
|
||||||
|
cmd.Stdin = bytes.NewReader(append(append(toPKCS8(priv), '\n'), toPEM(cert)...))
|
||||||
|
|
||||||
|
out := new(bytes.Buffer)
|
||||||
|
cmd.Stdout = out
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func toPEM(cert *x509.Certificate) []byte {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
if err := pem.Encode(buf, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func toDER(priv interface{}) []byte {
|
||||||
|
var (
|
||||||
|
der []byte
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
switch p := priv.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
der = x509.MarshalPKCS1PrivateKey(p)
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
der, err = x509.MarshalECPrivateKey(p)
|
||||||
|
default:
|
||||||
|
err = errors.New("unknown key type")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return der
|
||||||
|
}
|
||||||
|
|
||||||
|
func toPKCS8(priv interface{}) []byte {
|
||||||
|
cmd := exec.Command("openssl", "pkcs8", "-topk8", "-nocrypt", "-inform", "DER")
|
||||||
|
|
||||||
|
cmd.Stdin = bytes.NewReader(toDER(priv))
|
||||||
|
|
||||||
|
out := new(bytes.Buffer)
|
||||||
|
cmd.Stdout = out
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.Bytes()
|
||||||
|
}
|
51
pki/pki_test.go
Normal file
51
pki/pki_test.go
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
package pki
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
pki "github.com/InfiniteLoopSpace/go_S-MIME/pki"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCA(t *testing.T) {
|
||||||
|
|
||||||
|
pki.DefaultProvince = []string{"CO"}
|
||||||
|
pki.DefaultLocality = []string{"Denver"}
|
||||||
|
|
||||||
|
// Create a root CA.
|
||||||
|
root := pki.New(pki.IsCA, pki.Subject(pkix.Name{
|
||||||
|
CommonName: "root.myorg.com",
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Create an intermediate CA under the root.
|
||||||
|
intermediate := root.Issue(pki.IsCA, pki.Subject(pkix.Name{
|
||||||
|
CommonName: "intermediate.myorg.com",
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Create a leaf certificate under the intermediate.
|
||||||
|
leaf := intermediate.Issue(pki.Subject(pkix.Name{
|
||||||
|
CommonName: "leaf.myorg.com",
|
||||||
|
}))
|
||||||
|
|
||||||
|
Intermediate := x509.NewCertPool()
|
||||||
|
Intermediate.AddCert(intermediate.Certificate)
|
||||||
|
|
||||||
|
roots := x509.NewCertPool()
|
||||||
|
roots.AddCert(root.Certificate)
|
||||||
|
|
||||||
|
Opts := x509.VerifyOptions{
|
||||||
|
Intermediates: Intermediate,
|
||||||
|
Roots: roots,
|
||||||
|
CurrentTime: time.Now(),
|
||||||
|
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := leaf.Certificate.Verify(Opts)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user