DualControl - evanx/vellum GitHub Wiki

Labels: Dual Control, Split Knowledge, Key Management, Encryption, PAN, PCI, keytool, Java

Dual Control Key Management

We hereby start The Final Quadrilogy, a sequel to The Enigma Posts.

Problem overview

Encryption is great for information security and all that. But the problem with encryption is... key management. An analogy often bandied about is that we lock the door but leave the key in the lock. Or under the mat, but that's my personal favourite so ssh-ssh.

PCI DSS

The "Payment Card Industry Data Security Standard" (PCI DSS) advocates common sense policies for building a secure network and protecting our data. Actually every enterprise should adopt PCI DSS because it's the only and best such thing we got. Where it refers to "cardholder data," replace that with "identity data" or "access credentials" or what-have-you.

If we want information security, then PCI DSS is our new best friend forever, albeit a high-maintenance one.

PCI DSS suggests encrypting our data-encryption key (DEK) in order to protect it. Great, we now have a "key-encryption key" (KEK) that requires even more protection ;)

Furthermore, PCI DSS decrees that manual key management requires "split knowledge and dual control" e.g. for key generation and loading. The intent is that no single person can extract the clear-text data.

The glaring problem is that sysadmins are a single person, with god-like access to all our data, and de facto custodian of the proverbial keys to the kindgom. Consequently sysadmins have root access ;)

Solution overview

We'll store our key in a standard JceKeyStore, whose KeyProtector uses a Triple DES cipher for password-based encryption of the key.

We'll split the knowledge of the key-protection password between two custodians, so it's known to no single person. Clearly dual control by those two custodians is then required to load the key, and PCI DSS compliance is thus enabled.

We propose enrolling at least three custodians, so that if one is unavailable, we're still good to go with two others. For each duo, we'll store a copy of the key protected by the concatenation of their two personal passwords.

Since this combined password is used for password-based encryption of the key, it is effectively a key-encryption key, which is clearly well protected by virtue of being known to no single person, and not stored anywhere. We'll refer to such a "split-knowledge password" as a "dual password."

DualControlGenSecKey demo

Step 1 for any data security endeavour is to generate an encryption key. Whereas keytool prompts for a password entered by a single custodian, we introduce DualControlGenSecKey to handle multiple password submissions via SSL.

$ java -Ddualcontrol.submissions=3 -Ddualcontrol.minPassphraseLength=8 \
    -Ddualcontrol.ssl.keyStore=keystores/dualcontrolserver.jks \
    -Ddualcontrol.ssl.trustStore=keystores/dualcontrolserver.trust.jks \
    -Dkeystore=keystores/dek2013.jceks -Dstoretype=JCEKS \
    -Dalias=dek2013 -Dkeyalg=AES -Dkeysize=256 \
    dualcontrol.DualControlGenSecKey

Enter passphrase for dualcontrol.ssl:
Enter passphrase for keystore for new key dek2013: 

where we have requested a new 256bit AES secret key, aliased as dek2013.

Note that the -Ddualcontrol.ssl.keyStore property is the location of the "private keystore" for SSL. This contains the private key and its public certificate for RSA assymmetric encryption to establish an SSL connection. This should not be confused with our "secret keystore" which contains an AES or DESede secret key for symmetric encryption of our data.

The logs produced by DualControlGenSecKey are as follows.

INFO [DualControlManager] purpose: new key dek2013
INFO [DualControlManager] accept: 3
INFO [DualControlManager] Received evanx
INFO [DualControlManager] Received henry
INFO [DualControlManager] Received brent
INFO [DualControlManager] dualAlias: brent-evanx
INFO [DualControlManager] dualAlias: brent-henry
INFO [DualControlManager] dualAlias: evanx-henry
INFO [DualControlGenSecKey] alias: dek2013-brent-evanx
INFO [DualControlGenSecKey] alias: dek2013-brent-henry
INFO [DualControlGenSecKey] alias: dek2013-evanx-henry

where DualControlManager does the leg work of receiving passwords from custodians.

For this demo, three custodians submit their passwords via SSL sockets, using DualControlConsole as shown below, where they are identified as evanx, henry and brent by their SSL client certificate's common name (CN).

evanx$ java -Ddualcontrol.ssl.keyStore=keystores/evanx.jks \
         dualcontrol.DualControlConsole
Enter passphrase for dualcontrol.ssl:
Connected evanx
Enter passphrase for new key dek2013:
Re-enter passphrase:
Received evanx

We see that DualControlGenSecKey creates secret key entries under the following "dual aliases."

$ keytool -keystore keystores/dek2013.jceks -storetype JCEKS -list
Enter keystore password:  

Keystore type: JCEKS
Keystore provider: SunJCE

Your keystore contains 3 entries

dek2013-brent-henry, 27 Sep 2013, SecretKeyEntry, 
dek2013-brent-evanx, 27 Sep 2013, SecretKeyEntry, 
dek2013-evanx-henry, 27 Sep 2013, SecretKeyEntry, 

Actually these three keys are one and the same! However each copy has a different "dual password," which is a combination of a pair of personal passwords. Consequently the key password is known to no single person. The keystore password is shared, but that's just extra.

DualControlGenSecKey implementation

Let's walk through the code.

public class DualControlGenSecKey {
   private int submissionCount;
   private String keyAlias;
   private String keyStoreLocation;
   private String keyStoreType;
   private String keyAlg;
   private int keySize;
   private char[] keyStorePassword;
   private Map<String, char[]> dualMap;
   private SSLContext sslContext;

   public DualControlGenSecKey(Properties properties, 
            MockableConsole console) {
      this.props = new ExtendedProperties(properties);
      this.console = console;
      submissionCount = props.getInt("dualcontrol.submissions", 3);
      keyAlias = props.getString("alias");
   }
   ...
}

where we configure via Properties and provide a MockableConsole for entering keystore passwords.

Note that ExtendedProperties is a utility wrapper for java.util.Properties that will throw an exception if the property is not found and no default is provided.

We initialise the instance with an SSLContext as follows.

public void init() throws Exception {
   sslContext = SSLContexts.create(true, "dualcontrol.ssl", props, 
         console);
}

where we'll see further below that SSLContexts uses a keystore and truststore configured by properties e.g. dualcontrol.ssl.keyStore and dualcontrol.ssl.trustStore. These SSL keystores should not be confused with the keystore for our new secret key which we are generating.

Our main() method below passes System properties i.e. -D options, and the System console.

public static void main(String[] args) throws Exception {
   DualControlGenSecKey instance = new DualControlGenSecKey(
         System.getProperties(),
         new MockableConsoleAdapter(System.console()));
   try {
      instance.init();
      instance.call();
   } catch (DualControlException e) {
      instance.console.println(e.getMessage());
   } finally {
      instance.clear();
   }
}

where we instantiate, initialise, and then call this class.

public void call() throws Exception {
   keyStoreLocation = props.getString("keystore");
   if (new File(keyStoreLocation).exists()) {
      throw new Exception("Keystore file already exists: " + 
         keyStoreLocation);
   }
   keyStorePassword = props.getPassword("storepass", null);
   if (keyStorePassword == null) {
      keyStorePassword = console.readPassword(
              "Enter passphrase for keystore for new key %s: ", keyAlias);
      if (keyStorePassword == null) {
          throw new Exception("No keystore passphrase from console");
      }
   }
   KeyStore keyStore = createKeyStore();
   keyStore.store(new FileOutputStream(keyStoreLocation), 
         keyStorePassword);
}

public KeyStore createKeyStore() throws Exception {
   String purpose = "new key " + keyAlias;
   DualControlManager manager = new DualControlManager(properties, 
          submissionCount, purpose);
   manager.init(sslContext);
   manager.call();
   return buildKeyStore(manager.getDualMap());
}

where DualControlManager provides a map of dual aliases and passwords, composed from submissions via SSL. We pass this map to the buildKeyStore() method below.

public KeyStore buildKeyStore(Map<String, char[]> dualMap) 
         throws Exception {
   keyAlias = props.getString("alias");
   keyStoreType = props.getString("storetype");
   keyAlg = props.getString("keyalg");
   keySize = props.getInt("keysize");
   KeyGenerator keyGenerator = KeyGenerator.getInstance(keyAlg);
   keyGenerator.init(keySize);
   SecretKey secretKey = keyGenerator.generateKey();
   KeyStore keyStore = KeyStore.getInstance(keyStoreType);
   keyStore.load(null, null);
   setEntries(keyStore, secretKey, keyAlias, dualMap);
   return keyStore;
}

private static void setEntries(KeyStore keyStore, SecretKey secretKey,
         String keyAlias, Map<String, char[]> dualMap) throws Exception {
   KeyStore.Entry entry = new KeyStore.SecretKeyEntry(secretKey);
   for (String dualAlias : dualMap.keySet()) {
      char[] dualPassword = dualMap.get(dualAlias);
      String alias = keyAlias + "-" + dualAlias;
      logger.info("alias: " + alias);
      KeyStore.PasswordProtection passwordProtection =
            new KeyStore.PasswordProtection(dualPassword);
      keyStore.setEntry(alias, entry, passwordProtection);
      passwordProtection.destroy();
      Arrays.fill(dualPassword, (char) 0);
   }
}

where for each duo in the map we program a keystore entry containing the same key, but protected by a different dual password.

Incidently, we invariably use char arrays for passwords, and clear these as soon as possible. String's are immutable and will be garbage-collected and overwritten in memory at some stage, but that is too indeterminate to alleviate our paranoia. Having said that, SecretKeySpec.getEncoded() clones the byte array, which is somewhat disconcerting. So hopefully no one can scan or "debug" our JVM memory and thereby extract our passwords, or indeed the key itself.

Some argue that one should not write code per se, but rather tests with accompanying code, hand in glove. Indeed we observe that our implementation is well defined by the unit tests.

