Skip to content

Instantly share code, notes, and snippets.

@dev-ritik
Created August 24, 2019 09:38
Show Gist options
  • Save dev-ritik/e989f76e447f7e2e746efc28772f0a8a to your computer and use it in GitHub Desktop.
Save dev-ritik/e989f76e447f7e2e746efc28772f0a8a to your computer and use it in GitHub Desktop.
Code for running modified ndncert-ca-server for android-identity-manager
/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
/**
* Copyright (c) 2017-2019, Regents of the University of California.
*
* This file is part of ndncert, a certificate management system based on NDN.
*
* ndncert is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Foundation, either
* version 3 of the License, or (at your option) any later version.
*
* ndncert 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 copies of the GNU General Public License along with
* ndncert, e.g., in COPYING.md file. If not, see <http://www.gnu.org/licenses/>.
*
* See AUTHORS.md for complete list of ndncert authors and contributors.
*/
#include "ca-module.hpp"
#include "challenge-module.hpp"
#include "logging.hpp"
#include "crypto-support/enc-tlv.hpp"
#include <ndn-cxx/util/io.hpp>
#include <ndn-cxx/security/verification-helpers.hpp>
#include <ndn-cxx/security/signing-helpers.hpp>
#include <ndn-cxx/util/random.hpp>
namespace ndn {
namespace ndncert {
static const int IS_SUBNAME_MIN_OFFSET = 5;
static const time::seconds DEFAULT_DATA_FRESHNESS_PERIOD = 1_s;
_LOG_INIT(ndncert.ca);
CaModule::CaModule(Face& face, security::v2::KeyChain& keyChain,
const std::string& configPath, const std::string& storageType)
: m_face(face)
, m_keyChain(keyChain)
{
// load the config and create storage
m_config.load(configPath);
m_storage = CaStorage::createCaStorage(storageType);
registerPrefix();
}
CaModule::~CaModule()
{
for (auto handle : m_interestFilterHandles) {
handle.cancel();
}
for (auto handle : m_registeredPrefixHandles) {
handle.unregister();
}
}
void
CaModule::registerPrefix()
{
// register localhop discovery prefix
Name localhopProbePrefix("/localhop/CA/PROBE/INFO");
auto prefixId = m_face.setInterestFilter(InterestFilter(localhopProbePrefix),
bind(&CaModule::onProbe, this, _2),
bind(&CaModule::onRegisterFailed, this, _2));
m_registeredPrefixHandles.push_back(prefixId);
_LOG_TRACE("Prefix " << localhopProbePrefix << " got registered");
// register prefixes
Name prefix = m_config.m_caName;
prefix.append("CA");
prefixId = m_face.registerPrefix(prefix,
[&] (const Name& name) {
// register PROBE prefix
auto filterId = m_face.setInterestFilter(Name(name).append("_PROBE"),
bind(&CaModule::onProbe, this, _2));
m_interestFilterHandles.push_back(filterId);
// register NEW prefix
filterId = m_face.setInterestFilter(Name(name).append("_NEW"),
bind(&CaModule::onNew, this, _2));
m_interestFilterHandles.push_back(filterId);
// register SELECT prefix
filterId = m_face.setInterestFilter(Name(name).append("_CHALLENGE"),
bind(&CaModule::onChallenge, this, _2));
m_interestFilterHandles.push_back(filterId);
// register DOWNLOAD prefix
filterId = m_face.setInterestFilter(Name(name).append("_DOWNLOAD"),
bind(&CaModule::onDownload, this, _2));
m_interestFilterHandles.push_back(filterId);
_LOG_TRACE("Prefix " << name << " got registered");
},
bind(&CaModule::onRegisterFailed, this, _2));
m_registeredPrefixHandles.push_back(prefixId);
}
bool
CaModule::setProbeHandler(const ProbeHandler& handler)
{
m_config.m_probeHandler = handler;
return false;
}
bool
CaModule::setStatusUpdateCallback(const StatusUpdateCallback& onUpdateCallback)
{
m_config.m_statusUpdateCallback = onUpdateCallback;
return false;
}
void
CaModule::onProbe(const Interest& request)
{
// PROBE Naming Convention: /<CA-Prefix>/CA/PROBE/[ParametersSha256DigestComponent|INFO]
_LOG_TRACE("Receive PROBE request");
JsonSection contentJson;
// process PROBE INFO requests
if (readString(request.getName().at(-1)) == "INFO") {
contentJson = genProbeResponseJson();
}
else {
// if not a PROBE INFO, find an available name
std::string availableId = "";
const auto& parameterJson = jsonFromBlock(request.getApplicationParameters());
if (parameterJson.empty()) {
_LOG_ERROR("Empty JSON obtained from the Interest parameter.");
return;
}
//std::string probeInfoStr = parameterJson.get(JSON_CLIENT_PROBE_INFO, "");
if (m_config.m_probeHandler) {
try {
availableId = m_config.m_probeHandler(parameterJson);
}
catch (const std::exception& e) {
_LOG_TRACE("Cannot find PROBE input from PROBE parameters " << e.what());
return;
}
}
else {
// if there is no app-specified name lookup, use a random name id
availableId = std::to_string(random::generateSecureWord64());
}
Name newIdentityName = m_config.m_caName;
newIdentityName.append(availableId);
_LOG_TRACE("Handle PROBE: generate an identity " << newIdentityName);
contentJson = genProbeResponseJson(newIdentityName.toUri(), m_config.m_probe, parameterJson);
}
Data result;
result.setName(request.getName());
result.setContent(dataContentFromJson(contentJson));
result.setFreshnessPeriod(DEFAULT_DATA_FRESHNESS_PERIOD);
m_keyChain.sign(result, signingByIdentity(m_config.m_caName));
m_face.put(result);
_LOG_TRACE("Handle PROBE: send out the PROBE response");
}
void
CaModule::onNew(const Interest& request)
{
// NEW Naming Convention: /<CA-prefix>/CA/NEW/[SignedInterestParameters_Digest]
// get ECDH pub key and cert request
const auto& parameterJson = jsonFromBlock(request.getApplicationParameters());
if (parameterJson.empty()) {
_LOG_ERROR("Empty JSON obtained from the Interest parameter.");
return;
}
// std::string peerKeyBase64 = parameterJson.get(JSON_CLIENT_ECDH, "");
// get server's ECDH pub key
auto myEcdhPubKeyBase64 = m_ecdh.getBase64PubKey();
// m_ecdh.deriveSecret(peerKeyBase64);
// generate salt for HKDF
auto saltInt = random::generateSecureWord64();
uint8_t salt[sizeof(saltInt)];
std::memcpy(salt, &saltInt, sizeof(saltInt));
// hkdf
// hkdf(m_ecdh.context->sharedSecret, m_ecdh.context->sharedSecretLen,
// salt, sizeof(saltInt), m_aesKey, 32);
// parse certificate request
std::string certRequestStr = parameterJson.get(JSON_CLIENT_CERT_REQ, "");
shared_ptr<security::v2::Certificate> clientCert = nullptr;
try {
std::stringstream ss(certRequestStr);
clientCert = io::load<security::v2::Certificate>(ss);
}
catch (const std::exception& e) {
_LOG_ERROR("Unrecognized certificate request " << e.what());
return;
}
// parse probe token if any
std::string probeTokenStr = parameterJson.get("probe-token", "");
shared_ptr<Data> probeToken = nullptr;
if (probeTokenStr != "") {
try {
std::stringstream ss(probeTokenStr);
probeToken = io::load<Data>(ss);
}
catch (const std::exception& e) {
_LOG_ERROR("Unrecognized probe token " << e.what());
return;
}
}
if (probeToken == nullptr && m_config.m_probe != "") {
// the CA requires PROBE before NEW
_LOG_ERROR("CA requires PROBE but no PROBE token is found in NEW Interest.");
return;
}
else if (probeToken != nullptr) {
// check whether the carried probe token is a PROBE Data packet
Name prefix = m_config.m_caName;
prefix.append("CA").append("_PROBE");
if (!prefix.isPrefixOf(probeToken->getName())) {
_LOG_ERROR("Carried PROBE token is not a valid PROBE Data packet.");
return;
}
}
// verify the self-signed certificate, the request, and the token
if (!m_config.m_caName.isPrefixOf(clientCert->getName()) // under ca prefix
|| !security::v2::Certificate::isValidName(clientCert->getName()) // is valid cert name
|| clientCert->getName().size() != m_config.m_caName.size() + IS_SUBNAME_MIN_OFFSET) {
_LOG_ERROR("Invalid self-signed certificate name " << clientCert->getName());
return;
}
if (!security::verifySignature(*clientCert, *clientCert)) {
_LOG_TRACE("Cert request with bad signature.");
return;
}
if (!security::verifySignature(request, *clientCert)) {
_LOG_TRACE("Interest with bad signature.");
return;
}
if (probeToken != nullptr) {
const auto& pib = m_keyChain.getPib();
const auto& key = pib.getIdentity(m_config.m_caName).getDefaultKey();
const auto& caCert = key.getDefaultCertificate();
if (!security::verifySignature(*probeToken, caCert)) {
_LOG_TRACE("PROBE Token with bad signature.");
return;
}
}
// create new request instance
std::string requestId = std::to_string(random::generateWord64());
CertificateRequest certRequest(m_config.m_caName, requestId, STATUS_BEFORE_CHALLENGE, *clientCert);
if (probeToken != nullptr) {
certRequest.setProbeToken(probeToken);
}
try {
m_storage->addRequest(certRequest);
}
catch (const std::exception& e) {
_LOG_TRACE("Cannot add new request instance into the storage " << e.what());
return;
}
Data result;
result.setName(request.getName());
result.setFreshnessPeriod(DEFAULT_DATA_FRESHNESS_PERIOD);
result.setContent(dataContentFromJson(genNewResponseJson(myEcdhPubKeyBase64,
std::to_string(saltInt),
certRequest,
m_config.m_supportedChallenges)));
m_keyChain.sign(result, signingByIdentity(m_config.m_caName));
m_face.put(result);
if (m_config.m_statusUpdateCallback) {
m_config.m_statusUpdateCallback(certRequest);
}
}
void
CaModule::onChallenge(const Interest& request)
{
// get certificate request state
CertificateRequest certRequest = getCertificateRequest(request);
if (certRequest.m_requestId == "") {
// cannot get the request state
return;
}
// verify signature
if (!security::verifySignature(request, certRequest.m_cert)) {
_LOG_TRACE("Interest with bad signature.");
return;
}
// decrypt the parameters
// auto paramJsonPayload = parseEncBlock(m_ecdh.context->sharedSecret,
// m_ecdh.context->sharedSecretLen,
// request.getApplicationParameters());
// const auto& paramJsonPayload = request.getApplicationParameters().value();
// if (paramJsonPayload.size() == 0) {
// _LOG_ERROR("Got an empty buffer from content decryption.");
// return;
// }
// std::string paramJsonStr((const char*)paramJsonPayload.data(), paramJsonPayload.size());
// std::istringstream ss(paramJsonStr);
JsonSection paramJson = jsonFromBlock(request.getApplicationParameters());
// try {
// boost::property_tree::json_parser::read_json(ss, paramJson);
// }
// catch (const std::exception& e) {
// _LOG_ERROR("Cannot read JSON from decrypted content " << e.what());
// return;
// }
// load the corresponding challenge module
std::string challengeType = paramJson.get<std::string>(JSON_CLIENT_SELECTED_CHALLENGE);
auto challenge = ChallengeModule::createChallengeModule(challengeType);
JsonSection contentJson;
if (challenge == nullptr) {
_LOG_TRACE("Unrecognized challenge type " << challengeType);
certRequest.m_status = STATUS_FAILURE;
certRequest.m_challengeStatus = CHALLENGE_STATUS_UNKNOWN_CHALLENGE;
contentJson = genChallengeResponseJson(certRequest);
}
else {
_LOG_TRACE("CHALLENGE module to be load: " << challengeType);
// let challenge module handle the request
challenge->handleChallengeRequest(paramJson, certRequest);
if (certRequest.m_status == STATUS_FAILURE) {
// if challenge failed
m_storage->deleteRequest(certRequest.m_requestId);
contentJson = genChallengeResponseJson(certRequest);
_LOG_TRACE("Challenge failed");
}
else if (certRequest.m_status == STATUS_PENDING) {
// if challenge succeeded
auto issuedCert = issueCertificate(certRequest);
certRequest.m_cert = issuedCert;
certRequest.m_status = STATUS_SUCCESS;
try {
m_storage->addCertificate(certRequest.m_requestId, issuedCert);
m_storage->deleteRequest(certRequest.m_requestId);
_LOG_TRACE("New Certificate Issued " << issuedCert.getName());
}
catch (const std::exception& e) {
_LOG_ERROR("Cannot add issued cert and remove the request " << e.what());
return;
}
if (m_config.m_statusUpdateCallback) {
m_config.m_statusUpdateCallback(certRequest);
}
contentJson = genChallengeResponseJson(certRequest);
contentJson.add(JSON_CA_CERT_ID, readString(issuedCert.getName().at(-1)));
_LOG_TRACE("Challenge succeeded. Certificate has been issued");
}
else {
try {
m_storage->updateRequest(certRequest);
}
catch (const std::exception& e) {
_LOG_TRACE("Cannot update request instance " << e.what());
return;
}
contentJson = genChallengeResponseJson(certRequest);
_LOG_TRACE("No failure no success. Challenge moves on");
}
}
Data result;
result.setName(request.getName());
result.setFreshnessPeriod(DEFAULT_DATA_FRESHNESS_PERIOD);
// encrypt the content
std::stringstream ss2;
boost::property_tree::write_json(ss2, contentJson);
auto payload = ss2.str();
// auto contentBlock = genEncBlock(tlv::Content, m_ecdh.context->sharedSecret,
// m_ecdh.context->sharedSecretLen,
// (const uint8_t*)payload.c_str(), payload.size());
auto contentBlock = dataContentFromJson(contentJson);
result.setContent(contentBlock);
m_keyChain.sign(result, signingByIdentity(m_config.m_caName));
m_face.put(result);
if (m_config.m_statusUpdateCallback) {
m_config.m_statusUpdateCallback(certRequest);
}
}
void
CaModule::onDownload(const Interest& request)
{
auto requestId = readString(request.getName().at(-1));
security::v2::Certificate signedCert;
try {
signedCert = m_storage->getCertificate(requestId);
}
catch (const std::exception& e) {
_LOG_ERROR("Cannot read signed cert " << requestId << " from ca database " << e.what());
return;
}
Data result;
result.setName(request.getName());
result.setFreshnessPeriod(DEFAULT_DATA_FRESHNESS_PERIOD);
result.setContent(signedCert.wireEncode());
m_keyChain.sign(result, signingByIdentity(m_config.m_caName));
m_face.put(result);
}
security::v2::Certificate
CaModule::issueCertificate(const CertificateRequest& certRequest)
{
auto expectedPeriod =
certRequest.m_cert.getValidityPeriod().getPeriod();
time::system_clock::TimePoint startingTime, endingTime;
if (expectedPeriod.first > time::system_clock::now()
&& expectedPeriod.first < time::system_clock::now()
+ m_config.m_validityPeriod)
{
startingTime = expectedPeriod.first;
}
else {
startingTime = time::system_clock::now();
}
if (expectedPeriod.second < time::system_clock::now() + m_config.m_validityPeriod) {
endingTime = expectedPeriod.second;
}
else {
endingTime = time::system_clock::now() + m_config.m_validityPeriod;
}
security::ValidityPeriod period(startingTime, endingTime);
security::v2::Certificate newCert;
Name certName = certRequest.m_cert.getKeyName();
certName.append("NDNCERT").append(std::to_string(random::generateSecureWord64()));
newCert.setName(certName);
newCert.setContent(certRequest.m_cert.getContent());
_LOG_TRACE("cert request content " << certRequest.m_cert);
SignatureInfo signatureInfo;
signatureInfo.setValidityPeriod(period);
security::SigningInfo signingInfo(security::SigningInfo::SIGNER_TYPE_ID,
m_config.m_caName, signatureInfo);
newCert.setFreshnessPeriod(m_config.m_freshnessPeriod);
m_keyChain.sign(newCert, signingInfo);
_LOG_TRACE("new cert got signed" << newCert);
return newCert;
}
CertificateRequest
CaModule::getCertificateRequest(const Interest& request)
{
std::string requestId = "";
CertificateRequest certRequest;
try {
requestId = readString(request.getName().at(m_config.m_caName.size() + 2));
_LOG_TRACE("Request Id to query the database " << requestId);
}
catch (const std::exception& e) {
_LOG_ERROR(e.what());
}
try {
certRequest = m_storage->getRequest(requestId);
}
catch (const std::exception& e) {
_LOG_ERROR(e.what());
}
return certRequest;
}
/**
* @brief Generate JSON file to response PROBE insterest
*
* PROBE response JSON format:
* {
* "name": "@p identifier"
* }
*/
const JsonSection
CaModule::genProbeResponseJson(const Name& identifier, const std::string& m_probe, const JsonSection& parameterJson)
{
std::vector<std::string> fields;
std::string delimiter = ":";
size_t last = 0;
size_t next = 0;
while ((next = m_probe.find(delimiter, last)) != std::string::npos) {
fields.push_back(m_probe.substr(last, next - last));
last = next + 1;
}
fields.push_back(m_probe.substr(last));
JsonSection root;
for (size_t i = 0; i < fields.size(); ++i) {
root.put(fields.at(i), parameterJson.get(fields.at(i), ""));
}
root.put(JSON_CA_NAME, identifier.toUri());
return root;
}
/**
* @brief Generate JSON file to response NEW interest
*
* Target JSON format:
* {
* "ecdh-pub": "@p echdPub",
* "salt": "@p salt"
* "request-id": "@p requestId",
* "status": "@p status",
* "challenges": [
* {
* "challenge-id": ""
* },
* {
* "challenge-id": ""
* },
* ...
* ]
* }
*/
const JsonSection
CaModule::genProbeResponseJson()
{
JsonSection root;
// ca-prefix
Name caName = m_config.m_caName;
root.put("ca-prefix", caName.toUri());
// ca-info
const auto& pib = m_keyChain.getPib();
const auto& identity = pib.getIdentity(m_config.m_caName);
const auto& cert = identity.getDefaultKey().getDefaultCertificate();
std::string caInfo = "";
if (m_config.m_caInfo == "") {
caInfo = "Issued by " + cert.getSignature().getKeyLocator().getName().toUri();
}
else {
caInfo = m_config.m_caInfo;
}
root.put("ca-info", caInfo);
// probe
root.put("probe", m_config.m_probe);
// certificate
std::stringstream ss;
io::save(cert, ss);
root.put("certificate", ss.str());
return root;
}
const JsonSection
CaModule::genNewResponseJson(const std::string& ecdhKey, const std::string& salt,
const CertificateRequest& request,
const std::list<std::string>& challenges)
{
JsonSection root;
JsonSection challengesSection;
root.put(JSON_CA_ECDH, ecdhKey);
root.put(JSON_CA_SALT, salt);
root.put(JSON_CA_EQUEST_ID, request.m_requestId);
root.put(JSON_CA_STATUS, std::to_string(request.m_status));
for (const auto& entry : challenges) {
JsonSection challenge;
challenge.put(JSON_CA_CHALLENGE_ID, entry);
challengesSection.push_back(std::make_pair("", challenge));
}
root.add_child(JSON_CA_CHALLENGES, challengesSection);
return root;
}
const JsonSection
CaModule::genChallengeResponseJson(const CertificateRequest& request)
{
JsonSection root;
JsonSection challengesSection;
root.put(JSON_CA_STATUS, request.m_status);
root.put(JSON_CHALLENGE_STATUS, request.m_challengeStatus);
root.put(JSON_CHALLENGE_REMAINING_TRIES, std::to_string(request.m_remainingTries));
root.put(JSON_CHALLENGE_REMAINING_TIME, std::to_string(request.m_remainingTime));
return root;
}
void
CaModule::onRegisterFailed(const std::string& reason)
{
_LOG_ERROR("Failed to register prefix in local hub's daemon, REASON: " << reason);
}
Block
CaModule::dataContentFromJson(const JsonSection& jsonSection)
{
std::stringstream ss;
boost::property_tree::write_json(ss, jsonSection);
return makeStringBlock(ndn::tlv::Content, ss.str());
}
JsonSection
CaModule::jsonFromBlock(const Block& block)
{
std::string jsonString;
try {
jsonString = encoding::readString(block);
std::istringstream ss(jsonString);
JsonSection json;
boost::property_tree::json_parser::read_json(ss, json);
return json;
}
catch (const std::exception& e) {
_LOG_ERROR("Cannot read JSON string from TLV Value " << e.what());
return JsonSection();
}
}
} // namespace ndncert
} // namespace ndn
/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
/*
* Copyright (c) 2017-2019, Regents of the University of California.
*
* This file is part of ndncert, a certificate management system based on NDN.
*
* ndncert is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Foundation, either
* version 3 of the License, or (at your option) any later version.
*
* ndncert 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 copies of the GNU General Public License along with
* ndncert, e.g., in COPYING.md file. If not, see <http://www.gnu.org/licenses/>.
*
* See AUTHORS.md for complete list of ndncert authors and contributors.
*/
#include "ca-module.hpp"
#include "challenge-module.hpp"
#include <ndn-cxx/face.hpp>
#include <ndn-cxx/security/v2/key-chain.hpp>
#include <boost/asio/ip/tcp.hpp>
#if BOOST_VERSION < 106700
#include <boost/date_time/posix_time/posix_time_duration.hpp>
#endif
#include <boost/program_options/options_description.hpp>
#include <boost/program_options/parsers.hpp>
#include <boost/program_options/variables_map.hpp>
#include <iostream>
namespace ndn {
namespace ndncert {
static int
main(int argc, char* argv[])
{
ndn::Interest::setAutoCheckParametersDigest(false);
std::string configFilePath(SYSCONFDIR "/ndncert/ca.conf");
std::string repoHost("localhost");
std::string repoPort("7376");
bool wantRepoOut = false;
namespace po = boost::program_options;
po::options_description optsDesc("Options");
optsDesc.add_options()
("help,h", "print this help message and exit")
("config-file,c", po::value<std::string>(&configFilePath)->default_value(configFilePath),
"path to configuration file")
("repo-output,r", po::bool_switch(&wantRepoOut),
"when enabled, all issued certificates will be published to repo-ng")
("repo-host,H", po::value<std::string>(&repoHost)->default_value(repoHost), "repo-ng host")
("repo-port,P", po::value<std::string>(&repoPort)->default_value(repoPort), "repo-ng port");
po::variables_map vm;
try {
po::store(po::parse_command_line(argc, argv, optsDesc), vm);
po::notify(vm);
}
catch (const po::error& e) {
std::cerr << "ERROR: " << e.what() << std::endl;
return 2;
}
catch (const boost::bad_any_cast& e) {
std::cerr << "ERROR: " << e.what() << std::endl;
return 2;
}
if (vm.count("help") != 0) {
std::cout << "Usage: " << argv[0] << " [options]\n"
<< "\n"
<< optsDesc;
return 0;
}
Face face;
security::v2::KeyChain keyChain;
CaModule ca(face, keyChain, configFilePath);
if (wantRepoOut) {
ca.setStatusUpdateCallback([&] (const CertificateRequest& request) {
if (request.m_status == STATUS_SUCCESS) {
auto issuedCert = request.m_cert;
boost::asio::ip::tcp::iostream requestStream;
#if BOOST_VERSION >= 106700
requestStream.expires_after(std::chrono::seconds(3));
#else
requestStream.expires_from_now(boost::posix_time::seconds(3));
#endif // BOOST_VERSION >= 106700
requestStream.connect(repoHost, repoPort);
if (!requestStream) {
std::cerr << "ERROR: Cannot publish certificate to repo-ng"
<< " (" << requestStream.error().message() << ")" << std::endl;
return;
}
requestStream.write(reinterpret_cast<const char*>(issuedCert.wireEncode().wire()),
issuedCert.wireEncode().size());
}
});
}
face.processEvents();
return 0;
}
} // namespace ndncert
} // namespace ndn
int
main(int argc, char* argv[])
{
return ndn::ndncert::main(argc, argv);
}
@dev-ritik
Copy link
Author

⚠️ DO NOT DELETE ⚠️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment