Security Library Tutorial

Key Management

Identity, Key and Certificates

All keys, certificates and their corresponding identities are managed by KeyChain.

Before signing a packet, you need to assure that the signing key and its corresponding identity certificate exist in the KeyChain. The private part of the signing key is used to generate signature, while the identity certificate is used to constructed the KeyLocator.

In KeyChain, keys and certificates are managed in terms of identities which are expressed by namespaces (e.g., /ndn/edu/ucla/irl/yingdi, or /ndn/edu/ucla/boelter\_hall/room\_4805). Each pair of keys belongs to only one identity, and it is named by the identity name appended with a key ID (e.g., /ndn/edu/ucla/irl/yingdi/ksk-1234567890, or /ndn/edu/ucla/boelter\_hall/room\_4805/ksk-1357924680). However, one identity may have more than one pair of keys, but only one of them is the default key of the identity. A key pair without any identity certificates is not quite useful. A key pair may have more than one identity certificates, but only one of them is the default certificate. Therefore, for a given identity, there is at only one default identity certificate, which is the default certificates of its default key.

While keys and certificates can be created offline using NDN security tools ndnsec, they can be created online using the KeyChain API. The simplest way is to call KeyChain::createIdentity.

KeyChain keyChain;
Name identity("/ndn/test/alice");

Name certificateName = keyChain.createIdentity(identity);

KeyChain::createIdentity returns the default certificate name of the supplied identity, and always assures that the supplied identity has a default key and a default certificate. If the default key of the identity does not exist, createIdentity will create one. If the default certificate of the identity does not exist, createIdentity will generate a self-signed certificate of the default key as the default certificate.

System Default Identity

There is a default key for a particular identity, and a default identity certificate for a particular key. And, there is also a default identity for the system, which is the user of the system. The default identity can be configured using ndnsec tools only. You cannot configure through the security library API.

Get and Set Default Keys/Certificates

If you know the exact names of keys and certificates, you can call KeyChain::getPublicKey and KeyChain::getCertificate.

KeyChain keyChain;
Name aliceKeyName("/ndn/test/alice/ksk-1394129695025");
Name aliceCertName("/ndn/test/KEY/alice/ksk-1394129695025/ID-CERT/%FD%01D%98%9A%F2%3F");

shared_ptr<PublicKey> aliceKey = keyChain.getPublicKey(aliceKeyName);
shared_ptr<IdentityCertificate> aliceCert = keyChain.getCertificate(aliceCertName);

It might be difficult to remember the exact name of keys and certificates, but it might be easier to remember identity names. The security library provides a list of methods to locate the default key name and certificate name of an identity.

KeyChain keyChain;
Name alice("/ndn/test/alice");

Name aliceKeyName = keyChain.getDefaultKeyNameForIdentity(alice);
Name aliceCertName = keyChain.getDefaultCertificateNameForKey(aliceKeyName);

/* following code is equivalent to the two lines above */
Name aliceCertName2 = keyChain.getDefaultCertificateNameForIdentity(alice);

You can also manually set default key for an identity and default certificate for a key through KeyChain.

KeyChain keyChain;

Name aliceKeyName("/ndn/test/alice/ksk-1394129695025");
Name aliceCertName("/ndn/test/KEY/alice/ksk-1394129695025/ID-CERT/%FD%01D%98%9A%F2%3F");

keyChain.setDefaultKeyNameForIdentity(aliceKeyName);
keyChain.getDefaultCertificateNameForKey(aliceCertName);

Create Keys Manually

You can call KeyChain::generateRSAKeyPair to generate an RSA key pair. Note that generated key pair is not set as the default key of the identity, so you need to set it manually by calling KeyChain::setDefaultKeyNameForIdentity. There is also a helper method “KeyChain::generateRSAKeyPairAsDefault”, which combines the two steps into one.

KeyChain keyChain;
Name alice("/ndn/test/alice");

Name aliceKeyName = keyChain.generateRSAKeyPair(alice);
keyChain.setDefaultKeyNameForIdentity(aliceKeyName);

Name aliceKeyName2 = keyChain.generateRSAKeyPairAsDefault(alice); // Now the key with the name aliceKeyName2 becomes alice's default key

Create Identity Certificate Manually

If you have created a key pair, you can generate a self-signed certificate for the key by calling KeyChain::selfSign.

KeyChain keyChain;
Name aliceKeyName("/ndn/test/alice/ksk-1394129695025");

shared_ptr<IdentityCertificate> aliceCert = keyChain.selfSign(aliceKeyName);

You can sign a public key using a different key, but this signing process may take several steps. Note that this time, the signing key is vouching for the signed key, so you need to specify more details, such as the validity, subject descriptions. The first step is to prepare the unsigned identity certificate by calling KeyChain::prepareUnsignedIdentityCertificate. And the second step is to sign the identity certificate. We will talk about the signing methods in Packet Signing.

KeyChain keyChain;

Name signingIdentity("/ndn/test");
Name aliceKeyName("/ndn/test/alice/ksk-1394129695025");
MillisecondsSince1970 notBefore = getNow();
MillisecondsSince1970 notAfter = notBefore + 630720000;
vector<CertificateSubjectDescription> subjectDescription;
subjectDescription.push_back(CertificateSubjectDescription("2.5.4.41", "Alice")); // push any subject description into the list.

shared_ptr<IdentityCertificate> = aliceCert
  keyChain.prepareUnsignedIdentityCertificate(aliceKeyName, signingIdentity, notBefore, notAfter, subjectDescription);

keyChain.signByIdentity(*aliceCert, signingIdentity);

Packet Signing

When keys and certificates are ready for use, you can sign packet using them. There are two ways to sign a packet:

  1. by specifying the name of the identity certificate belonging to the signing key.
  2. by specifying the identity to which the signing key belongs