@Test
public void testGenKeyStore() throws Exception {
   dualMap.put("brent-evanx", "bbbb|eeee".toCharArray());
   dualMap.put("brent-henry", "bbbb|hhhh".toCharArray());
   dualMap.put("evanx-henry", "eeee|hhhh".toCharArray());
   MockConsole appConsole = new MockConsole("app", keyStorePass);
   DualControlGenSecKey instance = new DualControlGenSecKey(properties, 
      appConsole);
   KeyStore keyStore = instance.buildKeyStore(dualMap);
   Assert.assertEquals(3, Collections.list(keyStore.aliases()).size());
   Assert.assertEquals("dek2013-brent-evanx", 
      asSortedSet(keyStore.aliases()).first());
   SecretKey key = getSecretKey(keyStore, "dek2013-brent-evanx", 
      "bbbb|eeee".toCharArray());
   Assert.assertEquals("AES", key.getAlgorithm());
   Assert.assertTrue(Arrays.equals(key.getEncoded(), 
      getSecretKey(keyStore, "dek2013-brent-henry", 
      "bbbb|hhhh".toCharArray()).getEncoded()));
}

where we build a map of sample dual submissions, and inspect the KeyStore returned by buildKeyStore().

Note that the key passwords are concatenations of personal passwords, where we arbitrarily choose the vertical bar character as a delimiter. This is our so-called dual password, which can be used to recover the key in clear-text using the following utility method.

private static SecretKey getSecretKey(KeyStore keyStore, String keyAlias, 
      char[] keyPass) throws GeneralSecurityException {
   KeyStore.SecretKeyEntry entry = (KeyStore.SecretKeyEntry) 
      keyStore.getEntry(keyAlias, 
      new KeyStore.PasswordProtection(keyPass));
   return entry.getSecretKey();
}

which invokes KeyStore.getEntry().

SSLContexts

Similarly to the standard -Dnet.javax.ssl.keyStore et al command-line options used to specify the default keystore and truststore for SSL sockets, and in order to avoid any potential conflict with those, we use -Ddualcontrol.ssl.keyStore et al. These properties are used to create an SSLContext as follows.

public class SSLContexts {    

   public static SSLContext create(boolean strict, String sslPrefix, 
            Properties properties, MockableConsole console) throws 
            Exception {
      ExtendedProperties props = new ExtendedProperties(properties);
      sslPrefix = props.getString(sslPrefix, sslPrefix);
      String keyStoreLocation = props.getString(sslPrefix + ".keyStore");
      if (keyStoreLocation == null) {
         throw new Exception("Missing keystore property for " + 
               sslPrefix);
      }
      char[] pass = props.getPassword(sslPrefix + ".pass", null);
      if (pass == null) {
         pass = console.readPassword("Enter passphrase for %s: ", 
               sslPrefix);
      }
      String trustStoreLocation = props.getString(sslPrefix + 
            ".trustStore", keyStoreLocation);
      if (keyStoreLocation.equals(trustStoreLocation)) {
         throw new KeyStoreException("Require separate truststore");
      }
      KeyStore keyStore = KeyStores.loadKeyStore("JKS", 
         keyStoreLocation, pass);
      KeyStore trustStore = KeyStores.loadKeyStore("JKS", 
         trustStoreLocation, pass);
      SSLContext sslContext = create(keyStore, pass, 
         createTrustManager(trustStore));
      Arrays.fill(pass, (char) 0);
      return sslContext;
   }
   ...
}

where we reuse the same password for the SSL keystore, its private key, and the truststore. This password can specified on the command-line e.g. for automated test scripts, but otherwise we prompt for the SSL keystore password to be entered on the console. Moreover, we have introduced a MockableConsole for the benefit of automated unit testing.

Note that we forbid misusing the keystore as the truststore. The key certificate is naturally in the keystore, but actually is not a trusted certificate for peer connections. This thwarts an attack where our keystore is abused as a peer keystore, or is used to sign a rogue peer certificate. Thereby, rogue connections would be possible without tampering with our truststore. (See the Explicit Trust Manager article in this series.)

Ordinarily we create the SSLContext as follows.

    public static SSLContext create(KeyStore keyStore, char[] keyPassword,
           KeyStore trustStore) throws Exception {
        KeyManagerFactory keyManagerFactory = 
           KeyManagerFactory.getInstance("SunX509");
        keyManagerFactory.init(keyStore, keyPassword);
        TrustManagerFactory trustManagerFactory = 
           TrustManagerFactory.getInstance("SunX509");
        trustManagerFactory.init(trustStore);
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(keyManagerFactory.getKeyManagers(),
           trustManagerFactory.getTrustManagers(), new SecureRandom());
        return sslContext;
    }

keytool

Naturally we use keytool to create our private SSL keystore, e.g. as required by DualControlConsole, specified by our dualcontrol.ssl.keyStore property.

$ keytool -keystore evanx.jks -genkeypair -keyalg rsa -keysize 2048 \
   -validity 365 -alias evanx -dname "CN=evanx, OU=test"

