Skip to content

Instantly share code, notes, and snippets.

@jiahuif
Last active August 12, 2022 23:13
Show Gist options
  • Save jiahuif/07771dc46335e351a34ef72c9e455be4 to your computer and use it in GitHub Desktop.
Save jiahuif/07771dc46335e351a34ef72c9e455be4 to your computer and use it in GitHub Desktop.
cfssl initCA with Name Constraints
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
"log"
"math/big"
"os"
"time"
"github.com/cloudflare/cfssl/cli"
"github.com/cloudflare/cfssl/config"
"github.com/cloudflare/cfssl/csr"
"github.com/cloudflare/cfssl/helpers"
"github.com/cloudflare/cfssl/initca"
"github.com/cloudflare/cfssl/signer"
"github.com/cloudflare/cfssl/signer/local"
)
// main is the entry of the program
//
// the following fields are honored from the JSON-serialized certificate
//
// PermittedDNSDomainsCritical
// PermittedDNSDomains
// PermittedURIDomains
// PermittedIPRanges
// PermittedEmailAddresses
// ExcludedDNSDomains
// ExcludedURIDomains
// ExcludedIPRanges
// ExcludedEmailAddresses
//
// example certificate.json
//
// {
// "PermittedDNSDomainsCritical": true,
// "PermittedDNSDomains": [
// ".example.com"
// ]
// }
//
// example csr.json
//
// {
// "CN": "example.com",
// "names": [
// {
// "C": "US",
// "L": "San Francisco",
// "O": "Internet Widgets, Inc.",
// "OU": "WWW",
// "ST": "California"
// }
// ],
// "ca": {
// "expiry": "87600h"
// }
// }
func main() {
var csrFile, certFile string
flag.StringVar(&csrFile, "csr", "", "cfssl compatible csr JSON file (optional)")
flag.StringVar(&certFile, "cert", "", "golang certificate as JSON file (optional)")
flag.Parse()
req := csr.New()
if csrFile != "" {
err := func() error {
f, err := os.Open(csrFile)
if err != nil {
return err
}
defer func(f *os.File) {
_ = f.Close()
}(f)
return json.NewDecoder(f).Decode(req)
}()
if err != nil {
log.Fatalln(err)
}
}
cert := new(x509.Certificate)
if certFile != "" {
err := func() error {
f, err := os.Open(certFile)
if err != nil {
return err
}
defer func(f *os.File) {
_ = f.Close()
}(f)
return json.NewDecoder(f).Decode(cert)
}()
if err != nil {
log.Fatalln(err)
}
}
extensions, err := generateExtensions(cert)
if err != nil {
log.Fatalln(err)
}
req.Extensions = append(req.Extensions, extensions...)
err = initCA(req)
if err != nil {
log.Fatalln(err)
}
}
// initCA is borrowed from "github.com/cloudflare/cfssl/initca"
func initCA(req *csr.CertificateRequest) (err error) {
policy := initca.CAPolicy()
if policy.Default.ExtensionWhitelist == nil {
policy.Default.ExtensionWhitelist = make(map[string]bool)
}
// add each extension to the allowed list
for _, e := range req.Extensions {
policy.Default.ExtensionWhitelist[e.Id.String()] = true
}
if req.CA != nil {
if req.CA.Expiry != "" {
policy.Default.ExpiryString = req.CA.Expiry
policy.Default.Expiry, err = time.ParseDuration(req.CA.Expiry)
if err != nil {
return
}
}
if req.CA.Backdate != "" {
policy.Default.Backdate, err = time.ParseDuration(req.CA.Backdate)
if err != nil {
return
}
}
policy.Default.CAConstraint.MaxPathLen = req.CA.PathLength
if req.CA.PathLength != 0 && req.CA.PathLenZero {
log.Println("ignore invalid 'pathlenzero' value")
} else {
policy.Default.CAConstraint.MaxPathLenZero = req.CA.PathLenZero
}
}
if req.CRL != "" {
policy.Default.CRL = req.CRL
}
g := &csr.Generator{
Validator: func(req *csr.CertificateRequest) error {
if req.CN != "" {
return nil
}
if len(req.Names) == 0 {
return fmt.Errorf("missing subject information")
}
for i := range req.Names {
if csr.IsNameEmpty(req.Names[i]) {
return fmt.Errorf("missing subject information")
}
}
return nil
},
}
csrPEM, key, err := g.ProcessRequest(req)
if err != nil {
log.Fatalf("failed to process request: %v", err)
return
}
priv, err := helpers.ParsePrivateKeyPEM(key)
if err != nil {
log.Fatalf("failed to parse private key: %v", err)
return
}
s, err := local.NewSigner(priv, nil, signer.DefaultSigAlgo(priv), policy)
if err != nil {
log.Fatalf("failed to create signer: %v", err)
return
}
signReq := signer.SignRequest{
Hosts: req.Hosts,
Request: string(csrPEM),
Extensions: convertExtensions(req.Extensions),
}
cert, err := s.Sign(signReq)
if err != nil {
log.Fatalf("failed to sign: %v", err)
}
cli.PrintCert(key, csrPEM, cert)
return
}
// generateExtensions generates the X509v3 Name Constraints from corresponding
// fields of golang API.
func generateExtensions(cert *x509.Certificate) ([]pkix.Extension, error) {
newCert := &x509.Certificate{
SerialNumber: big.NewInt(1),
PermittedDNSDomainsCritical: cert.PermittedDNSDomainsCritical,
PermittedDNSDomains: cert.PermittedDNSDomains,
PermittedURIDomains: cert.PermittedURIDomains,
PermittedIPRanges: cert.PermittedIPRanges,
PermittedEmailAddresses: cert.PermittedEmailAddresses,
ExcludedDNSDomains: cert.ExcludedDNSDomains,
ExcludedURIDomains: cert.ExcludedURIDomains,
ExcludedIPRanges: cert.ExcludedIPRanges,
ExcludedEmailAddresses: cert.ExcludedEmailAddresses,
}
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
// generated and reparse so that the extension field is populated.
certBytes, err := x509.CreateCertificate(rand.Reader, newCert, newCert, &priv.PublicKey, priv)
if err != nil {
return nil, err
}
newCert, err = x509.ParseCertificate(certBytes)
return newCert.Extensions, err
}
func convertExtensions(in []pkix.Extension) []signer.Extension {
out := make([]signer.Extension, 0, len(in))
for _, e := range in {
out = append(out, signer.Extension{
ID: config.OID(e.Id),
Critical: e.Critical,
Value: hex.EncodeToString(e.Value),
})
}
return out
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment