From b13332c991ce2695626eaca367dd8208b174c9ca Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Thu, 14 Apr 2016 17:59:24 +0300 Subject: Add support for repository authentication --- bpkg/auth | 96 ++++++ bpkg/auth.cxx | 815 +++++++++++++++++++++++++++++++++++++++++++++++++ bpkg/buildfile | 2 + bpkg/cfg-create.cxx | 4 + bpkg/cfg-fetch.cxx | 52 +++- bpkg/common.cli | 57 ++++ bpkg/database.cxx | 11 +- bpkg/fetch | 37 +-- bpkg/fetch.cxx | 101 +++--- bpkg/openssl | 31 ++ bpkg/openssl.cxx | 59 ++++ bpkg/options-types | 18 ++ bpkg/package | 116 ++++++- bpkg/package.cxx | 16 + bpkg/package.xml | 13 +- bpkg/rep-create.cli | 17 +- bpkg/rep-create.cxx | 50 ++- bpkg/rep-info.cli | 17 +- bpkg/rep-info.cxx | 61 +++- bpkg/types-parsers | 8 + bpkg/types-parsers.cxx | 20 ++ bpkg/utility | 2 +- 22 files changed, 1496 insertions(+), 107 deletions(-) create mode 100644 bpkg/auth create mode 100644 bpkg/auth.cxx create mode 100644 bpkg/openssl create mode 100644 bpkg/openssl.cxx create mode 100644 bpkg/options-types (limited to 'bpkg') diff --git a/bpkg/auth b/bpkg/auth new file mode 100644 index 0000000..54071b7 --- /dev/null +++ b/bpkg/auth @@ -0,0 +1,96 @@ +// file : bpkg/auth -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BPKG_AUTH +#define BPKG_AUTH + +#include + +#include +#include + +#include +#include + +namespace bpkg +{ + // Authenticate a repository certificate. If the configuration directory is + // NULL, then perform without a certificate database. If it is empty, then + // check if the current working directory is a configuration. If it is, then + // use its certificate database. Otherwise, continue as if it was NULL. All + // other values (including '.') are assumed to be valid configuration paths + // and will be diagnosed if that's not the case. + // + // If the configuration is used, then check if we are already in transaction. + // If so, then assume the configuration database is already opened and use + // that. Otherwise, open the database and start a new transaction. + // + // Note that one drawback of doing this as part of an existing transaction + // is that if things go south and the transaction gets aborted, then all the + // user's confirmations will be lost. For example, cfg-fetch could fail + // because it was unable to fetch some prerequisite repositories. + // + shared_ptr + authenticate_certificate (const common_options&, + const dir_path* configuration, + const optional& cert_pem, + const repository_location&); + + // Authenticate a repository. First check that the certificate can be used + // to authenticate this repository by making sure their names match. Then + // recover the packages manifest file SHA256 checksum from the signature + // and compare the calculated checksum to the recovered one. + // + // If the configuration directory is NULL, then create a temporary + // certificate PEM file (cert_pem must be present). If the directory is + // empty, then check if the current working directory is a configuration. + // If it's not, then continue as if it was NULL (cert_pem must be present). + // If it is, then continue as if a valid configuration directory was + // specified. All other values (including '.') are assumed to be valid + // configuration paths and will be diagnosed if that's not the case. In the + // case of a valid configuration use the certificate PEM file from the + // configuration (the file is supposed to have been created by the preceding + // authenticate_certificate() call). + // + void + authenticate_repository (const common_options&, + const dir_path* configuration, + const optional& cert_pem, + const certificate&, + const signature_manifest&, + const repository_location&); + + // Sign a repository by calculating its packages manifest file signature. + // This is done by encrypting the file's SHA256 checksum with the repository + // certificate's private key and then base64-encoding the result. Issue + // diagnstics and fail if the certificate has expired, and issue a warning + // if it expires in less than 2 months. The repository argument is used for + // diagnostics only. + // + // Note that currently we don't check if the key matches the certificate. A + // relatively easy way to accomplish this would be to execute the following + // commands and match the results: + // + // openssl x509 -noout -modulus -in cert.pem + // openssl rsa -noout -modulus -in key.pem + // + // But taking into account that we need to be able to use custom engines to + // access keys, it seems to be impossible to provide the same additional + // openssl options to fit both the rsa and pkeyutl commands. The first would + // require "-engine pkcs11 -inform engine", while the second -- "-engine + // pkcs11 -keyform engine". Also it would require to enter the key password + // again, which is a showstopper. Maybe the easiest would be to recover the + // sum back from the signature using the certificate, and compare it with + // the original sum (like we do in authenticate_repository()). But that + // would require to temporarily save the certificate to file. + // + std::vector + sign_repository (const common_options&, + const string& sha256sum, + const string& key_name, // --key option value + const string& cert_pem, + const dir_path& repository); +} + +#endif // BPKG_AUTH diff --git a/bpkg/auth.cxx b/bpkg/auth.cxx new file mode 100644 index 0000000..5296bc8 --- /dev/null +++ b/bpkg/auth.cxx @@ -0,0 +1,815 @@ +// file : bpkg/auth.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include + +#include +#include // numeric_limits +#include +#include // strlen(), strcmp() +#include // ostreambuf_iterator, istreambuf_iterator + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace std; +using namespace butl; + +namespace bpkg +{ + // Find the repository location prefix that ends with the version component. + // We consider all repositories under this location to be related. + // + static string + name_prefix (const repository_location& rl) + { + assert (rl.absolute () || rl.remote ()); + + // Construct the prefix as a relative repository location. + // + string p ("."); + for (auto i (rl.path ().rbegin ()), e (rl.path ().rend ()); i != e; ++i) + { + const string& c (*i); + if (!c.empty () && c.find_first_not_of ("1234567890") == string::npos) + break; + + p = "../" + p; + } + + // If this is a remote location then use the canonical name prefix. For + // a local location this doesn't always work. Consider: + // + // .../pkg/1/build2.org/common/hello + // + // In this case we will end with an empty canonical name (because of + // the special pkg/1 treatment). So in case of local locations we will + // use the location rather than the name prefix. + // + if (rl.remote ()) + return repository_location (p, rl).canonical_name (); + else + { + path lp (rl.path () / path (p)); + lp.normalize (); + return lp.string (); + } + } + + // Authenticate a dummy certificate. If trusted, it will authenticate all + // the (unsigned) repositories under the location prefix of up-to-the- + // version component. + // + static shared_ptr + auth_dummy (const common_options& co, + const string& fp, + const repository_location& rl) + { + tracer trace ("auth_dummy"); + + shared_ptr cert ( + make_shared (fp, name_prefix (rl))); + + l4 ([&]{trace << "new cert: " << *cert;}); + + if (co.trust_yes ()) + { + if (verb) + info << "trusting unsigned repository " << rl.canonical_name (); + } + else + { + (co.trust_no () ? error : warn) << "repository " << rl.canonical_name () + << " is unsigned"; + } + + if (co.trust_no () || + (!co.trust_yes () && + !yn_prompt ( + string ("continue without authenticating repositories at " + + cert->name + "? [y/N]").c_str (), 'n'))) + throw failed (); + + return cert; + } + + // Calculate the real repository certificate fingerprint. Return the compact + // form (no colons, lower case). + // + static string + real_fingerprint (const common_options& co, + const string& pem, + const repository_location& rl) + { + tracer trace ("real_fingerprint"); + + try + { + process pr (start_openssl ( + co, "x509", {"-sha256", "-noout", "-fingerprint"}, true, true)); + + ifdstream is (pr.in_ofd); + is.exceptions (ifdstream::badbit); + + try + { + ofdstream os (pr.out_fd); + os.exceptions (ofdstream::badbit); + os << pem; + os.close (); + + string s; + const size_t n (19); + if (!(getline (is, s) && s.size () > n && + s.compare (0, n, "SHA256 Fingerprint=") == 0)) + throw istream::failure (""); + + string fp; + + try + { + fp = fingerprint_to_sha256 (string (s, n)); + } + catch (const invalid_argument&) + { + throw istream::failure (""); + } + + is.close (); + + if (pr.wait ()) + return fp; + + // Fall through. + // + } + catch (const istream::failure&) + { + // Child input writing or output reading error. + // + is.close (); + + // Child exit status doesn't matter. Just wait for the process + // completion and fall through. + // + pr.wait (); + } + + error << "unable to calculate certificate fingerprint for " + << rl.canonical_name (); + + // Fall through. + } + catch (const process_error& e) + { + error << "unable to calculate certificate fingerprint for " + << rl.canonical_name () << ": " << e.what (); + + // Fall through. + } + + throw failed (); + } + + // Calculate the repository certificate fingerprint. Return the compact form + // (no colons, lower case). + // + static string + cert_fingerprint (const common_options& co, + const optional& pem, + const repository_location& rl) + { + return pem + ? real_fingerprint (co, *pem, rl) + : sha256 (name_prefix (rl)).string (); + } + + // Parse the PEM-encoded certificate representation. + // + static shared_ptr + parse_cert (const common_options& co, + const string& fp, + const string& pem, + const string& repo) + { + tracer trace ("parse_cert"); + + try + { + // The order of the options we pass to openssl determines the order in + // which we get things in the output. And want we expect is this + // (leading space added): + // + // subject= + // CN=name:cppget.org + // O=Code Synthesis + // notBefore=Apr 7 12:20:58 2016 GMT + // notAfter=Apr 7 12:20:58 2017 GMT + // info@cppget.org + // + // The first line must be "subject=" (it cannot be omitted from the + // cert). After it we have one or more lines indented with four spaces + // that specify the components. We are interested in CN and O, though + // there could be others which we ignore. Then we must have the + // notBefore and notAfter dates, again they presumably must be there. + // The final line should be the email but will be silently missing if + // the cert has no email. + // + process pr (start_openssl ( + co, + "x509", + { + "-noout", + "-subject", + "-dates", + "-email", + "-nameopt", "RFC2253,sep_multiline" + }, + true, + true)); + + ifdstream is (pr.in_ofd); + is.exceptions (ifdstream::badbit); + + try + { + ofdstream os (pr.out_fd); + os.exceptions (ofdstream::badbit); + + // Reading from and writing to the child process standard streams from + // the same thread is generally a bad idea. Depending on the program + // implementation we can block on writing if the process input pipe + // buffer get filled. That can happen if the process do not read + // anymore, being blocked on writing to the filled output pipe, which + // get filled not being read on the other end. + // + // Fortunatelly openssl reads the certificate before performing any + // output. + // + os << pem; + os.close (); + + auto bad_cert ([](const string& d) {throw invalid_argument (d);}); + + auto get = [&is, &trace] (string& s) -> bool + { + bool r (getline (is, s)); + l6 ([&]{trace << s;}); + return r; + }; + + string s; + if (!get (s) || s != "subject= ") + bad_cert ("no subject"); + + // Parse RDN (relative distinguished name). + // + auto parse_rdn = [&s, &bad_cert] (size_t o, const char* name) -> string + { + string r (s.substr (o)); + if (r.empty ()) + bad_cert (name + string (" is empty")); + + return r; + }; + + auto parse_date = [&s](size_t o, const char* name) -> timestamp + { + // Certificate dates are internally represented as ASN.1 + // GeneralizedTime and UTCTime + // (http://www.obj-sys.com/asn1tutorial/node14.html). They are + // printed by openssl in the 'MON DD HH:MM:SS[.fff][ GMT]' format. + // MON is a month abbreviated name (C locale), .fff is a fraction + // of a second expressed in milliseconds, timezone is either GMT or + // absent (means local time). Examples: + // + // Apr 11 10:20:02 2016 GMT + // Apr 11 10:20:02 2016 + // Apr 11 10:20:02.123 2016 GMT + // Apr 11 10:20:02.123 2016 + // + // We will require the date to be in GMT, as generally can not + // interpret the certificate origin local time. Note: + // openssl-generated certificate dates are always in GMT, and with + // milliseconds omitted. + // + try + { + // Assume the global locale is not changed, and still "C". + // + const char* end; + timestamp t (from_string ( + s.c_str () + o, "%b %d %H:%M:%S%[.M] %Y", false, &end)); + + if (strcmp (end, " GMT") == 0) + return t; + } + catch (const system_error&) + { + } + + throw invalid_argument ("invalid " + string (name) + " date"); + }; + + string name; + string org; + while (get (s)) + { + if (s.compare (0, 7, " CN=") == 0) + name = parse_rdn (7, "common name"); + else if (s.compare (0, 6, " O=") == 0) + org = parse_rdn (6, "organization name"); + else if (s.compare (0, 4, " ") != 0) + break; // End of the subject sub-lines. + } + + if (name.empty ()) + bad_cert ("no common name (CN)"); + + if (name.compare (0, 5, "name:") != 0) + bad_cert ("no 'name:' prefix in the common name (CN)"); + + name = name.substr (5); + if (name.empty ()) + bad_cert ("no repository name in the common name (CN)"); + + if (org.empty ()) + bad_cert ("no organization name (O)"); + + if (!is || s.compare (0, 10, "notBefore=") != 0) + bad_cert ("no start date"); + + timestamp not_before (parse_date (10, "start")); + + if (!get (s) || s.compare (0, 9, "notAfter=") != 0) + bad_cert ("no end date"); + + timestamp not_after (parse_date (9, "end")); + + if (not_before >= not_after) + bad_cert ("invalid date range"); + + string email; + if (!get (email) || email.empty ()) + bad_cert ("no email"); + + // Ensure no data left in the stream. + // + if (is.peek () != ifdstream::traits_type::eof ()) + bad_cert ("unexpected data"); + + is.close (); + + shared_ptr cert ( + make_shared ( + fp, + move (name), + move (org), + move (email), + move (not_before), + move (not_after))); + + if (pr.wait ()) + return cert; + + // Fall through. + // + } + catch (const istream::failure&) + { + // Child input writing or output reading error. + // + is.close (); + + // Child exit status doesn't matter. Just wait for the process + // completion and fall through. + // + pr.wait (); + } + catch (const invalid_argument& e) + { + // Certificate parsing error. Skip until the end, not to offend the + // child with the broken pipe. Never knows how it will take it. + // + if (!is.eof ()) + is.ignore (numeric_limits::max ()); + + is.close (); + + // If the child exited with an error status, then omit any output + // parsing diagnostics since we were probably parsing garbage. + // + if (pr.wait ()) + { + error << "invalid certificate for " << repo << ": " << e.what (); + throw failed (); + } + + // Fall through. + } + + error << "unable to parse certificate for " << repo; + + // Fall through. + } + catch (const process_error& e) + { + error << "unable to parse certificate for " << repo << ": " << e.what (); + + // Fall through. + } + + throw failed (); + } + + // Verify the certificate (validity period and such). + // + static void + verify_cert (const certificate& cert, const repository_location& rl) + { + if (!cert.dummy ()) + { + if (cert.expired ()) + fail << "certificate for repository " << rl.canonical_name () + << " has expired"; + } + } + + // Authenticate a real certificate. + // + static shared_ptr + auth_real (const common_options& co, + const string& fp, + const string& pem, + const repository_location& rl) + { + tracer trace ("auth_real"); + + shared_ptr cert ( + parse_cert (co, fp, pem, rl.canonical_name ())); + + l4 ([&]{trace << "new cert: " << *cert;}); + + verify_cert (*cert, rl); + + string cert_fp (sha256_to_fingerprint (cert->fingerprint)); + + // @@ Is there a way to intercept CLI parsing for the specific option of + // the standard type to validate/convert the value? If there were, we could + // validate the option value converting fp to sha (internal representation + // of fp). + // + // @@ Not easily/cleanly. The best way is to derive a custom type which + // will probably be an overkill here. + // + bool trust (co.trust_yes () || + co.trust ().find (cert_fp) != co.trust ().end ()); + + if (trust) + { + if (verb) + info << "trusting non-authenticated certificate for repository " + << rl.canonical_name (); + + return cert; + } + + (co.trust_no () ? error : warn) + << "authenticity of the certificate for repository " + << rl.canonical_name () << " cannot be established"; + + if (!co.trust_no () && verb) + { + text << "certificate is for " << cert->name << ", \"" + << cert->organization << "\" <" << cert->email << ">"; + + text << "certificate SHA256 fingerprint is " << cert_fp; + } + + if (co.trust_no () || !yn_prompt ("trust this certificate? [y/N]", 'n')) + throw failed (); + + return cert; + } + + static const dir_path certs_dir (".bpkg/certs"); + + // Authenticate a certificate with the database. First check if it is + // already authenticated. If not, authenticate and add to the database. + // + static shared_ptr + auth_cert (const common_options& co, + const dir_path& conf, + database& db, + const optional& pem, + const repository_location& rl) + { + tracer trace ("auth_cert"); + tracer_guard tg (db, trace); + + string fp (cert_fingerprint (co, pem, rl)); + shared_ptr cert (db.find (fp)); + + if (cert != nullptr) + { + l4 ([&]{trace << "existing cert: " << *cert;}); + verify_cert (*cert, rl); + return cert; + } + + cert = pem ? auth_real (co, fp, *pem, rl) : auth_dummy (co, fp, rl); + db.persist (cert); + + // Save the certificate file. + // + if (pem) + { + dir_path d (conf / certs_dir); + if (!dir_exists (d)) + mk (d); + + path f (d / path (fp + ".pem")); + + try + { + ofstream ofs; + ofs.exceptions (ofstream::badbit | ofstream::failbit); + ofs.open (f.string ()); + ofs << *pem; + } + catch (const ofstream::failure&) + { + fail << "unable to write certificate to " << f; + } + } + + return cert; + } + + static const dir_path current_dir ("."); + + shared_ptr + authenticate_certificate (const common_options& co, + const dir_path* conf, + const optional& pem, + const repository_location& rl) + { + tracer trace ("authenticate_certificate"); + + if (co.trust_no () && co.trust_yes ()) + fail << "--trust-yes and --trust-no are mutually exclusive"; + + if (conf != nullptr && conf->empty ()) + conf = dir_exists (path (".bpkg")) ? ¤t_dir : nullptr; + + assert (conf == nullptr || !conf->empty ()); + + shared_ptr r; + + if (conf == nullptr) + { + // If we have no configuration, go straight to authenticating a new + // certificate. + // + string fp (cert_fingerprint (co, pem, rl)); + r = pem ? auth_real (co, fp, *pem, rl) : auth_dummy (co, fp, rl); + } + else if (transaction::has_current ()) + { + r = auth_cert (co, *conf, transaction::current ().database (), pem, rl); + } + else + { + database db (open (*conf, trace)); + transaction t (db.begin ()); + r = auth_cert (co, *conf, db, pem, rl); + t.commit (); + } + + return r; + } + + void + authenticate_repository (const common_options& co, + const dir_path* conf, + const optional& cert_pem, + const certificate& cert, + const signature_manifest& sm, + const repository_location& rl) + { + tracer trace ("authenticate_repository"); + + if (conf != nullptr && conf->empty ()) + conf = dir_exists (path (".bpkg")) ? ¤t_dir : nullptr; + + assert (conf == nullptr || !conf->empty ()); + + path f; + auto_rmfile rm; + + if (conf == nullptr) + { + // If we have no configuration, create the temporary certificate + // PEM file. + // + assert (cert_pem); + + try + { + f = path::temp_path ("bpkg"); + + ofstream ofs; + ofs.exceptions (ofstream::badbit | ofstream::failbit); + ofs.open (f.string ()); + rm = auto_rmfile (f); + ofs << *cert_pem; + } + catch (const ofstream::failure&) + { + fail << "unable to save certificate to temporary file " << f; + } + catch (const system_error& e) + { + fail << "unable to obtain temporary file: " << e.what (); + } + } + else + { + f = *conf / certs_dir / path (cert.fingerprint + ".pem"); + } + + const string& c (cert.name); + const string& r (rl.canonical_name ()); + const size_t cn (c.size ()); + const size_t rn (r.size ()); + + // Make sure the names are either equal or the certificate name is a + // prefix (at /-boundary) of the repository name. + // + if (!(r.compare (0, cn, c) == 0 && + (rn == cn || (rn > cn && r[cn] == '/')))) + fail << "certificate name mismatch for repository " << r << + info << "certificate name is " << c; + + try + { + process pr (start_openssl ( + co, "pkeyutl", + { + "-verifyrecover", + "-certin", + "-inkey", + f.string ().c_str () + }, + true, + true)); + + ifdstream is (pr.in_ofd); + is.exceptions (ifdstream::badbit); + + try + { + ofdstream os (pr.out_fd); + os.exceptions (ofdstream::badbit); + + for (const auto& c: sm.signature) + os.put (c); // Sets badbit on failure. + + os.close (); + + string s; + bool v (getline (is, s) && is.eof ()); + is.close (); + + if (pr.wait () && v) + { + if (s != sm.sha256sum) + fail << "packages manifest file signature mismatch for " + << rl.canonical_name (); + + return; // All good. + } + + // Fall through. + // + } + catch (const istream::failure&) + { + // Child input writing or output reading error. + // + is.close (); + + // Child exit status doesn't matter. Just wait for the process + // completion and fall through. + // + pr.wait (); + } + + error << "unable to authenticate repository " << rl.canonical_name (); + + // Fall through. + } + catch (const process_error& e) + { + error << "unable to authenticate repository " + << rl.canonical_name () << ": " << e.what (); + + // Fall through. + } + + throw failed (); + } + + vector + sign_repository (const common_options& co, + const string& sha256sum, + const string& key_name, + const string& cert_pem, + const dir_path& repository) + { + tracer trace ("sign_repository"); + + string r (repository.string () + dir_path::traits::directory_separator); + + // No sense to calculate the fingerprint for the certificate being used + // just to check the expiration date. + // + shared_ptr cert (parse_cert (co, "", cert_pem, r)); + + timestamp now (timestamp::clock::now ()); + + if (cert->end_date < now) + fail << "certificate for repository " << r << " has expired"; + + using days = chrono::duration>; + + days left (chrono::duration_cast (cert->end_date - now)); + if (left < days (60)) + warn << "certificate for repository " << r + << " expires in less than " << left.count () + 1 << " day(s)"; + + try + { + process pr (start_openssl ( + co, "pkeyutl", {"-sign", "-inkey", key_name.c_str ()}, true, true)); + + ifdstream is (pr.in_ofd); + is.exceptions (ifdstream::badbit); + + try + { + ofdstream os (pr.out_fd); + os.exceptions (ofdstream::badbit); + os << sha256sum; + os.close (); + + // Additional parentheses required to make compiler to distinguish + // the variable definition from a function declaration. + // + vector signature + ((istreambuf_iterator (is)), istreambuf_iterator ()); + + is.close (); + + if (pr.wait ()) + return signature; + + // Fall through. + // + } + catch (const istream::failure&) + { + // Child input writing or output reading error. + // + is.close (); + + // Child exit status doesn't matter. Just wait for the process + // completion and fall through. + // + pr.wait (); + } + + error << "unable to sign repository " << r; + + // Fall through. + } + catch (const process_error& e) + { + error << "unable to sign repository " << r << ": " << e.what (); + + // Fall through. + } + + throw failed (); + } +} diff --git a/bpkg/buildfile b/bpkg/buildfile index 5984b53..dec1aa7 100644 --- a/bpkg/buildfile +++ b/bpkg/buildfile @@ -9,6 +9,7 @@ import libs += libodb-sqlite%lib{odb-sqlite} exe{bpkg}: \ {hxx cxx}{ archive } \ +{hxx cxx}{ auth } \ {hxx }{ bpkg-version } \ { cxx}{ bpkg } {hxx ixx cxx}{ bpkg-options } \ {hxx cxx}{ cfg-add } {hxx ixx cxx}{ cfg-add-options } \ @@ -23,6 +24,7 @@ exe{bpkg}: \ {hxx }{ forward } \ {hxx cxx}{ help } {hxx ixx cxx}{ help-options } \ {hxx cxx}{ manifest-utility } \ +{hxx cxx}{ openssl } \ {hxx ixx cxx}{ package } \ {hxx ixx cxx}{ package-odb } file{ package.xml } \ {hxx cxx}{ pkg-build } {hxx ixx cxx}{ pkg-build-options } \ diff --git a/bpkg/cfg-create.cxx b/bpkg/cfg-create.cxx index e50da8d..6d727fd 100644 --- a/bpkg/cfg-create.cxx +++ b/bpkg/cfg-create.cxx @@ -138,6 +138,10 @@ namespace bpkg // run_b (o, c, "configure(" + c.string () + "/)", true, vars); // Run quiet. + // Create .bpkg/. + // + mk (c / dir_path (".bpkg")); + // Create the database. // database db (open (c, trace, true)); diff --git a/bpkg/cfg-fetch.cxx b/bpkg/cfg-fetch.cxx index d311a21..dfbcc98 100644 --- a/bpkg/cfg-fetch.cxx +++ b/bpkg/cfg-fetch.cxx @@ -8,6 +8,7 @@ #include +#include #include #include #include @@ -20,7 +21,7 @@ using namespace butl; namespace bpkg { static void - cfg_fetch (const common_options& co, + cfg_fetch (const configuration_options& co, transaction& t, const shared_ptr& r, const shared_ptr& root, @@ -55,25 +56,50 @@ namespace bpkg r->fetched = true; // Mark as being fetched. - // Load the 'packages' file. We do this first so that we can get and - // verify the checksum of the 'repositories' file which below. + // Load the 'repositories' file and use it to populate the prerequisite + // and complement repository sets. // - package_manifests pms (fetch_packages (co, rl, true)); + pair rmc ( + fetch_repositories (co, rl, true)); - // Load the 'repositories' file and use it to populate the prerequisite and - // complement repository sets. - // - repository_manifests rms; + repository_manifests& rms (rmc.first); + + bool a (co.auth () != auth::none && + (co.auth () == auth::all || rl.remote ())); - try + shared_ptr cert; + + if (a) { - rms = fetch_repositories (co, rl, pms.sha256sum, true); + cert = authenticate_certificate ( + co, &co.directory (), rms.back ().certificate, rl); + + a = !cert->dummy (); } - catch (const checksum_mismatch&) - { - fail << "repository files checksum mismatch for " + + // Load the 'packages' file. + // + pair pmc ( + fetch_packages (co, rl, true)); + + package_manifests& pms (pmc.first); + + if (rmc.second != pms.sha256sum) + fail << "repositories manifest file checksum mismatch for " << rl.canonical_name () << info << "try again"; + + if (a) + { + signature_manifest sm (fetch_signature (co, rl, true)); + + if (sm.sha256sum != pmc.second) + fail << "packages manifest file checksum mismatch for " + << rl.canonical_name () << + info << "try again"; + + assert (cert != nullptr); + authenticate_repository (co, &co.directory (), nullopt, *cert, sm, rl); } for (repository_manifest& rm: rms) diff --git a/bpkg/common.cli b/bpkg/common.cli index f15bb60..587d32d 100644 --- a/bpkg/common.cli +++ b/bpkg/common.cli @@ -2,7 +2,10 @@ // copyright : Copyright (c) 2014-2016 Code Synthesis Ltd // license : MIT; see accompanying LICENSE file +include ; + include ; +include ; "\section=1" "\name=bpkg-common-options" @@ -157,6 +160,60 @@ namespace bpkg multiple tar options." } + path --openssl = "openssl" + { + "", + "The openssl program to be used for crypto operations. You can also + specify additional options that should be passed to the openssl + program with \cb{--openssl-option}. If the openssl program is not + explicitly specified, then \cb{bpkg} will use \cb{openssl} by default." + } + + strings --openssl-option + { + "", + "Additional option to be passed to the openssl program. See + \cb{--openssl} for more information on the openssl program. Repeat this + option to specify multiple openssl options." + } + + bpkg::auth --auth = bpkg::auth::remote + { + "", + "Repository types be authenticated. Valid values for this option are + \cb{none}, \cb{remote}, \cb{all}. By default only remote repositories + are authenticated. You can request authentication of local repositories + by passing \cb{all} or disable authentication completely by passing + \cb{none}." + } + + std::set --trust + { + "", + "Trust repository certificate with a SHA256 . Such a + certificate is trusted automatically, without prompting the user for + a confirmation. Repeat this option to trust multiple certificates. + + Note that by default \cb{openssl} prints a SHA1 fingerprint and to + obtain a SHA256 one you will need to pass the \cb{-sha256} option, + for example: + + \ + openssl x509 -sha256 -fingerprint -noout -in cert.pem + \ + " + } + + bool --trust-yes|-y + { + "Assume the answer to all authentication prompts is \cb{yes}." + } + + bool --trust-no|-n + { + "Assume the answer to all authentication prompts is \cb{no}." + } + string --pager // String to allow empty value. { "", diff --git a/bpkg/database.cxx b/bpkg/database.cxx index 54672d1..bffba02 100644 --- a/bpkg/database.cxx +++ b/bpkg/database.cxx @@ -21,7 +21,16 @@ namespace bpkg { tracer trace ("open"); - path f (d / path ("bpkg.sqlite3")); + // @@ Shouldn't we create database file in d / ".bpkg" directory ? + // + // @@ Yes, let's do it. Also perhaps downloaded packages as well? + // We might as well. + // + // @@ Don't think would be natural to keep package archives there as, the + // user should see which packages are downloaded without need to look + // into the "hidden" directory. + // + path f (d / path (".bpkg/bpkg.sqlite3")); if (!create && !exists (f)) fail << d << " does not look like a bpkg configuration directory"; diff --git a/bpkg/fetch b/bpkg/fetch index 2867243..2153df4 100644 --- a/bpkg/fetch +++ b/bpkg/fetch @@ -14,23 +14,26 @@ namespace bpkg { - class checksum_mismatch: public std::exception {}; - - repository_manifests fetch_repositories (const dir_path&, - bool ignore_unknown); - - // Verify the checksum and throw checksum_mismatch if it doesn't match. - // - repository_manifests fetch_repositories (const common_options&, - const repository_location&, - const string& sha256sum, - bool ignore_unknown); - - package_manifests fetch_packages (const dir_path&, - bool ignore_unknown); - package_manifests fetch_packages (const common_options&, - const repository_location&, - bool ignore_unknown); + repository_manifests + fetch_repositories (const dir_path&, bool ignore_unknown); + + pair + fetch_repositories (const common_options&, + const repository_location&, + bool ignore_unknown); + + package_manifests + fetch_packages (const dir_path&, bool ignore_unknown); + + pair + fetch_packages (const common_options&, + const repository_location&, + bool ignore_unknown); + + signature_manifest + fetch_signature (const common_options&, + const repository_location&, + bool ignore_unknown); path fetch_archive (const common_options&, diff --git a/bpkg/fetch.cxx b/bpkg/fetch.cxx index 9a5c74a..89c93e2 100644 --- a/bpkg/fetch.cxx +++ b/bpkg/fetch.cxx @@ -524,16 +524,13 @@ namespace bpkg return r; } - // If sha256sum is empty, then don't verify. - // template - static M + static pair fetch_manifest (const common_options& o, protocol proto, const string& host, uint16_t port, const path& f, - const string& sha256sum, bool ignore_unknown) { string url (to_url (proto, host, port, f)); @@ -544,35 +541,24 @@ namespace bpkg ifdstream is (pr.in_ofd); is.exceptions (ifdstream::badbit | ifdstream::failbit); - M m; - if (sha256sum.empty ()) - { - manifest_parser mp (is, url); - m = M (mp, ignore_unknown); - is.close (); - } - else - { - // Unfortunately we cannot rewind STDOUT as we do below for files. - // There doesn't seem to be anything better than reading the entire - // file into memory and then streaming it twice, once to calculate - // the checksum and the second time to actually parse. - // - stringstream ss; - ss << is.rdbuf (); - is.close (); - - if (sha256sum != sha256 (o, ss)) - throw checksum_mismatch (); - - ss.clear (); ss.seekg (0); // Rewind. - - manifest_parser mp (ss, url); - m = M (mp, ignore_unknown); - } + // Unfortunately we cannot rewind STDOUT as we do below for files. There + // doesn't seem to be anything better than reading the entire file into + // memory and then streaming it twice, once to calculate the checksum + // and the second time to actually parse. + // + stringstream ss; + ss << is.rdbuf (); + is.close (); + + string sha256sum (sha256 (o, ss)); + + ss.clear (); ss.seekg (0); // Rewind. + + manifest_parser mp (ss, url); + M m (mp, ignore_unknown); if (pr.wait ()) - return m; + return make_pair (move (m), move (sha256sum)); // Child existed with an error, fall through. } @@ -645,13 +631,12 @@ namespace bpkg return r; } - // If sha256sum is empty, then don't verify. + // If o is nullptr, then don't calculate the checksum. // template - static M + static pair fetch_manifest (const common_options* o, const path& f, - const string& sha256sum, bool ignore_unknown) { if (!exists (f)) @@ -663,18 +648,15 @@ namespace bpkg ifs.exceptions (ofstream::badbit | ofstream::failbit); ifs.open (f.string ()); - if (!sha256sum.empty ()) + string sha256sum; + if (o != nullptr) { - assert (o != nullptr); - - if (sha256sum != sha256 (*o, ifs)) - throw checksum_mismatch (); - + sha256sum = sha256 (*o, ifs); ifs.seekg (0); // Rewind the file stream. } manifest_parser mp (ifs, f.string ()); - return M (mp, ignore_unknown); + return make_pair (M (mp, ignore_unknown), move (sha256sum)); } catch (const manifest_parsing& e) { @@ -694,16 +676,12 @@ namespace bpkg fetch_repositories (const dir_path& d, bool iu) { return fetch_manifest ( - nullptr, - d / repositories, - "", // No checksum verification. - iu); + nullptr, d / repositories, iu).first; } - repository_manifests + pair fetch_repositories (const common_options& o, const repository_location& rl, - const string& sha256sum, bool iu) { assert (rl.remote () || rl.absolute ()); @@ -712,8 +690,8 @@ namespace bpkg return rl.remote () ? fetch_manifest ( - o, rl.proto (), rl.host (), rl.port (), f, sha256sum, iu) - : fetch_manifest (&o, f, sha256sum, iu); + o, rl.proto (), rl.host (), rl.port (), f, iu) + : fetch_manifest (&o, f, iu); } static const path packages ("packages"); @@ -721,10 +699,10 @@ namespace bpkg package_manifests fetch_packages (const dir_path& d, bool iu) { - return fetch_manifest (nullptr, d / packages, "", iu); + return fetch_manifest (nullptr, d / packages, iu).first; } - package_manifests + pair fetch_packages (const common_options& o, const repository_location& rl, bool iu) @@ -735,8 +713,25 @@ namespace bpkg return rl.remote () ? fetch_manifest ( - o, rl.proto (), rl.host (), rl.port (), f, "", iu) - : fetch_manifest (&o, f, "", iu); + o, rl.proto (), rl.host (), rl.port (), f, iu) + : fetch_manifest (&o, f, iu); + } + + static const path signature ("signature"); + + signature_manifest + fetch_signature (const common_options& o, + const repository_location& rl, + bool iu) + { + assert (rl.remote () || rl.absolute ()); + + path f (rl.path () / signature); + + return rl.remote () + ? fetch_manifest ( + o, rl.proto (), rl.host (), rl.port (), f, iu).first + : fetch_manifest (nullptr, f, iu).first; } path diff --git a/bpkg/openssl b/bpkg/openssl new file mode 100644 index 0000000..c2f8712 --- /dev/null +++ b/bpkg/openssl @@ -0,0 +1,31 @@ +// file : bpkg/openssl -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BPKG_OPENSSL +#define BPKG_OPENSSL + +#include + +#include +#include + +#include + +namespace bpkg +{ + // Start the openssl process. Parameters in, out, err flags if the caller + // wish to write to, or read from the process STDIN, STDOUT, STDERR streams. + // If out and err are both true, then STDERR is redirected to STDOUT, and + // they both can be read from in_ofd descriptor. + // + butl::process + start_openssl (const common_options&, + const char* command, + const cstrings& options, + bool in = false, + bool out = false, + bool err = false); +} + +#endif // BPKG_OPENSSL diff --git a/bpkg/openssl.cxx b/bpkg/openssl.cxx new file mode 100644 index 0000000..67500b6 --- /dev/null +++ b/bpkg/openssl.cxx @@ -0,0 +1,59 @@ +// file : bpkg/openssl.cxx -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#include + +#include +#include + +#include + +using namespace std; +using namespace butl; + +namespace bpkg +{ + process + start_openssl (const common_options& co, + const char* command, + const cstrings& options, + bool in, + bool out, + bool err) + { + cstrings args {co.openssl ().string ().c_str (), command}; + + // Add extra options. Normally the order of options is not important + // (unless they override each other). However, openssl 1.0.1 seems to have + // bugs in that department (that were apparently fixed in 1.0.2). To work + // around these bugs we pass user-supplied options first. + // + for (const string& o: co.openssl_option ()) + args.push_back (o.c_str ()); + + args.insert (args.end (), options.begin (), options.end ()); + args.push_back (nullptr); + + if (verb >= 2) + print_process (args); + + try + { + // If the caller is interested in reading STDOUT and STDERR, then + // redirect STDERR to STDOUT, so both can be read from the same stream. + // + return process ( + args.data (), in ? -1 : 0, out ? -1 : 1, err ? (out ? 1 : -1): 2); + } + catch (const process_error& e) + { + error << "unable to execute " << args[0] << ": " << e.what (); + + if (e.child ()) + exit (1); + + throw failed (); + } + } +} diff --git a/bpkg/options-types b/bpkg/options-types new file mode 100644 index 0000000..88e6b67 --- /dev/null +++ b/bpkg/options-types @@ -0,0 +1,18 @@ +// file : bpkg/options-types -*- C++ -*- +// copyright : Copyright (c) 2014-2016 Code Synthesis Ltd +// license : MIT; see accompanying LICENSE file + +#ifndef BPKG_OPTIONS_TYPES +#define BPKG_OPTIONS_TYPES + +namespace bpkg +{ + enum class auth + { + none, + remote, + all + }; +} + +#endif // BPKG_OPTIONS_TYPES diff --git a/bpkg/package b/bpkg/package index 779f0e2..2bfc575 100644 --- a/bpkg/package +++ b/bpkg/package @@ -7,14 +7,19 @@ #include #include +#include +#include +#include // static_assert #include #include +#include + #include #include -#pragma db model version(2, 2, closed) +#pragma db model version(3, 3, open) namespace bpkg { @@ -50,6 +55,29 @@ namespace bpkg to((?) ? (?)->string () : bpkg::optional_string ()) \ from((?) ? bpkg::dir_path (*(?)) : bpkg::optional_dir_path ()) + // timestamp + // + using butl::timestamp; + using butl::timestamp_unknown; + + // Ensure that timestamp can be represented in nonoseconds without loss of + // accuracy, so the following ODB mapping is adequate. + // + static_assert ( + std::ratio_greater_equal::value, + "The following timestamp ODB mapping is invalid"); + + // As pointed out in butl/timestamp we will overflow in year 2262, but by + // that time some larger basic type will be available for mapping. + // + #pragma db map type(timestamp) as(uint64_t) \ + to(std::chrono::duration_cast ( \ + (?).time_since_epoch ()).count ()) \ + from(butl::timestamp ( \ + std::chrono::duration_cast ( \ + std::chrono::nanoseconds (?)))) + // An image type that is used to map version to the database since // there is no way to modify individual components directly. We have // to define it before including since some value @@ -176,7 +204,7 @@ namespace bpkg // repository // - #pragma db object pointer(std::shared_ptr) session + #pragma db object pointer(shared_ptr) session class repository { public: @@ -465,6 +493,90 @@ namespace bpkg selected_package () = default; }; + + // certificate + // + // Information extracted from a repository X.509 certificate. The actual + // certificate is stored on disk as .bpkg/certs/.pem (we have + // to store it as a file because that's the only way to pass it to openssl). + // + // If a repository is not authenticated (has no certificate/signature, + // called unauth from now on), then we ask for the user's confirmation and + // create a dummy certificate in order not to ask for the same confirmation + // (for this repository) on next fetch. The problem is, there could be + // multiple sections in such a repository and it would be annoying to + // confirm all of them. So what we are going to do is create a dummy + // certificate not for this specific repository location but for a + // repository location only up to the version, so the name member will + // contain the name prefix rather than the full name (just like a normal + // certificate would). The fingerprint member for such a dummy certificate + // contains the SHA256 checksum of this name. Members other then name and + // fingerprint are meaningless for the dummy certificate. + // + #pragma db object pointer(shared_ptr) session + class certificate + { + public: + string fingerprint; // Object id (note: SHA256 fingerprint). + + string name; // CN component of Subject. + string organization; // O component of Subject. + string email; // email: in Subject Alternative Name. + + timestamp start_date; // notBefore (UTC) + timestamp end_date; // notAfter (UTC) + + bool + dummy () const {return start_date == timestamp_unknown;} + + bool + expired () const + { + assert (!dummy ()); + return timestamp::clock::now () > end_date; + } + + public: + certificate (string f, + string n, + string o, + string e, + timestamp sd, + timestamp ed) + : fingerprint (move (f)), + name (move (n)), + organization (move (o)), + email (move (e)), + start_date (move (sd)), + end_date (move (ed)) + { + } + + // Create dummy certificate. + // + certificate (string f, string n) + : fingerprint (move (f)), + name (move (n)), + start_date (timestamp_unknown), + end_date (timestamp_unknown) + { + } + + // Database mapping. + // + #pragma db member(fingerprint) id + + private: + friend class odb::access; + certificate () = default; + }; + + // Note: prints all the certificate information on one line so mostly + // useful for tracing. + // + ostream& + operator<< (ostream&, const certificate&); + // Return a list of packages that depend on this package along with // their constraints. // diff --git a/bpkg/package.cxx b/bpkg/package.cxx index a72ab21..2cf96ff 100644 --- a/bpkg/package.cxx +++ b/bpkg/package.cxx @@ -129,4 +129,20 @@ namespace bpkg else if (s == "configured") return package_state::configured; else throw invalid_argument ("invalid package state '" + s + "'"); } + + // certificate + // + ostream& + operator<< (ostream& os, const certificate& c) + { + using butl::operator<<; + + if (c.dummy ()) + os << c.name << " (dummy)"; + else + os << c.name << ", \"" << c.organization << "\" <" << c.email << ">, " + << c.start_date << " - " << c.end_date << ", " << c.fingerprint; + + return os; + } } diff --git a/bpkg/package.xml b/bpkg/package.xml index 0e0872d..900794c 100644 --- a/bpkg/package.xml +++ b/bpkg/package.xml @@ -1,5 +1,5 @@ - + @@ -231,5 +231,16 @@
+ + + + + + + + + + +
diff --git a/bpkg/rep-create.cli b/bpkg/rep-create.cli index fc11a0c..a48922e 100644 --- a/bpkg/rep-create.cli +++ b/bpkg/rep-create.cli @@ -20,9 +20,12 @@ namespace bpkg \h|DESCRIPTION| The \cb{rep-create} command regenerates the \cb{packages} manifest file - based on the files present in the repository directory. If is not - specified, then the current working directory is used as the repository - root." + based on the files present in the repository directory. If the + \cb{repositories} manifest file contains a certificate, then the + \cb{signature} manifest file is regenerated as well. In this case the + \cb{--key} option must be used to specify the certificate's private + key. If is not specified, then the current working directory is + used as the repository root." } class rep_create_options: common_options @@ -33,5 +36,13 @@ namespace bpkg { "Ignore unknown manifest entries." } + + string --key + { + "", + "Private key to use to sign the repository. In most cases will + be a path to the key file but it can also be a key id when a custom + \cb{openssl} cryptographic engine is used." + } }; } diff --git a/bpkg/rep-create.cxx b/bpkg/rep-create.cxx index 9c6f275..2ab52fb 100644 --- a/bpkg/rep-create.cxx +++ b/bpkg/rep-create.cxx @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -52,6 +53,7 @@ namespace bpkg static const path repositories ("repositories"); static const path packages ("packages"); + static const path signature ("signature"); static void collect (const rep_create_options& o, @@ -91,7 +93,7 @@ namespace bpkg // if (d == root) { - if (p == repositories || p == packages) + if (p == repositories || p == packages || p == signature) continue; } @@ -208,18 +210,52 @@ namespace bpkg manifests.emplace_back (move (m)); } - // Serialize. + // Serialize packages manifest, optionally generate the signature manifest. // path p (d / packages); try { - ofstream ofs; - ofs.exceptions (ofstream::badbit | ofstream::failbit); - ofs.open (p.string ()); + { + ofstream ofs; + ofs.exceptions (ofstream::badbit | ofstream::failbit); + ofs.open (p.string ()); + + manifest_serializer s (ofs, p.string ()); + manifests.serialize (s); + } + + const optional& cert (rms.back ().certificate); + if (cert) + { + const string& key (o.key ()); + if (key.empty ()) + fail << "--key option required" << + info << "repository manifest contains a certificate" << + info << "run 'bpkg help rep-create' for more information"; + + signature_manifest m; + m.sha256sum = sha256 (o, p); + m.signature = sign_repository (o, m.sha256sum, key, *cert, d); - manifest_serializer s (ofs, p.string ()); - manifests.serialize (s); + p = path (d / signature); + + ofstream ofs; + ofs.exceptions (ofstream::badbit | ofstream::failbit); + ofs.open (p.string ()); + + manifest_serializer s (ofs, p.string ()); + m.serialize (s); + } + else + { + if (o.key_specified ()) + warn << "--key option ignored" << + info << "repository manifest contains no certificate" << + info << "run 'bpkg help rep-create' for more information"; + + try_rmfile (path (d / signature), true); + } } catch (const manifest_serialization& e) { diff --git a/bpkg/rep-info.cli b/bpkg/rep-info.cli index 7554620..5fa5d3d 100644 --- a/bpkg/rep-info.cli +++ b/bpkg/rep-info.cli @@ -24,7 +24,16 @@ namespace bpkg first line followed by the list of complement and prerequisite repositories and the list of available packages. This default behavior, however, can be altered in various ways using options listed below. Note - that the information is written to \cb{STDOUT}, not \cb{STDERR}." + that the information is written to \cb{STDOUT}, not \cb{STDERR}. + + If the current working directory contains a \cb{bpkg} configuration, then + \cb{rep-info} will use its certificate database for the repository + authentication. That is, it will trust the repository's certificate if it + is already trusted by the configuration. Otherwise it will add the + certificate to the configuration if you confirm it is trusted. You can + specify an alternative configuration directory with the + \cb{--directory|-d} option. To disable using the configuration in the + current working directory pass this option with an empty path." } class rep_info_options: common_options @@ -53,5 +62,11 @@ namespace bpkg \cb{--packages|-p} or \cb{--repositories|-r} to only dump one of the manifests." } + + string --directory|-d // String to allow empty value. + { + "", + "Use configuration in for the trusted certificate database." + } }; } diff --git a/bpkg/rep-info.cxx b/bpkg/rep-info.cxx index d2157af..9cad4fd 100644 --- a/bpkg/rep-info.cxx +++ b/bpkg/rep-info.cxx @@ -9,7 +9,9 @@ #include #include +#include #include +#include #include #include @@ -30,21 +32,64 @@ namespace bpkg repository_location rl (parse_location (args.next ())); // Fetch everything we will need before printing anything. Ignore - // unknown manifest entries unless we are dumping them. + // unknown manifest entries unless we are dumping them. First fetch + // the repositories list and authenticate the base's certificate. // - package_manifests pms (fetch_packages (o, rl, !o.manifest ())); + pair rmc ( + fetch_repositories (o, rl, !o.manifest ())); - repository_manifests rms; + repository_manifests& rms (rmc.first); - try + bool a (o.auth () != auth::none && + (o.auth () == auth::all || rl.remote ())); + + const optional cert_pem (rms.back ().certificate); + shared_ptr cert; + + if (a) { - rms = fetch_repositories (o, rl, pms.sha256sum, !o.manifest ()); + dir_path d (o.directory ()); + cert = authenticate_certificate ( + o, + o.directory_specified () && d.empty () ? nullptr : &d, + cert_pem, + rl); + + a = !cert->dummy (); } - catch (const checksum_mismatch&) - { - fail << "repository files checksum mismatch for " + + // Now fetch the packages list and make sure it matches the repositories + // we just fetched. + // + pair pmc ( + fetch_packages (o, rl, !o.manifest ())); + + package_manifests& pms (pmc.first); + + if (rmc.second != pms.sha256sum) + fail << "repositories manifest file checksum mismatch for " << rl.canonical_name () << info << "try again"; + + if (a) + { + signature_manifest sm (fetch_signature (o, rl, true)); + + if (sm.sha256sum != pmc.second) + fail << "packages manifest file checksum mismatch for " + << rl.canonical_name () << + info << "try again"; + + dir_path d (o.directory ()); + assert (cert != nullptr); + + authenticate_repository ( + o, + o.directory_specified () && d.empty () ? nullptr : &d, + cert_pem, + *cert, + sm, + rl); } // Now print. diff --git a/bpkg/types-parsers b/bpkg/types-parsers index 41046a1..41fab10 100644 --- a/bpkg/types-parsers +++ b/bpkg/types-parsers @@ -9,6 +9,7 @@ #define BPKG_TYPES_PARSERS #include +#include namespace bpkg { @@ -32,6 +33,13 @@ namespace bpkg static void parse (dir_path&, bool&, scanner&); }; + + template <> + struct parser + { + static void + parse (auth&, bool&, scanner&); + }; } } diff --git a/bpkg/types-parsers.cxx b/bpkg/types-parsers.cxx index 9e97ccd..6a61a3d 100644 --- a/bpkg/types-parsers.cxx +++ b/bpkg/types-parsers.cxx @@ -47,5 +47,25 @@ namespace bpkg xs = true; parse_path (x, s); } + + void parser:: + parse (auth& x, bool& xs, scanner& s) + { + xs = true; + const char* o (s.next ()); + + if (!s.more ()) + throw missing_value (o); + + const string v (s.next ()); + if (v == "none") + x = auth::none; + else if (v == "remote") + x = auth::remote; + else if (v == "all") + x = auth::all; + else + throw invalid_value (o, v); + } } } diff --git a/bpkg/utility b/bpkg/utility index 009ef76..7bb6111 100644 --- a/bpkg/utility +++ b/bpkg/utility @@ -35,7 +35,7 @@ namespace bpkg using butl::reverse_iterate; // Y/N prompt. The def argument, if specified, should be either 'y' - // or 'no'. It is used as the default answer, in case the user just + // or 'n'. It is used as the default answer, in case the user just // hits enter. Issue diagnostics and throw failed if no answer could // be extracted from STDOUT (e.g., because it was closed). // -- cgit v1.1