We export our certificate as follows.

$ keytool -keystore evanx.jks -alias evanx -exportcert -rfc

We cut and paste the exported PEM text into a file, which we can inspect using openssl as follows.

$ openssl x509 -text -in evanx.pem
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 1380030508 (0x5241982c)
    Signature Algorithm: sha1WithRSAEncryption
        Issuer: CN=evanx, OU=test
        Validity
            Not Before: Sep 24 13:48:28 2013 GMT
            Not After : Sep 24 13:48:28 2014 GMT
        Subject: CN=evanx, OU=test
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)

We import the cert into the server SSL truststore as required by DualControlManager on behalf of DualControlGenSecKey and our app.

$ keytool -keystore dualcontrolserver.trust.jks \
   -alias evanx -importcert -file evanx.pem

Similarly, the server cert is imported into the custodians' truststores as specified by dualcontrol.ssl.trustStore for DualControlConsole.

DualControlConsole

Let's try our DualControlConsole app, e.g. to submit a password to DualControlGenSecKey.

evanx$ java -Ddualcontrol.ssl.keyStore=keystores/evanx.jks \
         dualcontrol.DualControlConsole
Enter passphrase for dualcontrol.ssl:
Connected evanx
Enter passphrase for new key dek2013: 
Re-enter passphrase: 
Received evanx

where SSLContexts prompts for the SSL keystore password first. We then enter a passphrase for the new key.

The importance of not echoing passwords to the console, so that they are not cut and pasted by mistake, is illustrated above ;)

We implement the main() as follows.

public class DualControlConsole {
   Properties properties;
   MockableConsole console;
   SSLContext sslContext;

   public static void main(String[] args) throws Exception {
      DualControlConsole instance = new DualControlConsole(
         System.getProperties(), 
         new MockableConsoleAdapter(System.console()));
      try {
         instance.init();
         instance.call();
      } finally {            
         instance.clear();
      }
   }

   public void init() throws Exception {
       init(SSLContexts.create(false, "dualcontrol.ssl", 
          properties, console));
   }
   ...
}

where we create an SSLContext from the provided properties, which must specify the requisite keystore and truststore. (Later we'll see that our unit test provides an SSLContext from KeyStore's which it creates programmatically.)

We submit the password via SSL in the call() method below.

private final static int PORT = 4444;
private final static String HOST = "127.0.0.1";

public void call() throws Exception {
   Socket socket = sslContext.getSocketFactory().createSocket(HOST, PORT);
   DataInputStream dis = new DataInputStream(socket.getInputStream());
   String message = dis.readUTF();
   console.println(message);
   String purpose = dis.readUTF();
   if (purpose.length() == 0) {
      return;
   }
   DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
   char[] password = console.readPassword(
         "Enter passphrase for " + purpose + ": ");
   String invalidMessage = new DualControlPassphraseVerifier(properties).
         getInvalidMessage(password);
   if (invalidMessage != null) {
      console.println(invalidMessage);
      dos.writeShort(0);
   } else {
      char[] pass = console.readPassword(
             "Re-enter passphrase: ");
      if (!Arrays.equals(password, pass)) {
         console.println("Passwords don't match.");
         dos.writeShort(0);
      } else {
         writeChars(dos, password);
         String message = dis.readUTF();
         console.println(message);
      }
      Arrays.fill(pass, (char) 0);
   }
   Arrays.fill(password, (char) 0);
   socket.close();
}

where if the re-entered password does not match, we politely send an empty password to the server, which indicates an aborted attempt. Similarly, if the server rejects our connection, it will politely send us an empty purpose, so that we can abort gracefully.

Note that have hard-coded the host to localhost to enforce ssh port forwarding for remote access i.e. SSL over ssh :)

A known "bug" is that more godly sysadmins can socially-engineer other less godly admins to submit their passwords whilst a malicious socket server has been installed by the most godly one onto that frikking port! Other naughty tricks that come to mind is the creation of phantom custodians, or the impersonation of other custodians e.g. during a key generation procedure, by abusing root access to SSL keystores. So we must ensure that our logs and alerts prevent such events from going unnoticed.

Multi-factor authentication

On a positive note, we can setup "double-two-factor" authentication whereby the client requires a password-protected ssh key for port forwarding, and a password-protected KeyStore for the client-authenticated SSL connection.

For both the underlying ssh access, and the SSL connection over that, the custodian needs to have the private key, and know the password that is protecting it, and so arguably both require multi-factor authentication.

However root can also "have" the files containing the keys, but at least doesn't know other custodians' passwords protecting the keys therein.

Some argue that to really "have" something, it should not be so copyable as a key in a file, but rather it should be a hardware token or smartcard. However, Navigating PCI DSS 2.0 provides the following guidance:

A digital certificate is a valid option as a form of the authentication type "something you have" as long as it is unique.

A further challenge is that the existence of a password which protects an ssh or SSL key, cannot be verified by the server. So at least we double up to mitigate this ;)

