diff --git a/jdk/src/share/classes/sun/security/pkcs11/FIPSTokenLoginHandler.java b/jdk/src/share/classes/sun/security/pkcs11/FIPSTokenLoginHandler.java new file mode 100644 index 00000000000..3e75ef206ce --- /dev/null +++ b/jdk/src/share/classes/sun/security/pkcs11/FIPSTokenLoginHandler.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2022, Red Hat, Inc. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package sun.security.pkcs11; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.security.ProviderException; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; + +import sun.misc.IOUtils; +import sun.security.util.Debug; +import sun.security.util.SecurityProperties; + +final class FIPSTokenLoginHandler implements CallbackHandler { + + private static final String FIPS_NSSDB_PIN_PROP = "fips.nssdb.pin"; + + private static final Debug debug = Debug.getInstance("sunpkcs11"); + + public void handle(Callback[] callbacks) + throws IOException, UnsupportedCallbackException { + if (!(callbacks[0] instanceof PasswordCallback)) { + throw new UnsupportedCallbackException(callbacks[0]); + } + PasswordCallback pc = (PasswordCallback)callbacks[0]; + pc.setPassword(getFipsNssdbPin()); + } + + private static char[] getFipsNssdbPin() throws ProviderException { + if (debug != null) { + debug.println("FIPS: Reading NSS DB PIN for token..."); + } + String pinProp = SecurityProperties + .privilegedGetOverridable(FIPS_NSSDB_PIN_PROP); + if (pinProp != null && !pinProp.isEmpty()) { + String[] pinPropParts = pinProp.split(":", 2); + if (pinPropParts.length < 2) { + throw new ProviderException("Invalid " + FIPS_NSSDB_PIN_PROP + + " property value."); + } + String prefix = pinPropParts[0].toUpperCase(); + String value = pinPropParts[1]; + String pin = null; + if (prefix.equals("ENV")) { + if (debug != null) { + debug.println("FIPS: PIN value from the '" + value + + "' environment variable."); + } + pin = System.getenv(value); + } else if (prefix.equals("FILE")) { + if (debug != null) { + debug.println("FIPS: PIN value from the '" + value + + "' file."); + } + pin = getPinFromFile(Paths.get(value)); + } else if (prefix.equals("PIN")) { + if (debug != null) { + debug.println("FIPS: PIN value from the " + + FIPS_NSSDB_PIN_PROP + " property."); + } + pin = value; + } else { + throw new ProviderException("Unsupported prefix for " + + FIPS_NSSDB_PIN_PROP + "."); + } + if (pin != null && !pin.isEmpty()) { + if (debug != null) { + debug.println("FIPS: non-empty PIN."); + } + /* + * C_Login in libj2pkcs11 receives the PIN in a char[] and + * discards the upper byte of each char, before passing + * the value to the NSS Software Token. However, the + * NSS Software Token accepts any UTF-8 PIN value. Thus, + * expand the PIN here to account for later truncation. + */ + byte[] pinUtf8 = pin.getBytes(StandardCharsets.UTF_8); + char[] pinChar = new char[pinUtf8.length]; + for (int i = 0; i < pinChar.length; i++) { + pinChar[i] = (char)(pinUtf8[i] & 0xFF); + } + return pinChar; + } + } + if (debug != null) { + debug.println("FIPS: empty PIN."); + } + return new char[] {}; + } + + /* + * This method extracts the token PIN from the first line of a password + * file in the same way as NSS modutil. See for example the -newpwfile + * argument used to change the password for an NSS DB. + */ + private static String getPinFromFile(Path f) throws ProviderException { + try (InputStream is = + Files.newInputStream(f, StandardOpenOption.READ)) { + /* + * SECU_FilePasswd in NSS (nss/cmd/lib/secutil.c), used by modutil, + * reads up to 4096 bytes. In addition, the NSS Software Token + * does not accept PINs longer than 500 bytes (see SFTK_MAX_PIN + * in nss/lib/softoken/pkcs11i.h). + */ + BufferedReader in = + new BufferedReader(new InputStreamReader( + new ByteArrayInputStream(IOUtils.readNBytes(is, 4096)), + StandardCharsets.UTF_8)); + return in.readLine(); + } catch (IOException ioe) { + throw new ProviderException("Error reading " + FIPS_NSSDB_PIN_PROP + + " from the '" + f + "' file.", ioe); + } + } +} \ No newline at end of file diff --git a/jdk/src/share/classes/sun/security/pkcs11/Secmod.java b/jdk/src/share/classes/sun/security/pkcs11/Secmod.java index 3193e403683..253428d4631 100644 --- a/jdk/src/share/classes/sun/security/pkcs11/Secmod.java +++ b/jdk/src/share/classes/sun/security/pkcs11/Secmod.java @@ -206,7 +206,7 @@ public synchronized void initialize(DbMode dbMode, String configDir, if (configDir != null) { String configDirPath = null; - String sqlPrefix = "sql:/"; + String sqlPrefix = "sql:"; if (!configDir.startsWith(sqlPrefix)) { configDirPath = configDir; } else { diff --git a/jdk/src/share/classes/sun/security/pkcs11/SunPKCS11.java b/jdk/src/share/classes/sun/security/pkcs11/SunPKCS11.java index f9d70863bd1..542d94203ef 100644 --- a/jdk/src/share/classes/sun/security/pkcs11/SunPKCS11.java +++ b/jdk/src/share/classes/sun/security/pkcs11/SunPKCS11.java @@ -49,6 +49,7 @@ import sun.security.util.Debug; import sun.security.util.ResourcesMgr; +import sun.security.util.SecurityProperties; import sun.security.pkcs11.Secmod.*; @@ -86,6 +87,8 @@ public final class SunPKCS11 extends AuthProvider { fipsImportKey = fipsImportKeyTmp; } + private static final String FIPS_NSSDB_PATH_PROP = "fips.nssdb.path"; + private static final long serialVersionUID = -1354835039035306505L; static final Debug debug = Debug.getInstance("sunpkcs11"); @@ -147,13 +150,29 @@ private static synchronized String getDummyConfigName() { return "---DummyConfig-" + id + "---"; } + private static String fipsPreConfigHook() { + if (systemFipsEnabled) { + /* + * The nssSecmodDirectory attribute in the SunPKCS11 + * NSS configuration file takes the value of the + * fips.nssdb.path System property after expansion. + * Security properties expansion is unsupported. + */ + System.setProperty( + FIPS_NSSDB_PATH_PROP, + SecurityProperties.privilegedGetOverridable( + FIPS_NSSDB_PATH_PROP)); + } + return ""; + } + /** * @deprecated use new SunPKCS11(String) or new SunPKCS11(InputStream) * instead */ @Deprecated public SunPKCS11(String configName, InputStream configStream) { - super("SunPKCS11-" + + super(fipsPreConfigHook() + "SunPKCS11-" + Config.getConfig(configName, configStream).getName(), 1.8d, Config.getConfig(configName, configStream).getDescription()); this.configName = configName; @@ -401,24 +420,6 @@ public SunPKCS11(String configName, InputStream configStream) { if (nssModule != null) { nssModule.setProvider(this); } - if (systemFipsEnabled) { - // The NSS Software Token in FIPS 140-2 mode requires a user - // login for most operations. See sftk_fipsCheck. The NSS DB - // (/etc/pki/nssdb) PIN is empty. - Session session = null; - try { - session = token.getOpSession(); - p11.C_Login(session.id(), CKU_USER, new char[] {}); - } catch (PKCS11Exception p11e) { - if (debug != null) { - debug.println("Error during token login: " + - p11e.getMessage()); - } - throw p11e; - } finally { - token.releaseSession(session); - } - } } catch (Exception e) { if (config.getHandleStartupErrors() == Config.ERR_IGNORE_ALL) { throw new UnsupportedOperationException @@ -1106,6 +1107,27 @@ public Object newInstance(Object param) if (token.isValid() == false) { throw new NoSuchAlgorithmException("Token has been removed"); } + if (systemFipsEnabled && !token.fipsLoggedIn && + !getType().equals("KeyStore")) { + /* + * The NSS Software Token in FIPS 140-2 mode requires a + * user login for most operations. See sftk_fipsCheck + * (nss/lib/softoken/fipstokn.c). In case of a KeyStore + * service, let the caller perform the login with + * KeyStore::load. Keytool, for example, does this to pass a + * PIN from either the -srcstorepass or -deststorepass + * argument. In case of a non-KeyStore service, perform the + * login now with the PIN available in the fips.nssdb.pin + * property. + */ + try { + token.ensureLoggedIn(null); + } catch (PKCS11Exception | LoginException e) { + throw new ProviderException("FIPS: error during the Token" + + " login required for the " + getType() + + " service.", e); + } + } try { return newInstance0(param); } catch (PKCS11Exception e) { @@ -1444,6 +1466,9 @@ public void logout() throws LoginException { try { session = token.getOpSession(); p11.C_Logout(session.id()); + if (systemFipsEnabled) { + token.fipsLoggedIn = false; + } if (debug != null) { debug.println("logout succeeded"); } diff --git a/jdk/src/share/classes/sun/security/pkcs11/Token.java b/jdk/src/share/classes/sun/security/pkcs11/Token.java index 39d301ae7b8..e3581a99792 100644 --- a/jdk/src/share/classes/sun/security/pkcs11/Token.java +++ b/jdk/src/share/classes/sun/security/pkcs11/Token.java @@ -33,6 +33,7 @@ import java.security.*; import javax.security.auth.login.LoginException; +import sun.misc.SharedSecrets; import sun.security.jca.JCAUtil; import sun.security.pkcs11.wrapper.*; @@ -47,6 +48,9 @@ */ class Token implements Serializable { + private static final boolean systemFipsEnabled = SharedSecrets + .getJavaSecuritySystemConfiguratorAccess().isSystemFipsEnabled(); + // need to be serializable to allow SecureRandom to be serialized private static final long serialVersionUID = 2541527649100571747L; @@ -113,6 +117,10 @@ class Token implements Serializable { // flag indicating whether we are logged in private volatile boolean loggedIn; + // Flag indicating the login status for the NSS Software Token in FIPS mode. + // This Token is never asynchronously removed. Used from SunPKCS11. + volatile boolean fipsLoggedIn; + // time we last checked login status private long lastLoginCheck; @@ -231,7 +239,12 @@ boolean isLoggedInNow(Session session) throws PKCS11Exception { // call provider.login() if not void ensureLoggedIn(Session session) throws PKCS11Exception, LoginException { if (isLoggedIn(session) == false) { - provider.login(null, null); + if (systemFipsEnabled) { + provider.login(null, new FIPSTokenLoginHandler()); + fipsLoggedIn = true; + } else { + provider.login(null, null); + } } } diff --git a/jdk/src/share/lib/security/java.security-linux b/jdk/src/share/lib/security/java.security-linux index 9d1c8fe8a8e..a44e533453e 100644 --- a/jdk/src/share/lib/security/java.security-linux +++ b/jdk/src/share/lib/security/java.security-linux @@ -183,6 +183,42 @@ keystore.type=jks # fips.keystore.type=PKCS11 +# +# Location of the NSS DB keystore (PKCS11) in FIPS mode. +# +# The syntax for this property is identical to the 'nssSecmodDirectory' +# attribute available in the SunPKCS11 NSS configuration file. Use the +# 'sql:' prefix to refer to an SQLite DB. +# +# If the system property fips.nssdb.path is also specified, it supersedes +# the security property value defined here. +# +# Note: the default value for this property points to an NSS DB that might be +# readable by multiple operating system users and unsuitable to store keys. +# +fips.nssdb.path=sql:/etc/pki/nssdb + +# +# PIN for the NSS DB keystore (PKCS11) in FIPS mode. +# +# Values must take any of the following forms: +# 1) pin: +# Value: clear text PIN value. +# 2) env: +# Value: environment variable containing the PIN value. +# 3) file: +# Value: path to a file containing the PIN value in its first +# line. +# +# If the system property fips.nssdb.pin is also specified, it supersedes +# the security property value defined here. +# +# When used as a system property, UTF-8 encoded values are valid. When +# used as a security property (such as in this file), encode non-Basic +# Latin Unicode characters with \uXXXX. +# +fips.nssdb.pin=pin: + # # Controls compatibility mode for the JKS keystore type. # diff --git a/jdk/src/share/native/sun/security/pkcs11/j2secmod.c b/jdk/src/share/native/sun/security/pkcs11/j2secmod.c index 00c3485ad2b..9321287300f 100644 --- a/jdk/src/share/native/sun/security/pkcs11/j2secmod.c +++ b/jdk/src/share/native/sun/security/pkcs11/j2secmod.c @@ -69,9 +69,14 @@ JNIEXPORT jboolean JNICALL Java_sun_security_pkcs11_Secmod_nssInitialize int res = 0; FPTR_Initialize initialize = (FPTR_Initialize)findFunction(env, jHandle, "NSS_Initialize"); + #ifdef SECMOD_DEBUG + FPTR_GetError getError = + (FPTR_GetError)findFunction(env, jHandle, "PORT_GetError"); + #endif // SECMOD_DEBUG unsigned int flags = 0x00; const char *configDir = NULL; const char *functionName = NULL; + const char *configFile = NULL; /* If we cannot initialize, exit now */ if (initialize == NULL) { @@ -97,13 +102,18 @@ JNIEXPORT jboolean JNICALL Java_sun_security_pkcs11_Secmod_nssInitialize flags = 0x20; // NSS_INIT_OPTIMIZESPACE flag } + configFile = "secmod.db"; + if (configDir != NULL && strncmp("sql:", configDir, 4U) == 0) { + configFile = "pkcs11.txt"; + } + /* * If the NSS_Init function is requested then call NSS_Initialize to * open the Cert, Key and Security Module databases, read only. */ if (strcmp("NSS_Init", functionName) == 0) { flags = flags | 0x01; // NSS_INIT_READONLY flag - res = initialize(configDir, "", "", "secmod.db", flags); + res = initialize(configDir, "", "", configFile, flags); /* * If the NSS_InitReadWrite function is requested then call @@ -111,7 +121,7 @@ JNIEXPORT jboolean JNICALL Java_sun_security_pkcs11_Secmod_nssInitialize * read/write. */ } else if (strcmp("NSS_InitReadWrite", functionName) == 0) { - res = initialize(configDir, "", "", "secmod.db", flags); + res = initialize(configDir, "", "", configFile, flags); /* * If the NSS_NoDB_Init function is requested then call @@ -137,6 +147,13 @@ JNIEXPORT jboolean JNICALL Java_sun_security_pkcs11_Secmod_nssInitialize (*env)->ReleaseStringUTFChars(env, jConfigDir, configDir); } dprintf1("-res: %d\n", res); + #ifdef SECMOD_DEBUG + if (res == -1) { + if (getError != NULL) { + dprintf1("-NSS error: %d\n", getError()); + } + } + #endif // SECMOD_DEBUG return (res == 0) ? JNI_TRUE : JNI_FALSE; } diff --git a/jdk/src/solaris/native/sun/security/pkcs11/j2secmod_md.h b/jdk/src/solaris/native/sun/security/pkcs11/j2secmod_md.h index 8595a1700b8..08fc69a63e8 100644 --- a/jdk/src/solaris/native/sun/security/pkcs11/j2secmod_md.h +++ b/jdk/src/solaris/native/sun/security/pkcs11/j2secmod_md.h @@ -34,6 +34,10 @@ typedef int (*FPTR_Initialize)(const char *configdir, const char *certPrefix, const char *keyPrefix, const char *secmodName, unsigned int flags); +#ifdef SECMOD_DEBUG +typedef int (*FPTR_GetError)(void); +#endif //SECMOD_DEBUG + // in secmod.h //extern SECMODModule *SECMOD_LoadModule(char *moduleSpec,SECMODModule *parent, // PRBool recurse); diff --git a/jdk/test/java/security/testlibrary/Proc.java b/jdk/test/java/security/testlibrary/Proc.java index 95192cd1369..8623def52df 100644 --- a/jdk/test/java/security/testlibrary/Proc.java +++ b/jdk/test/java/security/testlibrary/Proc.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2019, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,6 +25,9 @@ import java.io.File; import java.io.IOException; import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintStream; +import java.io.UncheckedIOException; import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; @@ -107,6 +110,7 @@ public class Proc { private List args = new ArrayList<>(); private Map env = new HashMap<>(); private Map prop = new HashMap(); + private Map secprop = new HashMap(); private boolean inheritIO = false; private boolean noDump = false; @@ -167,6 +171,11 @@ public Proc prop(String a, String b) { prop.put(a, b); return this; } + // Specifies a security property. Can be called multiple times. + public Proc secprop(String a, String b) { + secprop.put(a, b); + return this; + } // Adds a perm to policy. Can be called multiple times. In order to make it // effective, please also call prop("java.security.manager", ""). public Proc perm(Permission p) { @@ -191,6 +200,17 @@ public Proc start() throws IOException { cp.append(url.getFile()); } cmd.add(cp.toString()); + if (!secprop.isEmpty()) { + Path p = Paths.get(getId("security")); + try (OutputStream fos = Files.newOutputStream(p); + PrintStream ps = new PrintStream(fos)) { + secprop.forEach((k,v) -> ps.println(k + "=" + v)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + prop.put("java.security.properties", p.toString()); + } + for (Entry e: prop.entrySet()) { cmd.add("-D" + e.getKey() + "=" + e.getValue()); } @@ -289,6 +309,12 @@ public int waitFor() throws Exception { } return p.waitFor(); } + // Wait for process end with expected exit code + public void waitFor(int expected) throws Exception { + if (p.waitFor() != expected) { + throw new RuntimeException("Exit code not " + expected); + } + } // The following methods are used inside a proc diff --git a/jdk/test/sun/security/pkcs11/Secmod/pkcs11.txt b/jdk/test/sun/security/pkcs11/Secmod/pkcs11.txt new file mode 100644 index 00000000000..60cc1c56553 --- /dev/null +++ b/jdk/test/sun/security/pkcs11/Secmod/pkcs11.txt @@ -0,0 +1,4 @@ +library= +name=NSS Internal PKCS #11 Module +parameters=configdir='sql:./tmpdb' certPrefix='' keyPrefix='' secmod='' flags= updatedir='' updateCertPrefix='' updateKeyPrefix='' updateid='' updateTokenDescription='' +NSS=Flags=internal,critical trustOrder=75 cipherOrder=100 slotParams=(1={slotFlags=[RSA,DSA,DH,RC2,RC4,DES,RANDOM,SHA1,MD5,MD2,SSL,TLS,AES,Camellia,SEED,SHA256,SHA512] askpw=any timeout=30}) diff --git a/jdk/test/sun/security/pkcs11/SecmodTest.java b/jdk/test/sun/security/pkcs11/SecmodTest.java index 50f2d5c4074..f870bb9d084 100644 --- a/jdk/test/sun/security/pkcs11/SecmodTest.java +++ b/jdk/test/sun/security/pkcs11/SecmodTest.java @@ -26,6 +26,11 @@ import java.io.*; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; import java.security.Provider; public class SecmodTest extends PKCS11Test { @@ -55,18 +60,20 @@ static boolean initSecmod() throws Exception { DBDIR = System.getProperty("test.classes", ".") + SEP + "tmpdb"; if (useSqlite) { - System.setProperty("pkcs11test.nss.db", "sql:/" + DBDIR); + System.setProperty("pkcs11test.nss.db", "sql:" + DBDIR); } else { System.setProperty("pkcs11test.nss.db", DBDIR); } File dbdirFile = new File(DBDIR); - if (dbdirFile.exists() == false) { - dbdirFile.mkdir(); + if (dbdirFile.exists()) { + deleteDir(dbdirFile.toPath()); } + dbdirFile.mkdir(); if (useSqlite) { copyFile("key4.db", BASE, DBDIR); copyFile("cert9.db", BASE, DBDIR); + copyFile("pkcs11.txt", BASE, DBDIR); } else { copyFile("secmod.db", BASE, DBDIR); copyFile("key3.db", BASE, DBDIR); @@ -90,6 +97,25 @@ private static void copyFile(String name, String srcDir, String dstDir) throws I out.close(); } + private static void deleteDir(final Path directory) throws IOException { + Files.walkFileTree(directory, new SimpleFileVisitor() { + + @Override + public FileVisitResult visitFile(Path file, + BasicFileAttributes attrs) throws IOException { + Files.delete(file); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) + throws IOException { + Files.delete(dir); + return FileVisitResult.CONTINUE; + } + }); + } + public void main(Provider p) throws Exception { // dummy } diff --git a/jdk/test/sun/security/pkcs11/fips/NssdbPin.java b/jdk/test/sun/security/pkcs11/fips/NssdbPin.java new file mode 100644 index 00000000000..2d8e1fd8280 --- /dev/null +++ b/jdk/test/sun/security/pkcs11/fips/NssdbPin.java @@ -0,0 +1,348 @@ +/* + * Copyright (c) 2022, Red Hat, Inc. + * + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.BufferedWriter; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.Provider; +import java.security.Security; +import java.util.Arrays; +import java.util.function.Consumer; +import java.util.List; +import javax.crypto.Cipher; +import javax.crypto.spec.SecretKeySpec; + +import jdk.testlibrary.FileUtils; + +/* + * @test + * @bug 9999999 + * @summary + * Test that the fips.nssdb.path and fips.nssdb.pin properties can be used + * for a successful login into an NSS DB. Some additional unitary testing + * is then performed. This test depends on NSS modutil and must be run in + * FIPS mode (the SunPKCS11-NSS-FIPS security provider has to be available). + * @library /java/security/testlibrary + * /lib/testlibrary + * @requires (jdk.version.major >= 8) + * @run main/othervm/timeout=600 NssdbPin + * @author Martin Balao (mbalao@redhat.com) + */ + +public final class NssdbPin { + + // Public properties and names + private static final String FIPS_NSSDB_PATH_PROP = "fips.nssdb.path"; + private static final String FIPS_NSSDB_PIN_PROP = "fips.nssdb.pin"; + private static final String FIPS_PROVIDER_NAME = "SunPKCS11-NSS-FIPS"; + private static final String NSSDB_TOKEN_NAME = + "NSS FIPS 140-2 Certificate DB"; + + // Data to be tested + private static final String[] PINS_TO_TEST = + new String[] { + "", + "1234567890abcdef1234567890ABCDEF\uA4F7" + }; + private static enum PropType { SYSTEM, SECURITY } + private static enum LoginType { IMPLICIT, EXPLICIT } + + // Internal test fields + private static final boolean DEBUG = true; + private static class TestContext { + String pin; + PropType propType; + Path workspace; + String nssdbPath; + Path nssdbPinFile; + LoginType loginType; + TestContext(String pin, Path workspace) { + this.pin = pin; + this.workspace = workspace; + this.nssdbPath = "sql:" + workspace; + this.loginType = LoginType.IMPLICIT; + } + } + + public static void main(String[] args) throws Throwable { + if (args.length == 3) { + // Executed by a child process. + mainChild(args[0], args[1], LoginType.valueOf(args[2])); + } else if (args.length == 0) { + // Executed by the parent process. + mainLauncher(); + // Test defaults + mainChild("sql:/etc/pki/nssdb", "", LoginType.IMPLICIT); + System.out.println("TEST PASS - OK"); + } else { + throw new Exception("Unexpected number of arguments."); + } + } + + private static void mainChild(String expectedPath, String expectedPin, + LoginType loginType) throws Throwable { + if (DEBUG) { + for (String prop : Arrays.asList(FIPS_NSSDB_PATH_PROP, + FIPS_NSSDB_PIN_PROP)) { + System.out.println(prop + " (System): " + + System.getProperty(prop)); + System.out.println(prop + " (Security): " + + Security.getProperty(prop)); + } + } + + /* + * Functional cross-test against an NSS DB generated by modutil + * with the same PIN. Check that we can perform a crypto operation + * that requires a login. The login might be explicit or implicit. + */ + Provider p = Security.getProvider(FIPS_PROVIDER_NAME); + if (DEBUG) { + System.out.println(FIPS_PROVIDER_NAME + ": " + p); + } + if (p == null) { + throw new Exception(FIPS_PROVIDER_NAME + " initialization failed."); + } + if (DEBUG) { + System.out.println("Login type: " + loginType); + } + if (loginType == LoginType.EXPLICIT) { + // Do the expansion to account for truncation, so C_Login in + // the NSS Software Token gets a UTF-8 encoded PIN. + byte[] pinUtf8 = expectedPin.getBytes(StandardCharsets.UTF_8); + char[] pinChar = new char[pinUtf8.length]; + for (int i = 0; i < pinChar.length; i++) { + pinChar[i] = (char)(pinUtf8[i] & 0xFF); + } + KeyStore.getInstance("PKCS11", p).load(null, pinChar); + if (DEBUG) { + System.out.println("Explicit login succeeded."); + } + } + if (DEBUG) { + System.out.println("Trying a crypto operation..."); + } + final int blockSize = 16; + Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding", p); + cipher.init(Cipher.ENCRYPT_MODE, + new SecretKeySpec(new byte[blockSize], "AES")); + if (cipher.doFinal(new byte[blockSize]).length != blockSize) { + throw new Exception("Could not perform a crypto operation."); + } + if (DEBUG) { + if (loginType == LoginType.IMPLICIT) { + System.out.println("Implicit login succeeded."); + } + System.out.println("Crypto operation after login succeeded."); + } + + if (loginType == LoginType.IMPLICIT) { + /* + * Additional unitary testing. Expected to succeed at this point. + */ + if (DEBUG) { + System.out.println("Trying unitary test..."); + } + String sysPathProp = System.getProperty(FIPS_NSSDB_PATH_PROP); + if (DEBUG) { + System.out.println("Path value (as a System property): " + + sysPathProp); + } + if (!expectedPath.equals(sysPathProp)) { + throw new Exception("Path is different than expected: " + + sysPathProp + " (actual) vs " + expectedPath + + " (expected)."); + } + Class c = Class + .forName("sun.security.pkcs11.FIPSTokenLoginHandler"); + Method m = c.getDeclaredMethod("getFipsNssdbPin"); + m.setAccessible(true); + char[] pinChar = (char[]) m.invoke(c); + byte[] pinUtf8 = new byte[pinChar.length]; + for (int i = 0; i < pinUtf8.length; i++) { + pinUtf8[i] = (byte) pinChar[i]; + } + String pin = new String(pinUtf8, StandardCharsets.UTF_8); + if (!pin.equals(expectedPin)) { + throw new Exception("PIN is different than expected: " + pin + + " (actual) vs " + expectedPin + " (expected)."); + } + if (DEBUG) { + System.out.println("PIN value: " + pin); + System.out.println("Unitary test succeeded."); + } + } + } + + private static void mainLauncher() throws Throwable { + for (String pin : PINS_TO_TEST) { + Path workspace = Files.createTempDirectory(null); + try { + TestContext ctx = new TestContext(pin, workspace); + createNSSDB(ctx); + { + ctx.loginType = LoginType.IMPLICIT; + for (PropType propType : PropType.values()) { + ctx.propType = propType; + pinLauncher(ctx); + envLauncher(ctx); + fileLauncher(ctx); + } + } + explicitLoginLauncher(ctx); + } finally { + FileUtils.deleteFileTreeWithRetry(workspace); + } + } + } + + private static void pinLauncher(TestContext ctx) throws Throwable { + launchTest(p -> {}, "pin:" + ctx.pin, ctx); + } + + private static void envLauncher(TestContext ctx) throws Throwable { + final String NSSDB_PIN_ENV_VAR = "NSSDB_PIN_ENV_VAR"; + launchTest(p -> p.env(NSSDB_PIN_ENV_VAR, ctx.pin), + "env:" + NSSDB_PIN_ENV_VAR, ctx); + } + + private static void fileLauncher(TestContext ctx) throws Throwable { + // The file containing the PIN (ctx.nssdbPinFile) was created by the + // generatePinFile method, called from createNSSDB. + launchTest(p -> {}, "file:" + ctx.nssdbPinFile, ctx); + } + + private static void explicitLoginLauncher(TestContext ctx) + throws Throwable { + ctx.loginType = LoginType.EXPLICIT; + ctx.propType = PropType.SYSTEM; + launchTest(p -> {}, "Invalid PIN, must be ignored", ctx); + } + + private static void launchTest(Consumer procCb, String pinPropVal, + TestContext ctx) throws Throwable { + if (DEBUG) { + System.out.println("Launching JVM with " + FIPS_NSSDB_PATH_PROP + + "=" + ctx.nssdbPath + " and " + FIPS_NSSDB_PIN_PROP + + "=" + pinPropVal); + } + Proc p = Proc.create(NssdbPin.class.getName()) + .args(ctx.nssdbPath, ctx.pin, ctx.loginType.name()); + if (ctx.propType == PropType.SYSTEM) { + p.prop(FIPS_NSSDB_PATH_PROP, ctx.nssdbPath); + p.prop(FIPS_NSSDB_PIN_PROP, pinPropVal); + // Make sure that Security properties defaults are not used. + p.secprop(FIPS_NSSDB_PATH_PROP, ""); + p.secprop(FIPS_NSSDB_PIN_PROP, ""); + } else if (ctx.propType == PropType.SECURITY) { + p.secprop(FIPS_NSSDB_PATH_PROP, ctx.nssdbPath); + pinPropVal = escapeForPropsFile(pinPropVal); + p.secprop(FIPS_NSSDB_PIN_PROP, pinPropVal); + } else { + throw new Exception("Unsupported property type."); + } + if (DEBUG) { + p.inheritIO(); + p.prop("java.security.debug", "sunpkcs11"); + p.debug(NssdbPin.class.getName()); + + // Need the launched process to connect to a debugger? + //System.setProperty("test.vm.opts", "-Xdebug -Xrunjdwp:" + + // "transport=dt_socket,address=localhost:8000,suspend=y"); + } else { + p.nodump(); + } + procCb.accept(p); + p.start().waitFor(0); + } + + private static String escapeForPropsFile(String str) throws Throwable { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < str.length(); i++) { + int cp = str.codePointAt(i); + if (Character.UnicodeBlock.of(cp) + == Character.UnicodeBlock.BASIC_LATIN) { + sb.append(Character.toChars(cp)); + } else { + sb.append("\\u").append(String.format("%04X", cp)); + } + } + return sb.toString(); + } + + private static void createNSSDB(TestContext ctx) throws Throwable { + ProcessBuilder pb = getModutilPB(ctx, "-create"); + if (DEBUG) { + System.out.println("Creating an NSS DB in " + ctx.workspace + + "..."); + System.out.println("cmd: " + String.join(" ", pb.command())); + } + if (pb.start().waitFor() != 0) { + throw new Exception("NSS DB creation failed."); + } + generatePinFile(ctx); + pb = getModutilPB(ctx, "-changepw", NSSDB_TOKEN_NAME, + "-newpwfile", ctx.nssdbPinFile.toString()); + if (DEBUG) { + System.out.println("NSS DB created."); + System.out.println("Changing NSS DB PIN..."); + System.out.println("cmd: " + String.join(" ", pb.command())); + } + if (pb.start().waitFor() != 0) { + throw new Exception("NSS DB PIN change failed."); + } + if (DEBUG) { + System.out.println("NSS DB PIN changed."); + } + } + + private static ProcessBuilder getModutilPB(TestContext ctx, String... args) + throws Throwable { + ProcessBuilder pb = new ProcessBuilder("modutil", "-force"); + List pbCommand = pb.command(); + if (args != null) { + pbCommand.addAll(Arrays.asList(args)); + } + pbCommand.add("-dbdir"); + pbCommand.add(ctx.nssdbPath); + if (DEBUG) { + pb.inheritIO(); + } else { + pb.redirectError(ProcessBuilder.Redirect.INHERIT); + } + return pb; + } + + private static void generatePinFile(TestContext ctx) throws Throwable { + ctx.nssdbPinFile = Files.createTempFile(ctx.workspace, null, null); + try (BufferedWriter bw = Files.newBufferedWriter(ctx.nssdbPinFile)) { + bw.write(ctx.pin); + bw.write(System.lineSeparator()); + bw.write("2nd line with garbage"); + } + } +}