diff --git a/testing/certutil/certutil.go b/testing/certutil/certutil.go index d8caec8..e45182d 100644 --- a/testing/certutil/certutil.go +++ b/testing/certutil/certutil.go @@ -40,19 +40,40 @@ type Pair struct { Key []byte } +type configs struct { + cnPrefix string + dnsNames []string +} + +type Option func(opt *configs) + +// WithCNPrefix adds cnPrefix as prefix for the CN. +func WithCNPrefix(cnPrefix string) Option { + return func(opt *configs) { + opt.cnPrefix = cnPrefix + } +} + +// WithDNSNames adds dnsNames to the DNSNames. +func WithDNSNames(dnsNames ...string) Option { + return func(opt *configs) { + opt.dnsNames = dnsNames + } +} + // NewRootCA generates a new x509 Certificate using ECDSA P-384 and returns: // - the private key // - the certificate // - the certificate and its key in PEM format as a byte slice. // // If any error occurs during the generation process, a non-nil error is returned. -func NewRootCA() (crypto.PrivateKey, *x509.Certificate, Pair, error) { +func NewRootCA(opts ...Option) (crypto.PrivateKey, *x509.Certificate, Pair, error) { rootKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) if err != nil { return nil, nil, Pair{}, fmt.Errorf("could not create private key: %w", err) } - cert, pair, err := newRootCert(rootKey, &rootKey.PublicKey) + cert, pair, err := newRootCert(rootKey, &rootKey.PublicKey, opts...) return rootKey, cert, pair, err } @@ -62,12 +83,12 @@ func NewRootCA() (crypto.PrivateKey, *x509.Certificate, Pair, error) { // - the certificate and its key in PEM format as a byte slice. // // If any error occurs during the generation process, a non-nil error is returned. -func NewRSARootCA() (crypto.PrivateKey, *x509.Certificate, Pair, error) { +func NewRSARootCA(opts ...Option) (crypto.PrivateKey, *x509.Certificate, Pair, error) { rootKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return nil, nil, Pair{}, fmt.Errorf("could not create private key: %w", err) } - cert, pair, err := newRootCert(rootKey, &rootKey.PublicKey) + cert, pair, err := newRootCert(rootKey, &rootKey.PublicKey, opts...) return rootKey, cert, pair, err } @@ -77,7 +98,36 @@ func NewRSARootCA() (crypto.PrivateKey, *x509.Certificate, Pair, error) { // - a Pair with the certificate and its key im PEM format // // If any error occurs during the generation process, a non-nil error is returned. -func GenerateChildCert(name string, ips []net.IP, caPrivKey crypto.PrivateKey, caCert *x509.Certificate) (*tls.Certificate, Pair, error) { +func GenerateChildCert(name string, ips []net.IP, caPrivKey crypto.PrivateKey, caCert *x509.Certificate, opts ...Option) (*tls.Certificate, Pair, error) { + priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + return nil, Pair{}, fmt.Errorf("could not create ECDSA private key: %w", err) + } + + cert, childPair, err := + GenerateGenericChildCert( + name, + ips, + priv, + &priv.PublicKey, + caPrivKey, + caCert, + opts...) + if err != nil { + return nil, Pair{}, fmt.Errorf( + "could not generate child TLS certificate CA: %w", err) + } + + return cert, childPair, nil +} + +// GenerateRSAChildCert generates a RSA with a 2048-bit key x509 Certificate as a +// child of caCert and returns the following: +// - the certificate and private key as a tls.Certificate +// - a Pair with the certificate and its key im PEM format +// +// If any error occurs during the generation process, a non-nil error is returned. +func GenerateRSAChildCert(name string, ips []net.IP, caPrivKey crypto.PrivateKey, caCert *x509.Certificate, opts ...Option) (*tls.Certificate, Pair, error) { priv, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return nil, Pair{}, fmt.Errorf("could not create RSA private key: %w", err) @@ -90,10 +140,11 @@ func GenerateChildCert(name string, ips []net.IP, caPrivKey crypto.PrivateKey, c priv, &priv.PublicKey, caPrivKey, - caCert) + caCert, + opts...) if err != nil { return nil, Pair{}, fmt.Errorf( - "could not generate child TLS certificate CA: %w", err) + "could not generate child TLS certificate: %w", err) } return cert, childPair, nil @@ -115,18 +166,26 @@ func GenerateGenericChildCert( priv crypto.PrivateKey, pub crypto.PublicKey, caPrivKey crypto.PrivateKey, - caCert *x509.Certificate) (*tls.Certificate, Pair, error) { + caCert *x509.Certificate, + opts ...Option) (*tls.Certificate, Pair, error) { + cfg := getCgf(opts) + + cn := "Police Public Call Box" + if cfg.cnPrefix != "" { + cn = fmt.Sprintf("[%s] %s", cfg.cnPrefix, cn) + } + dnsNames := append([]string{name}, cfg.dnsNames...) notBefore, notAfter := makeNotBeforeAndAfter() certTemplate := &x509.Certificate{ - DNSNames: []string{name}, + DNSNames: dnsNames, IPAddresses: ips, SerialNumber: big.NewInt(1658), Subject: pkix.Name{ Locality: []string{"anywhere in time and space"}, Organization: []string{"TARDIS"}, - CommonName: "Police Public Call Box", + CommonName: cn, }, NotBefore: notBefore, NotAfter: notAfter, @@ -220,7 +279,12 @@ func NewRSARootAndChildCerts() (Pair, Pair, error) { // - a Pair containing the certificate and private key in PEM format. // // If an error occurs during certificate creation, it returns a non-nil error. -func newRootCert(priv crypto.PrivateKey, pub crypto.PublicKey) (*x509.Certificate, Pair, error) { +func newRootCert(priv crypto.PrivateKey, pub crypto.PublicKey, opts ...Option) (*x509.Certificate, Pair, error) { + cn := "High Council" + cfg := getCgf(opts) + if cfg.cnPrefix != "" { + cn = fmt.Sprintf("[%s] %s", cfg.cnPrefix, cn) + } notBefore, notAfter := makeNotBeforeAndAfter() rootTemplate := x509.Certificate{ @@ -230,7 +294,7 @@ func newRootCert(priv crypto.PrivateKey, pub crypto.PublicKey) (*x509.Certificat Locality: []string{"The Capitol"}, OrganizationalUnit: []string{"Time Lords"}, Organization: []string{"High Council of the Time Lords"}, - CommonName: "High Council", + CommonName: cn, }, NotBefore: notBefore, NotAfter: notAfter, @@ -286,6 +350,14 @@ func newRootCert(priv crypto.PrivateKey, pub crypto.PublicKey) (*x509.Certificat }, nil } +func getCgf(opts []Option) configs { + cfg := configs{dnsNames: []string{}} + for _, opt := range opts { + opt(&cfg) + } + return cfg +} + // defaultChildCert generates a child certificate for localhost and 127.0.0.1. // It returns the certificate and its key as a Pair and an error if any happens. func defaultChildCert( diff --git a/testing/certutil/cmd/main.go b/testing/certutil/cmd/main.go index bd34829..c74d906 100644 --- a/testing/certutil/cmd/main.go +++ b/testing/certutil/cmd/main.go @@ -39,19 +39,20 @@ import ( ) func main() { - var caPath, caKeyPath, dest, name, ipList, filePrefix, pass string - var rsa bool + var caPath, caKeyPath, dest, name, ipList, prefix, pass string + var rsaflag bool flag.StringVar(&caPath, "ca", "", "File path for CA in PEM format") flag.StringVar(&caKeyPath, "ca-key", "", "File path for the CA key in PEM format") - flag.BoolVar(&rsa, "rsa", false, + flag.BoolVar(&rsaflag, "rsaflag", false, "") + // TODO: accept multiple DNS names flag.StringVar(&name, "name", "localhost", "used as \"distinguished name\" and \"Subject Alternate Name values\" for the child certificate") flag.StringVar(&ipList, "ips", "127.0.0.1", "a comma separated list of IP addresses for the child certificate") - flag.StringVar(&filePrefix, "prefix", "current timestamp", + flag.StringVar(&prefix, "prefix", "current timestamp", "a prefix to be added to the file name. If not provided a timestamp will be used") flag.StringVar(&pass, "pass", "", "a passphrase to encrypt the certificate key") @@ -64,10 +65,10 @@ func main() { caPath, caKeyPath) } - if filePrefix == "" { - filePrefix = fmt.Sprintf("%d", time.Now().Unix()) + if prefix == "current timestamp" { + prefix = fmt.Sprintf("%d", time.Now().Unix()) } - filePrefix += "-" + filePrefix := prefix + "-" wd, err := os.Getwd() if err != nil { @@ -81,8 +82,8 @@ func main() { netIPs = append(netIPs, net.ParseIP(ip)) } - rootCert, rootKey := getCA(rsa, caPath, caKeyPath, dest, filePrefix) - priv, pub := generateKey(rsa) + rootCert, rootKey := getCA(rsaflag, caPath, caKeyPath, dest, prefix) + priv, pub := generateKey(rsaflag) childCert, childPair, err := certutil.GenerateGenericChildCert( name, @@ -90,7 +91,8 @@ func main() { priv, pub, rootKey, - rootCert) + rootCert, + certutil.WithCNPrefix(prefix)) if err != nil { panic(fmt.Errorf("error generating child certificate: %w", err)) } @@ -114,7 +116,7 @@ func main() { } blockType := "EC PRIVATE KEY" - if rsa { + if rsaflag { blockType = "RSA PRIVATE KEY" } encPem, err := x509.EncryptPEMBlock( //nolint:staticcheck // we need to drop support for this, but while we don't, it needs to be tested. @@ -153,7 +155,7 @@ func generateKey(useRSA bool) (crypto.PrivateKey, crypto.PublicKey) { return priv, &priv.PublicKey } -func getCA(rsa bool, caPath, caKeyPath, dest, filePrefix string) (*x509.Certificate, crypto.PrivateKey) { +func getCA(rsa bool, caPath, caKeyPath, dest, prefix string) (*x509.Certificate, crypto.PrivateKey) { var rootCert *x509.Certificate var rootKey crypto.PrivateKey var err error @@ -165,12 +167,12 @@ func getCA(rsa bool, caPath, caKeyPath, dest, filePrefix string) (*x509.Certific } var pair certutil.Pair - rootKey, rootCert, pair, err = caFn() + rootKey, rootCert, pair, err = caFn(certutil.WithCNPrefix(prefix)) if err != nil { panic(fmt.Errorf("could not create root CA certificate: %w", err)) } - savePair(dest, filePrefix+"ca", pair) + savePair(dest, prefix+"-ca", pair) } else { rootKey, rootCert = loadCA(caPath, caKeyPath) } diff --git a/transport/tlscommon/tls_config.go b/transport/tlscommon/tls_config.go index 424a38f..09a2c8d 100644 --- a/transport/tlscommon/tls_config.go +++ b/transport/tlscommon/tls_config.go @@ -113,7 +113,7 @@ func (c *TLSConfig) ToConfig() *tls.Config { Certificates: c.Certificates, RootCAs: c.RootCAs, ClientCAs: c.ClientCAs, - InsecureSkipVerify: insecure, //nolint: gosec // we are using our own verification for now + InsecureSkipVerify: insecure, //nolint:gosec // we are using our own verification for now CipherSuites: convCipherSuites(c.CipherSuites), CurvePreferences: c.CurvePreferences, Renegotiation: c.Renegotiation, @@ -123,13 +123,13 @@ func (c *TLSConfig) ToConfig() *tls.Config { } } -// BuildModuleConfig takes the TLSConfig and transform it into a `tls.Config`. +// BuildModuleClientConfig takes the TLSConfig and transform it into a `tls.Config`. func (c *TLSConfig) BuildModuleClientConfig(host string) *tls.Config { if c == nil { // use default TLS settings, if config is empty. return &tls.Config{ ServerName: host, - InsecureSkipVerify: true, //nolint: gosec // we are using our own verification for now + InsecureSkipVerify: true, //nolint:gosec // we are using our own verification for now VerifyConnection: makeVerifyConnection(&TLSConfig{ Verification: VerifyFull, ServerName: host, @@ -154,15 +154,16 @@ func (c *TLSConfig) BuildModuleClientConfig(host string) *tls.Config { return config } -// BuildServerConfig takes the TLSConfig and transform it into a `tls.Config` for server side objects. +// BuildServerConfig takes the TLSConfig and transform it into a `tls.Config` +// for server side connections. func (c *TLSConfig) BuildServerConfig(host string) *tls.Config { if c == nil { // use default TLS settings, if config is empty. return &tls.Config{ ServerName: host, - InsecureSkipVerify: true, //nolint: gosec // we are using our own verification for now + InsecureSkipVerify: true, //nolint:gosec // we are using our own verification for now VerifyConnection: makeVerifyServerConnection(&TLSConfig{ - Verification: VerifyFull, + Verification: VerifyCertificate, ServerName: host, }), } @@ -285,27 +286,13 @@ func makeVerifyConnection(cfg *TLSConfig) func(tls.ConnectionState) error { func makeVerifyServerConnection(cfg *TLSConfig) func(tls.ConnectionState) error { switch cfg.Verification { - case VerifyFull: - return func(cs tls.ConnectionState) error { - if len(cs.PeerCertificates) == 0 { - if cfg.ClientAuth == tls.RequireAndVerifyClientCert { - return ErrMissingPeerCertificate - } - return nil - } - opts := x509.VerifyOptions{ - Roots: cfg.ClientCAs, - Intermediates: x509.NewCertPool(), - KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, - } - err := verifyCertsWithOpts(cs.PeerCertificates, cfg.CASha256, opts) - if err != nil { - return err - } - return verifyHostname(cs.PeerCertificates[0], cs.ServerName) - } - case VerifyCertificate: + // VerifyFull would attempt to match 'host' (c.ServerName) that is the host + // the client is trying to connect to with a DNS, IP or the CN from the + // client's certificate. Such validation, besides making no sense on the + // server side also causes errors as the client certificate usually does not + // contain a DNS, IP or CN matching the server's hostname. + case VerifyFull, VerifyCertificate: return func(cs tls.ConnectionState) error { if len(cs.PeerCertificates) == 0 { if cfg.ClientAuth == tls.RequireAndVerifyClientCert { @@ -331,7 +318,6 @@ func makeVerifyServerConnection(cfg *TLSConfig) func(tls.ConnectionState) error } return nil - } func verifyCertsWithOpts(certs []*x509.Certificate, casha256 []string, opts x509.VerifyOptions) error { diff --git a/transport/tlscommon/tls_config_test.go b/transport/tlscommon/tls_config_test.go index 07bb632..6b25619 100644 --- a/transport/tlscommon/tls_config_test.go +++ b/transport/tlscommon/tls_config_test.go @@ -71,14 +71,14 @@ func TestMakeVerifyServerConnection(t *testing.T) { expectedCallback: true, expectedError: x509.CertificateInvalidError{Cert: testCerts["expired"], Reason: x509.Expired}, }, - "default verification with certificates when required with incorrect server name in cert": { + "default verification with certificates when required do not verify hostname": { verificationMode: VerifyFull, clientAuth: tls.RequireAndVerifyClientCert, certAuthorities: certPool, peerCerts: []*x509.Certificate{testCerts["correct"]}, - serverName: "bad.example.com", + serverName: "some.example.com", expectedCallback: true, - expectedError: x509.HostnameError{Certificate: testCerts["correct"], Host: "bad.example.com"}, + expectedError: nil, }, "default verification with certificates when required with correct cert": { verificationMode: VerifyFull,