package certs import ( "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/x509" "net" "os" "os/exec" "strings" "testing" "time" . "github.com/smartystreets/goconvey/convey" ) const testOrganization = "yoorie.de" func parseLeafCertificate(t *testing.T, gc *GenerateCertificate) *x509.Certificate { t.Helper() result, err := gc.GenerateTLSConfig() if err != nil { t.Fatalf("GenerateTLSConfig returned error: %v", err) } if result == nil || len(result.Certificates) != 1 { t.Fatalf("expected one certificate, got %#v", result) } if len(result.Certificates[0].Certificate) == 0 { t.Fatal("expected leaf certificate bytes") } leaf, err := x509.ParseCertificate(result.Certificates[0].Certificate[0]) if err != nil { t.Fatalf("ParseCertificate returned error: %v", err) } return leaf } func TestPublicKey(t *testing.T) { Convey("publicKey returns the matching public key type", t, func() { gc := &GenerateCertificate{} rsaKey, err := rsa.GenerateKey(rand.Reader, 1024) So(err, ShouldBeNil) So(gc.publicKey(rsaKey), ShouldResemble, &rsaKey.PublicKey) ecdsaKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) So(err, ShouldBeNil) So(gc.publicKey(ecdsaKey), ShouldResemble, &ecdsaKey.PublicKey) _, ed25519Key, err := ed25519.GenerateKey(rand.Reader) So(err, ShouldBeNil) So(gc.publicKey(ed25519Key), ShouldResemble, ed25519Key.Public().(ed25519.PublicKey)) So(gc.publicKey(struct{}{}), ShouldBeNil) }) } func TestGenerateTLSConfig(t *testing.T) { Convey("GenerateTLSConfig creates valid certificates for supported key types", t, func() { Convey("RSA certificates include configured SANs and CA settings", func() { validFrom := "Jan 2 15:04:05 2006" leaf := parseLeafCertificate(t, &GenerateCertificate{ Organization: testOrganization, Host: "127.0.0.1,example.com", ValidFrom: validFrom, ValidFor: 24 * time.Hour, IsCA: true, RSABits: 1024, }) expectedNotBefore, err := time.Parse("Jan 2 15:04:05 2006", validFrom) So(err, ShouldBeNil) So(leaf.PublicKeyAlgorithm, ShouldEqual, x509.RSA) So(leaf.Subject.Organization, ShouldResemble, []string{testOrganization}) So(leaf.IsCA, ShouldBeTrue) So(leaf.NotBefore.Equal(expectedNotBefore), ShouldBeTrue) So(leaf.NotAfter.Sub(leaf.NotBefore), ShouldEqual, 24*time.Hour) So(leaf.DNSNames, ShouldResemble, []string{"example.com"}) So(len(leaf.IPAddresses), ShouldEqual, 1) So(leaf.IPAddresses[0].Equal(net.ParseIP("127.0.0.1")), ShouldBeTrue) }) Convey("Ed25519 certificates are supported when explicitly requested", func() { leaf := parseLeafCertificate(t, &GenerateCertificate{ Organization: testOrganization, Host: "localhost", ValidFor: 12 * time.Hour, Ed25519Key: true, }) So(leaf.PublicKeyAlgorithm, ShouldEqual, x509.Ed25519) So(leaf.IsCA, ShouldBeFalse) So(leaf.DNSNames, ShouldResemble, []string{"localhost"}) }) Convey("all supported ECDSA curves generate server certificates", func() { curves := []string{"P224", "P256", "P384", "P521"} for _, curve := range curves { leaf := parseLeafCertificate(t, &GenerateCertificate{ Organization: testOrganization, Host: "certs.example.test", ValidFor: 6 * time.Hour, EcdsaCurve: curve, }) So(leaf.PublicKeyAlgorithm, ShouldEqual, x509.ECDSA) So(leaf.DNSNames, ShouldResemble, []string{"certs.example.test"}) } }) }) } func TestGenerateTLSConfigFatalScenarios(t *testing.T) { if testCase := os.Getenv("CERTS_FATAL_TEST_CASE"); testCase != "" { gc := &GenerateCertificate{RSABits: 1024, ValidFor: time.Hour} switch testCase { case "invalid-curve": gc.EcdsaCurve = "invalid" case "invalid-valid-from": gc.ValidFrom = "not-a-date" default: t.Fatalf("unknown fatal test case: %s", testCase) } _, _ = gc.GenerateTLSConfig() return } Convey("GenerateTLSConfig terminates on invalid input", t, func() { testCases := []struct { name string message string }{ {name: "invalid-curve", message: "Unrecognized elliptic curve"}, {name: "invalid-valid-from", message: "Failed to parse creation date"}, } for _, testCase := range testCases { cmd := exec.Command(os.Args[0], "-test.run", "^TestGenerateTLSConfigFatalScenarios$") cmd.Env = append(os.Environ(), "CERTS_FATAL_TEST_CASE="+testCase.name) output, err := cmd.CombinedOutput() So(err, ShouldNotBeNil) So(strings.Contains(string(output), testCase.message), ShouldBeTrue) } }) }