diff --git a/README.md b/README.md
index 4430cc03..4b90794e 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
mkcert is a simple tool for making locally-trusted development certificates. It requires no configuration.
-```
+```sh
$ mkcert -install
Created a new local CA 💥
The local CA is now installed in the system trust store! ⚡️
@@ -21,8 +21,6 @@ Created a new certificate valid for the following names 📜
The certificate is at "./example.com+5.pem" and the key at "./example.com+5-key.pem" ✅
```
-

-
Using certificates from real certificate authorities (CAs) for development can be dangerous or impossible (for hosts like `example.test`, `localhost` or `127.0.0.1`), but self-signed certificates cause trust errors. Managing your own CA is the best solution, but usually involves arcane commands, specialized knowledge and manual steps.
mkcert automatically creates and installs a local CA in the system root store, and generates locally-trusted certificates. mkcert does not automatically configure servers to use the certificates, though, that's up to you.
@@ -35,14 +33,14 @@ mkcert automatically creates and installs a local CA in the system root store, a
On macOS, use [Homebrew](https://brew.sh/)
-```
+```sh
brew install mkcert
brew install nss # if you use Firefox
```
or [MacPorts](https://www.macports.org/).
-```
+```sh
sudo port selfupdate
sudo port install mkcert
sudo port install nss # if you use Firefox
@@ -52,7 +50,7 @@ sudo port install nss # if you use Firefox
On Linux, first install `certutil`.
-```
+```sh
sudo apt install libnss3-tools
-or-
sudo yum install nss-tools
@@ -64,20 +62,20 @@ sudo zypper install mozilla-nss-tools
Then you can install using [Homebrew on Linux](https://docs.brew.sh/Homebrew-on-Linux)
-```
+```sh
brew install mkcert
```
or build from source (requires Go 1.13+)
-```
+```sh
git clone https://github.com/FiloSottile/mkcert && cd mkcert
go build -ldflags "-X main.Version=$(git describe --tags)"
```
or use [the pre-built binaries](https://github.com/FiloSottile/mkcert/releases).
-```
+```sh
curl -JLO "https://dl.filippo.io/mkcert/latest?for=linux/amd64"
chmod +x mkcert-v*-linux-amd64
sudo cp mkcert-v*-linux-amd64 /usr/local/bin/mkcert
@@ -85,7 +83,7 @@ sudo cp mkcert-v*-linux-amd64 /usr/local/bin/mkcert
For Arch Linux users, [`mkcert`](https://archlinux.org/packages/extra/x86_64/mkcert/) is available on the official Arch Linux repository.
-```
+```sh
sudo pacman -Syu mkcert
```
@@ -93,13 +91,13 @@ sudo pacman -Syu mkcert
On Windows, use [Chocolatey](https://chocolatey.org)
-```
+```sh
choco install mkcert
```
or use Scoop
-```
+```sh
scoop bucket add extras
scoop install mkcert
```
@@ -115,9 +113,9 @@ mkcert supports the following root stores:
* macOS system store
* Windows system store
* Linux variants that provide either
- * `update-ca-trust` (Fedora, RHEL, CentOS) or
- * `update-ca-certificates` (Ubuntu, Debian, OpenSUSE, SLES) or
- * `trust` (Arch)
+ * `update-ca-trust` (Fedora, RHEL, CentOS) or
+ * `update-ca-certificates` (Ubuntu, Debian, OpenSUSE, SLES) or
+ * `trust` (Arch)
* Firefox (macOS and Linux only)
* Chrome and Chromium
* Java (when `JAVA_HOME` is set)
@@ -128,38 +126,52 @@ To only install the local root CA into a subset of them, you can set the `TRUST_
### Advanced options
-```
- -cert-file FILE, -key-file FILE, -p12-file FILE
- Customize the output paths.
+```text
+ -cert-file FILE, -key-file FILE, -p12-file FILE
+ Customize the output paths.
+
+ -client
+ Generate a certificate for client authentication.
+
+ -ecdsa
+ Generate a certificate with an ECDSA key.
- -client
- Generate a certificate for client authentication.
+ -pkcs12
+ Generate a ".p12" PKCS #12 file, also know as a ".pfx" file,
+ containing certificate and key for legacy applications.
- -ecdsa
- Generate a certificate with an ECDSA key.
+ -csr CSR
+ Generate a certificate based on the supplied CSR. Conflicts with
+ all other flags and arguments except -install and -cert-file.
- -pkcs12
- Generate a ".p12" PKCS #12 file, also know as a ".pfx" file,
- containing certificate and key for legacy applications.
+ -validity DURATION, -cert-validity DURATION
+ Set custom validity for the certificate (e.g. 2y3m, 365d, 30d, 24h).
+ Defaults to 2 years and 3 months.
- -csr CSR
- Generate a certificate based on the supplied CSR. Conflicts with
- all other flags and arguments except -install and -cert-file.
+ -days DAYS
+ Set custom certificate validity in days (alias for -validity DAYSd).
+
+ -ca-validity DURATION
+ Set custom validity for the CA certificate when creating a new CA
+ (e.g. 10y, 5y, 3650d). Defaults to 10 years.
```
> **Note:** You _must_ place these options before the domain names list.
#### Example
-```
+```sh
mkcert -key-file key.pem -cert-file cert.pem example.com *.example.com
+mkcert -validity 30d example.com
+mkcert -days 90 example.com
+mkcert -ca-validity 5y -install
```
### S/MIME
mkcert automatically generates an S/MIME certificate if one of the supplied names is an email address.
-```
+```sh
mkcert filippo@example.com
```
@@ -175,7 +187,7 @@ For Android, you will have to install the CA and then enable user roots in the d
Node does not use the system root store, so it won't accept mkcert certificates automatically. Instead, you will have to set the [`NODE_EXTRA_CA_CERTS`](https://nodejs.org/api/cli.html#cli_node_extra_ca_certs_file) environment variable.
-```
+```sh
export NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem"
```
@@ -194,4 +206,4 @@ Installing in the trust store does not require the CA key, so you can export the
* set `$CAROOT` to its directory
* run `mkcert -install`
-Remember that mkcert is meant for development purposes, not production, so it should not be used on end users' machines, and that you should *not* export or share `rootCA-key.pem`.
+Remember that mkcert is meant for development purposes, not production, so it should not be used on end users' machines, and that you should _not_ export or share `rootCA-key.pem`.
diff --git a/cert.go b/cert.go
index 4ce36ccf..b76502cb 100644
--- a/cert.go
+++ b/cert.go
@@ -56,10 +56,11 @@ func (m *mkcert) makeCert(hosts []string) {
fatalIfErr(err, "failed to generate certificate key")
pub := priv.(crypto.Signer).Public()
- // Certificates last for 2 years and 3 months, which is always less than
+ // Certificates last for 2 years and 3 months by default, which is always less than
// 825 days, the limit that macOS/iOS apply to all certificates,
// including custom roots. See https://support.apple.com/en-us/HT210176.
- expiration := time.Now().AddDate(2, 3, 0)
+ notBefore := time.Now()
+ expiration := m.certExpiration(notBefore)
tpl := &x509.Certificate{
SerialNumber: randomSerialNumber(),
@@ -68,7 +69,7 @@ func (m *mkcert) makeCert(hosts []string) {
OrganizationalUnit: []string{userAndHostname},
},
- NotBefore: time.Now(), NotAfter: expiration,
+ NotBefore: notBefore, NotAfter: expiration,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
}
@@ -225,13 +226,14 @@ func (m *mkcert) makeCertFromCSR() {
fatalIfErr(err, "failed to parse the CSR")
fatalIfErr(csr.CheckSignature(), "invalid CSR signature")
- expiration := time.Now().AddDate(2, 3, 0)
+ notBefore := time.Now()
+ expiration := m.certExpiration(notBefore)
tpl := &x509.Certificate{
SerialNumber: randomSerialNumber(),
Subject: csr.Subject,
ExtraExtensions: csr.Extensions, // includes requested SANs, KUs and EKUs
- NotBefore: time.Now(), NotAfter: expiration,
+ NotBefore: notBefore, NotAfter: expiration,
// If the CSR does not request a SAN extension, fix it up for them as
// the Common Name field does not work in modern browsers. Otherwise,
@@ -282,6 +284,8 @@ func (m *mkcert) makeCertFromCSR() {
func (m *mkcert) loadCA() {
if !pathExists(filepath.Join(m.CAROOT, rootName)) {
m.newCA()
+ } else if m.caValidity != "" {
+ log.Println("Note: the local CA already exists, -ca-validity was ignored (to recreate the CA, delete rootCA.pem and rootCA-key.pem from CAROOT)")
}
certPEMBlock, err := ioutil.ReadFile(filepath.Join(m.CAROOT, rootName))
@@ -324,6 +328,9 @@ func (m *mkcert) newCA() {
skid := sha1.Sum(spki.SubjectPublicKey.Bytes)
+ notBefore := time.Now()
+ expiration := m.caExpiration(notBefore)
+
tpl := &x509.Certificate{
SerialNumber: randomSerialNumber(),
Subject: pkix.Name{
@@ -337,8 +344,8 @@ func (m *mkcert) newCA() {
},
SubjectKeyId: skid[:],
- NotAfter: time.Now().AddDate(10, 0, 0),
- NotBefore: time.Now(),
+ NotAfter: expiration,
+ NotBefore: notBefore,
KeyUsage: x509.KeyUsageCertSign,
@@ -366,3 +373,24 @@ func (m *mkcert) newCA() {
func (m *mkcert) caUniqueName() string {
return "mkcert development CA " + m.caCert.SerialNumber.String()
}
+
+func (m *mkcert) certExpiration(notBefore time.Time) time.Time {
+ if m.certValidity != "" {
+ dur, err := parseValidity(m.certValidity)
+ fatalIfErr(err, "invalid certificate validity")
+ return dur.apply(notBefore)
+ }
+ // Certificates last for 2 years and 3 months, which is always less than
+ // 825 days, the limit that macOS/iOS apply to all certificates,
+ // including custom roots. See https://support.apple.com/en-us/HT210176.
+ return notBefore.AddDate(2, 3, 0)
+}
+
+func (m *mkcert) caExpiration(notBefore time.Time) time.Time {
+ if m.caValidity != "" {
+ dur, err := parseValidity(m.caValidity)
+ fatalIfErr(err, "invalid CA validity")
+ return dur.apply(notBefore)
+ }
+ return notBefore.AddDate(10, 0, 0)
+}
diff --git a/main.go b/main.go
index 6c5e835b..309a1e9b 100644
--- a/main.go
+++ b/main.go
@@ -21,6 +21,7 @@ import (
"regexp"
"runtime"
"runtime/debug"
+ "strconv"
"strings"
"sync"
@@ -65,6 +66,17 @@ const advancedUsage = `Advanced options:
Generate a certificate based on the supplied CSR. Conflicts with
all other flags and arguments except -install and -cert-file.
+ -validity DURATION, -cert-validity DURATION
+ Set custom validity for the certificate (e.g. 2y3m, 365d, 30d, 24h).
+ Defaults to 2 years and 3 months.
+
+ -days DAYS
+ Set custom certificate validity in days (alias for -validity DAYSd).
+
+ -ca-validity DURATION
+ Set custom validity for the CA certificate when creating a new CA
+ (e.g. 10y, 5y, 3650d). Defaults to 10 years.
+
-CAROOT
Print the CA certificate and key storage location.
@@ -72,6 +84,12 @@ const advancedUsage = `Advanced options:
Set the CA certificate and key storage location. (This allows
maintaining multiple local CAs in parallel.)
+ $CA_VALIDITY (environment variable)
+ Set custom validity for the CA certificate when creating a new CA.
+
+ $CERT_VALIDITY (environment variable)
+ Set custom validity for certificates.
+
$TRUST_STORES (environment variable)
A comma-separated list of trust stores to install the local
root CA into. Options are: "system", "java" and "nss" (includes
@@ -99,10 +117,14 @@ func main() {
helpFlag = flag.Bool("help", false, "")
carootFlag = flag.Bool("CAROOT", false, "")
csrFlag = flag.String("csr", "", "")
- certFileFlag = flag.String("cert-file", "", "")
- keyFileFlag = flag.String("key-file", "", "")
- p12FileFlag = flag.String("p12-file", "", "")
- versionFlag = flag.Bool("version", false, "")
+ certFileFlag = flag.String("cert-file", "", "")
+ keyFileFlag = flag.String("key-file", "", "")
+ p12FileFlag = flag.String("p12-file", "", "")
+ versionFlag = flag.Bool("version", false, "")
+ caValidityFlag = flag.String("ca-validity", "", "")
+ certValidityFlag = flag.String("cert-validity", "", "")
+ validityFlag = flag.String("validity", "", "")
+ daysFlag = flag.Int("days", 0, "")
)
flag.Usage = func() {
fmt.Fprint(flag.CommandLine.Output(), shortUsage)
@@ -142,10 +164,48 @@ func main() {
if *csrFlag != "" && flag.NArg() != 0 {
log.Fatalln("ERROR: can't specify extra arguments when using -csr")
}
+
+ caValidity := *caValidityFlag
+ if caValidity == "" {
+ caValidity = os.Getenv("CA_VALIDITY")
+ }
+ if caValidity != "" {
+ if _, err := parseValidity(caValidity); err != nil {
+ log.Fatalf("ERROR: invalid CA validity %q: %s\n", caValidity, err)
+ }
+ }
+
+ if *daysFlag < 0 {
+ log.Fatalln("ERROR: -days must be greater than zero")
+ }
+ if *daysFlag > 0 && (*certValidityFlag != "" || *validityFlag != "") {
+ log.Fatalln("ERROR: cannot specify both -days and -validity/-cert-validity")
+ }
+ if *certValidityFlag != "" && *validityFlag != "" && *certValidityFlag != *validityFlag {
+ log.Fatalln("ERROR: cannot specify differing values for -cert-validity and -validity")
+ }
+
+ certValidity := *certValidityFlag
+ if certValidity == "" {
+ certValidity = *validityFlag
+ }
+ if certValidity == "" && *daysFlag > 0 {
+ certValidity = strconv.Itoa(*daysFlag) + "d"
+ }
+ if certValidity == "" {
+ certValidity = os.Getenv("CERT_VALIDITY")
+ }
+ if certValidity != "" {
+ if _, err := parseValidity(certValidity); err != nil {
+ log.Fatalf("ERROR: invalid certificate validity %q: %s\n", certValidity, err)
+ }
+ }
+
(&mkcert{
installMode: *installFlag, uninstallMode: *uninstallFlag, csrPath: *csrFlag,
pkcs12: *pkcs12Flag, ecdsa: *ecdsaFlag, client: *clientFlag,
certFile: *certFileFlag, keyFile: *keyFileFlag, p12File: *p12FileFlag,
+ caValidity: caValidity, certValidity: certValidity,
}).Run(flag.Args())
}
@@ -157,6 +217,7 @@ type mkcert struct {
pkcs12, ecdsa, client bool
keyFile, certFile, p12File string
csrPath string
+ caValidity, certValidity string
CAROOT string
caCert *x509.Certificate
diff --git a/mise.toml b/mise.toml
new file mode 100644
index 00000000..a015672e
--- /dev/null
+++ b/mise.toml
@@ -0,0 +1,2 @@
+[tools]
+go = "1.27.1"
diff --git a/validity.go b/validity.go
new file mode 100644
index 00000000..c564d442
--- /dev/null
+++ b/validity.go
@@ -0,0 +1,94 @@
+// Copyright 2018 The mkcert Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+)
+
+type validityDuration struct {
+ years int
+ months int
+ days int
+ timeDur time.Duration
+}
+
+func (v validityDuration) apply(t time.Time) time.Time {
+ return t.AddDate(v.years, v.months, v.days).Add(v.timeDur)
+}
+
+var (
+ validityPartPattern = regexp.MustCompile(`(?i)^(\d+)\s*([a-z]+)$`)
+ validityTokenizer = regexp.MustCompile(`(?i)\d+\s*[a-z]+`)
+)
+
+func parseValidity(s string) (validityDuration, error) {
+ s = strings.TrimSpace(s)
+ if s == "" {
+ return validityDuration{}, fmt.Errorf("validity duration cannot be empty")
+ }
+
+ // Verify that the string is completely covered by valid component tokens
+ matches := validityTokenizer.FindAllStringIndex(s, -1)
+ if len(matches) == 0 {
+ return validityDuration{}, fmt.Errorf("invalid validity format %q (examples: 10y, 2y3m, 365d, 30d, 24h)", s)
+ }
+
+ // Check for extraneous characters between or around matches
+ lastIdx := 0
+ for _, idx := range matches {
+ gap := strings.TrimSpace(s[lastIdx:idx[0]])
+ if gap != "" {
+ return validityDuration{}, fmt.Errorf("invalid characters %q in validity %q", gap, s)
+ }
+ lastIdx = idx[1]
+ }
+ remaining := strings.TrimSpace(s[lastIdx:])
+ if remaining != "" {
+ return validityDuration{}, fmt.Errorf("invalid characters %q in validity %q", remaining, s)
+ }
+
+ var res validityDuration
+ for _, idx := range matches {
+ token := s[idx[0]:idx[1]]
+ parts := validityPartPattern.FindStringSubmatch(token)
+ if len(parts) != 3 {
+ return validityDuration{}, fmt.Errorf("invalid validity component %q", token)
+ }
+
+ val, err := strconv.Atoi(parts[1])
+ if err != nil || val < 0 {
+ return validityDuration{}, fmt.Errorf("invalid number %q in validity %q", parts[1], s)
+ }
+
+ unit := strings.ToLower(parts[2])
+ switch unit {
+ case "y", "yr", "yrs", "year", "years":
+ res.years += val
+ case "mo", "mon", "mons", "month", "months", "m":
+ res.months += val
+ case "d", "day", "days":
+ res.days += val
+ case "h", "hr", "hrs", "hour", "hours":
+ res.timeDur += time.Duration(val) * time.Hour
+ case "min", "mins", "minute", "minutes":
+ res.timeDur += time.Duration(val) * time.Minute
+ case "s", "sec", "secs", "second", "seconds":
+ res.timeDur += time.Duration(val) * time.Second
+ default:
+ return validityDuration{}, fmt.Errorf("unknown unit %q in validity %q (supported units: y, mo/m, d, h, min, s)", unit, s)
+ }
+ }
+
+ if res.years == 0 && res.months == 0 && res.days == 0 && res.timeDur == 0 {
+ return validityDuration{}, fmt.Errorf("validity must be greater than zero")
+ }
+
+ return res, nil
+}
diff --git a/validity_test.go b/validity_test.go
new file mode 100644
index 00000000..695f3ca7
--- /dev/null
+++ b/validity_test.go
@@ -0,0 +1,387 @@
+// Copyright 2018 The mkcert Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "crypto/x509"
+ "encoding/pem"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "testing"
+ "time"
+)
+
+func TestParseValidity(t *testing.T) {
+ tests := []struct {
+ input string
+ wantErr bool
+ checkDur func(validityDuration) bool
+ description string
+ }{
+ {
+ input: "10y",
+ checkDur: func(v validityDuration) bool {
+ return v.years == 10 && v.months == 0 && v.days == 0 && v.timeDur == 0
+ },
+ description: "10 years",
+ },
+ {
+ input: "2y3m",
+ checkDur: func(v validityDuration) bool {
+ return v.years == 2 && v.months == 3 && v.days == 0 && v.timeDur == 0
+ },
+ description: "2 years 3 months",
+ },
+ {
+ input: "2y 3m",
+ checkDur: func(v validityDuration) bool {
+ return v.years == 2 && v.months == 3 && v.days == 0 && v.timeDur == 0
+ },
+ description: "2 years 3 months with space",
+ },
+ {
+ input: "2 years 3 months",
+ checkDur: func(v validityDuration) bool {
+ return v.years == 2 && v.months == 3 && v.days == 0 && v.timeDur == 0
+ },
+ description: "2 years 3 months full words",
+ },
+ {
+ input: "1y6mo15d",
+ checkDur: func(v validityDuration) bool {
+ return v.years == 1 && v.months == 6 && v.days == 15 && v.timeDur == 0
+ },
+ description: "composite 1y6mo15d",
+ },
+ {
+ input: "825d",
+ checkDur: func(v validityDuration) bool {
+ return v.years == 0 && v.months == 0 && v.days == 825 && v.timeDur == 0
+ },
+ description: "825 days",
+ },
+ {
+ input: "30d",
+ checkDur: func(v validityDuration) bool {
+ return v.years == 0 && v.months == 0 && v.days == 30 && v.timeDur == 0
+ },
+ description: "30 days",
+ },
+ {
+ input: "24h",
+ checkDur: func(v validityDuration) bool {
+ return v.years == 0 && v.months == 0 && v.days == 0 && v.timeDur == 24*time.Hour
+ },
+ description: "24 hours",
+ },
+ {
+ input: "90min",
+ checkDur: func(v validityDuration) bool {
+ return v.years == 0 && v.months == 0 && v.days == 0 && v.timeDur == 90*time.Minute
+ },
+ description: "90 minutes",
+ },
+ {
+ input: "30s",
+ checkDur: func(v validityDuration) bool {
+ return v.years == 0 && v.months == 0 && v.days == 0 && v.timeDur == 30*time.Second
+ },
+ description: "30 seconds",
+ },
+ // Invalid cases
+ {
+ input: "",
+ wantErr: true,
+ description: "empty string",
+ },
+ {
+ input: " ",
+ wantErr: true,
+ description: "whitespace only",
+ },
+ {
+ input: "0d",
+ wantErr: true,
+ description: "zero duration",
+ },
+ {
+ input: "0y0m0d",
+ wantErr: true,
+ description: "zero composite",
+ },
+ {
+ input: "10",
+ wantErr: true,
+ description: "missing unit",
+ },
+ {
+ input: "10x",
+ wantErr: true,
+ description: "unknown unit",
+ },
+ {
+ input: "2y 3x",
+ wantErr: true,
+ description: "partially unknown unit",
+ },
+ {
+ input: "-5d",
+ wantErr: true,
+ description: "negative duration",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ res, err := parseValidity(tt.input)
+ if (err != nil) != tt.wantErr {
+ t.Fatalf("parseValidity(%q) err = %v, wantErr = %v", tt.input, err, tt.wantErr)
+ }
+ if !tt.wantErr && tt.checkDur != nil && !tt.checkDur(res) {
+ t.Errorf("parseValidity(%q) returned unexpected result: %+v", tt.input, res)
+ }
+ })
+ }
+}
+
+func TestApplyValidity(t *testing.T) {
+ now := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
+
+ v, err := parseValidity("10y")
+ if err != nil {
+ t.Fatal(err)
+ }
+ expected10y := time.Date(2036, 1, 1, 12, 0, 0, 0, time.UTC)
+ if got := v.apply(now); !got.Equal(expected10y) {
+ t.Errorf("apply 10y: got %v, want %v", got, expected10y)
+ }
+
+ v2, err := parseValidity("2y3m")
+ if err != nil {
+ t.Fatal(err)
+ }
+ expected2y3m := time.Date(2028, 4, 1, 12, 0, 0, 0, time.UTC)
+ if got := v2.apply(now); !got.Equal(expected2y3m) {
+ t.Errorf("apply 2y3m: got %v, want %v", got, expected2y3m)
+ }
+
+ v3, err := parseValidity("30d")
+ if err != nil {
+ t.Fatal(err)
+ }
+ expected30d := time.Date(2026, 1, 31, 12, 0, 0, 0, time.UTC)
+ if got := v3.apply(now); !got.Equal(expected30d) {
+ t.Errorf("apply 30d: got %v, want %v", got, expected30d)
+ }
+}
+
+func readCertFromPEM(path string) (*x509.Certificate, error) {
+ b, err := ioutil.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
+ block, _ := pem.Decode(b)
+ return x509.ParseCertificate(block.Bytes)
+}
+
+func TestCAAndCertGenerationWithValidity(t *testing.T) {
+ tmpDir, err := ioutil.TempDir("", "mkcert-test-*")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(tmpDir)
+
+ // Test 1: Generate CA with custom validity 5y and cert with 30d
+ m := &mkcert{
+ CAROOT: tmpDir,
+ caValidity: "5y",
+ certValidity: "30d",
+ certFile: filepath.Join(tmpDir, "test.pem"),
+ keyFile: filepath.Join(tmpDir, "test-key.pem"),
+ }
+
+ m.loadCA()
+
+ // Check CA cert validity
+ caCertPath := filepath.Join(tmpDir, rootName)
+ caCert, err := readCertFromPEM(caCertPath)
+ if err != nil {
+ t.Fatalf("failed to read CA cert: %v", err)
+ }
+
+ expectedCAExpire := caCert.NotBefore.AddDate(5, 0, 0)
+ if !caCert.NotAfter.Equal(expectedCAExpire) {
+ t.Errorf("CA NotAfter: got %v, want %v", caCert.NotAfter, expectedCAExpire)
+ }
+
+ // Generate leaf cert
+ m.makeCert([]string{"example.com"})
+
+ leafCert, err := readCertFromPEM(m.certFile)
+ if err != nil {
+ t.Fatalf("failed to read leaf cert: %v", err)
+ }
+
+ expectedLeafExpire := leafCert.NotBefore.AddDate(0, 0, 30)
+ if !leafCert.NotAfter.Equal(expectedLeafExpire) {
+ t.Errorf("Leaf cert NotAfter: got %v, want %v", leafCert.NotAfter, expectedLeafExpire)
+ }
+}
+
+func TestDefaultValidity(t *testing.T) {
+ tmpDir, err := ioutil.TempDir("", "mkcert-default-test-*")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(tmpDir)
+
+ m := &mkcert{
+ CAROOT: tmpDir,
+ certFile: filepath.Join(tmpDir, "default.pem"),
+ keyFile: filepath.Join(tmpDir, "default-key.pem"),
+ }
+
+ m.loadCA()
+
+ caCert, err := readCertFromPEM(filepath.Join(tmpDir, rootName))
+ if err != nil {
+ t.Fatalf("failed to read CA cert: %v", err)
+ }
+
+ // Default CA validity is 10 years
+ now := time.Now()
+ expectedCAExpire := now.AddDate(10, 0, 0)
+ if diff := caCert.NotAfter.Sub(expectedCAExpire); diff < -5*time.Second || diff > 5*time.Second {
+ t.Errorf("Default CA NotAfter: got %v, want near %v", caCert.NotAfter, expectedCAExpire)
+ }
+
+ // Generate leaf cert with defaults
+ m.makeCert([]string{"default.example.com"})
+
+ leafCert, err := readCertFromPEM(m.certFile)
+ if err != nil {
+ t.Fatalf("failed to read leaf cert: %v", err)
+ }
+
+ // Default leaf cert validity is 2 years 3 months
+ expectedLeafExpire := now.AddDate(2, 3, 0)
+ if diff := leafCert.NotAfter.Sub(expectedLeafExpire); diff < -5*time.Second || diff > 5*time.Second {
+ t.Errorf("Default leaf cert NotAfter: got %v, want near %v", leafCert.NotAfter, expectedLeafExpire)
+ }
+}
+
+func TestCLIFlagsExecution(t *testing.T) {
+ tmpDir, err := ioutil.TempDir("", "mkcert-cli-test-*")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(tmpDir)
+
+ // Test with -ca-validity 1y and -days 90
+ certPath := filepath.Join(tmpDir, "cli.pem")
+ keyPath := filepath.Join(tmpDir, "cli-key.pem")
+
+ cmd := exec.Command("go", "run", ".", "-ca-validity", "1y", "-days", "90", "-cert-file", certPath, "-key-file", keyPath, "cli.example.com")
+ cmd.Env = append(os.Environ(), "CAROOT="+tmpDir)
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("CLI command failed: %v, output: %s", err, out)
+ }
+
+ // Verify CA validity is 1 year
+ caCert, err := readCertFromPEM(filepath.Join(tmpDir, rootName))
+ if err != nil {
+ t.Fatalf("failed to read CA cert: %v", err)
+ }
+ now := time.Now()
+ expectedCAExpire := now.AddDate(1, 0, 0)
+ if diff := caCert.NotAfter.Sub(expectedCAExpire); diff < -5*time.Second || diff > 5*time.Second {
+ t.Errorf("CLI CA NotAfter: got %v, want near %v", caCert.NotAfter, expectedCAExpire)
+ }
+
+ // Verify leaf cert validity is 90 days
+ leafCert, err := readCertFromPEM(certPath)
+ if err != nil {
+ t.Fatalf("failed to read leaf cert: %v", err)
+ }
+ expectedLeafExpire := now.AddDate(0, 0, 90)
+ if diff := leafCert.NotAfter.Sub(expectedLeafExpire); diff < -5*time.Second || diff > 5*time.Second {
+ t.Errorf("CLI leaf NotAfter: got %v, want near %v", leafCert.NotAfter, expectedLeafExpire)
+ }
+}
+
+func TestCLIFlagsErrorHandling(t *testing.T) {
+ tmpDir, err := ioutil.TempDir("", "mkcert-err-test-*")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(tmpDir)
+
+ // Test invalid ca-validity
+ cmd := exec.Command("go", "run", ".", "-ca-validity", "invalid", "example.com")
+ cmd.Env = append(os.Environ(), "CAROOT="+tmpDir)
+ if err := cmd.Run(); err == nil {
+ t.Error("expected error for invalid -ca-validity, got success")
+ }
+
+ // Test conflicting -days and -cert-validity
+ cmd = exec.Command("go", "run", ".", "-days", "30", "-cert-validity", "60d", "example.com")
+ cmd.Env = append(os.Environ(), "CAROOT="+tmpDir)
+ if err := cmd.Run(); err == nil {
+ t.Error("expected error for conflicting -days and -cert-validity, got success")
+ }
+
+ // Test negative -days
+ cmd = exec.Command("go", "run", ".", "-days", "-5", "example.com")
+ cmd.Env = append(os.Environ(), "CAROOT="+tmpDir)
+ if err := cmd.Run(); err == nil {
+ t.Error("expected error for negative -days, got success")
+ }
+}
+
+func TestCLIEnvironmentVariables(t *testing.T) {
+ tmpDir, err := ioutil.TempDir("", "mkcert-env-test-*")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(tmpDir)
+
+ certPath := filepath.Join(tmpDir, "env.pem")
+ keyPath := filepath.Join(tmpDir, "env-key.pem")
+
+ cmd := exec.Command("go", "run", ".", "-cert-file", certPath, "-key-file", keyPath, "env.example.com")
+ cmd.Env = append(os.Environ(),
+ "CAROOT="+tmpDir,
+ "CA_VALIDITY=3y",
+ "CERT_VALIDITY=45d",
+ )
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("command failed: %v, output: %s", err, out)
+ }
+
+ // Check CA has 3y validity
+ caCert, err := readCertFromPEM(filepath.Join(tmpDir, rootName))
+ if err != nil {
+ t.Fatalf("failed to read CA cert: %v", err)
+ }
+ now := time.Now()
+ expectedCAExpire := now.AddDate(3, 0, 0)
+ if diff := caCert.NotAfter.Sub(expectedCAExpire); diff < -5*time.Second || diff > 5*time.Second {
+ t.Errorf("Env CA NotAfter: got %v, want near %v", caCert.NotAfter, expectedCAExpire)
+ }
+
+ // Check leaf has 45d validity
+ leafCert, err := readCertFromPEM(certPath)
+ if err != nil {
+ t.Fatalf("failed to read leaf cert: %v", err)
+ }
+ expectedLeafExpire := now.AddDate(0, 0, 45)
+ if diff := leafCert.NotAfter.Sub(expectedLeafExpire); diff < -5*time.Second || diff > 5*time.Second {
+ t.Errorf("Env leaf NotAfter: got %v, want near %v", leafCert.NotAfter, expectedLeafExpire)
+ }
+}