Password complexity vs length

We ensure that the complexity and/or length of the password is sufficient to counter brute-force attacks.

public class DualControlPassphraseVerifier {
   private final boolean verifyPassphrase;
   private final boolean verifyPassphraseComplexity;
   private final int minPassphraseLength;
   private final int minWordCount;

   public DualControlPassphraseVerifier(Properties properties) {
      ExtendedProperties props = new ExtendedProperties(properties);
      verifyPassphrase = props.getBoolean(
              "dualcontrol.verifyPassphrase", true);
      verifyPassphraseComplexity = props.getBoolean(
              "dualcontrol.verifyPassphraseComplexity", true);
      minPassphraseLength = props.getInt(
              "dualcontrol.minPassphraseLength", 12);
      minWordCount = props.getInt(
              "dualcontrol.minWordCount", 4);
   }
   ...

We note that PCI DSS mandates alphanumeric passwords, whereas SOX requires uppercase and lowercase, and we should cover all bases.

public String getInvalidMessage(char[] password) {
   if (verifyPassphrase) {
      if (password.length < minPassphraseLength) {
         return "Passphrase too short";
      }
      if (countWords(password) < minWordCount) {
         return "Too few words in passphrase";
      }
      if (verifyPassphraseComplexity) {
         if (!containsUpperCase(password) || !containsLowerCase(password) || 
                !containsDigit(password) || !containsPunctuation(password)) {
            return "Insufficient password complexity";
         }
      }
   }
   return null;
}

Clearly passphrases are easier to remember than complex passwords, and so perhaps we should enforce passphrases with a minimum word count, rather than complexity per se. Then we don't have to write down our complex password and stick it on our monitor.

Having said that, we can easily capitalise the first letter of our passphrase, add a punctuation mark at the end at least, and somewhere replace an 'o' with a zero or an 'e' with 3. Yeah b4by! (Darn, now I can't use that one anymore.)

Incidently, when we are submitting existing passwords using DualControlConsole to start our app, we might have to disable passphrase verification if our old password falls short of revised complexity requirements. However in that case, we should rather change our password accordingly, e.g. together with a key rotation for good measure.

We note that PCI DSS mandates that passwords be changed every 90 days. However, we hope that this applies to remote access passwords, and not to key-protection passwords. We might argue that key-protection passwords are key-encryption keys, and not remote access credentials per se, even when they protect remote access keys. Actually we argue the opposite in favour of multi-factor authentication, so it depends on the context ;)

Brutish Key Protector

Our keystore is unfortunately forever vulnerable to theft and/or brute-force attacks. Even when we have deleted it, someone else might have ill-gotten it earlier. On the upside, we might assume that after say 30 years the data is no longer of any value to anyone.

We observe that an Intel i5 can manage about 30 guesses per millisecond on a JCE keystore, using 4 threads to utilise its quad-cores.

evanx@beethoven:~$ java dualcontrol.KeyStoreBruteForceTimer 4 1000000 \
  keystores/dek2013.jceks "$pass" dek2013-evanx-henry eeeehhhh

threads 4, count 1000000, time 128s, avg 0.032ms
31 guesses per millisecond

If we assume an average of 10 guesses per core per millisecond, and consider a botnet with 1 million cores, then by my backroom calculations, 12 days are required to try all possible passwords up to 10 characters in length using a lazy subset of 40 characters.

guess=10
mach=1000*1000
40^10/(mach*guess*1000*60*60*24)
12 days

Considering that the most common 100 words make up a half of all written material (cit. Wikipedia), if we guess combinations of the 1500 most common words, then 8 days are required for such passphrases with 5 words.

(1500^5)/(mach*guess*1000*60*60*24)
8 days

Note that our mandatory minimums apply to each custodian's passphrase, to protect against a rogue custodian perpetrating the brute-force attack on the other half of the dual password.

A follow-up article will discuss this further, and conclude that we might want an even stronger KeyStore implementation than JCEKS, in particular with a stronger KeyProtector using PBKDF2 or even scrypt. Then we would specify huge number of iterations e.g. 500k, so that it takes a few seconds to load the key. That is still a tolerable startup delay in our production environment, but would thwart brute-force attacks.

DualControlDemoApp

The downside of all this malarkey, is that we have to re-engineer our application to be dual-controlled, in order to load the key it so desperately needs to cipher our data.

public class DualControlDemoApp {
   private SecretKey dek;     
   ...
   public void loadKey(String keyStoreLocation, String alias) 
            throws Exception {
      char[] storePass = System.console().readPassword(
            "Enter keystore password for %s: ", alias);
      dek = DualControlSessions.loadKey(keyStoreLocation, "JCEKS", 
            storePass, alias, "DualControlDemoApp");
      logger.info("loaded key {}: alg {}", alias, dek.getAlgorithm());
   }
}

where we prompt for the shared keystore password to be entered on the console.

$ java -Ddualcontrol.ssl.keyStore=keystores/dualcontrolserver.jks \
    -Ddualcontrol.ssl.trustStore=keystores/dualcontrolserver.trust.jks \
    dualcontrol.DualControlDemoApp keystores/dek2013.jceks dek2013
Enter passphrase for dualcontrol.ssl:
Enter keystore password for dek2013:
INFO [DualControlManager] purpose: key dek2013 for DualControlDemoApp
INFO [DualControlManager] accept: 2

The application's execution is then blocked whilst we are waiting on the SSLServerSocket for two custodians to submit their passwords using DualControlConsole.

evanx$ java -Ddualcontrol.ssl.keyStore=evanx.jks \
         dualcontrol.DualControlConsole
Enter passphrase for dualcontrol.ssl:
Connected evanx
Enter passphrase for key dek2013 for DualControlDemoApp: 
Re-enter passphrase:
Received evanx

where we specify the keystore for the SSL socket, which can double up as our truststore.

henry$ java -Ddualcontrol.ssl.keyStore=henry.jks dualcontrol.DualControlConsole
Enter passphrase for dualcontrol.ssl:
Connected henry
Enter passphrase for key dek2013 for DualControlDemoApp: 
Re-enter passphrase:
Received henry

We observe the following logs from DualControlDemoApp.

INFO [DualControlManager] Received evanx
INFO [DualControlManager] Received henry
INFO [DualControlManager] dualAlias: evanx-henry
INFO [DualControlSessions] dek2013-evanx-henry
INFO [DualControlDemoApp] loaded key dek2013: alg AES

where it accepts submissions from evanx and henry courtesy of DualControlManager, and can then load the key.

DualControlSessions

Our demo app above invokes the loadKey() method below to do the legwork.

public class DualControlSessions {

