[PATCH 1/3] Refactor the certification validation code

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



From: "Daniel P. Berrange" <berrange@xxxxxxxxxx>

There is some commonality between the code for sanity checking
certs when initializing libvirt and the code for validating
certs during a live TLS session handshake. This patchset splits
up the sanity checking function into several smaller functions
each doing a specific type of check. The cert validation code
is then updated to also call into these functions

* src/rpc/virnettlscontext.c: Refactor cert validation code
---
 src/rpc/virnettlscontext.c |  466 +++++++++++++++++++++++++++-----------------
 1 files changed, 283 insertions(+), 183 deletions(-)

diff --git a/src/rpc/virnettlscontext.c b/src/rpc/virnettlscontext.c
index 7d271bc..37b55ad 100644
--- a/src/rpc/virnettlscontext.c
+++ b/src/rpc/virnettlscontext.c
@@ -97,68 +97,51 @@ static void virNetTLSLog(int level, const char *str) {
 }
 
 
-static gnutls_x509_crt_t virNetTLSContextSanityCheckCert(bool isServer,
-                                                         bool isCA,
-                                                         const char *certFile)
+static int virNetTLSContextCheckCertTimes(gnutls_x509_crt_t cert,
+                                          const char *certFile,
+                                          bool isServer,
+                                          bool isCA)
 {
-    gnutls_datum_t data;
-    gnutls_x509_crt_t cert = NULL;
-    char *buf = NULL;
-    int ret = -1;
     time_t now;
-    int status;
-    int i;
-    char *buffer = NULL;
-    size_t size;
-    unsigned int usage;
-    unsigned int critical;
-    bool allowClient = false, allowServer = false;
-
-    VIR_DEBUG("isServer %d isCA %d certFile %s",
-              isServer, isCA, certFile);
 
     if ((now = time(NULL)) == ((time_t)-1)) {
         virReportSystemError(errno, "%s",
                              _("cannot get current time"));
-        goto cleanup;
-    }
-
-    if (gnutls_x509_crt_init(&cert) < 0) {
-        virNetError(VIR_ERR_SYSTEM_ERROR, "%s",
-                    _("Unable to initialize certificate"));
-        goto cleanup;
-    }
-
-    if (virFileReadAll(certFile, (1<<16), &buf) < 0)
-        goto cleanup;
-
-    data.data = (unsigned char *)buf;
-    data.size = strlen(buf);
-
-    if (gnutls_x509_crt_import(cert, &data, GNUTLS_X509_FMT_PEM) < 0) {
-        virNetError(VIR_ERR_SYSTEM_ERROR, isServer ?
-                    _("Unable to import server certificate %s") :
-                    _("Unable to import client certificate %s"),
-                    certFile);
-        goto cleanup;
+        return -1;
     }
 
     if (gnutls_x509_crt_get_expiration_time(cert) < now) {
-        virNetError(VIR_ERR_SYSTEM_ERROR, isServer ?
-                    _("The server certificate %s has expired") :
-                    _("The client certificate %s has expired"),
+        virNetError(VIR_ERR_SYSTEM_ERROR,
+                    (isCA ?
+                     _("The CA certificate %s has expired") :
+                     (isServer ?
+                      _("The server certificate %s has expired") :
+                      _("The client certificate %s has expired"))),
                     certFile);
-        goto cleanup;
+        return -1;
     }
 
     if (gnutls_x509_crt_get_activation_time(cert) > now) {
-        virNetError(VIR_ERR_SYSTEM_ERROR, isServer ?
-                    _("The server certificate %s is not yet active") :
-                    _("The client certificate %s is not yet active"),
+        virNetError(VIR_ERR_SYSTEM_ERROR,
+                    (isCA ?
+                     _("The CA certificate %s is not yet active") :
+                     (isServer ?
+                      _("The server certificate %s is not yet active") :
+                      _("The client certificate %s is not yet active"))),
                     certFile);
-        goto cleanup;
+        return -1;
     }
 
+    return 0;
+}
+
+static int virNetTLSContextCheckCertBasicConstraints(gnutls_x509_crt_t cert,
+                                                     const char *certFile,
+                                                     bool isServer,
+                                                     bool isCA)
+{
+    int status;
+
     status = gnutls_x509_crt_get_basic_constraints(cert, NULL, NULL, NULL);
     VIR_DEBUG("Cert %s basic constraints %d", certFile, status);
 
@@ -168,29 +151,40 @@ static gnutls_x509_crt_t virNetTLSContextSanityCheckCert(bool isServer,
                         _("The certificate %s basic constraints show a CA, but we need one for a server") :
                         _("The certificate %s basic constraints show a CA, but we need one for a client"),
                         certFile);
-            goto cleanup;
+            return -1;
         }
     } else if (status == 0) { /* It is not a CA cert */
         if (isCA) {
             virNetError(VIR_ERR_SYSTEM_ERROR,
                         _("The certificate %s basic constraints do not show a CA"),
                         certFile);
-            goto cleanup;
+            return -1;
         }
     } else if (status == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) { /* Missing basicConstraints */
         if (isCA) {
             virNetError(VIR_ERR_SYSTEM_ERROR,
                         _("The certificate %s is missing basic constraints for a CA"),
                         certFile);
-            goto cleanup;
+            return -1;
         }
     } else { /* General error */
         virNetError(VIR_ERR_SYSTEM_ERROR,
                     _("Unable to query certificate %s basic constraints %s"),
                     certFile, gnutls_strerror(status));
-        goto cleanup;
+        return -1;
     }
 
+    return 0;
+}
+
+static int virNetTLSContextCheckCertKeyUsage(gnutls_x509_crt_t cert,
+                                             const char *certFile,
+                                             bool isCA)
+{
+    int status;
+    unsigned int usage;
+    unsigned int critical;
+
     status = gnutls_x509_crt_get_key_usage(cert, &usage, &critical);
 
     VIR_DEBUG("Cert %s key usage status %d usage %d critical %u", certFile, status, usage, critical);
@@ -202,7 +196,7 @@ static gnutls_x509_crt_t virNetTLSContextSanityCheckCert(bool isServer,
             virNetError(VIR_ERR_SYSTEM_ERROR,
                         _("Unable to query certificate %s key usage %s"),
                         certFile, gnutls_strerror(status));
-            goto cleanup;
+            return -1;
         }
     }
 
