Skip to content

Commit

Permalink
Add new pcert create command
Browse files Browse the repository at this point in the history
  • Loading branch information
dvob committed Jul 25, 2024
1 parent 22d40e3 commit 540e8d8
Show file tree
Hide file tree
Showing 2 changed files with 342 additions and 0 deletions.
158 changes: 158 additions & 0 deletions cmd/pcert2/certificate_options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package main

import (
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"fmt"
"math/big"
"net"
"net/url"
"time"

"github.com/spf13/pflag"
)

const (
defaultDuration = time.Hour * 24 * 365
)

func NewCertificate(opts *CertificateOptions) *x509.Certificate {
if opts == nil {
opts = &CertificateOptions{}
}
cert := &x509.Certificate{
SignatureAlgorithm: opts.SignatureAlgorithm,
SerialNumber: opts.SerialNumber,
Subject: opts.Subject,
NotBefore: opts.NotBefore,
NotAfter: opts.NotAfter,
KeyUsage: opts.KeyUsage,
ExtraExtensions: opts.ExtraExtensions,
ExtKeyUsage: opts.ExtKeyUsage,
UnknownExtKeyUsage: opts.UnknownExtKeyUsage,
BasicConstraintsValid: opts.BasicConstraintsValid,
IsCA: opts.IsCA,
SubjectKeyId: opts.SubjectKeyId,
AuthorityKeyId: opts.AuthorityKeyId,
OCSPServer: opts.OCSPServer,
IssuingCertificateURL: opts.IssuingCertificateURL,
DNSNames: opts.DNSNames,
EmailAddresses: opts.EmailAddresses,
IPAddresses: opts.IPAddresses,
URIs: opts.URIs,
PermittedDNSDomainsCritical: opts.PermittedDNSDomainsCritical,
PermittedDNSDomains: opts.PermittedDNSDomains,
ExcludedDNSDomains: opts.ExcludedDNSDomains,
PermittedIPRanges: opts.PermittedIPRanges,
ExcludedIPRanges: opts.ExcludedIPRanges,
PermittedEmailAddresses: opts.PermittedEmailAddresses,
ExcludedEmailAddresses: opts.ExcludedEmailAddresses,
PermittedURIDomains: opts.PermittedURIDomains,
ExcludedURIDomains: opts.ExcludedURIDomains,
CRLDistributionPoints: opts.CRLDistributionPoints,
PolicyIdentifiers: opts.PolicyIdentifiers,
Policies: opts.Policies,

Check failure on line 56 in cmd/pcert2/certificate_options.go

View workflow job for this annotation

GitHub Actions / test

unknown field Policies in struct literal of type x509.Certificate
}

if opts.MaxPathLen != nil {
cert.MaxPathLen = *opts.MaxPathLen
cert.MaxPathLenZero = true
}

if cert.NotBefore.IsZero() {
cert.NotBefore = time.Now()
}
if cert.NotAfter.IsZero() {
if opts.Expiry == 0 {
cert.NotAfter = cert.NotBefore.Add(defaultDuration)
} else {
cert.NotAfter = cert.NotBefore.Add(opts.Expiry)
}
}

if cert.SerialNumber == nil {
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
randomSerialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
panic("failed to obtain randomness:" + err.Error())
}
cert.SerialNumber = randomSerialNumber
}
return cert
}

// CertificateOptions represents all options which can be set using
// CreateCertificate (see Go docs of it). Further it offers Expiry to set a
// validity duration instead of absolute times.
type CertificateOptions struct {
SignatureAlgorithm x509.SignatureAlgorithm

SerialNumber *big.Int
Subject pkix.Name

NotBefore, NotAfter time.Time // Validity bounds.
Expiry time.Duration

KeyUsage x509.KeyUsage
ExtKeyUsage []x509.ExtKeyUsage // Sequence of extended key usages.

ExtraExtensions []pkix.Extension
UnknownExtKeyUsage []asn1.ObjectIdentifier // Encountered extended key usages unknown to this package.

BasicConstraintsValid bool
IsCA bool
// if nil MaxPathLen = 0, MaxPathLenZero = false
// else: MaxPathLen = *this, MaxPathLenZero = true
MaxPathLen *int

// if CA defaults to sha something something
SubjectKeyId []byte
// gets defaulted to parent.SubjectKeyID
AuthorityKeyId []byte

OCSPServer []string
IssuingCertificateURL []string

// SAN
DNSNames []string
EmailAddresses []string
IPAddresses []net.IP
URIs []*url.URL

// Name constraints
PermittedDNSDomainsCritical bool // if true then the name constraints are marked critical.
PermittedDNSDomains []string
ExcludedDNSDomains []string
PermittedIPRanges []*net.IPNet
ExcludedIPRanges []*net.IPNet
PermittedEmailAddresses []string
ExcludedEmailAddresses []string
PermittedURIDomains []string
ExcludedURIDomains []string

// CRL Distribution Points
CRLDistributionPoints []string

PolicyIdentifiers []asn1.ObjectIdentifier
Policies []x509.OID

Check failure on line 139 in cmd/pcert2/certificate_options.go

View workflow job for this annotation

GitHub Actions / test

undefined: x509.OID
}