   public static SecretKey loadKey(String keyStoreLocation, 
         String keyStoreType, char[] keyStorePass, String keyAlias, 
         String purpose) throws Exception {
      KeyStore keyStore = DualControlKeyStores.loadKeyStore(
         keyStoreLocation, keyStoreType, keyStorePass);
      purpose = "key " + keyAlias + " for " + purpose;
      Map.Entry<String, char[]> entry = 
         DualControlManager.readDualEntry(purpose);
      String dualAlias = entry.getKey();
      char[] dualPassword = entry.getValue();
      keyAlias = keyAlias + "-" + dualAlias;
      SecretKey key = (SecretKey) keyStore.getKey(keyAlias, dualPassword);
      Arrays.fill(dualPassword, (char) 0);
      return key;
   }
}

where we read the dual info using DualControlManager, which opens an SSLServerSocket and waits for dual password submissions from any two custodians.

Note that we append the dual alias e.g. so that dek2013 becomes dek2013-evanx-henry, and get that copy of the key from the keystore.

Once we have used the dual password to load the key, we clear that password. However the key itself is now in memory in clear-text, and we must be wary of it being compromised by our application, or extracted by attaching a debugger to the JVM, or by a malicious memory scanner.

We instantiate, initialise and call DualControlManager as follows.

public static Map.Entry<String, char[]> readDualEntry(String purpose) 
       throws Exception {
    DualControlManager manager = new DualControlManager(
       System.getProperties(), 2, purpose);
    manager.setVerifyPassphrase(false);
    manager.init(new MockableConsoleAdapter(System.console()));
    manager.call();
    return manager.getDualMap().entrySet().iterator().next();
}   

where we read the first entry in dualMap. Actually this is the only entry in the map since we have required only two submissions.

Note that we are accepting custodians' existing passwords to load the key, and so we don't verify their complexity as we would for DualControlGenSecKey.

Remote keystore

We might wish to store our keystore centrally, or internally on a more secure server, and load it via SSL as follows.

public class DualControlKeyStores {

