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 }