From 98b7bc7df86fcd813fa71f2858c1b35a5e2a7ea3 Mon Sep 17 00:00:00 2001 From: Taylor Frey Date: Tue, 6 Sep 2022 16:46:20 -0600 Subject: [PATCH 01/10] initial client certificate auth code for login --- .../certificate_auth/generate_certs.go | 355 ++++++++++++++++++ .../traffic_ops_golang/auth/authorize.go | 8 +- .../traffic_ops_golang/auth/certificate.go | 163 ++++++++ .../auth/certificate_test.go | 300 +++++++++++++++ .../auth/test/fail/rsa.key.pem | 51 +++ ...Reference-DigiCertTLSECCP384RootG5.crt.pem | 14 + ...Reference-DigiCertTLSRSA4096RootG5.crt.pem | 31 ++ .../test/success/ignored/ignoredfile.crt.pem | 0 .../auth/test/success/rootca.crt.pem | 32 ++ traffic_ops/traffic_ops_golang/login/login.go | 232 +++++++----- .../traffic_ops_golang/traffic_ops_golang.go | 5 +- 11 files changed, 1091 insertions(+), 100 deletions(-) create mode 100644 experimental/certificate_auth/generate_certs.go create mode 100644 traffic_ops/traffic_ops_golang/auth/certificate.go create mode 100644 traffic_ops/traffic_ops_golang/auth/certificate_test.go create mode 100644 traffic_ops/traffic_ops_golang/auth/test/fail/rsa.key.pem create mode 100644 traffic_ops/traffic_ops_golang/auth/test/success/Reference-DigiCertTLSECCP384RootG5.crt.pem create mode 100644 traffic_ops/traffic_ops_golang/auth/test/success/Reference-DigiCertTLSRSA4096RootG5.crt.pem create mode 100644 traffic_ops/traffic_ops_golang/auth/test/success/ignored/ignoredfile.crt.pem create mode 100644 traffic_ops/traffic_ops_golang/auth/test/success/rootca.crt.pem diff --git a/experimental/certificate_auth/generate_certs.go b/experimental/certificate_auth/generate_certs.go new file mode 100644 index 0000000000..52f7d22091 --- /dev/null +++ b/experimental/certificate_auth/generate_certs.go @@ -0,0 +1,355 @@ +package main + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "encoding/pem" + "fmt" + "io/ioutil" + "log" + "math" + "math/big" + "net" + "time" +) + +// Public and Private key pair for the certificate. +type CertificateKeyPair struct { + Certificate *x509.Certificate + PrivateKey *rsa.PrivateKey // TODO convert to ECDSA +} + +type CertificatePEMPair struct { + CertificatePEM, PrivateKeyPEM string +} + +func main() { + + rootCAPEMPair, err := GenerateRootCACertificate() + if err != nil { + log.Fatalf("Failed to generate and sign Root CA certificate\nErr: %s\n", err) + } + ioutil.WriteFile("rootca.crt.pem", []byte(rootCAPEMPair.CertificatePEM), 0644) + ioutil.WriteFile("rootca.key.pem", []byte(rootCAPEMPair.PrivateKeyPEM), 0644) + + serverPEMPair, err := GenerateServerCertificate(rootCAPEMPair) + if err != nil { + log.Fatalf("Failed to generate and sign Server certificate\nErr: %s\n", err) + } + + log.Println("Certificate: ", serverPEMPair.CertificatePEM) + ioutil.WriteFile("server.crt.pem", []byte(serverPEMPair.CertificatePEM), 0644) + ioutil.WriteFile("server.key.pem", []byte(serverPEMPair.PrivateKeyPEM), 0644) + + clientPEMPair, err := GenerateClientCertificate(rootCAPEMPair) + if err != nil { + log.Fatalf("Failed to generate and sign Client certificate\nErr: %s\n", err) + } + + log.Println("Certificate: ", clientPEMPair.CertificatePEM) + ioutil.WriteFile("client.crt.pem", []byte(clientPEMPair.CertificatePEM), 0644) + ioutil.WriteFile("client.key.pem", []byte(clientPEMPair.PrivateKeyPEM), 0644) + + if err := VerifyCertificates(rootCAPEMPair, clientPEMPair, serverPEMPair); err != nil { + log.Fatalf("failed to verify certificate: %s", err) + } +} + +func ParseCertificateKeyPair(pemPair *CertificatePEMPair) (*CertificateKeyPair, error) { + + keyPair := new(CertificateKeyPair) + + privPemBlock, _ := pem.Decode([]byte(pemPair.PrivateKeyPEM)) + + privateKey, err := x509.ParsePKCS8PrivateKey(privPemBlock.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse private key: %w", err) + } + + keyPair.PrivateKey = privateKey.(*rsa.PrivateKey) + + certPemBlock, _ := pem.Decode([]byte(pemPair.CertificatePEM)) + + certificate, err := x509.ParseCertificate(certPemBlock.Bytes) + if err != nil { + return nil, fmt.Errorf("failed to parse public key: %w", err) + } + + keyPair.Certificate = certificate + + return keyPair, nil +} + +func GenerateRootCACertificate() (*CertificatePEMPair, error) { + + now := time.Now() + + serialNumber, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) + if err != nil { + return nil, fmt.Errorf("failed to generate random serial number: %w", err) + } + + cert := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + OrganizationalUnit: []string{"ATC"}, + Organization: []string{"Apache"}, + Country: []string{"US"}, + Province: []string{"Colorado"}, + Locality: []string{"Denver"}, + CommonName: "root.local", + }, + NotBefore: now, + NotAfter: now.AddDate(1, 0, 0), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + } + + certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, fmt.Errorf("failed to generate RSA key: %w", err) + } + + certDERBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, &certPrivKey.PublicKey, certPrivKey) + if err != nil { + return nil, fmt.Errorf("failed to create certificate: %w", err) + } + + certPEMPair := new(CertificatePEMPair) + + certPEM := new(bytes.Buffer) + pem.Encode(certPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: certDERBytes, + }) + + certPrivKeyPEM := new(bytes.Buffer) + certPrivKeyByes, err := x509.MarshalPKCS8PrivateKey(certPrivKey) + if err != nil { + return nil, fmt.Errorf("failed to marshal private key to PKCS8: %w", err) + } + + pem.Encode(certPrivKeyPEM, &pem.Block{ + Type: "PRIVATE KEY", + Bytes: certPrivKeyByes, + }) + + certPEMPair.CertificatePEM = certPEM.String() + certPEMPair.PrivateKeyPEM = certPrivKeyPEM.String() + + return certPEMPair, nil +} + +// GenerateClientCertificate creates and signs a certificate based on the provided RootCA. This differs +// from the Server certificate in that it includes the OID for LDAP UID as well as Client Auth key usage. +// +// Currently the key is an RSA key, which also entails adding KeyEncipherment key usage. +// +// TODO: CommonName for client? +// TODO: Elliptic Curve instead of RSA (Remember to drop KeyEncipherment key usage as well) +func GenerateClientCertificate(root *CertificatePEMPair) (*CertificatePEMPair, error) { + + rootKeyPair, err := ParseCertificateKeyPair(root) + if err != nil { + log.Fatalln("Failed to parse root cert and key") + } + + now := time.Now() + + serialNumber, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) + if err != nil { + return nil, fmt.Errorf("failed to generate random serial number: %w", err) + } + + // LDAP OID reference: https://ldap.com/ldap-oid-reference-guide/ + // 0.9.2342.19200300.100.1.1 uid Attribute Type + uidPkix := pkix.AttributeTypeAndValue{ + Type: asn1.ObjectIdentifier([]int{0, 9, 2342, 19200300, 100, 1, 1}), + Value: "userid", + } + + cert := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + OrganizationalUnit: []string{"ATC"}, + Organization: []string{"Apache"}, + Country: []string{"US"}, + Province: []string{"Colorado"}, + Locality: []string{"Denver"}, + CommonName: "client.local", + ExtraNames: []pkix.AttributeTypeAndValue{uidPkix}, + }, + NotBefore: now, + NotAfter: now.AddDate(1, 0, 0), + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyAgreement, + } + + certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, fmt.Errorf("failed to generate RSA key: %w", err) + } + + certDERBytes, err := x509.CreateCertificate(rand.Reader, cert, rootKeyPair.Certificate, &certPrivKey.PublicKey, rootKeyPair.PrivateKey) + if err != nil { + return nil, fmt.Errorf("failed to create certificate: %w", err) + } + + certPEMPair := new(CertificatePEMPair) + + certPEM := new(bytes.Buffer) + pem.Encode(certPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: certDERBytes, + }) + + certPrivKeyPEM := new(bytes.Buffer) + certPrivKeyByes, err := x509.MarshalPKCS8PrivateKey(certPrivKey) + if err != nil { + return nil, fmt.Errorf("failed to marshal private key to PKCS8: %w", err) + } + + pem.Encode(certPrivKeyPEM, &pem.Block{ + Type: "PRIVATE KEY", + Bytes: certPrivKeyByes, + }) + + certPEMPair.CertificatePEM = certPEM.String() + certPEMPair.PrivateKeyPEM = certPrivKeyPEM.String() + + return certPEMPair, nil +} + +// GenerateServerCertificate creates and signs a certificate based on the provided RootCA. This differs +// from the Client certificate in that it ServerAuth key usage. It also does NOT include the OID for LDAP UID. +// +// Currently the key is an RSA key, which also entails adding KeyEncipherment key usage. +// +// TODO: Elliptic Curve instead of RSA (Remember to drop KeyEncipherment key usage as well) +func GenerateServerCertificate(root *CertificatePEMPair) (*CertificatePEMPair, error) { + + rootKeyPair, err := ParseCertificateKeyPair(root) + if err != nil { + log.Fatalln("Failed to parse root cert and key") + } + + now := time.Now() + + serialNumber, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) + if err != nil { + return nil, fmt.Errorf("failed to generate random serial number: %w", err) + } + + cert := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + OrganizationalUnit: []string{"ATC"}, + Organization: []string{"Apache"}, + Country: []string{"US"}, + Province: []string{"Colorado"}, + Locality: []string{"Denver"}, + CommonName: "server.local", + }, + DNSNames: []string{"server.local"}, + IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, + NotBefore: now, + NotAfter: now.AddDate(1, 0, 0), + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement, + } + + certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, fmt.Errorf("failed to generate RSA key: %w", err) + } + + certDERBytes, err := x509.CreateCertificate(rand.Reader, cert, rootKeyPair.Certificate, &certPrivKey.PublicKey, rootKeyPair.PrivateKey) + if err != nil { + return nil, fmt.Errorf("failed to create certificate: %w", err) + } + + certPEMPair := new(CertificatePEMPair) + + certPEM := new(bytes.Buffer) + pem.Encode(certPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: certDERBytes, + }) + + certPrivKeyPEM := new(bytes.Buffer) + certPrivKeyByes, err := x509.MarshalPKCS8PrivateKey(certPrivKey) + if err != nil { + return nil, fmt.Errorf("failed to marshal private key to PKCS8: %w", err) + } + + pem.Encode(certPrivKeyPEM, &pem.Block{ + Type: "PRIVATE KEY", + Bytes: certPrivKeyByes, + }) + + certPEMPair.CertificatePEM = certPEM.String() + certPEMPair.PrivateKeyPEM = certPrivKeyPEM.String() + + return certPEMPair, nil +} + +func VerifyCertificates(root, client, server *CertificatePEMPair) error { + + rootKeyPair, err := ParseCertificateKeyPair(root) + if err != nil { + log.Fatalln("Failed to parse root cert and key") + } + + rootPool := x509.NewCertPool() + rootPool.AddCert(rootKeyPair.Certificate) + + opts := x509.VerifyOptions{ + Intermediates: x509.NewCertPool(), + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + Roots: rootPool, + } + + clientCert, err := ParseCertificateKeyPair(client) + if err != nil { + return fmt.Errorf("failed to parse client cert and key: %w", err) + } + + if _, err := clientCert.Certificate.Verify(opts); err != nil { + return fmt.Errorf("failed to verify client cert and key: %w", err) + } + + serverCert, err := ParseCertificateKeyPair(server) + if err != nil { + return fmt.Errorf("failed to parse server cert and key: %w", err) + } + + if _, err := serverCert.Certificate.Verify(opts); err != nil { + return fmt.Errorf("failed to verify client cert and key: %w", err) + } + + return nil +} diff --git a/traffic_ops/traffic_ops_golang/auth/authorize.go b/traffic_ops/traffic_ops_golang/auth/authorize.go index d1913d94ce..7a26f131eb 100644 --- a/traffic_ops/traffic_ops_golang/auth/authorize.go +++ b/traffic_ops/traffic_ops_golang/auth/authorize.go @@ -170,18 +170,18 @@ func GetCurrentUser(ctx context.Context) (*CurrentUser, error) { return &CurrentUser{"-", -1, PrivLevelInvalid, TenantIDInvalid, -1, "", []string{}, "", nil}, errors.New("No user found in Context") } -func CheckLocalUserIsAllowed(form PasswordForm, db *sqlx.DB, ctx context.Context) (bool, error, error) { +func CheckLocalUserIsAllowed(username string, db *sqlx.DB, ctx context.Context) (bool, error, error) { if usersCacheIsEnabled() { - u, exists := getUserFromCache(form.Username) + u, exists := getUserFromCache(username) if !exists { - return false, fmt.Errorf("user '%s' not found in cache", form.Username), nil + return false, fmt.Errorf("user '%s' not found in cache", username), nil } allowed := u.RoleName != disallowed return allowed, nil, nil } var roleName string - err := db.GetContext(ctx, &roleName, "SELECT role.name FROM role INNER JOIN tm_user ON tm_user.role = role.id where username=$1", form.Username) + err := db.GetContext(ctx, &roleName, "SELECT role.name FROM role INNER JOIN tm_user ON tm_user.role = role.id where username=$1", username) if err != nil { if err == context.DeadlineExceeded || err == context.Canceled { return false, nil, err diff --git a/traffic_ops/traffic_ops_golang/auth/certificate.go b/traffic_ops/traffic_ops_golang/auth/certificate.go new file mode 100644 index 0000000000..340e70f29c --- /dev/null +++ b/traffic_ops/traffic_ops_golang/auth/certificate.go @@ -0,0 +1,163 @@ +package auth + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "io/fs" + "io/ioutil" + "net/http" + "path/filepath" +) + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// ParseCertificate takes a http.Request, pulls the (optionally) provided client TLS +// certificates and attempts to verify them against the directory of provided Root CA +// certificates. The Root CA certificates can be different than those utilized by the +// http.Server. Returns a bool signifying whether the verification process was +// successful or an error if one was encountered. +func VerifyClientCertificate(r *http.Request, rootCertsDirPath string) (bool, error) { + // TODO: Parse client headers + + if err := loadRootCerts(rootCertsDirPath); err != nil { + return false, fmt.Errorf("failed to load root certificates") + } + + if err := verifyClientRootChain(r.TLS.PeerCertificates); err != nil { + return false, fmt.Errorf("failed to verify client to root certificate chain") + } + + return true, nil +} + +func verifyClientRootChain(clientChain []*x509.Certificate) error { + if len(clientChain) == 0 { + return fmt.Errorf("empty client chain") + } + + if rootPool == nil { + return fmt.Errorf("uninitialized root cert pool") + } + + intermediateCertPool := x509.NewCertPool() + for _, intermediate := range clientChain[1:] { + intermediateCertPool.AddCert(intermediate) + } + + opts := x509.VerifyOptions{ + Intermediates: intermediateCertPool, + Roots: rootPool, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + } + _, err := clientChain[0].Verify(opts) + if err != nil { + return fmt.Errorf("failed to verify client cert chain. err: %w", err) + } + return nil +} + +// Lazy initialized, only added to once, need mutex? +var rootPool *x509.CertPool + +func loadRootCerts(dirPath string) error { + // Root cert pool already populated + if rootPool != nil { + return nil + } + + if dirPath == "" { + return fmt.Errorf("empty path supplied for root cert directory") + } + + err := filepath.WalkDir(dirPath, + // walk function to perform on each file in the supplied + // directory path for root certificiates. + // + // For each file in the directory, first check if it, too, is a dir. If so, + // return the filepath.SkipDir error to allow for it to be skipped without + // stopping the subsequent executions. + // + // If of type File, then load the PEM encoded string from the file and + // attempt to decode the PEM block into an x509 certificate. If successful, + // add that certificate to the Root Cert Pool to be used for verification. + // + // Must be a closure for access to the `dirPath` value + func(path string, file fs.DirEntry, e error) error { + if e != nil { + return e + } + + // Skip logic if root directory + if path == dirPath { + return nil + } + + // Don't traverse nested directories + if file.IsDir() { + return filepath.SkipDir + } + + pemBytes, err := ioutil.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to open cert at %s. err: %w", path, err) + } + pemBlock, _ := pem.Decode(pemBytes) + certificate, err := x509.ParseCertificate(pemBlock.Bytes) + if err != nil { + return fmt.Errorf("failed to parse PEM into x509. err: %w", err) + } + + if rootPool == nil { + rootPool = x509.NewCertPool() + } + rootPool.AddCert(certificate) + + fmt.Printf("Added cert %s\n", path) + + return nil + }) + if err != nil { + return fmt.Errorf("failed to load root certs from path %s. err: %w", dirPath, err) + } + + return nil +} + +// ParseClientCertificateUID takes an x509 Certificate and loops through the Names in the +// Subject. If it finds an asn.ObjectIdentifier that matches UID, it returns the +// corresponding value. Otherwise returns empty string. If more than one UID is present, +// the first result found to match is returned (order not guaranteed). +func ParseClientCertificateUID(cert *x509.Certificate) string { + + // Object Identifier value for UID used within LDAP + // LDAP OID reference: https://ldap.com/ldap-oid-reference-guide/ + // 0.9.2342.19200300.100.1.1 uid Attribute Type + // asn1.ObjectIdentifier([]int{0, 9, 2342, 19200300, 100, 1, 1}) + + for _, name := range cert.Subject.Names { + t := name.Type + if len(t) == 7 && t[0] == 0 && t[1] == 9 && t[2] == 2342 && t[3] == 19200300 && t[4] == 100 && t[5] == 1 && t[6] == 1 { + return name.Value.(string) + } + } + + return "" +} diff --git a/traffic_ops/traffic_ops_golang/auth/certificate_test.go b/traffic_ops/traffic_ops_golang/auth/certificate_test.go new file mode 100644 index 0000000000..0a7142b91e --- /dev/null +++ b/traffic_ops/traffic_ops_golang/auth/certificate_test.go @@ -0,0 +1,300 @@ +package auth + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "net/http" + "testing" +) + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// TODO: Utilize expirimental/certificate_auth/generate_cert.go to create appropriate +// certs on demand for testing, such as expired Bofore/After dates + +func TestVerifyClientCertificateSuccess(t *testing.T) { + rootCertPEMBlock, _ := pem.Decode([]byte(rootCertPEM)) + rootCert, err := x509.ParseCertificate(rootCertPEMBlock.Bytes) + if err != nil { + t.Fatalf("failed to extract x509 from PEM string for rootCert. err: %s", err) + } + + rootPool = x509.NewCertPool() + rootPool.AddCert(rootCert) + + req, err := http.NewRequest("POST", "/login", bytes.NewBuffer([]byte{})) + if err != nil { + t.Fatal("failed to create request") + } + + clientCertPEMBlock, _ := pem.Decode([]byte(clientCertPEM)) + clientCert, err := x509.ParseCertificate(clientCertPEMBlock.Bytes) + if err != nil { + t.Fatalf("failed to extract x509 from PEM string for clientCert. err: %s", err) + } + connState := new(tls.ConnectionState) + connState.PeerCertificates = append(connState.PeerCertificates, clientCert) + req.TLS = connState + + success, err := VerifyClientCertificate(req, "root/pool/created/above") + if err != nil { + t.Fatalf("error attempting failed to verify client certificate: %s", err) + } + if !success { + t.Fatal("failed to verify client certificate") + } +} + +func TestLoadRootCertsSuccess(t *testing.T) { + rootPool = nil + + err := loadRootCerts("test/success") + + if err != nil { + t.Fatalf("failed to load certs. err: %s", err) + } + +} + +func TestLoadRootCertsEmptyDirPathFail(t *testing.T) { + rootPool = nil + + err := loadRootCerts("") + + if err == nil { + t.Fatalf("shoudl have failed to load certs with empty path. err: %s", err) + } + +} + +func TestLoadRootCertsFail(t *testing.T) { + rootPool = nil + + err := loadRootCerts("test/fail") + + if err == nil { + t.Fatalf("should have failed to load certs attempting to parse PEM key into x509 Cert. err: %s", err) + } + +} + +func TestVerifyClientChainSuccess(t *testing.T) { + rootCertPEMBlock, _ := pem.Decode([]byte(rootCertPEM)) + rootCert, err := x509.ParseCertificate(rootCertPEMBlock.Bytes) + if err != nil { + t.Fatalf("failed to extract x509 from PEM string for rootCert. err: %s", err) + } + + rootPool = x509.NewCertPool() + rootPool.AddCert(rootCert) + + clientCertPEMBlock, _ := pem.Decode([]byte(clientCertPEM)) + clientCert, err := x509.ParseCertificate(clientCertPEMBlock.Bytes) + if err != nil { + t.Fatalf("failed to extract x509 from PEM string for clientCert. err: %s", err) + } + + if err = verifyClientRootChain([]*x509.Certificate{clientCert}); err != nil { + t.Fatalf("failed to verify certificate chain with valid certs. err: %s", err) + } +} + +func TestVerifyClientChainEmptyClientFail(t *testing.T) { + rootCertPEMBlock, _ := pem.Decode([]byte(rootCertPEM)) + rootCert, err := x509.ParseCertificate(rootCertPEMBlock.Bytes) + if err != nil { + t.Fatalf("failed to extract x509 from PEM string for rootCert. err: %s", err) + } + + rootPool = x509.NewCertPool() + rootPool.AddCert(rootCert) + + if err = verifyClientRootChain([]*x509.Certificate{}); err == nil { + t.Fatalf("failed to verify certificate chain with valid certs. err: %s", err) + } +} + +func TestVerifyClientChainEmptyRootFail(t *testing.T) { + clientCertPEMBlock, _ := pem.Decode([]byte(clientCertPEM)) + clientCert, err := x509.ParseCertificate(clientCertPEMBlock.Bytes) + if err != nil { + t.Fatalf("failed to extract x509 from PEM string for clientCert. err: %s", err) + } + + if err = verifyClientRootChain([]*x509.Certificate{clientCert}); err == nil { + t.Fatalf("failed to verify certificate chain with valid certs. err: %s", err) + } +} + +func TestVerifyClientChainWrongCertKeyUsageFail(t *testing.T) { + rootCertPEMBlock, _ := pem.Decode([]byte(rootCertPEM)) + rootCert, err := x509.ParseCertificate(rootCertPEMBlock.Bytes) + if err != nil { + t.Fatalf("failed to extract x509 from PEM string for rootCert. err: %s", err) + } + + rootPool = x509.NewCertPool() + rootPool.AddCert(rootCert) + + // Server cert contains x509.ExtKeyUsageServerAuth (vs ClientAuth) + serverCertPEMBlock, _ := pem.Decode([]byte(serverCertPEM)) + serverCert, err := x509.ParseCertificate(serverCertPEMBlock.Bytes) + if err != nil { + t.Fatalf("failed to extract x509 from PEM string for serverCert. err: %s", err) + } + + if err = verifyClientRootChain([]*x509.Certificate{serverCert}); err == nil { + t.Fatalf("failed to verify certificate chain with valid certs. err: %s", err) + } +} + +func TestParseClientCertificateUIDSuccess(t *testing.T) { + clientCertPEMBlock, _ := pem.Decode([]byte(clientCertPEM)) + clientCert, err := x509.ParseCertificate(clientCertPEMBlock.Bytes) + if err != nil { + t.Fatalf("failed to extract x509 from PEM string for clientCert. err: %s", err) + } + result := ParseClientCertificateUID(clientCert) + if result != "userid" { + t.Fatal("failed to parse UID value from certificate") + } +} + +func TestParseClientCertificateUIDFail(t *testing.T) { + // Server cert does not contain a UID object identifier + serverCertPEMBlock, _ := pem.Decode([]byte(serverCertPEM)) + serverCert, err := x509.ParseCertificate(serverCertPEMBlock.Bytes) + if err != nil { + t.Fatalf("failed to extract x509 from PEM string for serverCert. err: %s", err) + } + result := ParseClientCertificateUID(serverCert) + if len(result) > 0 { + t.Fatal("unexpected UID value from certificate") + } +} + +const rootCertPEM = `-----BEGIN CERTIFICATE----- +MIIFjjCCA3agAwIBAgIIYW10BDhSkLgwDQYJKoZIhvcNAQELBQAwZTELMAkGA1UE +BhMCVVMxETAPBgNVBAgTCENvbG9yYWRvMQ8wDQYDVQQHEwZEZW52ZXIxDzANBgNV +BAoTBkFwYWNoZTEMMAoGA1UECxMDQVRDMRMwEQYDVQQDEwpyb290LmxvY2FsMB4X +DTIyMDkwNjIxMTc1N1oXDTIzMDkwNjIxMTc1N1owZTELMAkGA1UEBhMCVVMxETAP +BgNVBAgTCENvbG9yYWRvMQ8wDQYDVQQHEwZEZW52ZXIxDzANBgNVBAoTBkFwYWNo +ZTEMMAoGA1UECxMDQVRDMRMwEQYDVQQDEwpyb290LmxvY2FsMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEA5uKjtuvogDySfFUEIUPluer+1KeH84Vz1NSF +2yShaHSNtPVloPc5sRzs0SHDDMmzY8JPeW5kyFJspuw+hDq0OfOANqt7xkDguaD9 +GosxGbJNYU3BCFNXrxB7pOuYFJptlSWH+PWbbxnqEy3mKk/9SA3n7xyEbc+J4Jue +9QtQBBqgyk3updMrWf+bMDHmA6KFnWzfUZV9WrN9GijeqByiZMB7X7fMfM7bNy7d +EbOrBDlFdWmQWSfRgekNc5/dxXk4G3xKDXQysdLLTbMT5HVdxvCy3cgLj9hAwSz2 +K2ViyMbcahiYwQ3BVGiszg2wjZr6DoWg1eQqVBHPMSWZG+la5dqugTcX53SGoKVi +YYsX/Mcj+T/HYyzmTLIwVR9hYibgh4tPx6fjqPZRl9BMhSyebWvmbIqnTDzXIZUu +ZiEodweunCztcAG5oQMCwGzB8UTfnqp6juW5pSq+Tuz5wnr2iNZYcpsh7e9TdWEZ +B3/uyBE2i0L7/IadDjFNx8uIY+j9Usgwo9Od6cfhK26e2MKoICxiNx/KfOlDjEli +MaRn+owEaInLGVnu74HcWBm3lkv30k7T64yTeApxw/Qtd+WDR91RWFEE2gdbf2vL +w1WT0n/HFgTrLoiTEukEfcDXfjPkLsWSYydDMAJyymgBuY3GPjL8DmjNwZohe8sc ++hJRe7UCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8w +HQYDVR0OBBYEFKHzciB4emUO4tqzNwr+U0/Dy0kKMA0GCSqGSIb3DQEBCwUAA4IC +AQDVLadfhKTI/q3hpkIqdaANriMZ8EUSXzcgFu2ockdqh0UjQVg4ZuaIx0GHFkl1 +gtgra5L9F1bkEyCCFwiVbheZ99NKBmamEdb/ke3aXkRlsKxFPOWsnOqEEqqLTnjV +5jXv/6D93YbrL/L9rQHV35mYrWHGrEE7qYQfAdo7e9Cy805GuaCKk9BvjfxG+WnI +tmUZjOIIZ8tlcwXibcfKB5T5xUBhNUDaA02LcxYpEQhSANpG4129I6ckz9aOtemb +yHXu3UeMCrh6UOAq7nmTQXp1BCs+zgolHW7GRGBf/UI5IC3AV29LjuM0qs2oLFSP +h87lqobmDZDXgbsHaKY+IaM99sc0z8OtQEHk/b5kqxJGTbCnsDKhQuDICceSG5gS +ZZiC+l9c8BE5pHLGL9omsKQ15QWAo11RoOCeDQHdQ0YjXKa4dGlTomWFWge2qAQx +G4ltnOmj8WggYcYJoZG/XQaQN9iaL5L/0AIu8zFwIHaNCDo7s2Ow6QKpb99PhTML +pB+dlC+T7BZGoicPhcyh4wPyEF3ebNv3eAIuJmciYGy+0YwcxzhNshkzg1V/lIA8 +iZfqa7xERCPGS03yy6quR5s37py74osk8xV71jRn/Fp7ogGEsVunNfEObO1Se3O8 +EYvdxllGaU6j2bVE4v/8CukTlUNPQx/+ty6EYaNsoIgkBQ== +-----END CERTIFICATE----- +` + +const clientCertPEM = `-----BEGIN CERTIFICATE----- +MIIFrjCCA5agAwIBAgIIAOfaLIQ3CvUwDQYJKoZIhvcNAQELBQAwZTELMAkGA1UE +BhMCVVMxETAPBgNVBAgTCENvbG9yYWRvMQ8wDQYDVQQHEwZEZW52ZXIxDzANBgNV +BAoTBkFwYWNoZTEMMAoGA1UECxMDQVRDMRMwEQYDVQQDEwpyb290LmxvY2FsMB4X +DTIyMDkwNjIxMTgwMFoXDTIzMDkwNjIxMTgwMFowfzELMAkGA1UEBhMCVVMxETAP +BgNVBAgTCENvbG9yYWRvMQ8wDQYDVQQHEwZEZW52ZXIxDzANBgNVBAoTBkFwYWNo +ZTEMMAoGA1UECxMDQVRDMRUwEwYDVQQDEwxjbGllbnQubG9jYWwxFjAUBgoJkiaJ +k/IsZAEBEwZ1c2VyaWQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2 +jzAsGVL5CHOt3irZxabD147EKP8d7PRUG6b8MyHjwtJW4gRj72u/CYn3d37kGXCK +vPnhcEIgyeepeD1BmyPUZqPRLGvp7mssSOCBNPJ7ND+WtpqZOPWWEVRp8FVY8ecU +RAPLi3k2DYQj3JNh6p+s7tBrUitHUm2kJ3GJDVJyDEPIfmXXMwfZ9JkaxAZFiRBg +pXq4gI52pz38UM0N1yQUFasSM/HodG0Pk0f4ZN+AoGz9KC7niQfWUucr72E9DSRf +T4L1h0F6yIgshdTvcPrY7/kg3+waPH73sy8WVNvQFVckeGOF0sDHS47hSpOvnInB +EgbCzG9gr5DO2Ik+LNYboMLUbMziyRJBBm4QdVMMbmgc+sLiifo17HVXkgngPl8I +75m47yXUIN3IV8o60OgTxV/NW7Ix/9NEmJKxu2WALJmrMwru/ySp6E/5vmQBaYn3 +aczpTUi9FFQbHpkBRCBk0/Kty6fQ98ywYyJc+27cQIhH8qgOThbn4uux2LuJZSym +ek9UdSZVYIvIsZULQojvn8h+YZR94HdhfHccx32tXBqlVPMc+vApZTl7LI6FEa3J ++pAV1gRd9hsMMJ9bS0kYEDaeO5Bgw98IXdtecxuy3+s5Sq8RHIUbhpXPgn1H2T63 +T5xlr1g+88BA/S0St6BcVJ8U8E4NkhRlySrr2gnpvwIDAQABo0gwRjAOBgNVHQ8B +Af8EBAMCA4gwEwYDVR0lBAwwCgYIKwYBBQUHAwIwHwYDVR0jBBgwFoAUofNyIHh6 +ZQ7i2rM3Cv5TT8PLSQowDQYJKoZIhvcNAQELBQADggIBAIUzKgLHkWQMwo5fyXWH +yP/Tt52lrKLb+CyfhY617XhYFD6H5FHo+qRzNCd0FQqxhaHKAZmsvB5b960eWsAt +4P+1rKXPpaBLVYf5oGKY8/1O2YMKvJYcT7S+mqSLrESXzPLcV820o62ZBUzof/Yn +/H5pxeKZe9j7LcBiQ6dku7sQY8PUiY2sxBTRWm+EKM0GiVS8B2fasI6x8rrz8IJA +cgwyEqCyqzGkrCdmeMUpfeZcefDOAJoQsbMVhhSRkd2h4f0QGJjoLdfNJSjsvfKy +BaiE45qq4YoOfXn0M+WN1pDoxFcutfOoPII2JkQeZ3Avybrfa2+6B0h13QxfA0ww +3M/qrqe1p1dxw6hCWdemfRbiK56KpFNOP/O7AVBkZmRPTiWybmBeHOxNwxP+jz5y +hlKQBneGWZIp0svEt+yFKYxD/RBzVlLektriC1gbowZ5BBipVApjs9hcMfEpF9fu +yTqj6MkfUsEMyLNiYU5vgbE7vR3HjN2yKksOge3BGA59tivwKmMmZs4O+6RWm7lR +aXK9ttZ2bYILS5T7Br8eo3+n3+IKAxnLmUxJ+WaO87ID8yIdjHea0wanxv6ocE1/ +kVyluEJ8E2iAf4Ax2d3yAXBuMNZkW/laGWptrSKivBGGUuU5rZcSV76TUh+21zgU +reqlKjsX1XKA3+uPcxYJ/22o +-----END CERTIFICATE----- +` + +const serverCertPEM = `-----BEGIN CERTIFICATE----- +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 +-----END CERTIFICATE----- +` + +//TODO: Create and add intermediate to chain for testing. +const intermediateCertPEM = `` diff --git a/traffic_ops/traffic_ops_golang/auth/test/fail/rsa.key.pem b/traffic_ops/traffic_ops_golang/auth/test/fail/rsa.key.pem new file mode 100644 index 0000000000..0286033e26 --- /dev/null +++ b/traffic_ops/traffic_ops_golang/auth/test/fail/rsa.key.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/traffic_ops/traffic_ops_golang/auth/test/success/Reference-DigiCertTLSECCP384RootG5.crt.pem b/traffic_ops/traffic_ops_golang/auth/test/success/Reference-DigiCertTLSECCP384RootG5.crt.pem new file mode 100644 index 0000000000..c8e21b6980 --- /dev/null +++ b/traffic_ops/traffic_ops_golang/auth/test/success/Reference-DigiCertTLSECCP384RootG5.crt.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp +Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 +MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ +bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS +7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp +0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS +B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 +BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ +LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 +DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/traffic_ops/traffic_ops_golang/auth/test/success/Reference-DigiCertTLSRSA4096RootG5.crt.pem b/traffic_ops/traffic_ops_golang/auth/test/success/Reference-DigiCertTLSRSA4096RootG5.crt.pem new file mode 100644 index 0000000000..369d35b742 --- /dev/null +++ b/traffic_ops/traffic_ops_golang/auth/test/success/Reference-DigiCertTLSRSA4096RootG5.crt.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN +MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT +HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN +NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs +IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+ +ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0 +2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp +wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM +pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD +nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po +sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx +Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd +Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX +KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe +XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL +tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv +TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN +AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw +GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H +PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF +O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ +REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik +AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv +/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+ +p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw +MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF +qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK +ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ +-----END CERTIFICATE----- \ No newline at end of file diff --git a/traffic_ops/traffic_ops_golang/auth/test/success/ignored/ignoredfile.crt.pem b/traffic_ops/traffic_ops_golang/auth/test/success/ignored/ignoredfile.crt.pem new file mode 100644 index 0000000000..e69de29bb2 diff --git a/traffic_ops/traffic_ops_golang/auth/test/success/rootca.crt.pem b/traffic_ops/traffic_ops_golang/auth/test/success/rootca.crt.pem new file mode 100644 index 0000000000..a667faf8d7 --- /dev/null +++ b/traffic_ops/traffic_ops_golang/auth/test/success/rootca.crt.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFjjCCA3agAwIBAgIIT5rRhzhg8tYwDQYJKoZIhvcNAQELBQAwZTELMAkGA1UE +BhMCVVMxETAPBgNVBAgTCENvbG9yYWRvMQ8wDQYDVQQHEwZEZW52ZXIxDzANBgNV +BAoTBkFwYWNoZTEMMAoGA1UECxMDQVRDMRMwEQYDVQQDEwpyb290LmxvY2FsMB4X +DTIyMDgzMTIxMDM0OFoXDTIzMDgzMTIxMDM0OFowZTELMAkGA1UEBhMCVVMxETAP +BgNVBAgTCENvbG9yYWRvMQ8wDQYDVQQHEwZEZW52ZXIxDzANBgNVBAoTBkFwYWNo +ZTEMMAoGA1UECxMDQVRDMRMwEQYDVQQDEwpyb290LmxvY2FsMIICIjANBgkqhkiG +9w0BAQEFAAOCAg8AMIICCgKCAgEAwvsV2+PNj4ZszReTjbrhoAd1d76bY1/z4xpe +DElEqEWnXKfFiUw8WcgtA+WnNGZt5QKYdVZM91l11KhW40J5tfdlZtP8OkzSHKiZ +rjDHSQq8mgrurCrHc0K00DGCu/6UEN8wHjlLr8l0KLmrVh2WImaQohKm2eijprXN +UM+FBg+SdaMhn6oIqinJDDoFAYwXrAa0jEi+VCmjldnMO3JbQn3HmRmcCGctb68B +AAbP/24wzN1gcnOpxtOySQRGPObaQCm/CD19dIupDisEmF/HjYKtp53r+c+1geCF +WxJy8qx8GJWQ+EZjsF7j53yPbKRs9vPtvd4kPsCnk7yJxJ8ekwecr5ZZ6jkUrX0E +MejGE33ekmEZ0g2J3Dq+XFe0UKmXKUbDLKZf6158T7zxWn3t8vOxshIoBjOTExGu +fnFtyseZsfSmXeY364Ph0YcC0aseqNPdvnSQA4gzhs7OlzxggJrZWyTMDJpMpjfm +y0jHjwsYKZf/WsG4ipPuGE9iRGrU+2dDSiYrfU/SqWp+RcZb4W6rX18J8keZRYch +rEQSwF6bIrrGbshbyu4U9V7/bYXZMjMQ3DBd4ND9J+f2wV32QoamUfLMQp5WR1U3 +8FpVANgvFWO4glVtw8bjCp22MPUltBzGfwlT5vqHz+b/Au37K4FfjeKMYCEsatT3 +CN1L2RECAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8w +HQYDVR0OBBYEFH2wHtXCAjSENteF9/U+O/DN5fgUMA0GCSqGSIb3DQEBCwUAA4IC +AQBUKPdeq3UPLJ7FWTZDLR2lyNJpk/3ia2n5Gc+hVI7ipb23CngoEZLM6VnYZSQm +t60yZJHUBOP39/6Q3lD0CCy3tKjGnEST5W0DvZeiUJzpvdD+FRVMkI6zcpJbSHOV +r1v/dZc4CdG0BEtSUFVYzN/hJkGTeLf1EfZbtRwS9wNK0FD95Sjv6DhNFT4bpVPB +qMJIuD/+/z83OU7BuYi7lyE7rYLM5wnsnLSnlv0Q3HwIv5db2ZvUxOMukkdIjbLA +kgGKXvnm+HQpCaMGvNfFZXnMqey71Vmpr1rpz8GA/7iZ4aqa+yOaYyHpTaBraFsc +PtNxgscp8/FtcOKUSp1k+MBsb3ume5UPTPcFQK7DA/+7npLcEt3NqYpC1iYTPbCW +1h0mI+XsVHOH5IZc5ZimsyuavjkkZ3KpV3cCWyD8VE0xpXNL8uJJzoqmJpl+Qw/p +Bfj79H2Rr2ehYLDJTISsW7aGppHKfmILgN14Hwh9MJxJ2cBmZwAyLUBd9pmtWJ/1 +ZWjBdPOtdX0mnrPo1kLTAAx2AT0RSUEukbR9Gu0gHOr6yw4CsIUY0DyqSTMFEnbB +X6YkuXIBMWHELpABgg6H0pxcrjhx92hgcwxMl88FuW6yAsZDkxk2PHiuxWyXjr57 +3vS0IMRX8z6Vvz/SDct+snkKCmokR001HWCfwN6W/wnNqw== +-----END CERTIFICATE----- diff --git a/traffic_ops/traffic_ops_golang/login/login.go b/traffic_ops/traffic_ops_golang/login/login.go index b1700449ea..0d71e87101 100644 --- a/traffic_ops/traffic_ops_golang/login/login.go +++ b/traffic_ops/traffic_ops_golang/login/login.go @@ -108,36 +108,92 @@ Subject: {{.InstanceName}} Password Reset Request` + "\r\n\r" + ` `)) +// LoginHandler first attempts to verify and parse user information from an optionally +// provided client TLS certificate. If it fails at any point, it will fall back and +// continue with the standard submitted form authentication. func LoginHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() authenticated := false form := auth.PasswordForm{} - if err := json.NewDecoder(r.Body).Decode(&form); err != nil { - api.HandleErr(w, r, nil, http.StatusBadRequest, err, nil) - return - } - if form.Username == "" || form.Password == "" { - api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("username and password are required"), nil) - return - } - resp := struct { - tc.Alerts - }{} + var resp tc.Alerts dbCtx, cancelTx := context.WithTimeout(r.Context(), time.Duration(cfg.DBQueryTimeoutSeconds)*time.Second) defer cancelTx() - userAllowed, err, blockingErr := auth.CheckLocalUserIsAllowed(form, db, dbCtx) - if blockingErr != nil { - api.HandleErr(w, r, nil, http.StatusServiceUnavailable, nil, fmt.Errorf("error checking local user password: %s\n", blockingErr.Error())) - return - } - if err != nil { - log.Errorf("checking local user: %s\n", err.Error()) + + // Attempt to perform client certificate authentication. If fails, goto standard form auth + { + // No certs provided by the client (or enabled on the server), skip to form authentication + if len(r.TLS.PeerCertificates) == 0 { + goto FormAuth + } + + // Perform certificate verification to ensure it is valid against Root CAs + validCert, err := auth.VerifyClientCertificate(r, "") // TODO Pass in TLS Client Root Path from cfg + if err != nil { + log.Warnf("error attempting to verify client provided TLS certificate. err: %s\n", err) + goto FormAuth + } + + // Client provided a verified certificate. Extract UID value. + if validCert { + form.Username = auth.ParseClientCertificateUID(r.TLS.PeerCertificates[0]) + if len(form.Username) == 0 { + log.Infoln("client provided certificate did not contain a UID object identifier or value") + goto FormAuth + } + } + + // Only check if we successfully retrieved a UID from the certificate + if validCert && form.Username != "" { + // Check if user exists and has a role. If the certificate was verified, has a UID, and the UID matches + // an existing user we consider this to be a successful login. + var blockingErr error + authenticated, err, blockingErr = auth.CheckLocalUserIsAllowed(form.Username, db, dbCtx) + if blockingErr != nil { + api.HandleErr(w, r, nil, http.StatusServiceUnavailable, nil, fmt.Errorf("error checking local user has role: %s", blockingErr.Error())) + return + } + if err != nil { + log.Errorf("checking local user: %s\n", err.Error()) + } + } } - if userAllowed { + + FormAuth: + // Failed certificate-based auth, perform standard form auth + if !authenticated { + // Perform form authentication + if err := json.NewDecoder(r.Body).Decode(&form); err != nil { + api.HandleErr(w, r, nil, http.StatusBadRequest, err, nil) + return + } + if form.Username == "" || form.Password == "" { + api.HandleErr(w, r, nil, http.StatusBadRequest, errors.New("username and password are required"), nil) + return + } + + // Check if user exists and has a role + userAllowed, err, blockingErr := auth.CheckLocalUserIsAllowed(form.Username, db, dbCtx) + if blockingErr != nil { + api.HandleErr(w, r, nil, http.StatusServiceUnavailable, nil, fmt.Errorf("error checking local user has role: %s", blockingErr.Error())) + return + } + if err != nil { + log.Errorf("checking local user: %s\n", err.Error()) + } + + // User w/ role does not exist, return unauthorized + if !userAllowed { + resp = tc.CreateAlerts(tc.ErrorLevel, "Invalid username or password.") + w.WriteHeader(http.StatusUnauthorized) + api.WriteRespRaw(w, r, resp) + return + } + + // Check local DB or LDAP authenticated, err, blockingErr = auth.CheckLocalUserPassword(form, db, dbCtx) if blockingErr != nil { - api.HandleErr(w, r, nil, http.StatusServiceUnavailable, nil, fmt.Errorf("error checking local user password: %s\n", blockingErr.Error())) + api.HandleErr(w, r, nil, http.StatusServiceUnavailable, nil, fmt.Errorf("error checking local user password: %s", blockingErr.Error())) return } if err != nil { @@ -152,91 +208,77 @@ func LoginHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc { } } } - if authenticated { - httpCookie := tocookie.GetCookie(form.Username, defaultCookieDuration, cfg.Secrets[0]) - http.SetCookie(w, httpCookie) - - var jwtToken jwt.Token - var jwtSigned []byte - jwtBuilder := jwt.NewBuilder() - - emptyConf := config.CdniConf{} - if cfg.Cdni != nil && *cfg.Cdni != emptyConf { - ucdn, err := auth.GetUserUcdn(form, db, dbCtx) - if err != nil { - // log but do not error out since this is optional in the JWT for CDNi integration - log.Errorf("getting ucdn for user %s: %v", form.Username, err) - } - jwtBuilder.Claim("iss", ucdn) - jwtBuilder.Claim("aud", cfg.Cdni.DCdnId) - } + } - jwtBuilder.Claim("exp", httpCookie.Expires.Unix()) - jwtBuilder.Claim(api.MojoCookie, httpCookie.Value) - jwtToken, err = jwtBuilder.Build() - if err != nil { - api.HandleErr(w, r, nil, http.StatusInternalServerError, nil, fmt.Errorf("building token: %s", err)) - return - } + // User does not exist in either local DB or LDAP, return unauthorized + if !authenticated { + resp = tc.CreateAlerts(tc.ErrorLevel, "Invalid username or password.") + w.WriteHeader(http.StatusUnauthorized) + api.WriteRespRaw(w, r, resp) + return + } - jwtSigned, err = jwt.Sign(jwtToken, jwa.HS256, []byte(cfg.Secrets[0])) - if err != nil { - api.HandleErr(w, r, nil, http.StatusInternalServerError, nil, err) - return - } + // Successful authentication, write cookie and return + httpCookie := tocookie.GetCookie(form.Username, defaultCookieDuration, cfg.Secrets[0]) + http.SetCookie(w, httpCookie) - http.SetCookie(w, &http.Cookie{ - Name: api.AccessToken, - Value: string(jwtSigned), - Path: "/", - MaxAge: httpCookie.MaxAge, - Expires: httpCookie.Expires, - HttpOnly: true, // prevents the cookie being accessed by Javascript. DO NOT remove, security vulnerability - }) - - // If all's well until here, then update last authenticated time - tx, txErr := db.BeginTx(dbCtx, nil) - if txErr != nil { - api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("beginning transaction: %w", txErr)) - return - } - defer func() { - if err := tx.Commit(); err != nil && err != sql.ErrTxDone { - log.Errorln("committing transaction: " + err.Error()) - } - }() - _, dbErr := tx.Exec(UpdateLoginTimeQuery, form.Username) - if dbErr != nil { - log.Errorf("unable to update authentication time for a given user: %s\n", dbErr.Error()) - resp = struct { - tc.Alerts - }{tc.CreateAlerts(tc.ErrorLevel, "Unable to update authentication time for a given user")} - } else { - resp = struct { - tc.Alerts - }{tc.CreateAlerts(tc.SuccessLevel, "Successfully logged in.")} - } + var jwtToken jwt.Token + var jwtSigned []byte + jwtBuilder := jwt.NewBuilder() - } else { - resp = struct { - tc.Alerts - }{tc.CreateAlerts(tc.ErrorLevel, "Invalid username or password.")} + emptyConf := config.CdniConf{} + if cfg.Cdni != nil && *cfg.Cdni != emptyConf { + ucdn, err := auth.GetUserUcdn(form, db, dbCtx) + if err != nil { + // log but do not error out since this is optional in the JWT for CDNi integration + log.Errorf("getting ucdn for user %s: %v", form.Username, err) } - } else { - resp = struct { - tc.Alerts - }{tc.CreateAlerts(tc.ErrorLevel, "Invalid username or password.")} + jwtBuilder.Claim("iss", ucdn) + jwtBuilder.Claim("aud", cfg.Cdni.DCdnId) } - respBts, err := json.Marshal(resp) + + jwtBuilder.Claim("exp", httpCookie.Expires.Unix()) + jwtBuilder.Claim(api.MojoCookie, httpCookie.Value) + jwtToken, err := jwtBuilder.Build() + if err != nil { + api.HandleErr(w, r, nil, http.StatusInternalServerError, nil, fmt.Errorf("building token: %s", err)) + return + } + + jwtSigned, err = jwt.Sign(jwtToken, jwa.HS256, []byte(cfg.Secrets[0])) if err != nil { api.HandleErr(w, r, nil, http.StatusInternalServerError, nil, err) return } - w.Header().Set(rfc.ContentType, rfc.ApplicationJSON) - if !authenticated { - w.WriteHeader(http.StatusUnauthorized) + + http.SetCookie(w, &http.Cookie{ + Name: api.AccessToken, + Value: string(jwtSigned), + Path: "/", + MaxAge: httpCookie.MaxAge, + Expires: httpCookie.Expires, + HttpOnly: true, // prevents the cookie being accessed by Javascript. DO NOT remove, security vulnerability + }) + + // If all's well until here, then update last authenticated time + tx, txErr := db.BeginTx(dbCtx, nil) + if txErr != nil { + api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("beginning transaction: %w", txErr)) + return } - fmt.Fprintf(w, "%s", respBts) + defer func() { + if err := tx.Commit(); err != nil && err != sql.ErrTxDone { + log.Errorln("committing transaction: " + err.Error()) + } + }() + _, dbErr := tx.Exec(UpdateLoginTimeQuery, form.Username) + if dbErr != nil { + log.Errorf("unable to update authentication time for a given user: %s\n", dbErr.Error()) + } + + resp = tc.CreateAlerts(tc.SuccessLevel, "Successfully logged in.") + w.WriteHeader(http.StatusOK) + api.WriteRespRaw(w, r, resp) } } @@ -445,7 +487,7 @@ func OauthLoginHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc { dbCtx, cancelTx := context.WithTimeout(r.Context(), time.Duration(cfg.DBQueryTimeoutSeconds)*time.Second) defer cancelTx() - userAllowed, err, blockingErr := auth.CheckLocalUserIsAllowed(form, db, dbCtx) + userAllowed, err, blockingErr := auth.CheckLocalUserIsAllowed(form.Username, db, dbCtx) if blockingErr != nil { api.HandleErr(w, r, nil, http.StatusServiceUnavailable, nil, fmt.Errorf("error checking local user password: %s\n", blockingErr.Error())) return diff --git a/traffic_ops/traffic_ops_golang/traffic_ops_golang.go b/traffic_ops/traffic_ops_golang/traffic_ops_golang.go index 60b9b4c2ad..5321cfa7fe 100644 --- a/traffic_ops/traffic_ops_golang/traffic_ops_golang.go +++ b/traffic_ops/traffic_ops_golang/traffic_ops_golang.go @@ -198,7 +198,10 @@ func main() { ErrorLog: log.Error, } if httpServer.TLSConfig == nil { - httpServer.TLSConfig = &tls.Config{} + httpServer.TLSConfig = &tls.Config{ + // Allow for clients to optionally send TLS certificates for authentication + ClientAuth: tls.RequestClientCert, + } } // Deprecated in 5.0 httpServer.TLSConfig.InsecureSkipVerify = cfg.Insecure From aacdfc2a7ae29cac2910c2a4ad58dc036030b56f Mon Sep 17 00:00:00 2001 From: Taylor Frey Date: Tue, 4 Oct 2022 15:24:52 -0600 Subject: [PATCH 02/10] Add intermediate cert for chain testing. Add LDAP check for UID parsed from cert --- .../certificate_auth/generate_certs.go | 129 ++++++-- .../traffic_ops_golang/auth/certificate.go | 35 ++- .../auth/certificate_test.go | 281 +++++++++++------- .../traffic_ops_golang/config/config.go | 5 + traffic_ops/traffic_ops_golang/login/login.go | 56 ++-- .../traffic_ops_golang/traffic_ops_golang.go | 3 +- 6 files changed, 337 insertions(+), 172 deletions(-) diff --git a/experimental/certificate_auth/generate_certs.go b/experimental/certificate_auth/generate_certs.go index 52f7d22091..fa39cf0f65 100644 --- a/experimental/certificate_auth/generate_certs.go +++ b/experimental/certificate_auth/generate_certs.go @@ -36,12 +36,14 @@ import ( "time" ) -// Public and Private key pair for the certificate. +// CertificateKeyPair contains the parsed representation of a certificate +// and private key. type CertificateKeyPair struct { Certificate *x509.Certificate - PrivateKey *rsa.PrivateKey // TODO convert to ECDSA + PrivateKey *rsa.PrivateKey } +// CertificatePEMPair contains the PEM encoded certificate and private key. type CertificatePEMPair struct { CertificatePEM, PrivateKeyPEM string } @@ -55,7 +57,14 @@ func main() { ioutil.WriteFile("rootca.crt.pem", []byte(rootCAPEMPair.CertificatePEM), 0644) ioutil.WriteFile("rootca.key.pem", []byte(rootCAPEMPair.PrivateKeyPEM), 0644) - serverPEMPair, err := GenerateServerCertificate(rootCAPEMPair) + intermediatePEMPair, err := GenerateIntermediateCertificate(rootCAPEMPair) + if err != nil { + log.Fatalf("Failed to generate and sign Intermediate certificate\nErr: %s\n", err) + } + ioutil.WriteFile("intermediate.crt.pem", []byte(intermediatePEMPair.CertificatePEM), 0644) + ioutil.WriteFile("intermediate.key.pem", []byte(intermediatePEMPair.PrivateKeyPEM), 0644) + + serverPEMPair, err := GenerateServerCertificate(intermediatePEMPair) if err != nil { log.Fatalf("Failed to generate and sign Server certificate\nErr: %s\n", err) } @@ -64,7 +73,7 @@ func main() { ioutil.WriteFile("server.crt.pem", []byte(serverPEMPair.CertificatePEM), 0644) ioutil.WriteFile("server.key.pem", []byte(serverPEMPair.PrivateKeyPEM), 0644) - clientPEMPair, err := GenerateClientCertificate(rootCAPEMPair) + clientPEMPair, err := GenerateClientCertificate(intermediatePEMPair) if err != nil { log.Fatalf("Failed to generate and sign Client certificate\nErr: %s\n", err) } @@ -73,11 +82,13 @@ func main() { ioutil.WriteFile("client.crt.pem", []byte(clientPEMPair.CertificatePEM), 0644) ioutil.WriteFile("client.key.pem", []byte(clientPEMPair.PrivateKeyPEM), 0644) - if err := VerifyCertificates(rootCAPEMPair, clientPEMPair, serverPEMPair); err != nil { + if err := VerifyCertificates(rootCAPEMPair, intermediatePEMPair, clientPEMPair, serverPEMPair); err != nil { log.Fatalf("failed to verify certificate: %s", err) } } +// ParseCertificateKeyPair decodes the provided PEM pair (key, cert) and returns a +// parsed private key and x509 certificate. func ParseCertificateKeyPair(pemPair *CertificatePEMPair) (*CertificateKeyPair, error) { keyPair := new(CertificateKeyPair) @@ -103,6 +114,8 @@ func ParseCertificateKeyPair(pemPair *CertificatePEMPair) (*CertificateKeyPair, return keyPair, nil } +// GenereateRootCACertificate creates a Root CA certificate that can be used +// for signing intermediate, client, and server x509 certificates. func GenerateRootCACertificate() (*CertificatePEMPair, error) { now := time.Now() @@ -123,7 +136,7 @@ func GenerateRootCACertificate() (*CertificatePEMPair, error) { CommonName: "root.local", }, NotBefore: now, - NotAfter: now.AddDate(1, 0, 0), + NotAfter: now.AddDate(15, 0, 0), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign, BasicConstraintsValid: true, IsCA: true, @@ -164,16 +177,84 @@ func GenerateRootCACertificate() (*CertificatePEMPair, error) { return certPEMPair, nil } +// GenerateIntermediateCeertificate creates an intermediate based on the provided Root certificate. +// This certificate can be used for signing client and server certificates to establish +// a chain to the Root certificate. +func GenerateIntermediateCertificate(root *CertificatePEMPair) (*CertificatePEMPair, error) { + + rootKeyPair, err := ParseCertificateKeyPair(root) + if err != nil { + log.Fatalln("Failed to parse root cert and key") + } + + now := time.Now() + + serialNumber, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) + if err != nil { + return nil, fmt.Errorf("failed to generate random serial number: %w", err) + } + + cert := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + OrganizationalUnit: []string{"ATC"}, + Organization: []string{"Apache"}, + Country: []string{"US"}, + Province: []string{"Colorado"}, + Locality: []string{"Denver"}, + CommonName: "intermediate.local", + }, + NotBefore: now, + NotAfter: now.AddDate(10, 0, 0), + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + MaxPathLenZero: true, + IsCA: true, + } + + certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, fmt.Errorf("failed to generate RSA key: %w", err) + } + + certDERBytes, err := x509.CreateCertificate(rand.Reader, cert, rootKeyPair.Certificate, &certPrivKey.PublicKey, rootKeyPair.PrivateKey) + if err != nil { + return nil, fmt.Errorf("failed to create certificate: %w", err) + } + + certPEMPair := new(CertificatePEMPair) + + certPEM := new(bytes.Buffer) + pem.Encode(certPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: certDERBytes, + }) + + certPrivKeyPEM := new(bytes.Buffer) + certPrivKeyByes, err := x509.MarshalPKCS8PrivateKey(certPrivKey) + if err != nil { + return nil, fmt.Errorf("failed to marshal private key to PKCS8: %w", err) + } + + pem.Encode(certPrivKeyPEM, &pem.Block{ + Type: "PRIVATE KEY", + Bytes: certPrivKeyByes, + }) + + certPEMPair.CertificatePEM = certPEM.String() + certPEMPair.PrivateKeyPEM = certPrivKeyPEM.String() + + return certPEMPair, nil +} + // GenerateClientCertificate creates and signs a certificate based on the provided RootCA. This differs // from the Server certificate in that it includes the OID for LDAP UID as well as Client Auth key usage. // // Currently the key is an RSA key, which also entails adding KeyEncipherment key usage. -// -// TODO: CommonName for client? -// TODO: Elliptic Curve instead of RSA (Remember to drop KeyEncipherment key usage as well) -func GenerateClientCertificate(root *CertificatePEMPair) (*CertificatePEMPair, error) { +func GenerateClientCertificate(intermediate *CertificatePEMPair) (*CertificatePEMPair, error) { - rootKeyPair, err := ParseCertificateKeyPair(root) + intermediateKeyPair, err := ParseCertificateKeyPair(intermediate) if err != nil { log.Fatalln("Failed to parse root cert and key") } @@ -204,7 +285,7 @@ func GenerateClientCertificate(root *CertificatePEMPair) (*CertificatePEMPair, e ExtraNames: []pkix.AttributeTypeAndValue{uidPkix}, }, NotBefore: now, - NotAfter: now.AddDate(1, 0, 0), + NotAfter: now.AddDate(5, 0, 0), ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyAgreement, } @@ -214,7 +295,7 @@ func GenerateClientCertificate(root *CertificatePEMPair) (*CertificatePEMPair, e return nil, fmt.Errorf("failed to generate RSA key: %w", err) } - certDERBytes, err := x509.CreateCertificate(rand.Reader, cert, rootKeyPair.Certificate, &certPrivKey.PublicKey, rootKeyPair.PrivateKey) + certDERBytes, err := x509.CreateCertificate(rand.Reader, cert, intermediateKeyPair.Certificate, &certPrivKey.PublicKey, intermediateKeyPair.PrivateKey) if err != nil { return nil, fmt.Errorf("failed to create certificate: %w", err) } @@ -248,11 +329,9 @@ func GenerateClientCertificate(root *CertificatePEMPair) (*CertificatePEMPair, e // from the Client certificate in that it ServerAuth key usage. It also does NOT include the OID for LDAP UID. // // Currently the key is an RSA key, which also entails adding KeyEncipherment key usage. -// -// TODO: Elliptic Curve instead of RSA (Remember to drop KeyEncipherment key usage as well) -func GenerateServerCertificate(root *CertificatePEMPair) (*CertificatePEMPair, error) { +func GenerateServerCertificate(intermediate *CertificatePEMPair) (*CertificatePEMPair, error) { - rootKeyPair, err := ParseCertificateKeyPair(root) + intermediateKeyPair, err := ParseCertificateKeyPair(intermediate) if err != nil { log.Fatalln("Failed to parse root cert and key") } @@ -277,7 +356,7 @@ func GenerateServerCertificate(root *CertificatePEMPair) (*CertificatePEMPair, e DNSNames: []string{"server.local"}, IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, NotBefore: now, - NotAfter: now.AddDate(1, 0, 0), + NotAfter: now.AddDate(5, 0, 0), ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement, } @@ -287,7 +366,7 @@ func GenerateServerCertificate(root *CertificatePEMPair) (*CertificatePEMPair, e return nil, fmt.Errorf("failed to generate RSA key: %w", err) } - certDERBytes, err := x509.CreateCertificate(rand.Reader, cert, rootKeyPair.Certificate, &certPrivKey.PublicKey, rootKeyPair.PrivateKey) + certDERBytes, err := x509.CreateCertificate(rand.Reader, cert, intermediateKeyPair.Certificate, &certPrivKey.PublicKey, intermediateKeyPair.PrivateKey) if err != nil { return nil, fmt.Errorf("failed to create certificate: %w", err) } @@ -317,18 +396,26 @@ func GenerateServerCertificate(root *CertificatePEMPair) (*CertificatePEMPair, e return certPEMPair, nil } -func VerifyCertificates(root, client, server *CertificatePEMPair) error { +// VerifyCertificates checks that the client and server certificates match the +// Root and Intermediate chains. +func VerifyCertificates(root, intermediate, client, server *CertificatePEMPair) error { rootKeyPair, err := ParseCertificateKeyPair(root) if err != nil { log.Fatalln("Failed to parse root cert and key") } + intermediateKeyPair, err := ParseCertificateKeyPair(intermediate) + if err != nil { + log.Fatalln("Failed to parse intermediate cert and key") + } rootPool := x509.NewCertPool() rootPool.AddCert(rootKeyPair.Certificate) + intermediatePool := x509.NewCertPool() + intermediatePool.AddCert(intermediateKeyPair.Certificate) opts := x509.VerifyOptions{ - Intermediates: x509.NewCertPool(), + Intermediates: intermediatePool, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, Roots: rootPool, } diff --git a/traffic_ops/traffic_ops_golang/auth/certificate.go b/traffic_ops/traffic_ops_golang/auth/certificate.go index 340e70f29c..8126f38a5b 100644 --- a/traffic_ops/traffic_ops_golang/auth/certificate.go +++ b/traffic_ops/traffic_ops_golang/auth/certificate.go @@ -1,15 +1,5 @@ package auth -import ( - "crypto/x509" - "encoding/pem" - "fmt" - "io/fs" - "io/ioutil" - "net/http" - "path/filepath" -) - /* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -29,23 +19,32 @@ import ( * under the License. */ +import ( + "crypto/x509" + "encoding/pem" + "fmt" + "io/fs" + "io/ioutil" + "net/http" + "path/filepath" +) + // ParseCertificate takes a http.Request, pulls the (optionally) provided client TLS // certificates and attempts to verify them against the directory of provided Root CA // certificates. The Root CA certificates can be different than those utilized by the -// http.Server. Returns a bool signifying whether the verification process was -// successful or an error if one was encountered. -func VerifyClientCertificate(r *http.Request, rootCertsDirPath string) (bool, error) { - // TODO: Parse client headers +// http.Server. Returns an error if the verification process fails +func VerifyClientCertificate(r *http.Request, rootCertsDirPath string) error { + // TODO: Parse client headers as alternative to TLS in the request if err := loadRootCerts(rootCertsDirPath); err != nil { - return false, fmt.Errorf("failed to load root certificates") + return fmt.Errorf("failed to load root certificates") } if err := verifyClientRootChain(r.TLS.PeerCertificates); err != nil { - return false, fmt.Errorf("failed to verify client to root certificate chain") + return fmt.Errorf("failed to verify client to root certificate chain") } - return true, nil + return nil } func verifyClientRootChain(clientChain []*x509.Certificate) error { @@ -74,7 +73,7 @@ func verifyClientRootChain(clientChain []*x509.Certificate) error { return nil } -// Lazy initialized, only added to once, need mutex? +// Lazy initialized var rootPool *x509.CertPool func loadRootCerts(dirPath string) error { diff --git a/traffic_ops/traffic_ops_golang/auth/certificate_test.go b/traffic_ops/traffic_ops_golang/auth/certificate_test.go index 0a7142b91e..faba1bfe99 100644 --- a/traffic_ops/traffic_ops_golang/auth/certificate_test.go +++ b/traffic_ops/traffic_ops_golang/auth/certificate_test.go @@ -29,9 +29,9 @@ import ( */ // TODO: Utilize expirimental/certificate_auth/generate_cert.go to create appropriate -// certs on demand for testing, such as expired Bofore/After dates +// certs on demand for testing, such as expired Before/After dates -func TestVerifyClientCertificateSuccess(t *testing.T) { +func TestVerifyClientCertificate_Success(t *testing.T) { rootCertPEMBlock, _ := pem.Decode([]byte(rootCertPEM)) rootCert, err := x509.ParseCertificate(rootCertPEMBlock.Bytes) if err != nil { @@ -51,20 +51,54 @@ func TestVerifyClientCertificateSuccess(t *testing.T) { if err != nil { t.Fatalf("failed to extract x509 from PEM string for clientCert. err: %s", err) } + intermediateCertPEMBlock, _ := pem.Decode([]byte(intermediateCertPEM)) + intermediateCert, err := x509.ParseCertificate(intermediateCertPEMBlock.Bytes) + if err != nil { + t.Fatalf("failed to extract x509 from PEM string for intermediateCert. err: %s", err) + } connState := new(tls.ConnectionState) connState.PeerCertificates = append(connState.PeerCertificates, clientCert) + connState.PeerCertificates = append(connState.PeerCertificates, intermediateCert) req.TLS = connState - success, err := VerifyClientCertificate(req, "root/pool/created/above") + err = VerifyClientCertificate(req, "root/pool/created/above") + if err != nil { + t.Fatalf("error failed to verify client certificate: %s", err) + } +} + +func TestVerifyClientCertificate_NoIntermediate_Fail(t *testing.T) { + rootCertPEMBlock, _ := pem.Decode([]byte(rootCertPEM)) + rootCert, err := x509.ParseCertificate(rootCertPEMBlock.Bytes) + if err != nil { + t.Fatalf("failed to extract x509 from PEM string for rootCert. err: %s", err) + } + + rootPool = x509.NewCertPool() + rootPool.AddCert(rootCert) + + req, err := http.NewRequest("POST", "/login", bytes.NewBuffer([]byte{})) + if err != nil { + t.Fatal("failed to create request") + } + + clientCertPEMBlock, _ := pem.Decode([]byte(clientCertPEM)) + clientCert, err := x509.ParseCertificate(clientCertPEMBlock.Bytes) if err != nil { - t.Fatalf("error attempting failed to verify client certificate: %s", err) + t.Fatalf("failed to extract x509 from PEM string for clientCert. err: %s", err) } - if !success { - t.Fatal("failed to verify client certificate") + + connState := new(tls.ConnectionState) + connState.PeerCertificates = append(connState.PeerCertificates, clientCert) + req.TLS = connState + + err = VerifyClientCertificate(req, "root/pool/created/above") + if err == nil { + t.Fatalf("should have failed without intermediate certificate: %s", err) } } -func TestLoadRootCertsSuccess(t *testing.T) { +func TestLoadRootCerts_Success(t *testing.T) { rootPool = nil err := loadRootCerts("test/success") @@ -75,18 +109,18 @@ func TestLoadRootCertsSuccess(t *testing.T) { } -func TestLoadRootCertsEmptyDirPathFail(t *testing.T) { +func TestLoadRootCerts_EmptyDirPath_Fail(t *testing.T) { rootPool = nil err := loadRootCerts("") if err == nil { - t.Fatalf("shoudl have failed to load certs with empty path. err: %s", err) + t.Fatalf("should have failed to load certs with empty path. err: %s", err) } } -func TestLoadRootCertsFail(t *testing.T) { +func TestLoadRootCerts_InvalidDir_Fail(t *testing.T) { rootPool = nil err := loadRootCerts("test/fail") @@ -112,13 +146,18 @@ func TestVerifyClientChainSuccess(t *testing.T) { if err != nil { t.Fatalf("failed to extract x509 from PEM string for clientCert. err: %s", err) } + intermediateCertPEMBlock, _ := pem.Decode([]byte(intermediateCertPEM)) + intermediateCert, err := x509.ParseCertificate(intermediateCertPEMBlock.Bytes) + if err != nil { + t.Fatalf("failed to extract x509 from PEM string for intermediateCert. err: %s", err) + } - if err = verifyClientRootChain([]*x509.Certificate{clientCert}); err != nil { + if err = verifyClientRootChain([]*x509.Certificate{clientCert, intermediateCert}); err != nil { t.Fatalf("failed to verify certificate chain with valid certs. err: %s", err) } } -func TestVerifyClientChainEmptyClientFail(t *testing.T) { +func TestVerifyClientChain_EmptyClient_Fail(t *testing.T) { rootCertPEMBlock, _ := pem.Decode([]byte(rootCertPEM)) rootCert, err := x509.ParseCertificate(rootCertPEMBlock.Bytes) if err != nil { @@ -133,7 +172,7 @@ func TestVerifyClientChainEmptyClientFail(t *testing.T) { } } -func TestVerifyClientChainEmptyRootFail(t *testing.T) { +func TestVerifyClientChain_EmptyRoot_Fail(t *testing.T) { clientCertPEMBlock, _ := pem.Decode([]byte(clientCertPEM)) clientCert, err := x509.ParseCertificate(clientCertPEMBlock.Bytes) if err != nil { @@ -145,7 +184,7 @@ func TestVerifyClientChainEmptyRootFail(t *testing.T) { } } -func TestVerifyClientChainWrongCertKeyUsageFail(t *testing.T) { +func TestVerifyClientChain_WrongCertKeyUsage_Fail(t *testing.T) { rootCertPEMBlock, _ := pem.Decode([]byte(rootCertPEM)) rootCert, err := x509.ParseCertificate(rootCertPEMBlock.Bytes) if err != nil { @@ -167,7 +206,7 @@ func TestVerifyClientChainWrongCertKeyUsageFail(t *testing.T) { } } -func TestParseClientCertificateUIDSuccess(t *testing.T) { +func TestParseClientCertificateUID_Success(t *testing.T) { clientCertPEMBlock, _ := pem.Decode([]byte(clientCertPEM)) clientCert, err := x509.ParseCertificate(clientCertPEMBlock.Bytes) if err != nil { @@ -179,7 +218,7 @@ func TestParseClientCertificateUIDSuccess(t *testing.T) { } } -func TestParseClientCertificateUIDFail(t *testing.T) { +func TestParseClientCertificateUID_Fail(t *testing.T) { // Server cert does not contain a UID object identifier serverCertPEMBlock, _ := pem.Decode([]byte(serverCertPEM)) serverCert, err := x509.ParseCertificate(serverCertPEMBlock.Bytes) @@ -193,108 +232,142 @@ func TestParseClientCertificateUIDFail(t *testing.T) { } const rootCertPEM = `-----BEGIN CERTIFICATE----- -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 -----END CERTIFICATE----- ` -const clientCertPEM = `-----BEGIN CERTIFICATE----- -MIIFrjCCA5agAwIBAgIIAOfaLIQ3CvUwDQYJKoZIhvcNAQELBQAwZTELMAkGA1UE +const intermediateCertPEM = `-----BEGIN CERTIFICATE----- +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 -reqlKjsX1XKA3+uPcxYJ/22o +ZTEMMAoGA1UECxMDQVRDMRswGQYDVQQDExJpbnRlcm1lZGlhdGUubG9jYWwwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/77BSHSnuXFAnqu/FH2ecke/E +UvhfHzcF/01qXPRK4tXTJfA0whtYoJ2qIpdBDH5UcnMfHyHWHXnay/4OYbwFM8Fi +EpexX1ecgRxph9S8KTvh1pzkI3axfQoz55xoQQNFcJZ70QxgCs9WinCqY2Y+9SLo +P1rFZSRhCSYAuveyDfDVDzU21vDYFC9uLYZvolt5G/cBPHOOTF+KTgrk6Xg3XVYn +XvId6gva1guuxRzIIRDq1Lh6zLLH2Ox632OkQs/OwrkkfwFoczvNvAOIxMf7NmTd +9j1MVBH5Agu6RwZNQQ9JZg4VugugcHN94REiyg01ypQ3yfXZGATDVbp9qNTAyH0Q +lOeqqRxJjwS/9l99b28uRDE9bQFn4+uCU/pGJadAYAEg399Vp5/77o+f+kYnaeqr +NomCXJv5nGBTaqc7EwbV0/pjqzelfwy/O0wGSSqNddQWGdwQ2Sm6cAqlz4uB5Zpu +5yBDEToaQ0yhmNanoqhQq3pcEDeccnTP2aGa3P4SlDrjSDFrmJdffHg5xWeaslxI +mZfxH2ChiE+y0wtuQ91A/h/fIA0IRQIUpog2d8LNeKtsboErXZRORO7L6x5GFAYM +NVnWgw/eF+zl5fkk+OI2UK3PFMosjjMjtHTcWyKFuXpE2v0wj5vcXMTmuOGClOx3 +yq0sB7LWDo0yYkfo1QIDAQABo4GGMIGDMA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUE +FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNV +HQ4EFgQUdTW8W+avOivYqcxslZgJwkfR8CUwHwYDVR0jBBgwFoAUhwMwHT5LUaWx +rdwRZbyxrf3pq/MwDQYJKoZIhvcNAQELBQADggIBAMf0zJYiMyQgwrXEKKSChzKr +kZzoOxz/9Jey2IfWi6exsalj0lguX6IBE5oriiNHOOb56IT84EjBt4uHKolGdzKl +KL+RCFGoe7h40KigY/I9pBkUNm30N21QFV6lHh2fXhjkLCBExpgP0VxZJKwZn5uH +fLvXZSFxgvGKHiuA58eW41S8xE7jPsTC3eprLTXIpUF1Yh5en33bhVtVdtaw3YSu +lbKY5y+kZLgJIlcediz4IqZvZ5MiEaD1e+tNwmtN44yayA7JMihk64qUE4R/j20l +JYHNIyjnujYRKxHbU8oQq2XTvQbTLl3MlYYzlXXQ/g86pdIPiricPP4tpBC02ASz +8iMcdpFtkC96M4lf/n9GZkFBIyXStcoJXSFxcEDDzpW2FYzu8SOr5Lj7YFmAVw/q +p4B56wOWEEvRTcM9v7+uP1AbH95KoAr1hd2z/tp5JFtQQrnmSxIRcomrXbp5RBpt +dMugMKmkZJfI9waXLqRn8WhEQ6/MloyLNjfAUn2IoK+araSBbfusMlfc2skJK4eD +AO71dMq6EVQr6TrTzfL0pUjPJDRvff6DPIj2mNcXvRPPEjO8VQzi6NOEitssYbwL +QOlf+M07UmfY8RjqhbTQgB6nAtAu1g4y7oHf7XTMZlRc0EZkJiHXQ5EoHlz4D+3J +oXi7IrLNF0+1/RFnYmKY -----END CERTIFICATE----- ` -const serverCertPEM = `-----BEGIN CERTIFICATE----- -MIIFxzCCA6+gAwIBAgIIQ09gD+ZAsDUwDQYJKoZIhvcNAQELBQAwZTELMAkGA1UE +const clientCertPEM = `-----BEGIN CERTIFICATE----- +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 +83Q6ELF+UUDc7BsykJBtsxddSkFI9u9Hm7s= -----END CERTIFICATE----- ` -//TODO: Create and add intermediate to chain for testing. -const intermediateCertPEM = `` +const serverCertPEM = `-----BEGIN CERTIFICATE----- +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 +qrCR +-----END CERTIFICATE----- +` diff --git a/traffic_ops/traffic_ops_golang/config/config.go b/traffic_ops/traffic_ops_golang/config/config.go index 37cffbe7c2..fa8d9f1fb2 100644 --- a/traffic_ops/traffic_ops_golang/config/config.go +++ b/traffic_ops/traffic_ops_golang/config/config.go @@ -95,6 +95,7 @@ type Config struct { RoleBasedPermissions bool `json:"role_based_permissions"` DefaultCertificateInfo *DefaultCertificateInfo `json:"default_certificate_info"` Cdni *CdniConf `json:"cdni"` + ClientCertAuth *ClientCertAuth `json:"client_cert_auth"` } // ConfigHypnotoad carries http setting for hypnotoad (mojolicious) server @@ -274,6 +275,10 @@ type CdniConf struct { DCdnId string `json:"dcdn_id"` } +type ClientCertAuth struct { + RootCertsDir string `json:"root_certs_dir"` +} + // NewFakeConfig returns a fake Config struct with just enough data to view Routes. func NewFakeConfig() Config { c := Config{} diff --git a/traffic_ops/traffic_ops_golang/login/login.go b/traffic_ops/traffic_ops_golang/login/login.go index 0d71e87101..c790dd199a 100644 --- a/traffic_ops/traffic_ops_golang/login/login.go +++ b/traffic_ops/traffic_ops_golang/login/login.go @@ -120,41 +120,45 @@ func LoginHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc { dbCtx, cancelTx := context.WithTimeout(r.Context(), time.Duration(cfg.DBQueryTimeoutSeconds)*time.Second) defer cancelTx() - // Attempt to perform client certificate authentication. If fails, goto standard form auth + // Attempt to perform client certificate authentication. If fails, goto standard form auth. If the + // certificate was verified, has a UID, and the UID matches an existing user we consider this to + // be a successful login. { - // No certs provided by the client (or enabled on the server), skip to form authentication + // No certs provided by the client. Skip to form authentication if len(r.TLS.PeerCertificates) == 0 { goto FormAuth } // Perform certificate verification to ensure it is valid against Root CAs - validCert, err := auth.VerifyClientCertificate(r, "") // TODO Pass in TLS Client Root Path from cfg + err := auth.VerifyClientCertificate(r, cfg.ClientCertAuth.RootCertsDir) if err != nil { - log.Warnf("error attempting to verify client provided TLS certificate. err: %s\n", err) + log.Warnf("ClientCertAuth: error attempting to verify client provided TLS certificate. err: %s\n", err) goto FormAuth } // Client provided a verified certificate. Extract UID value. - if validCert { - form.Username = auth.ParseClientCertificateUID(r.TLS.PeerCertificates[0]) - if len(form.Username) == 0 { - log.Infoln("client provided certificate did not contain a UID object identifier or value") - goto FormAuth - } + form.Username = auth.ParseClientCertificateUID(r.TLS.PeerCertificates[0]) + if len(form.Username) == 0 { + log.Infoln("ClientCertAuth: client provided certificate did not contain a UID object identifier or value") + goto FormAuth } - // Only check if we successfully retrieved a UID from the certificate - if validCert && form.Username != "" { - // Check if user exists and has a role. If the certificate was verified, has a UID, and the UID matches - // an existing user we consider this to be a successful login. - var blockingErr error - authenticated, err, blockingErr = auth.CheckLocalUserIsAllowed(form.Username, db, dbCtx) - if blockingErr != nil { - api.HandleErr(w, r, nil, http.StatusServiceUnavailable, nil, fmt.Errorf("error checking local user has role: %s", blockingErr.Error())) - return - } + // Check if user exists locally (TODB) and has a role. + var blockingErr error + authenticated, err, blockingErr = auth.CheckLocalUserIsAllowed(form.Username, db, dbCtx) + if blockingErr != nil { + api.HandleErr(w, r, nil, http.StatusServiceUnavailable, nil, fmt.Errorf("error checking local user has role: %s", blockingErr.Error())) + return + } + if err != nil { + log.Warnf("ClientCertAuth: checking local user: %s\n", err.Error()) + } + + // Check LDAP if enabled + if !authenticated && cfg.LDAPEnabled { + _, authenticated, err = auth.LookupUserDN(form.Username, cfg.ConfigLDAP) if err != nil { - log.Errorf("checking local user: %s\n", err.Error()) + log.Warnf("ClientCertAuth: checking ldap user: %s\n", err.Error()) } } } @@ -200,12 +204,10 @@ func LoginHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc { log.Errorf("checking local user password: %s\n", err.Error()) } var ldapErr error - if !authenticated { - if cfg.LDAPEnabled { - authenticated, ldapErr = auth.CheckLDAPUser(form, cfg.ConfigLDAP) - if ldapErr != nil { - log.Errorf("checking ldap user: %s\n", ldapErr.Error()) - } + if !authenticated && cfg.LDAPEnabled { + authenticated, ldapErr = auth.CheckLDAPUser(form, cfg.ConfigLDAP) + if ldapErr != nil { + log.Errorf("checking ldap user: %s\n", ldapErr.Error()) } } } diff --git a/traffic_ops/traffic_ops_golang/traffic_ops_golang.go b/traffic_ops/traffic_ops_golang/traffic_ops_golang.go index 5321cfa7fe..fd56e1cbaf 100644 --- a/traffic_ops/traffic_ops_golang/traffic_ops_golang.go +++ b/traffic_ops/traffic_ops_golang/traffic_ops_golang.go @@ -199,8 +199,7 @@ func main() { } if httpServer.TLSConfig == nil { httpServer.TLSConfig = &tls.Config{ - // Allow for clients to optionally send TLS certificates for authentication - ClientAuth: tls.RequestClientCert, + ClientAuth: tls.NoClientCert, // Can still send, but don't verify } } // Deprecated in 5.0 From 8671a2abe7761988bfcef6e090336e4f09b999a1 Mon Sep 17 00:00:00 2001 From: Taylor Frey Date: Tue, 4 Oct 2022 15:41:42 -0600 Subject: [PATCH 03/10] Add nil check for client TLS connection state --- traffic_ops/traffic_ops_golang/login/login.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/traffic_ops/traffic_ops_golang/login/login.go b/traffic_ops/traffic_ops_golang/login/login.go index c790dd199a..621ea92088 100644 --- a/traffic_ops/traffic_ops_golang/login/login.go +++ b/traffic_ops/traffic_ops_golang/login/login.go @@ -125,7 +125,7 @@ func LoginHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc { // be a successful login. { // No certs provided by the client. Skip to form authentication - if len(r.TLS.PeerCertificates) == 0 { + if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { goto FormAuth } From 3f62e98447b77979125e6588d5b9783d47738242 Mon Sep 17 00:00:00 2001 From: Taylor Frey Date: Tue, 4 Oct 2022 16:05:38 -0600 Subject: [PATCH 04/10] Update cdn.conf to include Root cert location. Remove test certs --- traffic_ops/app/conf/cdn.conf | 5 +- .../auth/test/fail/rsa.key.pem | 51 ------------------- ...Reference-DigiCertTLSECCP384RootG5.crt.pem | 14 ----- ...Reference-DigiCertTLSRSA4096RootG5.crt.pem | 31 ----------- .../test/success/ignored/ignoredfile.crt.pem | 0 .../auth/test/success/rootca.crt.pem | 32 ------------ .../traffic_ops_golang/config/config_test.go | 9 ++-- 7 files changed, 10 insertions(+), 132 deletions(-) delete mode 100644 traffic_ops/traffic_ops_golang/auth/test/fail/rsa.key.pem delete mode 100644 traffic_ops/traffic_ops_golang/auth/test/success/Reference-DigiCertTLSECCP384RootG5.crt.pem delete mode 100644 traffic_ops/traffic_ops_golang/auth/test/success/Reference-DigiCertTLSRSA4096RootG5.crt.pem delete mode 100644 traffic_ops/traffic_ops_golang/auth/test/success/ignored/ignoredfile.crt.pem delete mode 100644 traffic_ops/traffic_ops_golang/auth/test/success/rootca.crt.pem diff --git a/traffic_ops/app/conf/cdn.conf b/traffic_ops/app/conf/cdn.conf index 9eceeefb35..b735598a5f 100644 --- a/traffic_ops/app/conf/cdn.conf +++ b/traffic_ops/app/conf/cdn.conf @@ -102,5 +102,8 @@ }, "cdni" : { "dcdn_id" : "" - } + }, + "client_cert_auth" : { + "root_certs_dir" : "/etc/pki/tls/certs/" + } } diff --git a/traffic_ops/traffic_ops_golang/auth/test/fail/rsa.key.pem b/traffic_ops/traffic_ops_golang/auth/test/fail/rsa.key.pem deleted file mode 100644 index 0286033e26..0000000000 --- a/traffic_ops/traffic_ops_golang/auth/test/fail/rsa.key.pem +++ /dev/null @@ -1,51 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -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 ------END RSA PRIVATE KEY----- diff --git a/traffic_ops/traffic_ops_golang/auth/test/success/Reference-DigiCertTLSECCP384RootG5.crt.pem b/traffic_ops/traffic_ops_golang/auth/test/success/Reference-DigiCertTLSECCP384RootG5.crt.pem deleted file mode 100644 index c8e21b6980..0000000000 --- a/traffic_ops/traffic_ops_golang/auth/test/success/Reference-DigiCertTLSECCP384RootG5.crt.pem +++ /dev/null @@ -1,14 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw -CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp -Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 -MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ -bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG -ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS -7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp -0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS -B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 -BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ -LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 -DXZDjC5Ty3zfDBeWUA== ------END CERTIFICATE----- \ No newline at end of file diff --git a/traffic_ops/traffic_ops_golang/auth/test/success/Reference-DigiCertTLSRSA4096RootG5.crt.pem b/traffic_ops/traffic_ops_golang/auth/test/success/Reference-DigiCertTLSRSA4096RootG5.crt.pem deleted file mode 100644 index 369d35b742..0000000000 --- a/traffic_ops/traffic_ops_golang/auth/test/success/Reference-DigiCertTLSRSA4096RootG5.crt.pem +++ /dev/null @@ -1,31 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN -MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT -HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN -NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs -IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi -MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+ -ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0 -2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp -wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM -pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD -nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po -sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx -Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd -Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX -KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe -XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL -tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv -TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN -AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw -GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H -PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF -O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ -REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik -AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv -/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+ -p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw -MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF -qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK -ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ ------END CERTIFICATE----- \ No newline at end of file diff --git a/traffic_ops/traffic_ops_golang/auth/test/success/ignored/ignoredfile.crt.pem b/traffic_ops/traffic_ops_golang/auth/test/success/ignored/ignoredfile.crt.pem deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/traffic_ops/traffic_ops_golang/auth/test/success/rootca.crt.pem b/traffic_ops/traffic_ops_golang/auth/test/success/rootca.crt.pem deleted file mode 100644 index a667faf8d7..0000000000 --- a/traffic_ops/traffic_ops_golang/auth/test/success/rootca.crt.pem +++ /dev/null @@ -1,32 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFjjCCA3agAwIBAgIIT5rRhzhg8tYwDQYJKoZIhvcNAQELBQAwZTELMAkGA1UE -BhMCVVMxETAPBgNVBAgTCENvbG9yYWRvMQ8wDQYDVQQHEwZEZW52ZXIxDzANBgNV -BAoTBkFwYWNoZTEMMAoGA1UECxMDQVRDMRMwEQYDVQQDEwpyb290LmxvY2FsMB4X -DTIyMDgzMTIxMDM0OFoXDTIzMDgzMTIxMDM0OFowZTELMAkGA1UEBhMCVVMxETAP -BgNVBAgTCENvbG9yYWRvMQ8wDQYDVQQHEwZEZW52ZXIxDzANBgNVBAoTBkFwYWNo -ZTEMMAoGA1UECxMDQVRDMRMwEQYDVQQDEwpyb290LmxvY2FsMIICIjANBgkqhkiG -9w0BAQEFAAOCAg8AMIICCgKCAgEAwvsV2+PNj4ZszReTjbrhoAd1d76bY1/z4xpe -DElEqEWnXKfFiUw8WcgtA+WnNGZt5QKYdVZM91l11KhW40J5tfdlZtP8OkzSHKiZ -rjDHSQq8mgrurCrHc0K00DGCu/6UEN8wHjlLr8l0KLmrVh2WImaQohKm2eijprXN -UM+FBg+SdaMhn6oIqinJDDoFAYwXrAa0jEi+VCmjldnMO3JbQn3HmRmcCGctb68B -AAbP/24wzN1gcnOpxtOySQRGPObaQCm/CD19dIupDisEmF/HjYKtp53r+c+1geCF -WxJy8qx8GJWQ+EZjsF7j53yPbKRs9vPtvd4kPsCnk7yJxJ8ekwecr5ZZ6jkUrX0E -MejGE33ekmEZ0g2J3Dq+XFe0UKmXKUbDLKZf6158T7zxWn3t8vOxshIoBjOTExGu -fnFtyseZsfSmXeY364Ph0YcC0aseqNPdvnSQA4gzhs7OlzxggJrZWyTMDJpMpjfm -y0jHjwsYKZf/WsG4ipPuGE9iRGrU+2dDSiYrfU/SqWp+RcZb4W6rX18J8keZRYch -rEQSwF6bIrrGbshbyu4U9V7/bYXZMjMQ3DBd4ND9J+f2wV32QoamUfLMQp5WR1U3 -8FpVANgvFWO4glVtw8bjCp22MPUltBzGfwlT5vqHz+b/Au37K4FfjeKMYCEsatT3 -CN1L2RECAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8w -HQYDVR0OBBYEFH2wHtXCAjSENteF9/U+O/DN5fgUMA0GCSqGSIb3DQEBCwUAA4IC -AQBUKPdeq3UPLJ7FWTZDLR2lyNJpk/3ia2n5Gc+hVI7ipb23CngoEZLM6VnYZSQm -t60yZJHUBOP39/6Q3lD0CCy3tKjGnEST5W0DvZeiUJzpvdD+FRVMkI6zcpJbSHOV -r1v/dZc4CdG0BEtSUFVYzN/hJkGTeLf1EfZbtRwS9wNK0FD95Sjv6DhNFT4bpVPB -qMJIuD/+/z83OU7BuYi7lyE7rYLM5wnsnLSnlv0Q3HwIv5db2ZvUxOMukkdIjbLA -kgGKXvnm+HQpCaMGvNfFZXnMqey71Vmpr1rpz8GA/7iZ4aqa+yOaYyHpTaBraFsc -PtNxgscp8/FtcOKUSp1k+MBsb3ume5UPTPcFQK7DA/+7npLcEt3NqYpC1iYTPbCW -1h0mI+XsVHOH5IZc5ZimsyuavjkkZ3KpV3cCWyD8VE0xpXNL8uJJzoqmJpl+Qw/p -Bfj79H2Rr2ehYLDJTISsW7aGppHKfmILgN14Hwh9MJxJ2cBmZwAyLUBd9pmtWJ/1 -ZWjBdPOtdX0mnrPo1kLTAAx2AT0RSUEukbR9Gu0gHOr6yw4CsIUY0DyqSTMFEnbB -X6YkuXIBMWHELpABgg6H0pxcrjhx92hgcwxMl88FuW6yAsZDkxk2PHiuxWyXjr57 -3vS0IMRX8z6Vvz/SDct+snkKCmokR001HWCfwN6W/wnNqw== ------END CERTIFICATE----- diff --git a/traffic_ops/traffic_ops_golang/config/config_test.go b/traffic_ops/traffic_ops_golang/config/config_test.go index ee01de555b..24043868fd 100644 --- a/traffic_ops/traffic_ops_golang/config/config_test.go +++ b/traffic_ops/traffic_ops_golang/config/config_test.go @@ -142,7 +142,10 @@ const ( "secrets" : [ "mONKEYDOmONKEYSEE." ], - "inactivity_timeout" : 60 + "inactivity_timeout" : 60, + "client_cert_auth" : { + "root_certs_dir" : "/etc/pki/tls/certs/" + } } ` @@ -259,11 +262,11 @@ func TestLoadConfig(t *testing.T) { } if cfg.CertPath != "/etc/pki/tls/certs/localhost.crt" { - t.Error("Expected KeyPath() == /etc/pki/tls/private/localhost.key") + t.Error("expected CertPath() == /etc/pki/tls/private/localhost.crt") } if cfg.KeyPath != "/etc/pki/tls/private/localhost.key" { - t.Error("Expected KeyPath() == /etc/pki/tls/private/localhost.key") + t.Error("expected KeyPath() == /etc/pki/tls/private/localhost.key") } } From 0dc8a6299b74e18d36388c5442095e0364ef6f0c Mon Sep 17 00:00:00 2001 From: Taylor Frey Date: Tue, 4 Oct 2022 16:12:38 -0600 Subject: [PATCH 05/10] Remove filesystem tests for root cert --- .../auth/certificate_test.go | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/traffic_ops/traffic_ops_golang/auth/certificate_test.go b/traffic_ops/traffic_ops_golang/auth/certificate_test.go index faba1bfe99..34241f39ff 100644 --- a/traffic_ops/traffic_ops_golang/auth/certificate_test.go +++ b/traffic_ops/traffic_ops_golang/auth/certificate_test.go @@ -32,6 +32,8 @@ import ( // certs on demand for testing, such as expired Before/After dates func TestVerifyClientCertificate_Success(t *testing.T) { + rootPool = nil // ensure root pool is empty + rootCertPEMBlock, _ := pem.Decode([]byte(rootCertPEM)) rootCert, err := x509.ParseCertificate(rootCertPEMBlock.Bytes) if err != nil { @@ -68,6 +70,8 @@ func TestVerifyClientCertificate_Success(t *testing.T) { } func TestVerifyClientCertificate_NoIntermediate_Fail(t *testing.T) { + rootPool = nil // ensure root pool is empty + rootCertPEMBlock, _ := pem.Decode([]byte(rootCertPEM)) rootCert, err := x509.ParseCertificate(rootCertPEMBlock.Bytes) if err != nil { @@ -98,17 +102,6 @@ func TestVerifyClientCertificate_NoIntermediate_Fail(t *testing.T) { } } -func TestLoadRootCerts_Success(t *testing.T) { - rootPool = nil - - err := loadRootCerts("test/success") - - if err != nil { - t.Fatalf("failed to load certs. err: %s", err) - } - -} - func TestLoadRootCerts_EmptyDirPath_Fail(t *testing.T) { rootPool = nil @@ -120,18 +113,9 @@ func TestLoadRootCerts_EmptyDirPath_Fail(t *testing.T) { } -func TestLoadRootCerts_InvalidDir_Fail(t *testing.T) { - rootPool = nil - - err := loadRootCerts("test/fail") - - if err == nil { - t.Fatalf("should have failed to load certs attempting to parse PEM key into x509 Cert. err: %s", err) - } - -} - func TestVerifyClientChainSuccess(t *testing.T) { + rootPool = nil // ensure root pool is empty + rootCertPEMBlock, _ := pem.Decode([]byte(rootCertPEM)) rootCert, err := x509.ParseCertificate(rootCertPEMBlock.Bytes) if err != nil { @@ -158,6 +142,8 @@ func TestVerifyClientChainSuccess(t *testing.T) { } func TestVerifyClientChain_EmptyClient_Fail(t *testing.T) { + rootPool = nil // ensure root pool is empty + rootCertPEMBlock, _ := pem.Decode([]byte(rootCertPEM)) rootCert, err := x509.ParseCertificate(rootCertPEMBlock.Bytes) if err != nil { @@ -173,6 +159,8 @@ func TestVerifyClientChain_EmptyClient_Fail(t *testing.T) { } func TestVerifyClientChain_EmptyRoot_Fail(t *testing.T) { + rootPool = nil // ensure root pool is empty + clientCertPEMBlock, _ := pem.Decode([]byte(clientCertPEM)) clientCert, err := x509.ParseCertificate(clientCertPEMBlock.Bytes) if err != nil { @@ -185,6 +173,8 @@ func TestVerifyClientChain_EmptyRoot_Fail(t *testing.T) { } func TestVerifyClientChain_WrongCertKeyUsage_Fail(t *testing.T) { + rootPool = nil // ensure root pool is empty + rootCertPEMBlock, _ := pem.Decode([]byte(rootCertPEM)) rootCert, err := x509.ParseCertificate(rootCertPEMBlock.Bytes) if err != nil { @@ -207,6 +197,8 @@ func TestVerifyClientChain_WrongCertKeyUsage_Fail(t *testing.T) { } func TestParseClientCertificateUID_Success(t *testing.T) { + rootPool = nil // ensure root pool is empty + clientCertPEMBlock, _ := pem.Decode([]byte(clientCertPEM)) clientCert, err := x509.ParseCertificate(clientCertPEMBlock.Bytes) if err != nil { @@ -219,6 +211,8 @@ func TestParseClientCertificateUID_Success(t *testing.T) { } func TestParseClientCertificateUID_Fail(t *testing.T) { + rootPool = nil // ensure root pool is empty + // Server cert does not contain a UID object identifier serverCertPEMBlock, _ := pem.Decode([]byte(serverCertPEM)) serverCert, err := x509.ParseCertificate(serverCertPEMBlock.Bytes) From 8a8f57f8f6e0e616050ce477f9a6b900fa2bca7b Mon Sep 17 00:00:00 2001 From: Taylor Frey Date: Tue, 4 Oct 2022 16:40:25 -0600 Subject: [PATCH 06/10] Use long descriptive form in JSON for cdn.conf --- traffic_ops/app/conf/cdn.conf | 4 ++-- traffic_ops/traffic_ops_golang/config/config.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/traffic_ops/app/conf/cdn.conf b/traffic_ops/app/conf/cdn.conf index b735598a5f..0c3edeeced 100644 --- a/traffic_ops/app/conf/cdn.conf +++ b/traffic_ops/app/conf/cdn.conf @@ -103,7 +103,7 @@ "cdni" : { "dcdn_id" : "" }, - "client_cert_auth" : { - "root_certs_dir" : "/etc/pki/tls/certs/" + "client_certificate_authentication" : { + "root_certificates_directory" : "/etc/pki/tls/certs/" } } diff --git a/traffic_ops/traffic_ops_golang/config/config.go b/traffic_ops/traffic_ops_golang/config/config.go index fa8d9f1fb2..84eb3c8799 100644 --- a/traffic_ops/traffic_ops_golang/config/config.go +++ b/traffic_ops/traffic_ops_golang/config/config.go @@ -95,7 +95,7 @@ type Config struct { RoleBasedPermissions bool `json:"role_based_permissions"` DefaultCertificateInfo *DefaultCertificateInfo `json:"default_certificate_info"` Cdni *CdniConf `json:"cdni"` - ClientCertAuth *ClientCertAuth `json:"client_cert_auth"` + ClientCertAuth *ClientCertAuth `json:"client_certificate_authentication"` } // ConfigHypnotoad carries http setting for hypnotoad (mojolicious) server @@ -276,7 +276,7 @@ type CdniConf struct { } type ClientCertAuth struct { - RootCertsDir string `json:"root_certs_dir"` + RootCertsDir string `json:"root_certificates_directory"` } // NewFakeConfig returns a fake Config struct with just enough data to view Routes. From 86ea81899549954ef428958420fc0d43a048c5a9 Mon Sep 17 00:00:00 2001 From: Taylor Frey Date: Mon, 10 Oct 2022 16:08:32 -0600 Subject: [PATCH 07/10] Add checks for config values. Update example logic --- .../{ => certs}/generate_certs.go | 60 ++++++++++--------- .../certificate_auth/example/client.go | 60 +++++++++++++++++++ .../certificate_auth/example/server.go | 37 ++++++++++++ traffic_ops/app/conf/cdn.conf | 3 +- .../traffic_ops_golang/auth/certificate.go | 9 ++- traffic_ops/traffic_ops_golang/login/login.go | 7 ++- .../traffic_ops_golang/traffic_ops_golang.go | 2 +- 7 files changed, 145 insertions(+), 33 deletions(-) rename experimental/certificate_auth/{ => certs}/generate_certs.go (91%) create mode 100644 experimental/certificate_auth/example/client.go create mode 100644 experimental/certificate_auth/example/server.go diff --git a/experimental/certificate_auth/generate_certs.go b/experimental/certificate_auth/certs/generate_certs.go similarity index 91% rename from experimental/certificate_auth/generate_certs.go rename to experimental/certificate_auth/certs/generate_certs.go index fa39cf0f65..3c0682b3e4 100644 --- a/experimental/certificate_auth/generate_certs.go +++ b/experimental/certificate_auth/certs/generate_certs.go @@ -27,6 +27,7 @@ import ( "crypto/x509/pkix" "encoding/asn1" "encoding/pem" + "flag" "fmt" "io/ioutil" "log" @@ -48,7 +49,21 @@ type CertificatePEMPair struct { CertificatePEM, PrivateKeyPEM string } +var ( + rootCN = "root.local" + interCN = "intermediate.local" + clientCN = "client.local" + serverCN = "server.local" + + uid = "userid" + // useEcdsa = false //TODO: Enable and refactor +) + func main() { + flag.StringVar(&uid, "uid", uid, "[Optional] The User ID value to be added to the client certificate") + // flag.BoolVar(&useEcdsa, "useEcdsa", useEcdsa, "[Optional] Use ECDSA 256 when generating the keys. Default is RSA 4096") + + flag.Parse() rootCAPEMPair, err := GenerateRootCACertificate() if err != nil { @@ -69,7 +84,6 @@ func main() { log.Fatalf("Failed to generate and sign Server certificate\nErr: %s\n", err) } - log.Println("Certificate: ", serverPEMPair.CertificatePEM) ioutil.WriteFile("server.crt.pem", []byte(serverPEMPair.CertificatePEM), 0644) ioutil.WriteFile("server.key.pem", []byte(serverPEMPair.PrivateKeyPEM), 0644) @@ -78,10 +92,12 @@ func main() { log.Fatalf("Failed to generate and sign Client certificate\nErr: %s\n", err) } - log.Println("Certificate: ", clientPEMPair.CertificatePEM) ioutil.WriteFile("client.crt.pem", []byte(clientPEMPair.CertificatePEM), 0644) ioutil.WriteFile("client.key.pem", []byte(clientPEMPair.PrivateKeyPEM), 0644) + clientIntermediateChain := clientPEMPair.CertificatePEM + intermediatePEMPair.CertificatePEM + ioutil.WriteFile("client-intermediate-chain.crt.pem", []byte(clientIntermediateChain), 0644) + if err := VerifyCertificates(rootCAPEMPair, intermediatePEMPair, clientPEMPair, serverPEMPair); err != nil { log.Fatalf("failed to verify certificate: %s", err) } @@ -120,10 +136,7 @@ func GenerateRootCACertificate() (*CertificatePEMPair, error) { now := time.Now() - serialNumber, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) - if err != nil { - return nil, fmt.Errorf("failed to generate random serial number: %w", err) - } + serialNumber, _ := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) cert := &x509.Certificate{ SerialNumber: serialNumber, @@ -133,10 +146,10 @@ func GenerateRootCACertificate() (*CertificatePEMPair, error) { Country: []string{"US"}, Province: []string{"Colorado"}, Locality: []string{"Denver"}, - CommonName: "root.local", + CommonName: rootCN, }, NotBefore: now, - NotAfter: now.AddDate(15, 0, 0), + NotAfter: now.AddDate(1, 0, 0), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign, BasicConstraintsValid: true, IsCA: true, @@ -189,10 +202,7 @@ func GenerateIntermediateCertificate(root *CertificatePEMPair) (*CertificatePEMP now := time.Now() - serialNumber, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) - if err != nil { - return nil, fmt.Errorf("failed to generate random serial number: %w", err) - } + serialNumber, _ := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) cert := &x509.Certificate{ SerialNumber: serialNumber, @@ -202,10 +212,10 @@ func GenerateIntermediateCertificate(root *CertificatePEMPair) (*CertificatePEMP Country: []string{"US"}, Province: []string{"Colorado"}, Locality: []string{"Denver"}, - CommonName: "intermediate.local", + CommonName: interCN, }, NotBefore: now, - NotAfter: now.AddDate(10, 0, 0), + NotAfter: now.AddDate(1, 0, 0), ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign, BasicConstraintsValid: true, @@ -261,16 +271,13 @@ func GenerateClientCertificate(intermediate *CertificatePEMPair) (*CertificatePE now := time.Now() - serialNumber, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) - if err != nil { - return nil, fmt.Errorf("failed to generate random serial number: %w", err) - } + serialNumber, _ := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) // LDAP OID reference: https://ldap.com/ldap-oid-reference-guide/ // 0.9.2342.19200300.100.1.1 uid Attribute Type uidPkix := pkix.AttributeTypeAndValue{ Type: asn1.ObjectIdentifier([]int{0, 9, 2342, 19200300, 100, 1, 1}), - Value: "userid", + Value: uid, } cert := &x509.Certificate{ @@ -281,11 +288,11 @@ func GenerateClientCertificate(intermediate *CertificatePEMPair) (*CertificatePE Country: []string{"US"}, Province: []string{"Colorado"}, Locality: []string{"Denver"}, - CommonName: "client.local", + CommonName: clientCN, ExtraNames: []pkix.AttributeTypeAndValue{uidPkix}, }, NotBefore: now, - NotAfter: now.AddDate(5, 0, 0), + NotAfter: now.AddDate(1, 0, 0), ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyAgreement, } @@ -338,10 +345,7 @@ func GenerateServerCertificate(intermediate *CertificatePEMPair) (*CertificatePE now := time.Now() - serialNumber, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) - if err != nil { - return nil, fmt.Errorf("failed to generate random serial number: %w", err) - } + serialNumber, _ := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) cert := &x509.Certificate{ SerialNumber: serialNumber, @@ -351,12 +355,12 @@ func GenerateServerCertificate(intermediate *CertificatePEMPair) (*CertificatePE Country: []string{"US"}, Province: []string{"Colorado"}, Locality: []string{"Denver"}, - CommonName: "server.local", + CommonName: serverCN, }, - DNSNames: []string{"server.local"}, + DNSNames: []string{serverCN}, IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, NotBefore: now, - NotAfter: now.AddDate(5, 0, 0), + NotAfter: now.AddDate(1, 0, 0), ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement, } diff --git a/experimental/certificate_auth/example/client.go b/experimental/certificate_auth/example/client.go new file mode 100644 index 0000000000..f80e4d2ba2 --- /dev/null +++ b/experimental/certificate_auth/example/client.go @@ -0,0 +1,60 @@ +package main + +import ( + "bytes" + "crypto/tls" + "fmt" + "io/ioutil" + "log" + "net/http" + "time" +) + +func main() { + // LoadX509KeyPair can also load certificate chain with intermediates + cert, _ := tls.LoadX509KeyPair("../certs/client-intermediate-chain.crt.pem", "../certs/client.key.pem") + + transport := &http.Transport{ + TLSClientConfig: &tls.Config{ + Certificates: []tls.Certificate{cert}, + }, + } + + client := http.Client{ + Timeout: time.Second * 60, + Transport: transport, + } + + // Send standard username/password form combo + // reqBody, err := json.Marshal(map[string]string{ + // "u": "userid", + // "p": "exampleuseridpassword", + // }) + // if err != nil { + // log.Fatalln(err) + // } + + req, err := http.NewRequest( + http.MethodPost, + "https://server.local:8443/api/4.0/user/login", + bytes.NewBufferString(""), + // bytes.NewBuffer(reqBody), // username/password + ) + if err != nil { + log.Fatalln(err) + } + + resp, err := client.Do(req) + if err != nil { + log.Fatalln(err) + } + defer resp.Body.Close() + + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Fatalln(err) + } + + fmt.Println(string(respBody)) // Verify Success + fmt.Println(resp.Cookies()) // Verify Cookie(s) +} diff --git a/experimental/certificate_auth/example/server.go b/experimental/certificate_auth/example/server.go new file mode 100644 index 0000000000..2ef7653fd6 --- /dev/null +++ b/experimental/certificate_auth/example/server.go @@ -0,0 +1,37 @@ +package main + +import ( + "crypto/tls" + "fmt" + "log" + "net/http" +) + +func main() { + handler := http.NewServeMux() + handler.HandleFunc("/", HelloHandler) + + tlsConfig := &tls.Config{ + ClientAuth: tls.RequestClientCert, + } + + server := http.Server{ + Addr: "server.local:8443", + Handler: handler, + TLSConfig: tlsConfig, + } + + if err := server.ListenAndServeTLS("../certs/server.crt.pem", "../certs/server.key.pem"); err != nil { + log.Fatalf("error listening to port: %v", err) + } +} + +func HelloHandler(w http.ResponseWriter, r *http.Request) { + + if r.TLS.PeerCertificates != nil { + clientCert := r.TLS.PeerCertificates[0] + fmt.Println("Client cert subject: ", clientCert.Subject) + } + + fmt.Println("Hello") +} diff --git a/traffic_ops/app/conf/cdn.conf b/traffic_ops/app/conf/cdn.conf index 0c3edeeced..dc5da17cd6 100644 --- a/traffic_ops/app/conf/cdn.conf +++ b/traffic_ops/app/conf/cdn.conf @@ -40,7 +40,8 @@ "profiling_enabled": false, "supported_ds_metrics": [ "kbps", "tps_total", "tps_2xx", "tps_3xx", "tps_4xx", "tps_5xx" ], "tls_config": { - "MinVersion": 769 + "MinVersion": 769, + "ClientAuth": 1 } }, "disable_auto_cert_deletion": false, diff --git a/traffic_ops/traffic_ops_golang/auth/certificate.go b/traffic_ops/traffic_ops_golang/auth/certificate.go index 8126f38a5b..eea3e1eb7e 100644 --- a/traffic_ops/traffic_ops_golang/auth/certificate.go +++ b/traffic_ops/traffic_ops_golang/auth/certificate.go @@ -78,6 +78,8 @@ var rootPool *x509.CertPool func loadRootCerts(dirPath string) error { // Root cert pool already populated + // TODO: This will prevent rolling cert renewals at runtime and will require a TO restart + // to pick up additional certificates. if rootPool != nil { return nil } @@ -114,11 +116,16 @@ func loadRootCerts(dirPath string) error { return filepath.SkipDir } + // Read file pemBytes, err := ioutil.ReadFile(path) if err != nil { return fmt.Errorf("failed to open cert at %s. err: %w", path, err) } pemBlock, _ := pem.Decode(pemBytes) + // Failed to decode PEM, skip file + if pemBlock == nil { + return nil + } certificate, err := x509.ParseCertificate(pemBlock.Bytes) if err != nil { return fmt.Errorf("failed to parse PEM into x509. err: %w", err) @@ -129,8 +136,6 @@ func loadRootCerts(dirPath string) error { } rootPool.AddCert(certificate) - fmt.Printf("Added cert %s\n", path) - return nil }) if err != nil { diff --git a/traffic_ops/traffic_ops_golang/login/login.go b/traffic_ops/traffic_ops_golang/login/login.go index 621ea92088..f193e36c24 100644 --- a/traffic_ops/traffic_ops_golang/login/login.go +++ b/traffic_ops/traffic_ops_golang/login/login.go @@ -129,6 +129,11 @@ func LoginHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc { goto FormAuth } + // If no configuration is set, skip to form auth + if cfg.ClientCertAuth == nil || len(cfg.ClientCertAuth.RootCertsDir) == 0 { + goto FormAuth + } + // Perform certificate verification to ensure it is valid against Root CAs err := auth.VerifyClientCertificate(r, cfg.ClientCertAuth.RootCertsDir) if err != nil { @@ -212,7 +217,7 @@ func LoginHandler(db *sqlx.DB, cfg config.Config) http.HandlerFunc { } } - // User does not exist in either local DB or LDAP, return unauthorized + // Failed to authenticate in either local DB or LDAP, return unauthorized if !authenticated { resp = tc.CreateAlerts(tc.ErrorLevel, "Invalid username or password.") w.WriteHeader(http.StatusUnauthorized) diff --git a/traffic_ops/traffic_ops_golang/traffic_ops_golang.go b/traffic_ops/traffic_ops_golang/traffic_ops_golang.go index fd56e1cbaf..154cf7eef3 100644 --- a/traffic_ops/traffic_ops_golang/traffic_ops_golang.go +++ b/traffic_ops/traffic_ops_golang/traffic_ops_golang.go @@ -199,7 +199,7 @@ func main() { } if httpServer.TLSConfig == nil { httpServer.TLSConfig = &tls.Config{ - ClientAuth: tls.NoClientCert, // Can still send, but don't verify + ClientAuth: tls.RequestClientCert, } } // Deprecated in 5.0 From 277a38b66d166ae0aae97d8ad58b72d5e5444912 Mon Sep 17 00:00:00 2001 From: Taylor Frey Date: Tue, 1 Nov 2022 10:20:40 -0600 Subject: [PATCH 08/10] Initial documentation commit instead of stash --- .../admin/quick_howto/client_cert_auth.rst | 25 +++++++++++++++++++ docs/source/admin/traffic_ops.rst | 7 +++++- traffic_ops/app/conf/production/database.conf | 1 - 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 docs/source/admin/quick_howto/client_cert_auth.rst diff --git a/docs/source/admin/quick_howto/client_cert_auth.rst b/docs/source/admin/quick_howto/client_cert_auth.rst new file mode 100644 index 0000000000..72b754dd36 --- /dev/null +++ b/docs/source/admin/quick_howto/client_cert_auth.rst @@ -0,0 +1,25 @@ +.. +.. +.. Licensed under the Apache License, Version 2.0 (the "License"); +.. you may not use this file except in compliance with the License. +.. You may obtain a copy of the License at +.. +.. http://www.apache.org/licenses/LICENSE-2.0 +.. +.. Unless required by applicable law or agreed to in writing, software +.. distributed under the License is distributed on an "AS IS" BASIS, +.. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +.. See the License for the specific language governing permissions and +.. limitations under the License. +.. + +.. _client-cert-auth: + +************************************** +Client Certificates for Authentication +************************************** + +An alternative mechanism for providing credentials and authenticating access. + +There are multiple mechanisms, specifically within Traffic Ops, that provide a means for authentication. + diff --git a/docs/source/admin/traffic_ops.rst b/docs/source/admin/traffic_ops.rst index 0a3c28c66c..9e3c58fd47 100644 --- a/docs/source/admin/traffic_ops.rst +++ b/docs/source/admin/traffic_ops.rst @@ -320,6 +320,12 @@ This file deals with the configuration parameters of running Traffic Ops itself. :renew_days_before_expiration: Set the number of days before expiration date to renew certificates. :summary_email: The email address to use for summarizing certificate expiration and renewal status. If it is blank, no email will be sent. +:client_certificate_authentication: This is an optional section of configurations client provided certificate based authentication. However, if ``"ClientAuth" : "1"``` is enabled in the ``tls_config`` section in ``traffic_ops_golang``, then this field is required. + + .. versionadded:: 7.0 + + :root_certificates_directory: A string representing the absolute path of the directory where Root CA certificates are located. These Root CA certificates are used for verifying the certificate provided by the client. + :default_certificate_info: This is an optional object to define default values when generating a self signed certificate when an HTTPS delivery service is created or updated. If this is an empty object or not present in the :ref:`cdn.conf` then the term "Placeholder" will be used for all fields. :business_unit: An optional field which, if present, will represent the business unit for which the SSL certificate was generated @@ -517,7 +523,6 @@ This file deals with the configuration parameters of running Traffic Ops itself. .. versionadded:: 7.0 - Example cdn.conf '''''''''''''''' .. include:: ../../../traffic_ops/app/conf/cdn.conf diff --git a/traffic_ops/app/conf/production/database.conf b/traffic_ops/app/conf/production/database.conf index 84284caafe..5760268309 100644 --- a/traffic_ops/app/conf/production/database.conf +++ b/traffic_ops/app/conf/production/database.conf @@ -1,4 +1,3 @@ - { "description": "Local PostgreSQL database on port 5432", "dbname": "traffic_ops", From 7b62b266ac29ea874a912d12aaf2a9ace77ee1fa Mon Sep 17 00:00:00 2001 From: Taylor Frey Date: Wed, 8 Feb 2023 15:38:46 -0700 Subject: [PATCH 09/10] Moved client.go and server.go to separate folders because they are both main functions --- experimental/certificate_auth/example/{ => client}/client.go | 0 experimental/certificate_auth/example/{ => server}/server.go | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename experimental/certificate_auth/example/{ => client}/client.go (100%) rename experimental/certificate_auth/example/{ => server}/server.go (100%) diff --git a/experimental/certificate_auth/example/client.go b/experimental/certificate_auth/example/client/client.go similarity index 100% rename from experimental/certificate_auth/example/client.go rename to experimental/certificate_auth/example/client/client.go diff --git a/experimental/certificate_auth/example/server.go b/experimental/certificate_auth/example/server/server.go similarity index 100% rename from experimental/certificate_auth/example/server.go rename to experimental/certificate_auth/example/server/server.go From c6596f0dd2c074e3127829404a6fb9b51892302b Mon Sep 17 00:00:00 2001 From: Taylor Frey Date: Wed, 8 Feb 2023 16:04:32 -0700 Subject: [PATCH 10/10] Add Apache license to example server+client --- .../certificate_auth/example/client/client.go | 19 +++++++++++++++++++ .../certificate_auth/example/server/server.go | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/experimental/certificate_auth/example/client/client.go b/experimental/certificate_auth/example/client/client.go index f80e4d2ba2..c0b197691e 100644 --- a/experimental/certificate_auth/example/client/client.go +++ b/experimental/certificate_auth/example/client/client.go @@ -1,5 +1,24 @@ package main +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + import ( "bytes" "crypto/tls" diff --git a/experimental/certificate_auth/example/server/server.go b/experimental/certificate_auth/example/server/server.go index 2ef7653fd6..a5608ff262 100644 --- a/experimental/certificate_auth/example/server/server.go +++ b/experimental/certificate_auth/example/server/server.go @@ -1,5 +1,24 @@ package main +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + import ( "crypto/tls" "fmt"