From b257f54d9d6d3a7b181c76c0b74b0e780800faa7 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Mon, 29 Jan 2018 14:37:25 +0100 Subject: [PATCH] Support SSL client authentication --- demo/ssl/.gitignore | 1 + demo/ssl/openssl.cnf | 26 ++-- demo/ssl/openssl_root.cnf | 120 ++++++++++++++++++ demo/ssl/ssl.sh | 79 +++++++----- .../src/org/argeo/cms/auth/CmsAuthUtils.java | 1 + .../cms/auth/HttpSessionLoginModule.java | 21 ++- .../argeo/cms/auth/UserAdminLoginModule.java | 55 +++++--- .../argeo/cms/internal/kernel/PkiUtils.java | 2 +- 8 files changed, 231 insertions(+), 74 deletions(-) create mode 100644 demo/ssl/openssl_root.cnf diff --git a/demo/ssl/.gitignore b/demo/ssl/.gitignore index 6bff114ef..bc77402d0 100644 --- a/demo/ssl/.gitignore +++ b/demo/ssl/.gitignore @@ -4,3 +4,4 @@ /nssdb/ /*.pem /old/ +/rootCA/ diff --git a/demo/ssl/openssl.cnf b/demo/ssl/openssl.cnf index 62f76bac0..05bb6f77f 100644 --- a/demo/ssl/openssl.cnf +++ b/demo/ssl/openssl.cnf @@ -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 index 000000000..c68945955 --- /dev/null +++ b/demo/ssl/openssl_root.cnf @@ -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 diff --git a/demo/ssl/ssl.sh b/demo/ssl/ssl.sh index 91690f02e..46b72d8b1 100644 --- a/demo/ssl/ssl.sh +++ b/demo/ssl/ssl.sh @@ -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 diff --git a/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java b/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java index d50535eae..4762eb96c 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java @@ -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) { diff --git a/org.argeo.cms/src/org/argeo/cms/auth/HttpSessionLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/HttpSessionLoginModule.java index d3103627c..d2f0fe738 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/HttpSessionLoginModule.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/HttpSessionLoginModule.java @@ -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; } } diff --git a/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java index ebe81b671..e99387639 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java @@ -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; } diff --git a/org.argeo.cms/src/org/argeo/cms/internal/kernel/PkiUtils.java b/org.argeo.cms/src/org/argeo/cms/internal/kernel/PkiUtils.java index 031515caa..dbd045665 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/kernel/PkiUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/kernel/PkiUtils.java @@ -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); -- 2.30.2