/*
* Part of the CCNx Java Library.
*
* Copyright (C) 2008, 2009, 2010, 2011 Palo Alto Research Center, Inc.
*
* This library is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License version 2.1
* as published by the Free Software Foundation.
* This library 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
* Lesser General Public License for more details. You should have received
* a copy of the GNU Lesser General Public License along with this library;
* if not, write to the Free Software Foundation, Inc., 51 Franklin Street,
* Fifth Floor, Boston, MA 02110-1301 USA.
*/
package org.ccnx.ccn.impl.security.keys;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.logging.Level;
import org.ccnx.ccn.CCNHandle;
import org.ccnx.ccn.TrustManager;
import org.ccnx.ccn.config.UserConfiguration;
import org.ccnx.ccn.impl.support.Log;
import static org.ccnx.ccn.impl.support.Log.FAC_KEYS;
import org.ccnx.ccn.io.ErrorStateException;
import org.ccnx.ccn.io.content.ContentGoneException;
import org.ccnx.ccn.io.content.ContentNotReadyException;
import org.ccnx.ccn.io.content.PublicKeyObject;
import org.ccnx.ccn.profiles.security.KeyProfile;
import org.ccnx.ccn.protocol.CCNTime;
import org.ccnx.ccn.protocol.ContentName;
import org.ccnx.ccn.protocol.ContentObject;
import org.ccnx.ccn.protocol.Exclude;
import org.ccnx.ccn.protocol.Interest;
import org.ccnx.ccn.protocol.KeyLocator;
import org.ccnx.ccn.protocol.PublisherID;
import org.ccnx.ccn.protocol.PublisherPublicKeyDigest;
import org.ccnx.ccn.protocol.SignedInfo.ContentType;
/**
* This class performs the following:
* - manages published public keys.
* - answers CCN interestes for these public keys.
* - allow the caller to look for public keys: retrieve the keys from cache or CCN
*
* This class is used by the initial KeyManager bootstrapping code. As such,
* it has to be very careful not to introduce a circular dependency -- to rely
* on parts of the network stack that need that boostrapping to be complete in
* order to work. At the same time, we'd like to not have to reimplement segmentation,
* etc, in order to cache keys; we'd like to be able to use those parts of
* the library. So we allow the KeyRepository to have a CCNHandle, we can use
* all of the library functionality to write keys once that handle is sufficiently
* initialized.
*/
public class PublicKeyCache {
// Stop logging to key cache by default.
protected static final boolean _DEBUG = false;
// Reference count in case we are shared.
protected int _refCount = 0;
protected HashMap<ContentName, PublicKeyObject> _keyMap = new HashMap<ContentName, PublicKeyObject>();
protected HashMap<PublisherPublicKeyDigest, ArrayList<ContentName>> _idMap = new HashMap<PublisherPublicKeyDigest, ArrayList<ContentName>>();
protected HashMap<PublisherPublicKeyDigest, PublicKey> _rawKeyMap = new HashMap<PublisherPublicKeyDigest, PublicKey>();
protected HashMap<PublisherPublicKeyDigest, ArrayList<Certificate>> _rawCertificateMap = new HashMap<PublisherPublicKeyDigest, ArrayList<Certificate>>();
protected HashMap<PublisherPublicKeyDigest, CCNTime> _rawVersionMap = new HashMap<PublisherPublicKeyDigest, CCNTime>();
public PublicKeyCache() {
}
/**
* Remember a public key and the corresponding key object.
* @param theKey public key to remember
* @param keyObject key Object to remember
* @throws ContentGoneException
* @throws ContentNotReadyException
* @throws ErrorStateException
*/
public void remember(PublicKeyObject theKey) throws ContentNotReadyException, ContentGoneException, ErrorStateException, IOException {
_keyMap.put(theKey.getVersionedName(), theKey);
PublisherPublicKeyDigest id = theKey.publicKeyDigest();
rememberContentName(id, theKey.getVersionedName());
_rawKeyMap.put(id, theKey.publicKey());
_rawVersionMap.put(id, theKey.getVersion());
if (_DEBUG) {
recordKeyToFile(theKey);
}
}
protected void rememberContentName(PublisherPublicKeyDigest id, ContentName name) {
synchronized(_idMap) {
ArrayList<ContentName> nameList = _idMap.get(id);
if (null == nameList) {
nameList = new ArrayList<ContentName>();
_idMap.put(id, nameList);
}
nameList.add(name);
}
}
/**
* Remember a public key
* @param theKey public key to remember
*/
public void remember(PublicKey theKey, CCNTime version) {
PublisherPublicKeyDigest keyDigest = new PublisherPublicKeyDigest(theKey);
_rawKeyMap.put(keyDigest, theKey);
if (null != version) {
_rawVersionMap.put(keyDigest, version);
}
}
/**
* Remember a certificate.
* @param theCertificate the certificate to remember
*/
public void remember(Certificate theCertificate, CCNTime version) {
PublisherPublicKeyDigest keyDigest = new PublisherPublicKeyDigest(theCertificate.getPublicKey());
rememberCertificate(keyDigest, theCertificate);
_rawKeyMap.put(keyDigest, theCertificate.getPublicKey());
if (null != version) {
_rawVersionMap.put(keyDigest, version);
}
}
protected void rememberCertificate(PublisherPublicKeyDigest id, Certificate certificate) {
synchronized(_rawCertificateMap) {
ArrayList<Certificate> certificateList = _rawCertificateMap.get(id);
if (null == certificateList) {
certificateList = new ArrayList<Certificate>();
_rawCertificateMap.put(id, certificateList);
}
certificateList.add(certificate);
}
}
/**
* Write encoded key to file for debugging purposes.
* @throws ContentGoneException
* @throws ContentNotReadyException
* @throws ErrorStateException
*/
protected void recordKeyToFile(PublicKeyObject keyObject) throws ContentNotReadyException, ContentGoneException, ErrorStateException {
File keyDir = new File(UserConfiguration.keyRepositoryDirectory());
if (!keyDir.exists()) {
if (!keyDir.mkdirs()) {
Log.warning(FAC_KEYS, "recordKeyToFile: Cannot create user CCN key repository directory: {0}", keyDir.getAbsolutePath());
return;
}
}
PublisherPublicKeyDigest id = keyObject.publicKeyDigest();
File keyFile = new File(keyDir, KeyProfile.keyIDToNameComponentAsString(keyObject.publicKeyDigest()));
if (keyFile.exists()) {
Log.info(FAC_KEYS, "Already stored key {0} to file.", id);
// return; // temporarily store it anyway, to overwrite old-format data.
}
try {
FileOutputStream fos = new FileOutputStream(keyFile);
try {
fos.write(keyObject.publicKey().getEncoded());
} finally {
fos.close();
}
} catch (Exception e) {
Log.info(FAC_KEYS, "recordKeyToFile: cannot record key: {0} to file {1} error: {2}: {3}", id, keyFile.getAbsolutePath(), e.getClass().getName(), e.getMessage());
return;
}
Log.info(FAC_KEYS, "Logged key {0} to file: {1}", id, keyFile.getAbsolutePath());
}
/**
* Retrieve the public key from CCN given a key digest and a key locator
* the function blocks and waits for the public key until a certain timeout.
* As a side effect, caches network storage information for this key, which can
* be obtained using retrieve();.
* @param desiredKeyID the digest of the desired public key.
* @param locator locator for the key
* @param timeout timeout value
* @throws IOException
*/
public PublicKey getPublicKey(
PublisherPublicKeyDigest desiredKeyID, KeyLocator locator,
long timeout, CCNHandle handle) throws IOException {
// Look for it in our cache first.
PublicKey publicKey = getPublicKeyFromCache(desiredKeyID);
if (null != publicKey) {
return publicKey;
}
if (null == locator) {
Log.warning(FAC_KEYS, "Cannot retrieve key -- no key locator for key {0}", desiredKeyID);
throw new IOException("Cannot retrieve key -- no key locator for key " + desiredKeyID + ".");
}
if (locator.type() != KeyLocator.KeyLocatorType.NAME) {
Log.info(FAC_KEYS, "Repository looking up a key that is contained in the locator...");
if (locator.type() == KeyLocator.KeyLocatorType.KEY) {
PublicKey key = locator.key();
remember(key, null);
return key;
} else if (locator.type() == KeyLocator.KeyLocatorType.CERTIFICATE) {
Certificate certificate = locator.certificate();
PublicKey key = certificate.getPublicKey();
remember(certificate, null);
return key;
}
} else {
PublicKeyObject publicKeyObject = getPublicKeyObject(desiredKeyID, locator, timeout, handle);
if (null == publicKeyObject) {
Log.info(FAC_KEYS, "Could not retrieve key {0} from network with locator {1}!", desiredKeyID, locator);
} else {
Log.info(FAC_KEYS, "Retrieved key {0} from network with locator {1}!", desiredKeyID, locator);
return publicKeyObject.publicKey();
}
}
return null;
}
public PublicKeyObject getPublicKeyObject(
PublisherPublicKeyDigest desiredKeyID, KeyLocator locator,
long timeout, CCNHandle handle) throws IOException {
// take code from #BasicKeyManager.getKey, to validate more complex publisher constraints
PublicKeyObject theKey = retrieve(locator.name().name(), locator.name().publisher());
if ((null != theKey) && (theKey.available())) {
Log.info(FAC_KEYS, "retrieved key {0} from cache.", locator.name().name());
return theKey;
}
// How many pieces of bad content do we wade through?
final int ITERATION_LIMIT = 5;
// how many times do we time out get? Try 2 just in case we drop one.
final int TIMEOUT_ITERATION_LIMIT = 2;
PublicKey publicKey = null;
Interest keyInterest = new Interest(locator.name().name(), locator.name().publisher());
// we could have from 1 (content digest only) to 3 (version, segment, content digest)
// additional name components.
keyInterest.minSuffixComponents(1);
keyInterest.maxSuffixComponents(3);
ContentObject retrievedContent = null;
int iterationCount = 0;
int timeoutCount = 0; // be super-aggressive about pulling keys for now.
IOException lastException = null;
while ((null == publicKey) && (iterationCount < ITERATION_LIMIT)) {
// it would be really good to know how many additional name components to expect...
while ((null == retrievedContent) && (timeoutCount < TIMEOUT_ITERATION_LIMIT)) {
try {
Log.fine(FAC_KEYS, "Trying network retrieval of key: {0} ", keyInterest.name());
// use more aggressive high-level get
retrievedContent = handle.get(keyInterest, timeout);
} catch (IOException e) {
Log.warning(FAC_KEYS, "IOException attempting to retrieve key: {0}: {1}", keyInterest.name(), e.getMessage());
Log.warningStackTrace(e);
lastException = e;
// go around again
}
if (null != retrievedContent) {
if( Log.isLoggable(FAC_KEYS, Level.INFO)) {
Log.info(FAC_KEYS, "Retrieved key {0} using locator {1}.", desiredKeyID, locator);
Log.info(FAC_KEYS, "Retrieved key {0} using locator {1} - got {2}", desiredKeyID, locator, retrievedContent.name());
}
break;
}
timeoutCount++;
}
if (null == retrievedContent) {
Log.info(FAC_KEYS, "No data returned when we attempted to retrieve key using interest {0}, timeout {1} exception : {2}", keyInterest, timeout, ((null == lastException) ? "none" : lastException.getMessage()));
if (null != lastException) {
throw lastException;
}
break;
}
if ((retrievedContent.signedInfo().getType().equals(ContentType.KEY)) ||
(retrievedContent.signedInfo().getType().equals(ContentType.LINK))) {
theKey = new PublicKeyObject(retrievedContent, handle);
if ((null != theKey) && (theKey.available())) {
if ((null != desiredKeyID) && (!theKey.publicKeyDigest().equals(desiredKeyID))) {
Log.fine(FAC_KEYS, "Got key at expected name {0} from locator {1}, but it wasn't the right key, wanted {2}, got {3}",
retrievedContent.name(),
locator,
desiredKeyID, theKey.publicKeyDigest());
} else {
// either we don't have a preferred key ID, or we matched
Log.info(FAC_KEYS, "Retrieved public key using name: {0}", locator.name().name());
// TODO make a key object instead of just retrieving
// content, use it to decode
remember(theKey);
return theKey;
}
} else {
Log.severe(FAC_KEYS, "Decoded key at name {0} without error, but result was null!", retrievedContent.name());
throw new IOException("Decoded key at name " + retrievedContent.name() + " without error, but result was null!");
}
} else {
Log.info(FAC_KEYS, "Retrieved an object when looking for key {0} at {1}, but type is {2}", locator.name().name(), retrievedContent.name(), retrievedContent.signedInfo().getTypeName());
}
// TODO -- not sure this is exactly right, but a start...
// It isn't right - this would only work for locators with no version or segment. And since I think most keys are
// PublicKeyObjects which would have segments and probably versions it would almost always be wrong...
// But is a little unclear what problem this code is concerned about anyway...
Exclude currentExclude = keyInterest.exclude();
if (null == currentExclude) {
currentExclude = new Exclude();
}
currentExclude.add(new byte [][]{retrievedContent.digest()});
keyInterest.exclude(currentExclude);
iterationCount++;
}
return null;
}
/**
* Retrieve the public key from cache given a key digest
* @param desiredKeyID the digest of the desired public key.
*/
public PublicKey getPublicKeyFromCache(PublisherPublicKeyDigest desiredKeyID) {
PublicKey theKey = _rawKeyMap.get(desiredKeyID);
if (null == theKey) {
if (_rawCertificateMap.containsKey(desiredKeyID)) {
Certificate theCertificate = _rawCertificateMap.get(desiredKeyID).get(0);
if (null != theCertificate) {
theKey = theCertificate.getPublicKey();
}
}
}
return theKey;
}
public CCNTime getPublicKeyVersionFromCache(PublisherPublicKeyDigest desiredKeyID) {
return _rawVersionMap.get(desiredKeyID);
}
/**
* Retrieve key object from cache given key name
* @param keyName key digest
*/
public PublicKeyObject retrieve(PublisherPublicKeyDigest keyID) {
if (!_idMap.containsKey(keyID)) {
return null;
}
ContentName name = _idMap.get(keyID).get(0);
if (null != name) {
return _keyMap.get(name);
}
return null;
}
/**
* Retrieve key object from cache given content name and publisher id
* check if the retrieved content has the expected publisher id
* @param name contentname of the key
* @param publisherID publisher id
* @throws IOException
*/
public PublicKeyObject retrieve(ContentName name, PublisherID publisherID) throws IOException {
PublicKeyObject result = _keyMap.get(name);
if (null != result) {
if (null != publisherID) {
if (TrustManager.getTrustManager().matchesRole(
publisherID,
result.getContentPublisher())) {
return result;
}
}
}
return null;
}
public ArrayList<Certificate> retrieveCertificates(PublisherPublicKeyDigest keyID) {
return _rawCertificateMap.get(keyID);
}
}