Sign With Certificate Name

If we specify the exact certificate name when signing a packet, the certificate name (without version number) is put into the KeyLocator TLV in the SignatureInfo. KeyChain will look up the corresponding private key in the Trusted Platform Module (TPM), and use the private key to generate the signature.

KeyChain keyChain;

Name aliceCertName("/ndn/test/KEY/alice/ksk-1394129695025/ID-CERT/%FD%01D%98%9A%F2%3F");
Data data("/ndn/test/alice/test-data");

keyChain.sign(data, aliceCertName);

When KeyChain::sing returns, the SignatureInfo and SignatureValue TLVs of the supplied data will be set.

Sign With Identity Name

If we only specify the identity name when signing a packet, the name of the identity’s default certificate will be put into the KeyLocator TLV in the SingatureInfo, and the identity’s default key is used to sign the packet. Please make sure the default key and certificates of the signing identity is initialized correctly before signing, otherwise, KeyChain will create key and self-signed certificate for signing (which is not quite useful).

KeyChain keyChain;

Name alice("/ndn/test/alice");
Data data("/ndn/test/alice/test-data");

keyChain.signByIdentity(data, alice);

Sign Interest Packet

Signing an interest packet is the same as signing a Data packet. The only difference is that the SignatureInfo And SignatureValue TLV are encoded as the last two components of the interest name.

Packet Validation

Packet validation is done through a Validator. Validator is a virtual class, two pure virtual methods must be implemented in order to construct a working validator:

class Validator {
  ...
protected:
  virtual void
  checkPolicy (const Data& data,
               int stepCount,
               const OnDataValidated &onValidated,
               const OnDataValidationFailed &onValidationFailed,
               std::vector<shared_ptr<ValidationRequest> >& nextSteps) = 0;

  virtual void
  checkPolicy (const Interest& interest,
               int stepCount,
               const OnInterestValidated &onValidated,
               const OnInterestValidationFailed &onValidationFailed,
               std::vector<shared_ptr<ValidationRequest> >& nextSteps) = 0;
...

What you need to do inside these two methods is to check whether the packet and signer comply with your policies, and whether their signature can be verified. If the packet can be validated, you should call the onValidated callback function to trigger packet processing, otherwise the onValidationFailed callback should be invoked. If you need more information (e.g., other certificates), you can construct several ValidationRequest and push them into nextSteps.

class ValidationRequest {
public:
  Interest m_interest;                      // An interest packet to fetch the requested data.
  OnDataValidated m_onValidated;            // A callback function if the requested certificate is validated.
  OnDataValidationFailed m_onDataValidated; // A callback function if the requested certificate validation fails.
  int m_retry;                              // The number of retrials when there is an interest timeout.
  int m_stepCount;                          // The count of validation steps.
};

Security library also provides an Validator, ValidatorRegex which has already implemented the two methods (basically for Data policy checking, the Interest policy checking method always calls onValidationFailed).

class ValidatorRegex : public Validator
{
public:
  ...

  void
  addDataVerificationRule(shared_ptr<SecRuleRelative> rule);

  void
  addTrustAnchor(shared_ptr<IdentityCertificate> certificate);

  ...
};

With ValidatorRegex, you can specify the validation rules in terms of [[Regex|NDN Regular Expression]] via ValidatorRegex::addDataVerificationRule, and set trust anchor via ValidatorRegex::addTrustAnchor.

How to specify regex-based validation rule

To specify a SecRuleRelative, you needs to specify two NDN regular expressions: one for data name matching, and the other for KeyLocator matching. For each regex, you also need to specify the back reference pattern to extract parts of the name. Moreover, you need to specify the relation between two extracted patterns. For example, a typical hierarchical rule can be written as

SecRuleRelative rule("^(<>*)$", "^([^<KEY>]*)<KEY>(<>*)<ksk-.*><ID-CERT>$",
                     ">", "\\1", "\\1\\2", true);

This rule indicates that the data name must be under the signer’s namespace.

How to use validator

Here is an example of how to use the validator

class Example {
public:
  Example(ndn::shared_ptr<ndn::Face> face>)
    : m_face(face)
  {
    ndn::shared_ptr<ndn::ValidatorRegex> validator(new ndn::ValidatorRegex(m_face));
    validator->addDataVerificationRule(ndn::make_shared<ndn::SecRuleRelative>("^(<>*)$",
                                                                              "^([^<KEY>]*)<KEY>(<>*)<ksk-.*><ID-CERT>$",
                                                                              ">", "\\1", "\\1\\2", true));
    ndn::shared_ptr<ndn::IdentityCertificate> anchor = ndn::io::load<IdentityCertificate>("ndn-root.cert");
    validator->addTrustAnchor(anchor);
    m_validator = validator;
  }

  virtual
  ~Example() {}

  void
  sendInterest()
  {
    Interest interest ("/ndn/test/data");
    m_face->expressInterest(interest,
                            bind(&Example::onData, this, _1, _2),
                            bind(&Example::onTimeout, this, _1));
  }

  void
  onData(const ndn::Interest& interest, const ndn::Data& data)
  {
    m_validator->validate(data,
                          ndn::bind(&Example::onValidated, this, _1),
                          ndn::bind(&Example::onValidationFailed, this, _1, _2));
  }

  void onTimeout(const ndn::Interest& interest) {}

  void onValidated(const ndn::shared_ptr<const ndn::Data>& data) {}

  void onValidationFailed(const ndn::shared_ptr<const ndn::Data>& data, const std::string& failInfo) {}

private:
  ndn::shared_ptr<ndn::Face> m_face;
  ndn::shared_ptr<ndn::Validator> m_validator;
};