   public static KeyStore loadKeyStore(String keyStoreLocation,
           String keyStoreType,
           char[] keyStorePassword) throws Exception {
      KeyStore keyStore = KeyStore.getInstance(keyStoreType);
      if (keyStoreLocation.contains(":")) {
         String[] array = keyStoreLocation.split(":");
         String keyStoreHost = array[0];
         int keyStorePort = Integer.parseInt(array[1]);
         SSLContext sslContext = SSLContexts.create(false, 
                 "fileclient.ssl", System.getProperties(), 
                 new MockableConsoleAdapter(System.console()));
         Socket socket = sslContext.getSocketFactory().createSocket(
                 keyStoreHost, keyStorePort);
         keyStore.load(socket.getInputStream(), keyStorePassword);
         socket.close();
      } else if (new File(keyStoreLocation).exists()) {
         FileInputStream fis = new FileInputStream(keyStoreLocation);
         keyStore.load(fis, keyStorePassword);
         fis.close();
      } else {
         keyStore.load(null, null);
      }
      return keyStore;
   }

where if the keystore location is formatted as host:port, then we open an SSLSocket from which to read a remote keystore file.

Note that we use the properties fileclient.ssl.keyStore et al for SSLContexts to configure this client SSL connection.

The following utility demonstrates a trivial server for a remote keystore.

public class FileServer {
   private static Logger logger = Logger.getLogger(FileServer.class);
   private InetAddress localAddress;
   private int port;
   private int backlog;
   private Set<String> allowedHosts = new TreeSet();
   private String fileName;
   ...
   public void call() throws Exception {
      SSLContext sslContext = SSLContexts.create(true, "fileserver.ssl", 
         System.getProperties(), new MockableConsoleAdapter(
         System.console()));
      SSLServerSocket serverSocket = (SSLServerSocket) 
         sslContext.getServerSocketFactory().
         createServerSocket(port, backlog, localAddress);
      serverSocket.setNeedClientAuth(true);
      FileInputStream stream = new FileInputStream(fileName);
      int length = (int) new File(fileName).length();
      byte[] bytes = new byte[length];
      stream.read(bytes);
      while (true) {
         Socket socket = serverSocket.accept();
         logger.info("hostAddress " + socket.getInetAddress().
               getHostAddress());
         if (allowedHosts.contains(socket.getInetAddress().
               getHostAddress())) {
            socket.getOutputStream().write(bytes);
         }
         socket.close();
      }        
   }    
}

where we create an SSLServerSocket requiring client authentication, and write out the keystore file to client connections from allowed remote hosts.

DualControlManager

Finally, we present DualControlManager which accepts the dual password submissions.

public class DualControlManager {
   private Properties properties;
   private String purpose;
   private int submissionCount;
   private SSLContext sslContext;
   private Map<String, char[]> submissions = new TreeMap();
    
   public DualControlManager(Properties properties, int submissionCount, 
         String purpose) {
      this.properties = properties;
      this.submissionCount = submissionCount;
      this.purpose = purpose;
   }

