blob: d276fb14505d7eeafc30c6b505135a3b7602eb2b [file] [log] [blame]
Zhiyi Zhang08e0e982017-03-01 10:10:42 -08001/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
Davide Pesaventob48bbda2020-07-27 19:41:37 -04002/*
Zhiyi Zhangad9e04f2020-03-27 12:04:31 -07003 * Copyright (c) 2017-2020, Regents of the University of California.
Zhiyi Zhang08e0e982017-03-01 10:10:42 -08004 *
5 * This file is part of ndncert, a certificate management system based on NDN.
6 *
7 * ndncert is free software: you can redistribute it and/or modify it under the terms
8 * of the GNU General Public License as published by the Free Software Foundation, either
9 * version 3 of the License, or (at your option) any later version.
10 *
11 * ndncert is distributed in the hope that it will be useful, but WITHOUT ANY
12 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
13 * PARTICULAR PURPOSE. See the GNU General Public License for more details.
14 *
15 * You should have received copies of the GNU General Public License along with
16 * ndncert, e.g., in COPYING.md file. If not, see <http://www.gnu.org/licenses/>.
17 *
18 * See AUTHORS.md for complete list of ndncert authors and contributors.
19 */
20
Zhiyi Zhang08e0e982017-03-01 10:10:42 -080021#include "challenge-module.hpp"
Davide Pesaventob48bbda2020-07-27 19:41:37 -040022#include "client-module.hpp"
23
24#include <csignal>
Zhiyi Zhangb6fab0f2017-09-21 16:26:27 -070025#include <iostream>
Zhiyi Zhang1c0bd372017-12-18 18:32:55 +080026#include <string>
Davide Pesaventob48bbda2020-07-27 19:41:37 -040027
Zhiyi Zhangad9e04f2020-03-27 12:04:31 -070028#include <boost/asio.hpp>
Davide Pesaventob48bbda2020-07-27 19:41:37 -040029#include <boost/program_options/options_description.hpp>
30#include <boost/program_options/parsers.hpp>
31#include <boost/program_options/variables_map.hpp>
32
Zhiyi Zhang1c0bd372017-12-18 18:32:55 +080033#include <ndn-cxx/security/verification-helpers.hpp>
Zhiyi Zhang08e0e982017-03-01 10:10:42 -080034
35namespace ndn {
36namespace ndncert {
37
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -070038static void startApplication();
39
Zhiyi Zhang1c0bd372017-12-18 18:32:55 +080040int nStep;
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -070041Face face;
Davide Pesaventob48bbda2020-07-27 19:41:37 -040042security::KeyChain keyChain;
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -070043std::string challengeType;
Zhiyi Zhang36706832019-07-04 21:33:03 -070044int validityPeriod = -1;
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -070045ClientModule client(keyChain);
Zhiyi Zhang08e0e982017-03-01 10:10:42 -080046
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -070047static std::list<std::string>
48captureParams(const JsonSection& requirement)
Zhiyi Zhang08e0e982017-03-01 10:10:42 -080049{
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -070050 std::list<std::string> results;
Zhiyi Zhang547c8512019-06-18 23:46:14 -070051 for (const auto& item : requirement) {
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -070052 std::cerr << item.second.get<std::string>("") << std::endl;
53 std::cerr << "Please provide the argument: " << item.first << " : " << std::endl;
54 std::string tempParam;
55 getline(std::cin, tempParam);
56 results.push_back(tempParam);
Zhiyi Zhang08e0e982017-03-01 10:10:42 -080057 }
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -070058 std::cerr << "Got it. This is what you've provided:" << std::endl;
59 auto it1 = results.begin();
60 auto it2 = requirement.begin();
61 for (; it1 != results.end() && it2 != requirement.end(); it1++, it2++) {
62 std::cerr << it2->first << " : " << *it1 << std::endl;
Zhiyi Zhang08e0e982017-03-01 10:10:42 -080063 }
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -070064 return results;
65}
Zhiyi Zhang08e0e982017-03-01 10:10:42 -080066
Zhiyi Zhang547c8512019-06-18 23:46:14 -070067static std::list<std::string>
68captureParams(const std::vector<std::string>& requirement)
69{
70 std::list<std::string> results;
71 for (const auto& item : requirement) {
72 std::cerr << "Please provide the argument: " << item << " : " << std::endl;
73 std::string tempParam;
74 getline(std::cin, tempParam);
75 results.push_back(tempParam);
76 }
77 std::cerr << "Got it. This is what you've provided:" << std::endl;
78 auto it1 = results.begin();
79 auto it2 = requirement.begin();
80 for (; it1 != results.end() && it2 != requirement.end(); it1++, it2++) {
81 std::cerr << *it2 << " : " << *it1 << std::endl;
82 }
83 return results;
84}
85
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -070086static void
Zhiyi Zhang36706832019-07-04 21:33:03 -070087captureValidityPeriod()
88{
Zhiyi Zhangad9e04f2020-03-27 12:04:31 -070089 if (validityPeriod > 0) {
90 return;
91 }
92 std::cerr << "Step " << nStep++
93 << ": Please type in your expected validity period of your certificate."
94 << " Type the number of hours (168 for week, 730 for month, 8760 for year)."
95 << " The CA may reject your application if your expected period is too long." << std::endl;
96 std::string periodStr = "";
97 getline(std::cin, periodStr);
98 try {
99 validityPeriod = std::stoi(periodStr);
100 }
101 catch (const std::exception& e) {
102 validityPeriod = -1;
Zhiyi Zhang36706832019-07-04 21:33:03 -0700103 }
104}
105
106static void
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700107onNackCb()
108{
109 std::cerr << "Got NACK\n";
110}
111
112static void
113timeoutCb()
114{
115 std::cerr << "Interest sent time out\n";
116}
117
118static void
119downloadCb(const Data& reply)
120{
Zhiyi Zhangad9e04f2020-03-27 12:04:31 -0700121 auto cert = client.onDownloadResponse(reply);
122 if (cert == nullptr) {
123 std::cerr << "Certificate cannot be installed to your local keychain" << std::endl;
124 return;
125 }
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700126 std::cerr << "Step " << nStep++
Zhiyi Zhangad9e04f2020-03-27 12:04:31 -0700127 << ": DONE! Certificate has already been installed to local keychain\n"
128 << "Certificate Name: " << cert->getName().toUri() << std::endl;
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700129}
130
131static void
132challengeCb(const Data& reply)
133{
134 client.onChallengeResponse(reply);
135 if (client.getApplicationStatus() == STATUS_SUCCESS) {
136 std::cerr << "DONE! Certificate has already been issued \n";
137 face.expressInterest(*client.generateDownloadInterest(), bind(&downloadCb, _2),
138 bind(&onNackCb), bind(&timeoutCb));
Zhiyi Zhang4d89fe02017-04-28 18:51:51 -0700139 return;
140 }
141
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700142 auto challenge = ChallengeModule::createChallengeModule(challengeType);
Zhiyi Zhang36706832019-07-04 21:33:03 -0700143 auto requirement = challenge->getRequirementForChallenge(client.getApplicationStatus(),
144 client.getChallengeStatus());
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700145 if (requirement.size() > 0) {
Zhiyi Zhang916ba2d2018-02-01 18:16:27 -0800146 std::cerr << "Step " << nStep++ << ": Please satisfy following instruction(s)\n";
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700147 std::string redo = "";
148 std::list<std::string> capturedParams;
Zhiyi Zhangad9e04f2020-03-27 12:04:31 -0700149 capturedParams = captureParams(requirement);
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700150 auto it1 = capturedParams.begin();
151 auto it2 = requirement.begin();
152 for (; it1 != capturedParams.end() && it2 != requirement.end(); it1++, it2++) {
153 it2->second.put("", *it1);
Zhiyi Zhang08e0e982017-03-01 10:10:42 -0800154 }
Zhiyi Zhang08e0e982017-03-01 10:10:42 -0800155 }
Zhiyi Zhang36706832019-07-04 21:33:03 -0700156 face.expressInterest(*client.generateChallengeInterest(challenge->genChallengeRequestJson(client.getApplicationStatus(),
157 client.getChallengeStatus(),
158 requirement)),
159 bind(&challengeCb, _2), bind(&onNackCb), bind(&timeoutCb));
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700160}
Zhiyi Zhang08e0e982017-03-01 10:10:42 -0800161
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700162static void
163newCb(const Data& reply)
164{
Zhiyi Zhang36706832019-07-04 21:33:03 -0700165 int challengeIndex = 0;
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700166 auto challengeList = client.onNewResponse(reply);
Zhiyi Zhang36706832019-07-04 21:33:03 -0700167 if (challengeList.size() < 1) {
168 std::cerr << "There is no available challenge provided by the CA. Exit" << std::endl;
169 return;
Zhiyi Zhang08e0e982017-03-01 10:10:42 -0800170 }
Zhiyi Zhang36706832019-07-04 21:33:03 -0700171 else if (challengeList.size() > 1) {
172 int count = 0;
173 std::string choice = "";
174 std::cerr << "Step " << nStep++ << ": Please type in the challenge index that you want to perform\n";
Zhiyi Zhangad9e04f2020-03-27 12:04:31 -0700175 count = 0;
176 for (auto item : challengeList) {
177 std::cerr << "\t" << count++ << " : "<< item << std::endl;
178 }
179 getline(std::cin, choice);
180 try {
181 challengeIndex = std::stoi(choice);
182 }
183 catch (const std::exception& e) {
184 challengeIndex = -1;
185 }
186 if (challengeIndex < 0 || challengeIndex >= count) {
187 std::cerr << "Your input index is out of range. Exit." << std::endl;
188 return;
189 }
Zhiyi Zhang36706832019-07-04 21:33:03 -0700190 }
191 auto it = challengeList.begin();
192 std::advance(it, challengeIndex);
193 unique_ptr<ChallengeModule> challenge = ChallengeModule::createChallengeModule(*it);
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700194 if (challenge != nullptr) {
Zhiyi Zhang36706832019-07-04 21:33:03 -0700195 challengeType = *it;
196 std::cerr << "The challenge has been selected: " << challengeType << std::endl;
Zhiyi Zhang08e0e982017-03-01 10:10:42 -0800197 }
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700198 else {
Zhiyi Zhang36706832019-07-04 21:33:03 -0700199 std::cerr << "Error. Cannot load selected Challenge Module. Exit." << std::endl;
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700200 return;
201 }
202 auto requirement = challenge->getRequirementForChallenge(client.getApplicationStatus(),
203 client.getChallengeStatus());
204 if (requirement.size() > 0) {
205 std::cerr << "Step " << nStep++ << ": Please satisfy following instruction(s)\n";
206 std::string redo = "";
207 std::list<std::string> capturedParams;
Zhiyi Zhangad9e04f2020-03-27 12:04:31 -0700208 capturedParams = captureParams(requirement);
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700209 auto it1 = capturedParams.begin();
210 auto it2 = requirement.begin();
211 for (; it1 != capturedParams.end() && it2 != requirement.end(); it1++, it2++) {
212 it2->second.put("", *it1);
213 }
214 }
Zhiyi Zhang36706832019-07-04 21:33:03 -0700215 face.expressInterest(*client.generateChallengeInterest(challenge->genChallengeRequestJson(client.getApplicationStatus(),
216 client.getChallengeStatus(),
217 requirement)),
218 bind(&challengeCb, _2), bind(&onNackCb), bind(&timeoutCb));
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700219}
Zhiyi Zhang08e0e982017-03-01 10:10:42 -0800220
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700221static void
222probeInfoCb(const Data& reply)
223{
Zhiyi Zhangcaab5462019-10-18 13:41:02 -0700224 if (!client.verifyProbeInfoResponse(reply)) {
225 std::cerr << "The fetched CA information cannot be trusted because its integrity is broken" << std::endl;
226 return;
227 }
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700228 auto contentJson = ClientModule::getJsonFromData(reply);
229 auto caItem = ClientConfig::extractCaItem(contentJson);
230
Zhiyi Zhangcaab5462019-10-18 13:41:02 -0700231 std::cerr << "Will use a new trust anchor, please double check the identity info: \n"
Davide Pesaventob48bbda2020-07-27 19:41:37 -0400232 << "This trust anchor information is signed by " << reply.getSignatureInfo().getKeyLocator()
233 << std::endl
234 << "The certificate is " << caItem.m_anchor << std::endl
235 << "Do you trust the information? Type in YES or NO" << std::endl;
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700236
237 std::string answer;
238 getline(std::cin, answer);
Zhiyi Zhang36706832019-07-04 21:33:03 -0700239 boost::algorithm::to_lower(answer);
240 if (answer == "yes") {
Zhiyi Zhangcaab5462019-10-18 13:41:02 -0700241 std::cerr << "You answered YES: new CA will be used" << std::endl;
242 client.addCaFromProbeInfoResponse(reply);
243 // client.getClientConf().save(std::string(SYSCONFDIR) + "/ndncert/client.conf");
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700244 startApplication();
245 }
246 else {
Zhiyi Zhangcaab5462019-10-18 13:41:02 -0700247 std::cerr << "You answered NO: new CA will not be used" << std::endl;
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700248 return;
249 }
250}
251
252static void
253probeCb(const Data& reply)
254{
Zhiyi Zhang781a5602019-06-26 19:05:04 -0700255 client.onProbeResponse(reply);
Zhiyi Zhang36706832019-07-04 21:33:03 -0700256 captureValidityPeriod();
Zhiyi Zhangad9e04f2020-03-27 12:04:31 -0700257 if (validityPeriod <= 0) {
258 std::cerr << "Invalid period time. Exit." << std::endl;
259 return;
260 }
Zhiyi Zhang781a5602019-06-26 19:05:04 -0700261 auto probeToken = make_shared<Data>(reply);
Zhiyi Zhang1a735bc2019-07-04 21:36:49 -0700262 auto now = time::system_clock::now();
Zhiyi Zhang36706832019-07-04 21:33:03 -0700263 std::cerr << "The validity period of your certificate will be: " << validityPeriod << " hours" << std::endl;
Zhiyi Zhangad9e04f2020-03-27 12:04:31 -0700264 auto interest = client.generateNewInterest(now, now + time::hours(validityPeriod), Name(), probeToken);
265 if (interest != nullptr) {
266 face.expressInterest(*interest, bind(&newCb, _2), bind(&onNackCb), bind(&timeoutCb));
267 }
268 else {
269 std::cerr << "Cannot generate the Interest for NEW step. Exit" << std::endl;
270 }
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700271}
272
273static void
274startApplication()
275{
276 nStep = 0;
277 auto caList = client.getClientConf().m_caItems;
278 int count = 0;
279 for (auto item : caList) {
280 std::cerr << "***************************************\n"
281 << "Index: " << count++ << "\n"
282 << "CA prefix:" << item.m_caName << "\n"
283 << "Introduction: " << item.m_caInfo << "\n"
284 << "***************************************\n";
285 }
286 std::vector<ClientCaItem> caVector{std::begin(caList), std::end(caList)};
287 std::cerr << "Step "
288 << nStep++ << ": Please type in the CA INDEX that you want to apply"
289 << " or type in NONE if your expected CA is not in the list\n";
290
Zhiyi Zhang36706832019-07-04 21:33:03 -0700291 std::string caIndexS, caIndexSLower;
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700292 getline(std::cin, caIndexS);
Zhiyi Zhang36706832019-07-04 21:33:03 -0700293 caIndexSLower = caIndexS;
294 boost::algorithm::to_lower(caIndexSLower);
295 if (caIndexSLower == "none") {
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700296 std::cerr << "Step " << nStep << ": Please type in the CA Name\n";
Zhiyi Zhangcaab5462019-10-18 13:41:02 -0700297 std::string expectedCAName;
298 getline(std::cin, expectedCAName);
299 face.expressInterest(*client.generateProbeInfoInterest(Name(expectedCAName)),
Zhiyi Zhang36706832019-07-04 21:33:03 -0700300 bind(&probeInfoCb, _2), bind(&onNackCb), bind(&timeoutCb));
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700301 }
302 else {
Zhiyi Zhang36706832019-07-04 21:33:03 -0700303 int caIndex;
304 try {
305 caIndex = std::stoi(caIndexS);
306 }
307 catch (const std::exception& e) {
308 std::cerr << "Your input is neither NONE nor a valid index. Exit" << std::endl;
309 return;
310 }
311 if (caIndex < 0 || caIndex >= count) {
312 std::cerr << "Your input is not an existing index. Exit" << std::endl;
313 return;
314 }
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700315 auto targetCaItem = caVector[caIndex];
316
317 if (targetCaItem.m_probe != "") {
Zhiyi Zhang547c8512019-06-18 23:46:14 -0700318 std::cerr << "Step " << nStep++ << ": Please provide information for name assignment" << std::endl;
319 std::vector<std::string> probeFields = ClientModule::parseProbeComponents(targetCaItem.m_probe);
320 std::string redo = "";
321 std::list<std::string> capturedParams;
Zhiyi Zhangad9e04f2020-03-27 12:04:31 -0700322 capturedParams = captureParams(probeFields);
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700323 std::string probeInfo;
Zhiyi Zhang547c8512019-06-18 23:46:14 -0700324 for (const auto& item : capturedParams) {
325 probeInfo += item;
326 probeInfo += ":";
327 }
328 probeInfo = probeInfo.substr(0, probeInfo.size() - 1);
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700329 face.expressInterest(*client.generateProbeInterest(targetCaItem, probeInfo),
Zhiyi Zhang36706832019-07-04 21:33:03 -0700330 bind(&probeCb, _2), bind(&onNackCb), bind(&timeoutCb));
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700331 }
332 else {
Zhiyi Zhangad9e04f2020-03-27 12:04:31 -0700333 std::cerr << "Step " << nStep++ << ": Please type in the full identity name you want to get (with CA prefix)\n";
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700334 std::string identityNameStr;
335 getline(std::cin, identityNameStr);
Zhiyi Zhang36706832019-07-04 21:33:03 -0700336 captureValidityPeriod();
Zhiyi Zhangad9e04f2020-03-27 12:04:31 -0700337 if (validityPeriod <= 0) {
338 std::cerr << "Invalid period time. Exit." << std::endl;
339 return;
340 }
341 Name idName(identityNameStr);
Zhiyi Zhang36706832019-07-04 21:33:03 -0700342 std::cerr << "The validity period of your certificate will be: " << validityPeriod << " hours" << std::endl;
Zhiyi Zhang1a735bc2019-07-04 21:36:49 -0700343 auto now = time::system_clock::now();
Zhiyi Zhangad9e04f2020-03-27 12:04:31 -0700344 auto interest = client.generateNewInterest(now, now + time::hours(validityPeriod), idName);
345 if (interest != nullptr) {
346 face.expressInterest(*interest, bind(&newCb, _2), bind(&onNackCb), bind(&timeoutCb));
347 }
348 else {
349 std::cerr << "Cannot generate the Interest for NEW step. Exit" << std::endl;
350 }
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700351 }
352 }
353}
354
Zhiyi Zhangad9e04f2020-03-27 12:04:31 -0700355static void
356handleSignal(const boost::system::error_code& error, int signalNum)
357{
358 if (error) {
359 return;
360 }
361 const char* signalName = ::strsignal(signalNum);
362 std::cerr << "Exiting on signal ";
363 if (signalName == nullptr) {
364 std::cerr << signalNum;
365 }
366 else {
367 std::cerr << signalName;
368 }
369 std::cerr << std::endl;
370 client.endSession();
371 face.getIoService().stop();
372}
Zhiyi Zhang08e0e982017-03-01 10:10:42 -0800373
374int
375main(int argc, char* argv[])
376{
Zhiyi Zhangad9e04f2020-03-27 12:04:31 -0700377 boost::asio::signal_set terminateSignals(face.getIoService());
378 terminateSignals.add(SIGINT);
379 terminateSignals.add(SIGTERM);
380 terminateSignals.async_wait(handleSignal);
381
Zhiyi Zhang08e0e982017-03-01 10:10:42 -0800382 namespace po = boost::program_options;
383 std::string configFilePath = std::string(SYSCONFDIR) + "/ndncert/client.conf";
Zhiyi Zhang36706832019-07-04 21:33:03 -0700384 po::options_description description("General Usage\n ndncert-client [-h] [-c] [-v]\n");
Zhiyi Zhang08e0e982017-03-01 10:10:42 -0800385 description.add_options()
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700386 ("help,h", "produce help message")
Davide Pesaventob48bbda2020-07-27 19:41:37 -0400387 ("config-file,c", po::value<std::string>(&configFilePath), "configuration file name")
388 ("validity-period,v", po::value<int>(&validityPeriod)->default_value(-1),
389 "desired validity period (hours) of the certificate being requested");
Zhiyi Zhang08e0e982017-03-01 10:10:42 -0800390 po::positional_options_description p;
391
392 po::variables_map vm;
393 try {
394 po::store(po::command_line_parser(argc, argv).options(description).positional(p).run(), vm);
395 po::notify(vm);
396 }
397 catch (const std::exception& e) {
398 std::cerr << "ERROR: " << e.what() << std::endl;
399 return 1;
400 }
401 if (vm.count("help") != 0) {
402 std::cerr << description << std::endl;
403 return 0;
404 }
Zhiyi Zhangd8993b92019-07-04 21:58:10 -0700405 try {
406 client.getClientConf().load(configFilePath);
407 }
408 catch (const std::exception& e) {
409 std::cerr << "Cannot load the configuration file: " << e.what() << std::endl;
410 return 1;
411 }
Zhiyi Zhangaf7c2902019-03-14 22:13:21 -0700412 startApplication();
Zhiyi Zhang08e0e982017-03-01 10:10:42 -0800413 face.processEvents();
414 return 0;
415}
416
417} // namespace ndncert
418} // namespace ndn
419
Zhiyi Zhang916ba2d2018-02-01 18:16:27 -0800420int main(int argc, char* argv[])
Zhiyi Zhang08e0e982017-03-01 10:10:42 -0800421{
422 return ndn::ndncert::main(argc, argv);
423}