func (co *CertificateOptions) BindFlags(fs *pflag.FlagSet) {
fs.StringSliceVar(&co.DNSNames, "dns", []string{}, "DNS subject alternative name.")
fs.StringSliceVar(&co.EmailAddresses, "email", []string{}, "Email subject alternative name.")
fs.IPSliceVar(&co.IPAddresses, "ip", []net.IP{}, "IP subject alternative name.")
fs.Var(newURISliceValue(&co.URIs), "uri", "URI subject alternative name.")

Check failure on line 146 in cmd/pcert2/certificate_options.go

View workflow job for this annotation

GitHub Actions / test

undefined: newURISliceValue
fs.Var(newSignAlgValue(&co.SignatureAlgorithm), "sign-alg", "Signature Algorithm. See 'pcert list' for available algorithms.")

Check failure on line 147 in cmd/pcert2/certificate_options.go

View workflow job for this annotation

GitHub Actions / test

undefined: newSignAlgValue
fs.Var(newTimeValue(&co.NotBefore), "not-before", fmt.Sprintf("Not valid before time in RFC3339 format (e.g. '%s').", time.Now().UTC().Format(time.RFC3339)))

Check failure on line 148 in cmd/pcert2/certificate_options.go

View workflow job for this annotation

GitHub Actions / test

undefined: newTimeValue
fs.Var(newTimeValue(&co.NotAfter), "not-after", fmt.Sprintf("Not valid after time in RFC3339 format (e.g. '%s').", time.Now().Add(time.Hour*24*60).UTC().Format(time.RFC3339)))

Check failure on line 149 in cmd/pcert2/certificate_options.go

View workflow job for this annotation

GitHub Actions / test

undefined: newTimeValue
fs.Var(newSubjectValue(&co.Subject), "subject", "Subject in the form '/C=CH/O=My Org/OU=My Team'.")

Check failure on line 150 in cmd/pcert2/certificate_options.go

View workflow job for this annotation

GitHub Actions / test

undefined: newSubjectValue

//fs.BoolVar(&co.BasicConstraintsValid, "basic-constraints", cert.BasicConstraintsValid, "Add basic constraints extension.")
//fs.BoolVar(&co.IsCA, "is-ca", cert.IsCA, "Mark certificate as CA in the basic constraints. Only takes effect if --basic-constraints is true.")
//fs.Var(newMaxPathLengthValue(co), "max-path-length", "Sets the max path length in the basic constraints.")

fs.Var(newKeyUsageValue(&co.KeyUsage), "key-usage", "Set the key usage. See 'pcert list' for available key usages.")

Check failure on line 156 in cmd/pcert2/certificate_options.go

View workflow job for this annotation

GitHub Actions / test

undefined: newKeyUsageValue
fs.Var(newExtKeyUsageValue(&co.ExtKeyUsage), "ext-key-usage", "Set the extended key usage. See 'pcert list' for available extended key usages.")

Check failure on line 157 in cmd/pcert2/certificate_options.go

View workflow job for this annotation

GitHub Actions / test

undefined: newExtKeyUsageValue
}
184 changes: 184 additions & 0 deletions cmd/pcert2/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
package main

import (
"crypto/rand"
"crypto/x509"
"fmt"
"io"
"os"
"path/filepath"
"strings"

"github.com/dvob/pcert"
"github.com/spf13/cobra"
"log/slog"
)

type createCommand struct {
Out io.Writer
In io.Writer

CertificateOutputLocation string
KeyOutputLocation string

SignCertificateLocation string
SignKeyLocation string

Profile []string
CertificateOptions CertificateOptions
KeyOptions pcert.KeyOptions
}

func getKeyRelativeToCert(certPath string) string {
outputDir := filepath.Dir(certPath)
certFileName := filepath.Base(certPath)
certExtension := filepath.Ext(certFileName)
keyFileName := strings.TrimSuffix(certFileName, certExtension) + ".key"

keyFilePath := filepath.Join(outputDir, keyFileName)
return keyFilePath
}