   public void init(SSLContext sslContent) {
      this.sslContext = sslContent;
   }
   ...
}

where we specify the required number of password submissions for some purpose e.g. the generate a key or load a key, and provide an SSLContext for the SSLServerSocket.

Our app calls DualControlManager to accept submissions via an SSLServerSocket as follows.

private final static int PORT = 4444;
private final static String HOST = "127.0.0.1";
private final static String REMOTE_ADDRESS = "127.0.0.1";
...
public void call() throws Exception {
   logger.info("purpose: " + purpose);
   SSLServerSocket serverSocket = (SSLServerSocket) sslContext.
            getServerSocketFactory().createServerSocket(
               PORT, submissionCount, 
               InetAddress.getByName(HOST));
   try {
      serverSocket.setNeedClientAuth(true);
      accept(serverSocket);
   } finally {
      serverSocket.close();
   }
   buildDualMap();
}

where we create a client-authenticated SSL server socket.

Note that we have hard-wired the SSLServerSocket to localhost. Therefore DualControlConsole must be invoked either in a local ssh session, or a remote session using ssh port forwarding.

private void accept(SSLServerSocket serverSocket) throws Exception {
   logger.info("accept: " + submissionCount);        
   while (submissions.size() < submissionCount) {
      SSLSocket socket = (SSLSocket) serverSocket.accept();
      try {
         if (!socket.getInetAddress().getHostAddress().equals(
               REMOTE_ADDRESS)) {
            throw new Exception("Invalid remote address: "
                    + socket.getInetAddress().getHostAddress());
         }
         read(socket);
      } finally {
          socket.close();
      }
   }
}

where we accumulate the required number of submissions in a loop, handing each socket connection as follows.

private void read(SSLSocket socket) throws Exception {
   DataOutputStream dos = new DataOutputStream(socket.getOutputStream());
   String name = getCN(socket.getSession().getPeerPrincipal());
   if (submissions.keySet().contains(name)) {
      String errorMessage = "Duplicate submission from " + name;
      dos.writeUTF(errorMessage);
      dos.writeUTF("");
      throw new Exception(errorMessage);
   }
   dos.writeUTF("Connected " + name);        
   dos.writeUTF(purpose);
   DataInputStream dis = new DataInputStream(socket.getInputStream());
   char[] passphrase = readChars(dis);
   try {
      String resultMessage = verify(name, passphrase);
      dos.writeUTF(resultMessage);
      logger.info(resultMessage);
   } catch (Exception e) {
      dos.writeUTF(e.getMessage());
      logger.warn(e.getMessage());
      throw e;
   }
}

where the custodian's username is determined from their SSL certificate, in particular the CN field.

DualControlGenSecKey will require DualControlManager to verify the length and complexity of new passphrases, but otherwise we'll set verifyPassphrase to false e.g. for existing passphrases as required to load the key into our app.

private String verify(String name, char[] passphrase) throws Exception {
   if (passphrase.length == 0) {
      return "Empty submission from " + name;
   }
   String responseMessage = "Received " + name;
   if (verifyPassphrase && !verifiedNames.contains(name)) {
      String invalidMessage = new DualControlPassphraseVerifier(
              properties).getInvalidMessage(passphrase);
      if (invalidMessage != null) {
         throw new Exception(responseMessage + ": " + invalidMessage);
      }
   }
   submissions.put(name, passphrase);
   return responseMessage;
}

where an empty passhrase is sent by DualControlConsole to abort if re-entered passphrase doesn't match, and we'll let the custodian retry in that case.

Finally, we compose a map of dual aliases and dual passwords as follows.

private void buildDualMap() {
   for (String name : submissions.keySet()) {
      for (String otherName : submissions.keySet()) {
         if (name.compareTo(otherName) < 0) {
            String dualAlias = String.format("%s-%s", name, otherName);
            char[] dualPassword = combineDualPassword(
                    submissions.get(name), submissions.get(otherName));
            dualMap.put(dualAlias, dualPassword);
            logger.info("dualAlias: " + dualAlias);
         }
      }
   }
   for (char[] password : submissions.values()) {
      Arrays.fill(password, (char) 0);
   }
}

where the compareTo() in the nested loop above ensures that we exclude the alphabetically-challenged evanx-brent in favour of brent-evanx, and the likes of evanx-evanx, which is just silly.

Incidently, we combine the dual passwords by simply concatenating them.

public static char[] combineDualPassword(char[] password, char[] other) {
   char[] dualPassword = new char[password.length + other.length + 1];
   int index = 0;
   for (char ch : password) {
      dualPassword[index++] = ch;
   }
   dualPassword[index++] = '|';
   for (char ch : other) {
      dualPassword[index++] = ch;
   }
   return dualPassword;
}

where we arbitrarily decided to delimit the two personal passwords with the vertical bar character.

Unit test

In DualControlTest, we test DualControlManager and DualControlConsole in concert, using threads, mock consoles and what-not. This will be presented in a follow-on article, as this article is already too long.

Crypto server

We might wish to create a central crypto server which is dual-controlled, rather than burden our app. In this case, we can restart our application without dual control, and indeed have any number of apps using this server. This simplifies key management, and enables us to isolate our keys to improve security.

We'll implement the crypto server in a subsequent article. Then we can say, "Dual control? We have an app for that." ;)

Conclusion

The problem with encryption is secure key management. We shouldn't leave the key under the mat.

PCI requires that "split knowledge and dual control" be used to protect our data-encryption key so that no single person can extract the data in clear-text, not even our most trustworthy employee today, rogue tomorrow. Or victim of blackmail, or government coercion ;)

We introduce our DualControlGenSecKey utility for generating a new secret key. We protect this key using password-based encryption, courtesy of JceKeyStore. We enforce split knowledge of the key password, so that dual control is required to load the key. Each dual password is effectively a key-encryption key split between two custodians, and so known to no single person.

We propose keeping at least three copies of the same key, but where each copy is password-protected by a different duo of custodians. Then when any one custodian is on vacation or otherwise indisposed, the other two custodians can restart our app.

We note that we require two shared passwords (server SSL keystore/truststore and secret keystore), and as well two private passwords per custodian (client SSL keystore/truststore and secret key dual password), and thereby hopefully keep our secret key, erm, secret.

Furthermore

In "Dual Control Mock Console" we will present a unit test orchestrating DualControlManager and DualControlConsole threads - preview DualControlTest.java

In "Dual Control Enroll" we will present tools to enroll and revoke custodians - preview DualControlEnroll.java and DualControlRevoke.java

In "Dual Control Crypto Server" we will implement a dual-controlled crypto server to unburden our apps, simplify key management and enhance security - preview CryptoServer.java and its CryptoHandler.java

In "Dual Control Key Protection" we will address increased protection against brute-force password attacks e.g. via PBE of the keystore using PBKDF2 with a high number of iterations, so that the key takes a second or two to recover, rather than half a millisecond - preview KeyStoreBruteForceTimer.java, RecryptedKeyStore.java and AesPbeStore.java

In "Dual Control Key Rotation" we will address periodic key revision e.g. migrating from an older "dek2013" key to a new "dek2014" key.

You can browse the code for this exercise at github.com/evanx/vellum in src/dualcontrol and test/dualcontrol.

At some stage, the code was copied to github.com/evanx/dualcontrol. It has a dependency on github.com/evanx/vellumcore.

Further reading

This is part of The Final Quadrilogy on Java crypto.

In Client Authentication, we introduce a trust manager to automate certificate enrollment, where new clients' certificates are automatically imported into an SQL truststore when they connect for the first time.

In Explicit Trust Manager, we present a custom X509TrustManager that only trusts client certificates that are explicitly imported into our truststore, for a secure "private network" of clients connecting to a "private" Java server.

In Local CA, we discuss how one could setup a local private CA facility to issue client certificates.

Also see the previous series, The Enigma Posts.

Also relating to Java crypto, see my blog articles: Password Salt for secure passwords; and leveraging the Google Authenticator mobile app for multi-factor authentication for your own sites.

See more articles on my wiki.

⚠️ **GitHub.com Fallback** ⚠️