diff --git a/cmd/pcert/create.go b/cmd/pcert/create.go index d740beb..6ac2f42 100644 --- a/cmd/pcert/create.go +++ b/cmd/pcert/create.go @@ -1,78 +1,188 @@ 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.Reader + + CertificateOutputLocation string + KeyOutputLocation string + + SignCertificateLocation string + SignKeyLocation string + + Profiles []string + CertificateOptions pcert.CertificateOptions + KeyOptions pcert.KeyOptions +} + +func getKeyRelativeToFile(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 { - var ( - cert = &cert{ - cert: &x509.Certificate{}, - } - signPair = &signPair{} - key = &key{} - ) + createCommand := &createCommand{ + CertificateOutputLocation: "", + KeyOutputLocation: "", + SignCertificateLocation: "", + SignKeyLocation: "", + CertificateOptions: pcert.CertificateOptions{}, + KeyOptions: pcert.KeyOptions{}, + } cmd := &cobra.Command{ - Use: "create ", - Short: "Create a signed certificate and a key", - Long: `Creates a key and certificate. If --with or --sign-cert and --sign-key -are specified the certificate is signed by these. Otherwise it will be self-signed. -The argument is used as common name in the certificate if not overwritten -with the --subject option and as file name for the certificate (.crt) and -the key (.key).`, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - name := args[0] + 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: - if cert.cert.Subject.CommonName == "" { - cert.cert.Subject.CommonName = name +pcert create tls.crt +`, + Args: cobra.MaximumNArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + createCommand.In = cmd.InOrStdin() + createCommand.Out = cmd.OutOrStdout() + // default key output file relative to certificate + if len(args) == 1 && args[0] != "-" { + createCommand.CertificateOutputLocation = args[0] + createCommand.KeyOutputLocation = getKeyRelativeToFile(args[0]) } - - if cert.path == "" { - cert.path = name + certFileSuffix + if len(args) == 2 { + createCommand.CertificateOutputLocation = args[0] + createCommand.KeyOutputLocation = args[1] } - if key.path == "" { - key.path = name + keyFileSuffix - } + certTemplate := pcert.NewCertificate(&createCommand.CertificateOptions) - cert.configure() + for _, p := range createCommand.Profiles { + 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) + } + } - err := signPair.load() + privateKey, publicKey, err := pcert.GenerateKey(createCommand.KeyOptions) if err != nil { return err } - certDER, privateKey, err := pcert.CreateWithKeyOptions(cert.cert, key.opts, signPair.cert, signPair.key) + 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(createCommand.In) + 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", getKeyRelativeToFile(createCommand.SignCertificateLocation)) + signKey, err = pcert.LoadKey(getKeyRelativeToFile(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 } - certPEM := pcert.Encode(certDER) - - err = os.WriteFile(key.path, keyPEM, 0600) - if err != nil { - return fmt.Errorf("failed to write key '%s': %w", key.path, err) + if createCommand.CertificateOutputLocation == "" || createCommand.CertificateOutputLocation == "-" { + _, err := createCommand.Out.Write(certPEM) + if err != nil { + return err + } + } else { + err := os.WriteFile(createCommand.CertificateOutputLocation, certPEM, 0664) + if err != nil { + return err + } } - err = os.WriteFile(cert.path, certPEM, 0640) - if err != nil { - return fmt.Errorf("failed to write certificate '%s': %w", key.path, 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 }, } - cert.bindFlags(cmd) - key.bindFlags(cmd) - signPair.bindFlags(cmd) + 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.Profiles, "profile", createCommand.Profiles, "Certificates profiles to apply (server, client, ca)") + BindCertificateOptionsFlags(cmd.Flags(), &createCommand.CertificateOptions) + BindKeyFlags(cmd.Flags(), &createCommand.KeyOptions) return cmd } diff --git a/cmd/pcert/create2.go b/cmd/pcert/create2.go deleted file mode 100644 index 2f80c36..0000000 --- a/cmd/pcert/create2.go +++ /dev/null @@ -1,187 +0,0 @@ -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.Reader - - CertificateOutputLocation string - KeyOutputLocation string - - SignCertificateLocation string - SignKeyLocation string - - Profile []string - CertificateOptions pcert.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 newCreate2Cmd() *cobra.Command { - createCommand := &createCommand{ - CertificateOutputLocation: "", - KeyOutputLocation: "", - SignCertificateLocation: "", - SignKeyLocation: "", - CertificateOptions: pcert.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 { - createCommand.In = cmd.InOrStdin() - createCommand.Out = cmd.OutOrStdout() - // 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 := pcert.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(createCommand.In) - 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 == "-" { - _, err := createCommand.Out.Write(certPEM) - if err != nil { - return err - } - } 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)") - BindCertificateOptionsFlags(cmd.Flags(), &createCommand.CertificateOptions) - return cmd -} diff --git a/cmd/pcert/main.go b/cmd/pcert/main.go index e318de1..d1be64b 100644 --- a/cmd/pcert/main.go +++ b/cmd/pcert/main.go @@ -41,7 +41,7 @@ prefix (e.g PCERT_CERT instad of --cert).`, }, } cmd.AddCommand( - newCreate2Cmd(), + newCreateCmd(), newRequestCmd(), newSignCmd(), newShowCmd(), diff --git a/cmd/pcert/request.go b/cmd/pcert/request.go index fa37ce9..1e4c139 100644 --- a/cmd/pcert/request.go +++ b/cmd/pcert/request.go @@ -24,7 +24,7 @@ func newRequestCmd() *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 1 && args[0] != "-" { csrOutput = args[0] - keyOutput = getKeyRelativeToCert(args[0]) + keyOutput = getKeyRelativeToFile(args[0]) } if len(args) == 2 { diff --git a/cmd/pcert/sign.go b/cmd/pcert/sign.go index 508d902..4bcefaf 100644 --- a/cmd/pcert/sign.go +++ b/cmd/pcert/sign.go @@ -2,8 +2,9 @@ package main import ( "crypto/x509" + "fmt" + "io" "os" - "strings" "github.com/dvob/pcert" "github.com/spf13/cobra" @@ -11,53 +12,115 @@ import ( func newSignCmd() *cobra.Command { var ( - cert = &cert{ - cert: &x509.Certificate{}, - } - signPair = &signPair{} + profiles []string + + csrLocation string + + certOpts pcert.CertificateOptions + certLocation string + + defaultSignCertLocation = "ca.crt" + signCertLocation = defaultSignCertLocation + signKeyLocation string ) cmd := &cobra.Command{ - Use: "sign ", - Short: "Sign a CSR.", - Args: cobra.ExactArgs(1), + Use: "sign INPUT-CSR OUTPUT-CERTIFICATE", + Short: "Create a certificate based on a CSR", + Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) error { - csrFile := args[0] - name := csrFile + csrLocation = args[0] + certLocation = args[1] - if strings.HasSuffix(csrFile, csrFileSuffix) { - // myfile.csr -> myfile - name = csrFile[:len(csrFile)-len(csrFileSuffix)] + if signKeyLocation == "" && isFile(signCertLocation) { + signKeyLocation = getKeyRelativeToFile(signCertLocation) } - if cert.path == "" { - cert.path = name + certFileSuffix + var ( + stdin []byte + err error + csr *x509.CertificateRequest + signCert *x509.Certificate + signKey any + ) + + if !isFile(csrLocation) || !isFile(signCertLocation) || !isFile(signKeyLocation) { + stdin, err = io.ReadAll(cmd.InOrStdin()) + if err != nil { + return err + } } - cert.configure() + // CSR + if isFile(csrLocation) { + csr, err = pcert.LoadCSR(csrLocation) + if err != nil { + return err + } + } else { + csr, err = pcert.ParseCSR(stdin) + if err != nil { + return err + } + } - err := signPair.load() - if err != nil { - return err + // sign cert + if isFile(signCertLocation) { + signCert, err = pcert.Load(signCertLocation) + if os.IsNotExist(err) && signCertLocation == defaultSignCertLocation { + return fmt.Errorf("sign cert '%s' does not exist. set --sign-cert accordingly.", signCertLocation) + } else { + return err + } + } else { + signCert, err = pcert.Parse(stdin) + if err != nil { + return err + } } - csr, err := pcert.LoadCSR(csrFile) - if err != nil { - return err + // sign key + if isFile(signKeyLocation) { + signKey, err = pcert.LoadKey(signKeyLocation) + if err != nil { + return err + } + } else { + signKey, err = pcert.ParseKey(stdin) + if err != nil { + return err + } } - certDER, err := pcert.CreateCertificateWithCSR(csr, cert.cert, signPair.cert, signPair.key) + cert := pcert.NewCertificate(&certOpts) + + certDER, err := pcert.CreateCertificateWithCSR(csr, cert, signCert, signKey) if err != nil { return err } certPEM := pcert.Encode(certDER) - err = os.WriteFile(cert.path, certPEM, 0o640) - return err + if isFile(certLocation) { + err := os.WriteFile(certLocation, certPEM, 0640) + if err != nil { + return err + } + } else { + _, err := cmd.OutOrStdout().Write(certPEM) + if err != nil { + return err + } + } + + return nil }, } - cert.bindFlags(cmd) - signPair.bindFlags(cmd) + cmd.Flags().StringVarP(&signCertLocation, "sign-cert", "s", signCertLocation, "Certificate used to sign. If not specified a self-signed certificate is created") + cmd.Flags().StringVar(&signKeyLocation, "sign-key", 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(&profiles, "profile", profiles, "profile to set on the certificate (server, client, ca)") + BindCertificateOptionsFlags(cmd.Flags(), &certOpts) + return cmd } diff --git a/cmd/pcert/util.go b/cmd/pcert/util.go index f344217..b86bc38 100644 --- a/cmd/pcert/util.go +++ b/cmd/pcert/util.go @@ -1,104 +1,5 @@ package main -import ( - "crypto/x509" - "time" - - "github.com/dvob/pcert" - "github.com/spf13/cobra" -) - -type cert struct { - path string - cert *x509.Certificate - expiry time.Duration - ca bool - client bool - server bool -} - -func (c *cert) bindFlags(cmd *cobra.Command) { - cmd.Flags().BoolVar(&c.ca, "ca", false, "Set settings typical for a CA certificate.") - cmd.Flags().BoolVar(&c.server, "server", false, "Set settings typical for a server certificate.") - cmd.Flags().BoolVar(&c.client, "client", false, "Set settings typical for a client certificate.") - cmd.Flags().Var(newDurationValue(&c.expiry), "expiry", "Validity period of the certificate. If --not-after is set this option has no effect.") - cmd.Flags().StringVar(&c.path, "cert", "", "Output file for the certificate. Defaults to .crt") - - BindCertificateFlags(cmd.Flags(), c.cert) - RegisterCertificateCompletionFuncs(cmd) -} - -// set options on certificate -func (c *cert) configure() { - // profile - if c.ca { - pcert.SetCAProfile(c.cert) - } - - if c.server { - pcert.SetServerProfile(c.cert) - } - - if c.client { - pcert.SetClientProfile(c.cert) - } - - // expiry - if c.expiry == time.Duration(0) { - return - } - - if c.cert.NotBefore.IsZero() { - c.cert.NotBefore = time.Now() - } - - if c.cert.NotAfter.IsZero() { - c.cert.NotAfter = c.cert.NotBefore.Add(c.expiry) - return - } -} - -type key struct { - path string - opts pcert.KeyOptions -} - -func (k *key) bindFlags(cmd *cobra.Command) { - cmd.Flags().StringVar(&k.path, "key", "", "Output file for the key. Defaults to .key") - - BindKeyFlags(cmd.Flags(), &k.opts) - RegisterKeyCompletionFuncs(cmd) -} - -type signPair struct { - key any - keyFile string - cert *x509.Certificate - certFile string - shortPath string -} - -func (s *signPair) bindFlags(cmd *cobra.Command) { - cmd.Flags().StringVar(&s.certFile, "sign-cert", "", "Certificate used to sign the certificate") - cmd.Flags().StringVar(&s.keyFile, "sign-key", "", "Key used to sign the certificates") - cmd.Flags().StringVar(&s.shortPath, "with", "", "Specify a name of a key pair (.crt, .key) which you want to sign your certificate with. This can be used insted of --sign-cert and --sign-key") -} - -func (s *signPair) load() error { - var err error - if s.shortPath != "" { - s.certFile = s.shortPath + certFileSuffix - s.keyFile = s.shortPath + keyFileSuffix - } - - if s.certFile == "" && s.keyFile == "" { - return nil - } - - s.key, err = pcert.LoadKey(s.keyFile) - if err != nil { - return err - } - s.cert, err = pcert.Load(s.certFile) - return err +func isFile(name string) bool { + return name != "" && name != "-" }