go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/cmd/cert-gen/main.go (about)

     1  /*
     2  
     3  Copyright (c) 2023 - Present. Will Charczuk. All rights reserved.
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository.
     5  
     6  */
     7  
     8  package main
     9  
    10  import (
    11  	"fmt"
    12  	"os"
    13  	"time"
    14  
    15  	"github.com/urfave/cli/v2"
    16  
    17  	"go.charczuk.com/sdk/certutil"
    18  	"go.charczuk.com/sdk/cliutil"
    19  )
    20  
    21  func main() {
    22  	app := &cli.App{
    23  		Name:  "cert-gen",
    24  		Usage: "Generate certificate authorities, client, and server certificates for development or production (maybe, no guarantees).",
    25  		Commands: []*cli.Command{
    26  			certificateAuthority,
    27  			server,
    28  			client,
    29  		},
    30  	}
    31  	if err := app.Run(os.Args); err != nil {
    32  		cliutil.Fatal(err)
    33  	}
    34  }
    35  
    36  var certificateAuthority = &cli.Command{
    37  	Name:    "certificate-authority",
    38  	Usage:   "Generate a certificate authority as a PEM encoded key pair",
    39  	Aliases: []string{"ca"},
    40  	Flags: []cli.Flag{
    41  		&cli.StringFlag{
    42  			Name:  "ca-cert",
    43  			Value: "ca.crt",
    44  			Usage: "the output path to the ca key pair certificate file in pem encoding",
    45  			// Required: true,
    46  		},
    47  		&cli.StringFlag{
    48  			Name:  "ca-key",
    49  			Value: "ca.key",
    50  			Usage: "the output path to the ca key pair key file in pem encoding",
    51  			// Required: true,
    52  		},
    53  		&cli.StringFlag{
    54  			Name:  "organization",
    55  			Value: "Gen Cert CA",
    56  			Usage: "the subject organization field value",
    57  			// Required: true,
    58  		},
    59  		&cli.StringFlag{
    60  			Name:  "organizational-unit",
    61  			Value: hostname(),
    62  			Usage: "the subject organizational unit field value",
    63  			// Required: true,
    64  		},
    65  		&cli.StringFlag{
    66  			Name:  "common-name",
    67  			Value: "gen cert " + hostname(),
    68  			Usage: "the subject common name field value",
    69  			// Required: true,
    70  		},
    71  		&cli.StringFlag{
    72  			Name:  "not-before",
    73  			Value: time.Now().UTC().AddDate(0, -1, 0).Format("2006-01-02"),
    74  			Usage: "the date to use as the not-before field value in the certificate. must be in 2006-01-02 format.",
    75  		},
    76  		&cli.StringFlag{
    77  			Name:  "not-after",
    78  			Value: time.Now().UTC().AddDate(10, 0, 0).Format("2006-01-02"),
    79  			Usage: "the date to use as the not-after field value in the certificate. must be in 2006-01-02 format.",
    80  		},
    81  	},
    82  	Action: func(ctx *cli.Context) error {
    83  		opts, err := resolveNotBeforeAfter(ctx)
    84  		if err != nil {
    85  			return err
    86  		}
    87  
    88  		opts = append(opts, certutil.OptSubjectOrganization(ctx.String("organization")),
    89  			certutil.OptSubjectOrganizationalUnit(ctx.String("organizational-unit")),
    90  			certutil.OptSubjectCommonName(ctx.String("common-name")),
    91  		)
    92  
    93  		ca, err := certutil.CreateCertificateAuthority(opts...)
    94  		if err != nil {
    95  			return err
    96  		}
    97  		if err = ca.WriteCertPemPath(ctx.String("ca-cert")); err != nil {
    98  			return err
    99  		}
   100  		return ca.WriteKeyPemPath(ctx.String("ca-key"))
   101  	},
   102  }
   103  
   104  var server = &cli.Command{
   105  	Name:  "server",
   106  	Usage: "Generate a server certificate as a PEM encoded key pair",
   107  	Flags: []cli.Flag{
   108  		&cli.StringFlag{
   109  			Name:  "ca-cert",
   110  			Value: "ca.crt",
   111  			Usage: "the input or output path to the ca key pair certificate file in pem encoding",
   112  		},
   113  		&cli.StringFlag{
   114  			Name:  "ca-key",
   115  			Value: "ca.key",
   116  			Usage: "the input or output path to the ca key pair key file in pem encoding",
   117  		},
   118  		&cli.BoolFlag{
   119  			Name:  "generate-ca",
   120  			Value: false,
   121  			Usage: "if we should generate a certificate authority or re-use an existing ca key pair",
   122  		},
   123  		&cli.StringFlag{
   124  			Name:  "organization",
   125  			Value: "Gen Cert Server",
   126  			Usage: "the subject organization field value",
   127  		},
   128  		&cli.StringFlag{
   129  			Name:  "organizational-unit",
   130  			Value: hostname(),
   131  			Usage: "the subject organizational unit field value",
   132  		},
   133  		&cli.StringFlag{
   134  			Name:  "common-name",
   135  			Value: "local.example.com",
   136  			Usage: "the common name is the main name the clients will check against the host they're requesting",
   137  		},
   138  		&cli.StringSliceFlag{
   139  			Name:  "dns-name",
   140  			Usage: "additional names to add to the certificate signing requests in addition to the common name",
   141  		},
   142  		&cli.StringSliceFlag{
   143  			Name:  "ip-san",
   144  			Usage: "additional ip subject alternate names to add to the certificate signing requests in addition to the common name",
   145  		},
   146  		&cli.StringFlag{
   147  			Name:  "cert-output",
   148  			Value: "tls.crt",
   149  			Usage: "the output path of the server certificate key pair certificate portion in pem encoding",
   150  		},
   151  		&cli.StringFlag{
   152  			Name:  "key-output",
   153  			Value: "tls.key",
   154  			Usage: "the output path of the server certificate key pair key portion in pem encoding",
   155  		},
   156  		&cli.StringFlag{
   157  			Name:  "not-before",
   158  			Value: time.Now().UTC().AddDate(0, -1, 0).Format("2006-01-02"),
   159  			Usage: "the date to use as the not-before field value in the certificate. must be in 2006-01-02 format.",
   160  		},
   161  		&cli.StringFlag{
   162  			Name:  "not-after",
   163  			Value: time.Now().UTC().AddDate(2, 3, 0).Format("2006-01-02"),
   164  			Usage: "the date to use as the not-after field value in the certificate. must be in 2006-01-02 format.",
   165  		},
   166  	},
   167  	Action: func(ctx *cli.Context) error {
   168  		ca, err := resolveCA(ctx)
   169  		if err != nil {
   170  			return err
   171  		}
   172  		opts, err := resolveNotBeforeAfter(ctx)
   173  		if err != nil {
   174  			return err
   175  		}
   176  		opts = append(opts,
   177  			certutil.OptSubjectOrganization(ctx.String("organization")),
   178  			certutil.OptSubjectOrganizationalUnit(ctx.String("organizational-unit")),
   179  		)
   180  		dnsNames := ctx.StringSlice("dns-name")
   181  		if len(dnsNames) > 0 {
   182  			opts = append(opts, certutil.OptDNSNames(dnsNames...))
   183  		}
   184  		ipSANs := ctx.StringSlice("ip-san")
   185  		if len(ipSANs) > 0 {
   186  			opts = append(opts, certutil.OptIPSANs(ipSANs...))
   187  		}
   188  		server, err := certutil.CreateServer(ctx.String("common-name"), ca, opts...)
   189  		if err != nil {
   190  			return err
   191  		}
   192  		if err = server.WriteCertPemPath(ctx.String("cert-output")); err != nil {
   193  			return err
   194  		}
   195  		return server.WriteKeyPemPath(ctx.String("key-output"))
   196  	},
   197  }
   198  
   199  var client = &cli.Command{
   200  	Name:  "client",
   201  	Usage: "Generate a client certificate as a PEM encoded key pair",
   202  	Flags: []cli.Flag{
   203  		&cli.StringFlag{
   204  			Name:  "ca-cert",
   205  			Value: "ca.crt",
   206  			Usage: "the input or output path to the ca key pair certificate file in pem encoding",
   207  		},
   208  		&cli.StringFlag{
   209  			Name:  "ca-key",
   210  			Value: "ca.key",
   211  			Usage: "the input or output path to the ca key pair key file in pem encoding",
   212  		},
   213  		&cli.BoolFlag{
   214  			Name:  "generate-ca",
   215  			Value: false,
   216  			Usage: "if we should generate a certificate authority or re-use an existing ca key pair",
   217  		},
   218  		&cli.StringFlag{
   219  			Name:  "organization",
   220  			Value: "Gen Cert Client",
   221  			Usage: "the subject organization field value",
   222  		},
   223  		&cli.StringFlag{
   224  			Name:  "organizational-unit",
   225  			Value: hostname(),
   226  			Usage: "the subject organizational unit field value",
   227  		},
   228  		&cli.StringFlag{
   229  			Name:  "common-name",
   230  			Value: "local-client.example.com",
   231  			Usage: "the subject common name is most often used in client certificates as the username value",
   232  		},
   233  		&cli.StringSliceFlag{
   234  			Name:  "dns-name",
   235  			Usage: "additional names to add to the certificate signing requests in addition to the common name",
   236  		},
   237  		&cli.StringSliceFlag{
   238  			Name:  "ip-san",
   239  			Usage: "additional ip subject alternate names to add to the certificate signing requests in addition to the common name",
   240  		},
   241  		&cli.StringFlag{
   242  			Name:  "cert-output",
   243  			Value: "client.tls.crt",
   244  			Usage: "the output path of the client certificate key pair certificate portion in pem encoding",
   245  		},
   246  		&cli.StringFlag{
   247  			Name:  "key-output",
   248  			Value: "client.tls.key",
   249  			Usage: "the output path of the client certificate key pair key portion in pem encoding",
   250  		},
   251  		&cli.StringFlag{
   252  			Name:  "not-before",
   253  			Value: time.Now().UTC().AddDate(0, -1, 0).Format("2006-01-02"),
   254  			Usage: "the date to use as the not-before field value in the certificate. must be in 2006-01-02 format.",
   255  		},
   256  		&cli.StringFlag{
   257  			Name:  "not-after",
   258  			Value: time.Now().UTC().AddDate(2, 3, 0).Format("2006-01-02"),
   259  			Usage: "the date to use as the not-after field value in the certificate. must be in 2006-01-02 format.",
   260  		},
   261  	},
   262  	Action: func(ctx *cli.Context) error {
   263  		ca, err := resolveCA(ctx)
   264  		if err != nil {
   265  			return err
   266  		}
   267  		opts, err := resolveNotBeforeAfter(ctx)
   268  		if err != nil {
   269  			return err
   270  		}
   271  
   272  		opts = append(opts,
   273  			certutil.OptSubjectOrganization(ctx.String("organization")),
   274  			certutil.OptSubjectOrganizationalUnit(ctx.String("organizational-unit")),
   275  		)
   276  
   277  		dnsNames := ctx.StringSlice("dns-name")
   278  		if len(dnsNames) > 0 {
   279  			opts = append(opts, certutil.OptDNSNames(dnsNames...))
   280  		}
   281  		ipSANs := ctx.StringSlice("ip-san")
   282  		if len(ipSANs) > 0 {
   283  			opts = append(opts, certutil.OptIPSANs(ipSANs...))
   284  		}
   285  		client, err := certutil.CreateClient(ctx.String("common-name"), ca, opts...)
   286  		if err != nil {
   287  			return err
   288  		}
   289  		if err = client.WriteCertPemPath(ctx.String("cert-output")); err != nil {
   290  			return err
   291  		}
   292  		if err = client.WriteKeyPemPath(ctx.String("key-output")); err != nil {
   293  			return err
   294  		}
   295  		return nil
   296  	},
   297  }
   298  
   299  func resolveNotBeforeAfter(ctx *cli.Context) ([]certutil.CertOption, error) {
   300  	notBefore, err := time.Parse("2006-01-02", ctx.String("not-before"))
   301  	if err != nil {
   302  		return nil, fmt.Errorf("invalid not-before; must be in 2006-01-02 format: %w", err)
   303  	}
   304  	notAfter, err := time.Parse("2006-01-02", ctx.String("not-after"))
   305  	if err != nil {
   306  		return nil, fmt.Errorf("invalid not-after; must be in 2006-01-02 format: %w", err)
   307  	}
   308  	if notBefore.IsZero() {
   309  		return nil, fmt.Errorf("invalid not-before; must not be zero")
   310  	}
   311  	if notAfter.IsZero() {
   312  		return nil, fmt.Errorf("invalid not-after; must not be zero")
   313  	}
   314  	if notBefore.After(notAfter) {
   315  		return nil, fmt.Errorf("invalid not-before and not-after; not-before must be before not-after")
   316  
   317  	}
   318  	return []certutil.CertOption{
   319  		certutil.OptNotBefore(notBefore),
   320  		certutil.OptNotAfter(notAfter),
   321  	}, nil
   322  }
   323  
   324  func resolveCA(ctx *cli.Context) (*certutil.CertBundle, error) {
   325  	var ca *certutil.CertBundle
   326  	var err error
   327  	shouldGenerateCA := ctx.Bool("generate-ca")
   328  	if shouldGenerateCA {
   329  		ca, err = certutil.CreateCertificateAuthority()
   330  	} else {
   331  		caCertInput := ctx.String("ca-cert")
   332  		caKeyInput := ctx.String("ca-key")
   333  		if caCertInput == "" || caKeyInput == "" {
   334  			return nil, fmt.Errorf("--ca-cert and --ca-key are both required to generate the ca")
   335  		}
   336  		ca, err = certutil.NewCertBundle(certutil.NewKeyPairFromPaths(caCertInput, caKeyInput))
   337  	}
   338  	if err != nil {
   339  		return nil, err
   340  	}
   341  	if shouldGenerateCA {
   342  		if err = ca.WriteCertPemPath(ctx.String("ca-cert")); err != nil {
   343  			return nil, err
   344  		}
   345  		if err = ca.WriteKeyPemPath(ctx.String("ca-key")); err != nil {
   346  			return nil, err
   347  		}
   348  	}
   349  	return ca, nil
   350  }
   351  
   352  func hostname() (hostname string) {
   353  	if h, err := os.Hostname(); err == nil {
   354  		hostname = h
   355  		return
   356  	}
   357  	hostname = "local"
   358  	return
   359  }