func newCreateCmd() *cobra.Command {
createCommand := &createCommand{
Out: os.Stdout,
In: os.Stdin,
CertificateOutputLocation: "",
KeyOutputLocation: "",
SignCertificateLocation: "",
SignKeyLocation: "",
CertificateOptions: CertificateOptions{},
KeyOptions: pcert.KeyOptions{},
}
cmd := &cobra.Command{
Use: "create [OUTPUT-CERTIFICATE [OUTPUT-KEY]]",
Short: "Create a key and certificate",
Long: `Creates a key and certificate. If OUTPUT-CERTIFICATE and OUTPUT-KEY are specified
the certificate and key are stored in the respective files. If only
OUTPUT-CERTIFICATE is specifed the key is stored next to the certificate. For
example the following invocation would store the certificate in tls.crt and the
key in tls.key:
pcert create tls.crt
`,
Args: cobra.MaximumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
// default key output file relative to certificate
if len(args) == 1 && args[0] != "-" {
createCommand.CertificateOutputLocation = args[0]
createCommand.KeyOutputLocation = getKeyRelativeToCert(args[0])
}
if len(args) == 2 {
createCommand.CertificateOutputLocation = args[0]
createCommand.KeyOutputLocation = args[1]
}

certTemplate := NewCertificate(&createCommand.CertificateOptions)

for _, p := range createCommand.Profile {
switch p {
case "client":
pcert.SetClientProfile(certTemplate)
case "server":
pcert.SetServerProfile(certTemplate)
case "ca":
pcert.SetCAProfile(certTemplate)
default:
return fmt.Errorf("unknown profile '%s'", p)
}
}

privateKey, publicKey, err := pcert.GenerateKey(createCommand.KeyOptions)
if err != nil {
return err
}

var (
stdin []byte
signCert *x509.Certificate
signKey any
)

// if set we sign certificate
if createCommand.SignCertificateLocation != "" {
slog.Info("process signer")
if createCommand.SignCertificateLocation == "-" {
stdin, err = io.ReadAll(os.Stdin)
if err != nil {
return err
}

slog.Info("read certificate from stdin")
signCert, err = pcert.Parse(stdin)
if err != nil {
return err
}
} else {
slog.Info("read certificate from file", "file", createCommand.SignCertificateLocation)
signCert, err = pcert.Load(createCommand.SignCertificateLocation)
if err != nil {
return err
}
}

if createCommand.SignKeyLocation == "" && createCommand.SignCertificateLocation != "-" {
slog.Info("read key from relatvie location", "file", getKeyRelativeToCert(createCommand.SignCertificateLocation))
signKey, err = pcert.LoadKey(getKeyRelativeToCert(createCommand.SignCertificateLocation))
if err != nil {
return err
}
} else if createCommand.SignKeyLocation == "" && createCommand.SignCertificateLocation == "-" {
slog.Info("read key from stdin")
signKey, err = pcert.ParseKey(stdin)
if err != nil {
return err
}
} else {
slog.Info("read key from file", "file", createCommand.SignKeyLocation)
signKey, err = pcert.LoadKey(createCommand.SignKeyLocation)
if err != nil {
return err
}
}
} else {
signCert = certTemplate
signKey = privateKey
}

certDER, err := x509.CreateCertificate(rand.Reader, certTemplate, signCert, publicKey, signKey)
if err != nil {
return err
}

certPEM := pcert.Encode(certDER)
keyPEM, err := pcert.EncodeKey(privateKey)
if err != nil {
return err
}

if createCommand.CertificateOutputLocation == "" || createCommand.CertificateOutputLocation == "-" {
createCommand.Out.Write(certPEM)
} else {
err := os.WriteFile(createCommand.CertificateOutputLocation, certPEM, 0664)
if err != nil {
return err
}
}

if createCommand.KeyOutputLocation == "" || createCommand.KeyOutputLocation == "-" {
createCommand.Out.Write(keyPEM)
} else {
err := os.WriteFile(createCommand.KeyOutputLocation, keyPEM, 0600)
if err != nil {
return err
}
}
return nil
},
}
cmd.Flags().StringVarP(&createCommand.SignCertificateLocation, "sign-cert", "s", createCommand.SignCertificateLocation, "Certificate used to sign. If not specified a self-signed certificate is created")
cmd.Flags().StringVar(&createCommand.SignKeyLocation, "sign-key", createCommand.SignKeyLocation, "Key used to sign. If not specified but --sign-cert is specified we use the key file relative to the certificate specified with --sign-cert.")
cmd.Flags().StringSliceVar(&createCommand.Profile, "profile", createCommand.Profile, "Certificates profiles to apply (server, client, ca)")
createCommand.CertificateOptions.BindFlags(cmd.Flags())
return cmd
}

0 comments on commit 540e8d8

Please sign in to comment.