@@ -212,7 +206,7 @@ static gnutls_x509_crt_t virNetTLSContextSanityCheckCert(bool isServer,
                 virNetError(VIR_ERR_SYSTEM_ERROR,
                             _("Certificate %s usage does not permit certificate signing"),
                             certFile);
-                goto cleanup;
+                return -1;
             } else {
                 VIR_WARN("Certificate %s usage does not permit certificate signing",
                          certFile);
@@ -224,7 +218,7 @@ static gnutls_x509_crt_t virNetTLSContextSanityCheckCert(bool isServer,
                 virNetError(VIR_ERR_SYSTEM_ERROR,
                             _("Certificate %s usage does not permit digital signature"),
                             certFile);
-                goto cleanup;
+                return -1;
             } else {
                 VIR_WARN("Certificate %s usage does not permit digital signature",
                          certFile);
@@ -235,7 +229,7 @@ static gnutls_x509_crt_t virNetTLSContextSanityCheckCert(bool isServer,
                 virNetError(VIR_ERR_SYSTEM_ERROR,
                             _("Certificate %s usage does not permit key encipherment"),
                             certFile);
-                goto cleanup;
+                return -1;
             } else {
                 VIR_WARN("Certificate %s usage does not permit key encipherment",
                          certFile);
@@ -243,10 +237,25 @@ static gnutls_x509_crt_t virNetTLSContextSanityCheckCert(bool isServer,
         }
     }
 
+    return 0;
+}
+
+
+static int virNetTLSContextCheckCertKeyPurpose(gnutls_x509_crt_t cert,
+                                               const char *certFile,
+                                               bool isServer)
+{
+    int status;
+    int i;
+    unsigned int purposeCritical;
+    unsigned int critical;
+    char *buffer;
+    size_t size;
+    bool allowClient = false, allowServer = false;
+
     critical = 0;
     for (i = 0 ; ; i++) {
         size = 0;
-        unsigned int purposeCritical;
         status = gnutls_x509_crt_get_key_purpose_oid(cert, i, buffer, &size, NULL);
 
         if (status == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) {
@@ -261,20 +270,21 @@ static gnutls_x509_crt_t virNetTLSContextSanityCheckCert(bool isServer,
             virNetError(VIR_ERR_SYSTEM_ERROR,
                         _("Unable to query certificate %s key purpose %s"),
                         certFile, gnutls_strerror(status));
-            goto cleanup;
+            return -1;
         }
 
         if (VIR_ALLOC_N(buffer, size) < 0) {
             virReportOOMError();
-            goto cleanup;
+            return -1;
         }
 
         status = gnutls_x509_crt_get_key_purpose_oid(cert, i, buffer, &size, &purposeCritical);
         if (status < 0) {
+            VIR_FREE(buffer);
             virNetError(VIR_ERR_SYSTEM_ERROR,
                         _("Unable to query certificate %s key purpose %s"),
                         certFile, gnutls_strerror(status));
-            goto cleanup;
+            return -1;
         }
         if (purposeCritical)
             critical = true;
@@ -291,24 +301,25 @@ static gnutls_x509_crt_t virNetTLSContextSanityCheckCert(bool isServer,
         VIR_FREE(buffer);
     }
 
-    if (!isCA) { /* No purpose checks required for CA certs */
-        if (isServer && !allowServer) {
+    if (isServer) {
+        if (!allowServer) {
             if (critical) {
                 virNetError(VIR_ERR_SYSTEM_ERROR,
                             _("Certificate %s purpose does not allow use for with a TLS server"),
                             certFile);
-                goto cleanup;
+                return -1;
             } else {
                 VIR_WARN("Certificate %s purpose does not allow use for with a TLS server",
                          certFile);
             }
         }
-        if (!isServer && !allowClient) {
+    } else {
+        if (!allowClient) {
             if (critical) {
                 virNetError(VIR_ERR_SYSTEM_ERROR,
                             _("Certificate %s purpose does not allow use for with a TLS client"),
                             certFile);
-                goto cleanup;
+                return -1;
             } else {
                 VIR_WARN("Certificate %s purpose does not allow use for with a TLS client",
                          certFile);
@@ -316,6 +327,181 @@ static gnutls_x509_crt_t virNetTLSContextSanityCheckCert(bool isServer,
         }
     }
 
+    return 0;
+}
+
+/* Check DN is on tls_allowed_dn_list. */
+static int
+virNetTLSContextCheckCertDNWhitelist(const char *dname,
+                                     const char *const*wildcards)
+{
+    while (*wildcards) {
+        int ret = fnmatch (*wildcards, dname, 0);
+        if (ret == 0) /* Succesful match */
+            return 1;
+        if (ret != FNM_NOMATCH) {
+            virNetError(VIR_ERR_INTERNAL_ERROR,
+                        _("Malformed TLS whitelist regular expression '%s'"),
+                        *wildcards);
+            return -1;
+        }
+
+        wildcards++;
+    }
+
+    /* Log the client's DN for debugging */
+    VIR_DEBUG("Failed whitelist check for client DN '%s'", dname);
+
+    /* This is the most common error: make it informative. */
+    virNetError(VIR_ERR_SYSTEM_ERROR, "%s",
+                _("Client's Distinguished Name is not on the list "
+                  "of allowed clients (tls_allowed_dn_list).  Use "
+                  "'certtool -i --infile clientcert.pem' to view the"
+                  "Distinguished Name field in the client certificate,"
+                  "or run this daemon with --verbose option."));
+    return 0;
+}
+
+
+static int
+virNetTLSContextCheckCertDN(gnutls_x509_crt_t cert,
+                            const char *certFile,
+                            const char *hostname,
+                            const char *const* whitelist)
+{
+    int ret;
+    char name[256];
+    size_t namesize = sizeof name;
+
+    memset(name, 0, namesize);
+
+    ret = gnutls_x509_crt_get_dn(cert, name, &namesize);
+    if (ret != 0) {
+        virNetError(VIR_ERR_SYSTEM_ERROR,
+                    _("Failed to get certificate %s distinguished name: %s"),
+                    certFile, gnutls_strerror(ret));
+        return -1;
+    }
+
+    if (whitelist &&
+        virNetTLSContextCheckCertDNWhitelist(name, whitelist) <= 0)
+        return -1;
+
+    if (hostname &&
+        !gnutls_x509_crt_check_hostname(cert, hostname)) {
+        virNetError(VIR_ERR_RPC,
+                    _("Certificate %s owner does not match the hostname %s"),
+                    certFile, hostname);
+        return -1;
+    }
+
+    return 0;
+}
+
+
+static int virNetTLSContextCheckCert(gnutls_x509_crt_t cert,
+                                     const char *certFile,
+                                     bool isServer,
+                                     bool isCA)
+{
+    if (virNetTLSContextCheckCertTimes(cert, certFile,
+                                       isServer, isCA) < 0)
+        return -1;
+
+    if (virNetTLSContextCheckCertBasicConstraints(cert, certFile,
+                                                  isServer, isCA) < 0)
+        return -1;
+
+    if (virNetTLSContextCheckCertKeyUsage(cert, certFile,
+                                          isCA) < 0)
+        return -1;
+
+    if (!isCA &&
+        virNetTLSContextCheckCertKeyPurpose(cert, certFile,
+                                            isServer) < 0)
+        return -1;
+
+    return 0;
+}
+
+
+static int virNetTLSContextCheckCertPair(gnutls_x509_crt_t cert,
+                                         const char *certFile,
+                                         gnutls_x509_crt_t cacert,
+                                         const char *cacertFile,
+                                         bool isServer)
+{
+    unsigned int status;
+
+    if (gnutls_x509_crt_list_verify(&cert, 1,
+                                    &cacert, 1,
+                                    NULL, 0,
+                                    0, &status) < 0) {
+        virNetError(VIR_ERR_SYSTEM_ERROR, isServer ?
+                    _("Unable to verify server certificate %s against CA certificate %s") :
+                    _("Unable to verify client certificate %s against CA certificate %s"),
+                    certFile, cacertFile);
+        return -1;
+    }
+
+    if (status != 0) {
+        const char *reason = _("Invalid certificate");
+
+        if (status & GNUTLS_CERT_INVALID)
+            reason = _("The certificate is not trusted.");
+
+        if (status & GNUTLS_CERT_SIGNER_NOT_FOUND)
+            reason = _("The certificate hasn't got a known issuer.");
+
+        if (status & GNUTLS_CERT_REVOKED)
+            reason = _("The certificate has been revoked.");
+
+#ifndef GNUTLS_1_0_COMPAT
+        if (status & GNUTLS_CERT_INSECURE_ALGORITHM)
+            reason = _("The certificate uses an insecure algorithm");
+#endif
+
+        virNetError(VIR_ERR_SYSTEM_ERROR,
+                    _("Our own certificate %s failed validation against %s: %s"),
+                    certFile, cacertFile, reason);
+        return -1;
+    }
+
+    return 0;
+}
+
+
+static gnutls_x509_crt_t virNetTLSContextLoadCertFromFile(const char *certFile,
+                                                          bool isServer,
+                                                          bool isCA)
+{
+    gnutls_datum_t data;
+    gnutls_x509_crt_t cert = NULL;
+    char *buf = NULL;
+    int ret = -1;
+
+    VIR_DEBUG("isServer %d isCA %d certFile %s",
+              isServer, isCA, certFile);
+
+    if (gnutls_x509_crt_init(&cert) < 0) {
+        virNetError(VIR_ERR_SYSTEM_ERROR, "%s",
+                    _("Unable to initialize certificate"));
+        goto cleanup;
+    }
+
+    if (virFileReadAll(certFile, (1<<16), &buf) < 0)
+        goto cleanup;
+
+    data.data = (unsigned char *)buf;
+    data.size = strlen(buf);
+
+    if (gnutls_x509_crt_import(cert, &data, GNUTLS_X509_FMT_PEM) < 0) {
+        virNetError(VIR_ERR_SYSTEM_ERROR, isServer ?
+                    _("Unable to import server certificate %s") :
+                    _("Unable to import client certificate %s"),
+                    certFile);
+        goto cleanup;
+    }
 
     ret = 0;
 
@@ -324,7 +510,6 @@ cleanup:
         gnutls_x509_crt_deinit(cert);
         cert = NULL;
     }
-    VIR_FREE(buffer);
     VIR_FREE(buf);
     return cert;
 }
@@ -337,51 +522,25 @@ static int virNetTLSContextSanityCheckCredentials(bool isServer,
     gnutls_x509_crt_t cert = NULL;
     gnutls_x509_crt_t cacert = NULL;
     int ret = -1;
-    unsigned int status;
 
-    if (access(certFile, R_OK) == 0) {
-        if (!(cert = virNetTLSContextSanityCheckCert(isServer, false, certFile)))
-            goto cleanup;
-    }
-    if (access(cacertFile, R_OK) == 0) {
-        if (!(cacert = virNetTLSContextSanityCheckCert(isServer, true, cacertFile)))
-            goto cleanup;
-    }
-
-    if (cert && cacert) {
-        if (gnutls_x509_crt_list_verify(&cert, 1,
-                                        &cacert, 1,
-                                        NULL, 0,
-                                        0, &status) < 0) {
-            virNetError(VIR_ERR_SYSTEM_ERROR, "%s", isServer ?
-                        _("Unable to verify server certificate against CA certificate") :
-                        _("Unable to verify client certificate against CA certificate"));
-            goto cleanup;
-        }
-
-        if (status != 0) {
-            const char *reason = _("Invalid certificate");
-
-            if (status & GNUTLS_CERT_INVALID)
-                reason = _("The certificate is not trusted.");
-
-            if (status & GNUTLS_CERT_SIGNER_NOT_FOUND)
-                reason = _("The certificate hasn't got a known issuer.");
+    if ((access(certFile, R_OK) == 0) &&
+        !(cert = virNetTLSContextLoadCertFromFile(certFile, isServer, false)))
+        goto cleanup;
+    if ((access(cacertFile, R_OK) == 0) &&
+        !(cacert = virNetTLSContextLoadCertFromFile(cacertFile, isServer, false)))
+        goto cleanup;
 
-            if (status & GNUTLS_CERT_REVOKED)
-                reason = _("The certificate has been revoked.");
+    if (cert &&
+        virNetTLSContextCheckCert(cert, certFile, isServer, false) < 0)
+        goto cleanup;
 
-#ifndef GNUTLS_1_0_COMPAT
-            if (status & GNUTLS_CERT_INSECURE_ALGORITHM)
-                reason = _("The certificate uses an insecure algorithm");
-#endif
+    if (cacert &&
+        virNetTLSContextCheckCert(cacert, cacertFile, isServer, true) < 0)
+        goto cleanup;
 
-            virNetError(VIR_ERR_SYSTEM_ERROR,
-                        _("Our own certificate %s failed validation against %s: %s"),
-                        certFile, cacertFile, reason);
-            goto cleanup;
-        }
-    }
+    if (cert && cacert &&
+        virNetTLSContextCheckCertPair(cert, certFile, cacert, cacertFile, isServer) < 0)
+        goto cleanup;
 
     ret = 0;
 
@@ -760,45 +919,6 @@ void virNetTLSContextRef(virNetTLSContextPtr ctxt)
 }
 
 
-/* Check DN is on tls_allowed_dn_list. */
-static int
-virNetTLSContextCheckDN(virNetTLSContextPtr ctxt,
-                        const char *dname)
-{
-    const char *const*wildcards;
-
-    /* If the list is not set, allow any DN. */
-    wildcards = ctxt->x509dnWhitelist;
-    if (!wildcards)
-        return 1;
-
-    while (*wildcards) {
-        int ret = fnmatch (*wildcards, dname, 0);
-        if (ret == 0) /* Succesful match */
-            return 1;
-        if (ret != FNM_NOMATCH) {
-            virNetError(VIR_ERR_INTERNAL_ERROR,
-                        _("Malformed TLS whitelist regular expression '%s'"),
-                        *wildcards);
-            return -1;
-        }
-
-        wildcards++;
-    }
-
-    /* Log the client's DN for debugging */
-    VIR_DEBUG("Failed whitelist check for client DN '%s'", dname);
-
-    /* This is the most common error: make it informative. */
-    virNetError(VIR_ERR_SYSTEM_ERROR, "%s",
-                _("Client's Distinguished Name is not on the list "
-                  "of allowed clients (tls_allowed_dn_list).  Use "
-                  "'certtool -i --infile clientcert.pem' to view the"
-                  "Distinguished Name field in the client certificate,"
-                  "or run this daemon with --verbose option."));
-    return 0;
-}
-
 static int virNetTLSContextValidCertificate(virNetTLSContextPtr ctxt,
                                             virNetTLSSessionPtr sess)
 {
@@ -806,11 +926,6 @@ static int virNetTLSContextValidCertificate(virNetTLSContextPtr ctxt,
     unsigned int status;
     const gnutls_datum_t *certs;
     unsigned int nCerts, i;
-    time_t now;
-    char name[256];
-    size_t namesize = sizeof name;
-
-    memset(name, 0, namesize);
 
     if ((ret = gnutls_certificate_verify_peers2(sess->session, &status)) < 0){
         virNetError(VIR_ERR_SYSTEM_ERROR,
@@ -819,12 +934,6 @@ static int virNetTLSContextValidCertificate(virNetTLSContextPtr ctxt,
         goto authdeny;
     }
 
-    if ((now = time(NULL)) == ((time_t)-1)) {
-        virReportSystemError(errno, "%s",
-                             _("cannot get current time"));
-        goto authfail;
-    }
-
     if (status != 0) {
         const char *reason = _("Invalid certificate");
 
@@ -876,46 +985,37 @@ static int virNetTLSContextValidCertificate(virNetTLSContextPtr ctxt,
             goto authfail;
         }
 
-        if (gnutls_x509_crt_get_expiration_time(cert) < now) {
-            /* Warning is reversed from what you expect, since with
-             * this code it is the Server checking the client and
-             * vica-versa */
-            virNetError(VIR_ERR_SYSTEM_ERROR, "%s", sess->isServer ?
-                        _("The client certificate has expired") :
-                        _("The server certificate has expired"));
-            gnutls_x509_crt_deinit(cert);
-            goto authdeny;
-        }
-
-        if (gnutls_x509_crt_get_activation_time(cert) > now) {
-            /* client/server order reversed. see above */
-            virNetError(VIR_ERR_SYSTEM_ERROR, "%s", sess->isServer ?
-                        _("The client certificate is not yet active") :
-                        _("The server certificate is not yet active"));
+        if (virNetTLSContextCheckCertTimes(cert, "[session]",
+                                           sess->isServer, i > 0) < 0) {
             gnutls_x509_crt_deinit(cert);
             goto authdeny;
         }
 
         if (i == 0) {
-            ret = gnutls_x509_crt_get_dn(cert, name, &namesize);
-            if (ret != 0) {
-                virNetError(VIR_ERR_SYSTEM_ERROR,
-                            _("Failed to get certificate distinguished name: %s"),
-                            gnutls_strerror(ret));
+            if (virNetTLSContextCheckCertDN(cert, "[session]", sess->hostname,
+                                            ctxt->x509dnWhitelist) < 0) {
                 gnutls_x509_crt_deinit(cert);
-                goto authfail;
+                goto authdeny;
+            }
+
+            /* !sess->isServer, since on the client, we're validating the
+             * server's cert, and on the server, the client's cert
+             */
+            if (virNetTLSContextCheckCertBasicConstraints(cert, "[session]",
+                                                          !sess->isServer, false) < 0) {
+                gnutls_x509_crt_deinit(cert);
+                goto authdeny;
             }
 
-            if (virNetTLSContextCheckDN(ctxt, name) <= 0) {
+            if (virNetTLSContextCheckCertKeyUsage(cert, "[session]",
+                                                  false) < 0) {
                 gnutls_x509_crt_deinit(cert);
                 goto authdeny;
             }
 
-            if (sess->hostname &&
-                !gnutls_x509_crt_check_hostname(cert, sess->hostname)) {
-                virNetError(VIR_ERR_RPC,
-                            _("Certificate's owner does not match the hostname (%s)"),
-                            sess->hostname);
+            /* !sess->isServer - as above */
+            if (virNetTLSContextCheckCertKeyPurpose(cert, "[session]",
+                                                    !sess->isServer) < 0) {
                 gnutls_x509_crt_deinit(cert);
                 goto authdeny;
             }
-- 
1.7.6

--
libvir-list mailing list
libvir-list@xxxxxxxxxx
https://www.redhat.com/mailman/listinfo/libvir-list


[Index of Archives]     [Virt Tools]     [Libvirt Users]     [Lib OS Info]     [Fedora Users]     [Fedora Desktop]     [Fedora SELinux]     [Big List of Linux Books]     [Yosemite News]     [KDE Users]     [Fedora Tools]