Support SSL client authentication
authorMathieu Baudier <mbaudier@argeo.org>
Mon, 29 Jan 2018 13:37:25 +0000 (14:37 +0100)
committerMathieu Baudier <mbaudier@argeo.org>
Mon, 29 Jan 2018 13:37:25 +0000 (14:37 +0100)
demo/ssl/.gitignore
demo/ssl/openssl.cnf
demo/ssl/openssl_root.cnf [new file with mode: 0644]
demo/ssl/ssl.sh
org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java
org.argeo.cms/src/org/argeo/cms/auth/HttpSessionLoginModule.java
org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java
org.argeo.cms/src/org/argeo/cms/internal/kernel/PkiUtils.java

index 6bff114ef1d588d8d011139381d8b58d160f5a1d..bc77402d0faabd073cd21af01e861a9637fb1bee 100644 (file)
@@ -4,3 +4,4 @@
 /nssdb/
 /*.pem
 /old/
+/rootCA/
index 62f76bac03e08b3b357bc6f5f882c079ddc9b7f1..05bb6f77f6eee365410da003bb4da7274360079b 100644 (file)
@@ -41,7 +41,7 @@ commonName            = optional
 emailAddress           = optional
 
 [ req ]
-default_bits           = 1024
+default_bits           = 4096
 default_md             = sha1
 default_keyfile        = privkey.pem
 distinguished_name     = req_distinguished_name
@@ -49,8 +49,8 @@ attributes            = req_attributes
 x509_extensions        = v3_ca # The extensions to add to the self signed cert
 
 # Passwords for private keys if not present they will be prompted for
-# input_password = secret
-# output_password = secret
+input_password = demo
+output_password = demo
 
 string_mask = utf8only
 req_extensions = v3_req # The extensions to add to a certificate request
@@ -62,7 +62,7 @@ countryName_max                       = 2
 #stateOrProvinceName           = State or Province Name (full name)
 #localityName                  = Locality Name (eg, city)
 0.organizationName             = Organization Name (eg, company)
-#organizationalUnitName                = Organizational Unit Name (eg, section)
+organizationalUnitName         = Organizational Unit Name (eg, section)
 commonName                     = Common Name (eg, your name or your server\'s hostname)
 commonName_max                 = 64
 emailAddress                   = Email Address
@@ -76,8 +76,8 @@ countryName_default           = DE
 #stateOrProvinceName_default   = Berlin
 #localityName_default  = Berlin
 0.organizationName_default     = Example
-#organizationalUnitName_default        = Certificate Authorities
-commonName_default     = Certificate Authority
+organizationalUnitName_default = Certificate Authorities
+commonName_default     = Intermediate CA
 
 [ req_attributes ]
 #challengePassword             = A challenge password
@@ -99,11 +99,15 @@ keyUsage = nonRepudiation, digitalSignature, keyEncipherment
 [ v3_ca ]
 subjectKeyIdentifier=hash
 authorityKeyIdentifier=keyid:always,issuer
-basicConstraints = critical,CA:true
-# keyUsage = cRLSign, keyCertSign
-
-#subjectAltName=email:copy
-issuerAltName=issuer:copy
+basicConstraints = critical, CA:true
+keyUsage = critical, digitalSignature, cRLSign, keyCertSign
+
+[ v3_intermediate_ca ]
+# Extensions for a typical intermediate CA (`man x509v3_config`).
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always,issuer
+basicConstraints = critical, CA:true, pathlen:0
+keyUsage = critical, digitalSignature, cRLSign, keyCertSign
 
 [ crl_ext ]
 issuerAltName=issuer:copy
diff --git a/demo/ssl/openssl_root.cnf b/demo/ssl/openssl_root.cnf
new file mode 100644 (file)
index 0000000..c689459
--- /dev/null
@@ -0,0 +1,120 @@
+dir            = ./rootCA              # Where everything is kept
+
+[ ca ]
+default_ca     = CA_default            # The default ca section
+
+[ CA_default ]
+certs          = $dir/certs            # Where the issued certs are kept
+crl_dir                = $dir/crl              # Where the issued crl are kept
+database       = $dir/index.txt        # database index file.
+new_certs_dir  = $dir/newcerts         # default place for new certs.
+certificate    = $dir/cacert.pem       # The CA certificate
+serial         = $dir/serial           # The current serial number
+crlnumber      = $dir/crlnumber        # the current crl number
+crl            = $dir/crl.pem          # The current CRL
+private_key    = $dir/private/cakey.pem # The private key
+x509_extensions        = usr_cert              # The extentions to add to the cert
+name_opt       = ca_default            # Subject Name options
+cert_opt       = ca_default            # Certificate field options
+crl_extensions = crl_ext
+default_days   = 3650          # how long to certify for
+default_crl_days= 30                   # how long before next CRL
+default_md     = default               # use public key default MD
+preserve       = no                    # keep passed DN ordering
+policy         = policy_match
+
+[ policy_match ]
+countryName            = optional
+stateOrProvinceName    = optional
+organizationName       = optional
+organizationalUnitName = optional
+commonName             = optional
+emailAddress           = optional
+
+[ policy_anything ]
+countryName            = optional
+stateOrProvinceName    = optional
+localityName           = optional
+organizationName       = optional
+organizationalUnitName = optional
+commonName             = optional
+emailAddress           = optional
+
+[ req ]
+default_bits           = 4096
+default_md             = sha1
+default_keyfile        = privkey.pem
+distinguished_name     = req_distinguished_name
+attributes             = req_attributes
+x509_extensions        = v3_ca # The extensions to add to the self signed cert
+
+# Passwords for private keys if not present they will be prompted for
+input_password = demo
+output_password = demo
+
+string_mask = utf8only
+req_extensions = v3_req # The extensions to add to a certificate request
+
+[ req_distinguished_name ]
+countryName                    = Country Name (2 letter code)
+countryName_min                        = 2
+countryName_max                        = 2
+#stateOrProvinceName           = State or Province Name (full name)
+#localityName                  = Locality Name (eg, city)
+0.organizationName             = Organization Name (eg, company)
+organizationalUnitName         = Organizational Unit Name (eg, section)
+commonName                     = Common Name (eg, your name or your server\'s hostname)
+commonName_max                 = 64
+emailAddress                   = Email Address
+emailAddress_max               = 64
+# SET-ex3                      = SET extension number 3
+
+##
+## DEFAULT VALUES
+##
+countryName_default            = DE
+#stateOrProvinceName_default   = Berlin
+#localityName_default  = Berlin
+0.organizationName_default     = Example
+organizationalUnitName_default = Certificate Authorities
+commonName_default     = Root CA
+
+[ req_attributes ]
+#challengePassword             = A challenge password
+#challengePassword_min         = 4
+#challengePassword_max         = 20
+#unstructuredName              = An optional company name
+
+[ usr_cert ]
+basicConstraints=CA:FALSE
+subjectKeyIdentifier=hash
+authorityKeyIdentifier=keyid,issuer
+subjectAltName=email:move
+issuerAltName=issuer:copy
+
+[ v3_req ]
+basicConstraints = CA:FALSE
+keyUsage = nonRepudiation, digitalSignature, keyEncipherment
+
+[ v3_ca ]
+subjectKeyIdentifier=hash
+authorityKeyIdentifier=keyid:always,issuer
+basicConstraints = critical, CA:true
+keyUsage = critical, digitalSignature, cRLSign, keyCertSign
+
+[ v3_intermediate_ca ]
+# Extensions for a typical intermediate CA (`man x509v3_config`).
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always,issuer
+basicConstraints = critical, CA:true, pathlen:0
+keyUsage = critical, digitalSignature, cRLSign, keyCertSign
+
+[ crl_ext ]
+issuerAltName=issuer:copy
+authorityKeyIdentifier=keyid:always
+
+[ server_ext ]
+extendedKeyUsage=serverAuth
+
+[ user_ext ]
+extendedKeyUsage=clientAuth,emailProtection
index 91690f02e520b9fab3ca5e7f2a79ea4a5ab80cd3..46b72d8b185f2cebfa348b12462f71f27cacf53c 100644 (file)
@@ -5,51 +5,58 @@
 # all *.p12 passwords are 'demo'
 # all *.jks passwords are 'changeit'
 
+INTERMEDIATE_CA_DN="/C=DE/O=Example/OU=Certificate Authorities/CN=Intermediate CA/"
 SERVER_DN=/C=DE/O=Example/OU=Systems/CN=$HOSTNAME/
-USERS_BASE_DN=/DC=com/DC=example/OU=users
-
-export OPENSSL_CONF=./openssl.cnf
-export CATOP=./CA
+USERS_BASE_DN=/DC=com/DC=example/OU=People
 
+echo ## Init directory structure
+# Root
+export OPENSSL_CONF=./openssl_root.cnf
+export CATOP=./rootCA
 /etc/pki/tls/misc/CA -newca
+# Intermediate
+mkdir -p ./CA/{certs,crl,csr,newcerts,private}
 
-#openssl req -x509 -new -newkey rsa:4096 -extensions server_ext -days 365 \
-# -subj $SERVER_DN \
-# -keyout newkey.pem -passout pass:demo -out newcrt.pem
-# Self-signed server certificate
-#openssl pkcs12 -export -passin pass:demo -passout pass:changeit \
-# -name "jetty" -inkey newkey.pem -in newcrt.pem \
-# -certfile ./CA/cacert.pem \
-# -out server.p12
- # Convert PKCS12 keystore into a JKS keystore
-#keytool -importkeystore \
-# -srckeystore server.p12 -srcstoretype pkcs12 -srcstorepass changeit \
-# -alias jetty  -destkeystore server.jks -deststorepass changeit
-#rm -f server.p12
+echo ## Create intermediate certificate
+openssl req -new -newkey rsa:4096 -extensions v3_intermediate_ca \
+ -subj "$INTERMEDIATE_CA_DN" \
+ -keyout ./CA/private/cakey.pem -passout pass:demo -out ica_csr.pem
+openssl ca -batch -passin pass:demo -in ica_csr.pem -out ./CA/cacert.pem
+
+# create index and serial
+touch ./CA/index.txt
+# (below is from openssl CA script)
+openssl x509 -in ./CA/cacert.pem -noout -next_serial -out ./CA/serial
 
-# Import People CA
-#keytool -importcert -keystore server.jks -storepass changeit \
-# -alias CA -file CA/cacert.pem
+# Switch to intermediate CA                  
+export OPENSSL_CONF=./openssl.cnf
+export CATOP=./CA
 
-openssl req -new -newkey rsa:4096 -extensions server_ext -days 365 \
+echo ## Create server key and certificate
+openssl req -new -newkey rsa:4096 -extensions server_ext \
  -subj $SERVER_DN \
  -keyout node_key.pem -passout pass:demo -out node_csr.pem
 openssl ca -batch -passin pass:demo -in node_csr.pem -out node_crt.pem
-cat node_crt.pem CA/cacert.pem > node.pem
-openssl pkcs12 -export -passin pass:demo -passout pass:demo \
- -name "node" -inkey node_key.pem -in node.pem \
+cat node_crt.pem ./CA/cacert.pem ./rootCA/cacert.pem > chain.pem
+openssl pkcs12 -export -passin pass:demo -passout pass:changeit \
+ -name "$HOSTNAME" -inkey node_key.pem -in chain.pem \
  -out node.p12
 
+echo ## Import Certificate Authority into keystore
+keytool -importcert -noprompt -keystore node.p12 -storepass changeit \
+ -alias "rootCA" -file ./rootCA/cacert.pem
+keytool -importcert -noprompt -keystore node.p12 -storepass changeit \
+ -alias "CA" -file ./CA/cacert.pem
+cp node.p12 ../init/node/
 
-# root user
-openssl req -new -newkey rsa:4096 -extensions user_ext -days 365 \
+echo ## Create 'root' user client certificate
+openssl req -new -newkey rsa:4096 -extensions user_ext \
  -subj $USERS_BASE_DN/UID=root/ \
  -keyout newkey.pem -passout pass:demo -out newcsr.pem
 openssl ca -preserveDN -batch -passin pass:demo -in newcsr.pem -out newcrt.pem
+cat newcrt.pem ./CA/cacert.pem ./rootCA/cacert.pem > newchain.pem
 openssl pkcs12 -export -passin pass:demo -passout pass:demo \
- -name "root" -inkey newkey.pem -in newcrt.pem \
+ -name "root" -inkey newkey.pem -in newchain.pem \
  -out root.p12
 
 # demo user
@@ -61,5 +68,15 @@ openssl pkcs12 -export -passin pass:demo -passout pass:demo \
 # -name "demo" -inkey newkey.pem -in newcrt.pem \
 # -out demo.p12
 
-# Clean up
-#rm -vf new*.pem
+# Self-signed
+#openssl req -x509 -new -newkey rsa:4096 -extensions server_ext -days 365 \
+# -subj $SERVER_DN \
+# -keyout newkey.pem -passout pass:demo -out newcrt.pem
+# Self-signed server certificate
+#openssl pkcs12 -export -passin pass:demo -passout pass:changeit \
+# -name "jetty" -inkey newkey.pem -in newcrt.pem \
+# -certfile ./CA/cacert.pem \
+# -out server.p12
+
+echo ## Clean up
+rm -vf *.pem
index d50535eaefcedaa791f06b93342c94634f68b552..4762eb96c765578396650ba1044c0c92b9818e20 100644 (file)
@@ -38,6 +38,7 @@ class CmsAuthUtils {
        final static String SHARED_STATE_HTTP_REQUEST = "org.argeo.cms.auth.http.request";
        final static String SHARED_STATE_SPNEGO_TOKEN = "org.argeo.cms.auth.spnegoToken";
        final static String SHARED_STATE_SPNEGO_OUT_TOKEN = "org.argeo.cms.auth.spnegoOutToken";
+       final static String SHARED_STATE_CERTIFICATE_CHAIN = "org.argeo.cms.auth.certificateChain";
 
        static void addAuthorization(Subject subject, Authorization authorization, Locale locale,
                        HttpServletRequest request) {
index d3103627c294259f8d4218e3534f2a8d98ce89c1..d2f0fe738d751957b684925d67dfaf895403f829 100644 (file)
@@ -20,7 +20,6 @@ import org.apache.commons.codec.binary.Base64;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.argeo.cms.CmsException;
-import org.argeo.naming.LdapAttrs;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.FrameworkUtil;
 import org.osgi.framework.InvalidSyntaxException;
@@ -178,21 +177,21 @@ public class HttpSessionLoginModule implements LoginModule {
                }
 
                // auth token
-//             String mail = request.getParameter(LdapAttrs.mail.name());
-//             String authPassword = request.getParameter(LdapAttrs.authPassword.name());
-//             if (authPassword != null) {
-//                     sharedState.put(CmsAuthUtils.SHARED_STATE_PWD, authPassword);
-//                     if (mail != null)
-//                             sharedState.put(CmsAuthUtils.SHARED_STATE_NAME, mail);
-//             }
+               // String mail = request.getParameter(LdapAttrs.mail.name());
+               // String authPassword = request.getParameter(LdapAttrs.authPassword.name());
+               // if (authPassword != null) {
+               // sharedState.put(CmsAuthUtils.SHARED_STATE_PWD, authPassword);
+               // if (mail != null)
+               // sharedState.put(CmsAuthUtils.SHARED_STATE_NAME, mail);
+               // }
        }
 
-       private X509Certificate[] extractClientCertificate(HttpServletRequest req) {
+       private void extractClientCertificate(HttpServletRequest req) {
                X509Certificate[] certs = (X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate");
                if (null != certs && certs.length > 0) {
-                       return certs;
+                       sharedState.put(CmsAuthUtils.SHARED_STATE_NAME, certs[0].getSubjectX500Principal().getName());
+                       sharedState.put(CmsAuthUtils.SHARED_STATE_CERTIFICATE_CHAIN, certs);
                }
-               return null;
        }
 
 }
index ebe81b6714c49feffbcdae26aceb4b901c6b8a61..e9938763927bc57b1e1e39ed549820e0c9a60c2d 100644 (file)
@@ -2,6 +2,7 @@ package org.argeo.cms.auth;
 
 import java.io.IOException;
 import java.security.PrivilegedAction;
+import java.security.cert.X509Certificate;
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
@@ -71,12 +72,19 @@ public class UserAdminLoginModule implements LoginModule {
                UserAdmin userAdmin = bc.getService(bc.getServiceReference(UserAdmin.class));
                final String username;
                final char[] password;
+               X509Certificate[] certificateChain = null;
                if (sharedState.containsKey(CmsAuthUtils.SHARED_STATE_NAME)
                                && sharedState.containsKey(CmsAuthUtils.SHARED_STATE_PWD)) {
                        // NB: required by Basic http auth
                        username = (String) sharedState.get(CmsAuthUtils.SHARED_STATE_NAME);
                        password = (char[]) sharedState.get(CmsAuthUtils.SHARED_STATE_PWD);
                        // // TODO locale?
+               } else if (sharedState.containsKey(CmsAuthUtils.SHARED_STATE_NAME)
+                               && sharedState.containsKey(CmsAuthUtils.SHARED_STATE_CERTIFICATE_CHAIN)) {
+                       // NB: required by Basic http auth
+                       username = (String) sharedState.get(CmsAuthUtils.SHARED_STATE_NAME);
+                       certificateChain = (X509Certificate[]) sharedState.get(CmsAuthUtils.SHARED_STATE_CERTIFICATE_CHAIN);
+                       password = null;
                } else {
                        // ask for username and password
                        NameCallback nameCallback = new NameCallback("User");
@@ -95,7 +103,7 @@ public class UserAdminLoginModule implements LoginModule {
                        if (locale == null)
                                locale = Locale.getDefault();
                        // FIXME add it to Subject
-//                     Locale.setDefault(locale);
+                       // Locale.setDefault(locale);
 
                        username = nameCallback.getName();
                        if (username == null || username.trim().equals("")) {
@@ -107,30 +115,37 @@ public class UserAdminLoginModule implements LoginModule {
                        else
                                throw new CredentialNotFoundException("No credentials provided");
                }
-
                User user = searchForUser(userAdmin, username);
                if (user == null)
                        return true;// expect Kerberos
-               
-               // try bind first
-               try {
-                       AuthenticatingUser authenticatingUser = new AuthenticatingUser(user.getName(), password);
-                       bindAuthorization = userAdmin.getAuthorization(authenticatingUser);
-                       // TODO check tokens as well
-                       if (bindAuthorization != null) {
-                               authenticatedUser = user;
-                               return true;
+
+               if (password != null) {
+                       // try bind first
+                       try {
+                               AuthenticatingUser authenticatingUser = new AuthenticatingUser(user.getName(), password);
+                               bindAuthorization = userAdmin.getAuthorization(authenticatingUser);
+                               // TODO check tokens as well
+                               if (bindAuthorization != null) {
+                                       authenticatedUser = user;
+                                       return true;
+                               }
+                       } catch (Exception e) {
+                               // silent
+                               if (log.isTraceEnabled())
+                                       log.trace("Bind failed", e);
                        }
-               } catch (Exception e) {
-                       // silent
-                       if(log.isTraceEnabled())
-                               log.trace("Bind failed", e);
-               }
-               
-               // works only if a connection password is provided
-               if (!user.hasCredential(null, password)) {
-                       return false;
+
+                       // works only if a connection password is provided
+                       if (!user.hasCredential(null, password)) {
+                               return false;
+                       }
+               } else if (certificateChain != null) {
+                       // TODO check CRLs/OSCP validity?
+                       // NB: authorization in commit() will work only if an LDAP connection password is provided
+               }else {
+                       throw new CredentialNotFoundException("No credentials provided");
                }
+
                authenticatedUser = user;
                return true;
        }
index 031515caadbcd0705ec08fd9cc13fe2429224ef4..dbd0456654d6e7f70c6eab431ecaa0bbff24f9f1 100644 (file)
@@ -61,7 +61,7 @@ class PkiUtils {
 
        public static KeyStore getKeyStore(File keyStoreFile, char[] keyStorePassword) {
                try {
-                       KeyStore store = KeyStore.getInstance("PKCS12", SECURITY_PROVIDER);
+                       KeyStore store = KeyStore.getInstance("JKS", SECURITY_PROVIDER);
                        if (keyStoreFile.exists()) {
                                try (FileInputStream fis = new FileInputStream(keyStoreFile)) {
                                        store.load(fis, keyStorePassword);