/*
* A CCNx library test.
*
* Copyright (C) 2008-2012 Palo Alto Research Center, Inc.
*
* This work is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License version 2 as published by the
* Free Software Foundation.
* This work 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
* for more details. You should have received a copy of the GNU General Public
* License along with this program; if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
package org.ccnx.ccn.test.profiles.security.access.group;
import java.io.IOException;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Random;
import java.util.TreeSet;
import javax.crypto.KeyGenerator;
import org.ccnx.ccn.CCNHandle;
import org.ccnx.ccn.KeyManager;
import org.ccnx.ccn.impl.security.crypto.CCNDigestHelper;
import org.ccnx.ccn.impl.support.ByteArrayCompare;
import org.ccnx.ccn.impl.support.Log;
import org.ccnx.ccn.io.content.Link;
import org.ccnx.ccn.io.content.WrappedKey;
import org.ccnx.ccn.io.content.WrappedKey.WrappedKeyObject;
import org.ccnx.ccn.profiles.VersioningProfile;
import org.ccnx.ccn.profiles.security.access.group.Group;
import org.ccnx.ccn.profiles.security.access.group.GroupAccessControlManager;
import org.ccnx.ccn.profiles.security.access.group.GroupAccessControlProfile;
import org.ccnx.ccn.profiles.security.access.group.PrincipalKeyDirectory;
import org.ccnx.ccn.protocol.ContentName;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
public class KeyDirectoryTestRepo {
static Random rand = new Random();
static final String directoryBase = "/test";
static final String keyDirectoryBase = "/test/KeyDirectoryTestRepo-";
static ContentName keyDirectoryName;
static ContentName userStore;
static String principalName = "pgolle-";
static ContentName publicKeyName;
static ContentName versionedDirectoryName;
static PrivateKey wrappedPrivateKey;
static Key AESSecretKey;
static KeyPair wrappingKeyPair;
static byte[] wrappingPKID;
static GroupAccessControlManager acm;
class TestPrincipalKeyDirectory extends PrincipalKeyDirectory {
public TestPrincipalKeyDirectory() throws IOException {
super(acm, versionedDirectoryName, handle);
}
/*
* Retrieve the wrapped key from the KD by principalName.
* As above, the key is unwrapped and we check that the result is as expected.
*
*/
public void testGetWrappedKeyForPrincipal() throws Exception {
// unwrap the key and check that the unwrapped secret key is correct
WrappedKeyObject wko = kd.getWrappedKeyForPrincipal(principalName);
Assert.assertNotNull(wko);
WrappedKey wk = wko.wrappedKey();
Key unwrappedSecretKey = wk.unwrapKey(wrappingKeyPair.getPrivate());
Assert.assertEquals(AESSecretKey, unwrappedSecretKey);
}
}
static TestPrincipalKeyDirectory kd;
static int testCount = 0;
static CCNHandle handle;
@AfterClass
public static void tearDownAfterClass() throws Exception {
kd.stopEnumerating();
KeyManager.closeDefaultKeyManager();
}
@BeforeClass
public static void setUpBeforeClass() throws Exception {
// randomize names to minimize stateful effects of ccnd/repo caches.
keyDirectoryName = ContentName.fromNative(keyDirectoryBase + Integer.toString(rand.nextInt(10000)));
principalName = principalName + Integer.toString(rand.nextInt(10000));
handle = CCNHandle.getHandle();
ContentName cnDirectoryBase = ContentName.fromNative(directoryBase);
ContentName groupStore = GroupAccessControlProfile.groupNamespaceName(cnDirectoryBase);
userStore = new ContentName(cnDirectoryBase, "Users");
acm = new GroupAccessControlManager(cnDirectoryBase, groupStore, userStore);
versionedDirectoryName = VersioningProfile.addVersion(keyDirectoryName);
}
/**
* Ensures that the tests run in the correct order.
* @throws Exception
*/
@Test
public void testInOrder() throws Exception {
Log.info(Log.FAC_TEST, "Starting testInOrder");
testKeyDirectoryCreation();
testAddPrivateKey();
testGetUnwrappedKeyGroupMember();
testAddWrappedKey();
addWrappingKeyToACM();
testGetWrappedKeyForKeyID();
kd.testGetWrappedKeyForPrincipal();
testGetUnwrappedKey();
testGetPrivateKey();
testGetUnwrappedKeySuperseded();
testAddPreviousKeyBlock();
Log.info(Log.FAC_TEST, "Completed testInOrder");
}
/*
* Create a new versioned KeyDirectory
*/
public void testKeyDirectoryCreation() throws Exception {
kd = new TestPrincipalKeyDirectory();
// verify that the keyDirectory is created
Assert.assertNotNull(kd);
}
public void testAddPrivateKey() throws Exception {
// generate a private key to wrap
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(1024);
wrappingKeyPair = kpg.generateKeyPair();
wrappedPrivateKey = wrappingKeyPair.getPrivate();
// generate a AES wrapping key
KeyGenerator kg = KeyGenerator.getInstance("AES");
AESSecretKey = kg.generateKey();
// add private key block
kd.addPrivateKeyBlock(wrappedPrivateKey, AESSecretKey);
kd.waitForChildren(); // this was the first add; need to wait till we have any data, hopefully NE responder will fast path
Assert.assertTrue(kd.hasPrivateKeyBlock());
}
/*
* Unwrap the private key via membership in a group
*/
public void testGetUnwrappedKeyGroupMember() throws Exception {
ContentName myIdentity = new ContentName(userStore, "pgolle");
acm.publishMyIdentity(myIdentity, null);
// add myself to a newly created group
String randomGroupName = "testGroup" + rand.nextInt(10000);
ArrayList<Link> newMembers = new ArrayList<Link>();
newMembers.add(new Link(myIdentity));
Group myGroup = acm.groupManager().createGroup(randomGroupName, newMembers, 0);
Assert.assertTrue(acm.groupManager().haveKnownGroupMemberships());
Thread.sleep(5000); // FIXME: this delay is necessary for the repo-write to complete
// it should not be needed, as the data should be available locally.
PrincipalKeyDirectory pkd = myGroup.privateKeyDirectory(acm);
pkd.waitForChildren();
Assert.assertTrue(pkd.hasPrivateKeyBlock());
// add to the KeyDirectory the secret key wrapped in the public key
ContentName versionDirectoryName2 = VersioningProfile.addVersion(
ContentName.fromNative(keyDirectoryBase + Integer.toString(rand.nextInt(10000)) ));
PrincipalKeyDirectory kd2 = new PrincipalKeyDirectory(acm, versionDirectoryName2, handle);
PublicKey groupPublicKey = myGroup.publicKey();
ContentName groupPublicKeyName = myGroup.publicKeyName();
kd2.addWrappedKeyBlock(AESSecretKey, groupPublicKeyName, groupPublicKey);
// retrieve the secret key
byte[] expectedKeyID = CCNDigestHelper.digest(AESSecretKey.getEncoded());
kd2.waitForChildren();
Thread.sleep(10000);
Key unwrappedSecretKey = kd2.getUnwrappedKey(expectedKeyID);
Assert.assertEquals(AESSecretKey, unwrappedSecretKey);
kd2.stopEnumerating();
}
/*
* Wraps the AES key in an RSA wrapping key
* and adds the wrapped key to the KeyDirectory
*/
public void testAddWrappedKey() throws Exception {
// generate a public key to wrap
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(1024);
wrappingKeyPair = kpg.generateKeyPair();
PublicKey publicKey = wrappingKeyPair.getPublic();
wrappingPKID = CCNDigestHelper.digest(publicKey.getEncoded());
publicKeyName = new ContentName(userStore, principalName);
ContentName versionPublicKeyName = VersioningProfile.addVersion(publicKeyName);
// add to the KeyDirectory the secret key wrapped in the public key
kd.addWrappedKeyBlock(AESSecretKey, versionPublicKeyName, publicKey);
}
/*
* Adds the wrapping key to the AccessControlManager.
*/
public void addWrappingKeyToACM() throws Exception {
PrivateKey privKey = wrappingKeyPair.getPrivate();
byte[] publicKeyIdentifier = CCNDigestHelper.digest(wrappingKeyPair.getPublic().getEncoded());
handle.keyManager().getSecureKeyCache().addMySigningKey(publicKeyIdentifier, privKey);
}
/*
* Create a new (unversioned) KeyDirectory object with the same
* directory name as above. Check that the constructor retrieves the latest
* version of the KeyDirectory.
* Retrieve the wrapped key from the KeyDirectory by its KeyID,
* unwrap it, and check that the secret key retrieved is as expected.
*
*/
public void testGetWrappedKeyForKeyID() throws Exception {
CCNHandle handle = CCNHandle.open();
// Use unversioned constructor so KeyDirectory returns the latest version
PrincipalKeyDirectory uvkd = new PrincipalKeyDirectory(acm, keyDirectoryName, handle);
while (!uvkd.hasChildren() || uvkd.getCopyOfWrappingKeyIDs().size() == 0) {
uvkd.waitForNewChildren();
}
// check the ID of the wrapping key
TreeSet<byte[]> wkid = uvkd.getCopyOfWrappingKeyIDs();
Assert.assertEquals(1, wkid.size());
Comparator<byte[]> byteArrayComparator = new ByteArrayCompare();
Assert.assertEquals(0, byteArrayComparator.compare(wkid.first(), wrappingPKID));
// check name
ContentName wkName = uvkd.getWrappedKeyNameForKeyID(wrappingPKID);
Assert.assertNotNull(wkName);
// unwrap the key and check that the unwrapped secret key is correct
WrappedKeyObject wko = uvkd.getWrappedKeyForKeyID(wrappingPKID);
WrappedKey wk = wko.wrappedKey();
Key unwrappedSecretKey = wk.unwrapKey(wrappingKeyPair.getPrivate());
Assert.assertEquals(AESSecretKey, unwrappedSecretKey);
uvkd.stopEnumerating();
}
/*
* Retrieve the wrapped key directly from the KD
* and check that the result is as expected.
*
*/
public void testGetUnwrappedKey() throws Exception {
byte[] expectedKeyID = CCNDigestHelper.digest(AESSecretKey.getEncoded());
Key unwrappedSecretKey = kd.getUnwrappedKey(expectedKeyID);
Assert.assertEquals(AESSecretKey, unwrappedSecretKey);
}
/*
* Retrieve the private key from KD and check the result
*/
public void testGetPrivateKey() throws Exception {
Assert.assertTrue(kd.hasPrivateKeyBlock());
Key privKey = kd.getPrivateKey();
Assert.assertEquals(wrappedPrivateKey, privKey);
}
/*
* Create an "old" key directory, which is superseded by the kd created above
* Check that the unwrappedKey for the old superseded KD can be obtained.
*/
public void testGetUnwrappedKeySuperseded() throws Exception {
// create a superseded key directory
ContentName supersededKeyDirectoryName = ContentName.fromNative(keyDirectoryBase + rand.nextInt(10000) + "/superseded");
ContentName versionSupersededKeyDirectoryName = VersioningProfile.addVersion(supersededKeyDirectoryName);
CCNHandle handle = CCNHandle.open();
PrincipalKeyDirectory skd = new PrincipalKeyDirectory(acm, versionSupersededKeyDirectoryName, handle);
// generate a AES wrapping key
KeyGenerator kg = KeyGenerator.getInstance("AES");
Key supersededAESSecretKey = kg.generateKey();
byte[] expectedKeyID = CCNDigestHelper.digest(supersededAESSecretKey.getEncoded());
// add a superseded block
ContentName supersedingKeyName = keyDirectoryName;
skd.addSupersededByBlock(supersededAESSecretKey, supersedingKeyName, null, AESSecretKey);
while (!skd.hasChildren() || !skd.hasSupersededBlock())
skd.waitForNewChildren();
Assert.assertTrue(skd.hasSupersededBlock());
Assert.assertNotNull(skd.getSupersededBlockName());
// get unwrapped key for superseded KD
Key unwrappedSecretKey = skd.getUnwrappedKey(expectedKeyID);
Assert.assertEquals(supersededAESSecretKey, unwrappedSecretKey);
skd.stopEnumerating();
}
/*
* Add a previous key block
*/
public void testAddPreviousKeyBlock() throws Exception {
Assert.assertTrue(! kd.hasPreviousKeyBlock());
// generate a new AES wrapping key
KeyGenerator kg = KeyGenerator.getInstance("AES");
Key newAESSecretKey = kg.generateKey();
ContentName supersedingKeyName = ContentName.fromNative(keyDirectoryBase + "previous");
kd.addPreviousKeyBlock(AESSecretKey, supersedingKeyName, newAESSecretKey);
kd.waitForNewChildren();
Assert.assertTrue(kd.hasPreviousKeyBlock());
}
}