-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
342 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} | ||
|
||
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 | ||
} | ||
|
||
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.") | ||
fs.Var(newSignAlgValue(&co.SignatureAlgorithm), "sign-alg", "Signature Algorithm. See 'pcert list' for available algorithms.") | ||
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))) | ||
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))) | ||
fs.Var(newSubjectValue(&co.Subject), "subject", "Subject in the form '/C=CH/O=My Org/OU=My Team'.") | ||
|
||
//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.") | ||
fs.Var(newExtKeyUsageValue(&co.ExtKeyUsage), "ext-key-usage", "Set the extended key usage. See 'pcert list' for available extended key usages.") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |