aboutsummaryrefslogtreecommitdiff
path: root/bpkg
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2016-04-14 17:59:24 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2016-05-04 20:47:45 +0300
commitb13332c991ce2695626eaca367dd8208b174c9ca (patch)
tree809dc321b47d5ef9c72935637f94bf5b84ed640d /bpkg
parentc9831f760a83e36a3a2ac84b1bd3f573e47ef195 (diff)
Add support for repository authentication
Diffstat (limited to 'bpkg')
-rw-r--r--bpkg/auth96
-rw-r--r--bpkg/auth.cxx815
-rw-r--r--bpkg/buildfile2
-rw-r--r--bpkg/cfg-create.cxx4
-rw-r--r--bpkg/cfg-fetch.cxx52
-rw-r--r--bpkg/common.cli57
-rw-r--r--bpkg/database.cxx11
-rw-r--r--bpkg/fetch37
-rw-r--r--bpkg/fetch.cxx101
-rw-r--r--bpkg/openssl31
-rw-r--r--bpkg/openssl.cxx59
-rw-r--r--bpkg/options-types18
-rw-r--r--bpkg/package116
-rw-r--r--bpkg/package.cxx16
-rw-r--r--bpkg/package.xml13
-rw-r--r--bpkg/rep-create.cli17
-rw-r--r--bpkg/rep-create.cxx50
-rw-r--r--bpkg/rep-info.cli17
-rw-r--r--bpkg/rep-info.cxx61
-rw-r--r--bpkg/types-parsers8
-rw-r--r--bpkg/types-parsers.cxx20
-rw-r--r--bpkg/utility2
22 files changed, 1496 insertions, 107 deletions
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 <bpkg/manifest>
+
+#include <bpkg/types>
+#include <bpkg/utility>
+
+#include <bpkg/package>
+#include <bpkg/common-options>
+
+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<const certificate>
+ authenticate_certificate (const common_options&,
+ const dir_path* configuration,
+ const optional<string>& 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<string>& 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<char>
+ 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 <bpkg/auth>
+
+#include <ratio>
+#include <limits> // numeric_limits
+#include <fstream>
+#include <cstring> // strlen(), strcmp()
+#include <iterator> // ostreambuf_iterator, istreambuf_iterator
+
+#include <butl/sha256>
+#include <butl/base64>
+#include <butl/process>
+#include <butl/fdstream>
+#include <butl/filesystem>
+
+#include <bpkg/openssl>
+#include <bpkg/package>
+#include <bpkg/package-odb>
+#include <bpkg/database>
+#include <bpkg/diagnostics>
+
+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<certificate>
+ auth_dummy (const common_options& co,
+ const string& fp,
+ const repository_location& rl)
+ {
+ tracer trace ("auth_dummy");
+
+ shared_ptr<certificate> cert (
+ make_shared<certificate> (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<string>& 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<certificate>
+ 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<certificate> cert (
+ make_shared<certificate> (
+ 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<streamsize>::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<certificate>
+ auth_real (const common_options& co,
+ const string& fp,
+ const string& pem,
+ const repository_location& rl)
+ {
+ tracer trace ("auth_real");
+
+ shared_ptr<certificate> 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<certificate>
+ auth_cert (const common_options& co,
+ const dir_path& conf,
+ database& db,
+ const optional<string>& pem,
+ const repository_location& rl)
+ {
+ tracer trace ("auth_cert");
+ tracer_guard tg (db, trace);
+
+ string fp (cert_fingerprint (co, pem, rl));
+ shared_ptr<certificate> cert (db.find<certificate> (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<const certificate>
+ authenticate_certificate (const common_options& co,
+ const dir_path* conf,
+ const optional<string>& 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")) ? &current_dir : nullptr;
+
+ assert (conf == nullptr || !conf->empty ());
+
+ shared_ptr<certificate> 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<string>& 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")) ? &current_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<char>
+ 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<certificate> 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<size_t, ratio<3600 * 24>>;
+
+ days left (chrono::duration_cast<days> (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<char> signature
+ ((istreambuf_iterator<char> (is)), istreambuf_iterator<char> ());
+
+ 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 <bpkg/manifest>
+#include <bpkg/auth>
#include <bpkg/fetch>
#include <bpkg/package>
#include <bpkg/package-odb>
@@ -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<repository>& r,
const shared_ptr<repository>& 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<repository_manifests, string/*checksum*/> 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<const certificate> 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<package_manifests, string/*checksum*/> 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 <set>;
+
include <bpkg/types>;
+include <bpkg/options-types>;
"\section=1"
"\name=bpkg-common-options"
@@ -157,6 +160,60 @@ namespace bpkg
multiple tar options."
}
+ path --openssl = "openssl"
+ {
+ "<path>",
+ "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
+ {
+ "<opt>",
+ "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
+ {
+ "<type>",
+ "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<string> --trust
+ {
+ "<fingerprint>",
+ "Trust repository certificate with a SHA256 <fingerprint>. 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.
{
"<path>",
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<repository_manifests, string/*checksum*/>
+ fetch_repositories (const common_options&,
+ const repository_location&,
+ bool ignore_unknown);
+
+ package_manifests
+ fetch_packages (const dir_path&, bool ignore_unknown);
+
+ pair<package_manifests, string/*checksum*/>
+ 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 <typename M>
- static M
+ static pair<M, string/*checksum*/>
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 <typename M>
- static M
+ static pair<M, string/*checksum*/>
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<repository_manifests> (
- nullptr,
- d / repositories,
- "", // No checksum verification.
- iu);
+ nullptr, d / repositories, iu).first;
}
- repository_manifests
+ pair<repository_manifests, string/*checksum*/>
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<repository_manifests> (
- o, rl.proto (), rl.host (), rl.port (), f, sha256sum, iu)
- : fetch_manifest<repository_manifests> (&o, f, sha256sum, iu);
+ o, rl.proto (), rl.host (), rl.port (), f, iu)
+ : fetch_manifest<repository_manifests> (&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<package_manifests> (nullptr, d / packages, "", iu);
+ return fetch_manifest<package_manifests> (nullptr, d / packages, iu).first;
}
- package_manifests
+ pair<package_manifests, string/*checksum*/>
fetch_packages (const common_options& o,
const repository_location& rl,
bool iu)
@@ -735,8 +713,25 @@ namespace bpkg
return rl.remote ()
? fetch_manifest<package_manifests> (
- o, rl.proto (), rl.host (), rl.port (), f, "", iu)
- : fetch_manifest<package_manifests> (&o, f, "", iu);
+ o, rl.proto (), rl.host (), rl.port (), f, iu)
+ : fetch_manifest<package_manifests> (&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<signature_manifest> (
+ o, rl.proto (), rl.host (), rl.port (), f, iu).first
+ : fetch_manifest<signature_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 <butl/process>
+
+#include <bpkg/types>
+#include <bpkg/utility>
+
+#include <bpkg/common-options>
+
+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 <bpkg/openssl>
+
+#include <butl/process>
+#include <butl/fdstream>
+
+#include <bpkg/diagnostics>
+
+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 <map>
#include <set>
+#include <ratio>
+#include <chrono>
+#include <type_traits> // static_assert
#include <odb/core.hxx>
#include <odb/nested-container.hxx>
+#include <butl/timestamp>
+
#include <bpkg/types>
#include <bpkg/utility>
-#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<timestamp::period,
+ std::chrono::nanoseconds::period>::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<std::chrono::nanoseconds> ( \
+ (?).time_since_epoch ()).count ()) \
+ from(butl::timestamp ( \
+ std::chrono::duration_cast<butl::timestamp::duration> ( \
+ 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 <bpkg/manifest> 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/<fingerprint>.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 @@
<changelog xmlns="http://www.codesynthesis.com/xmlns/odb/changelog" database="sqlite" version="1">
- <model version="2">
+ <model version="3">
<table name="repository" kind="object">
<column name="name" type="TEXT" null="true"/>
<column name="location" type="TEXT" null="true"/>
@@ -231,5 +231,16 @@
</references>
</foreign-key>
</table>
+ <table name="certificate" kind="object">
+ <column name="fingerprint" type="TEXT" null="true"/>
+ <column name="name" type="TEXT" null="true"/>
+ <column name="organization" type="TEXT" null="true"/>
+ <column name="email" type="TEXT" null="true"/>
+ <column name="start_date" type="INTEGER" null="true"/>
+ <column name="end_date" type="INTEGER" null="true"/>
+ <primary-key>
+ <column name="fingerprint"/>
+ </primary-key>
+ </table>
</model>
</changelog>
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 <dir> 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 <dir> 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
+ {
+ "<name>",
+ "Private key to use to sign the repository. In most cases <name> 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 <bpkg/manifest>
#include <bpkg/manifest-serializer>
+#include <bpkg/auth>
#include <bpkg/fetch>
#include <bpkg/archive>
#include <bpkg/checksum>
@@ -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<string>& 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.
+ {
+ "<dir>",
+ "Use configuration in <dir> 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 <bpkg/manifest>
#include <bpkg/manifest-serializer>
+#include <bpkg/auth>
#include <bpkg/fetch>
+#include <bpkg/package>
#include <bpkg/diagnostics>
#include <bpkg/manifest-utility>
@@ -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<repository_manifests, string/*checksum*/> 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<string> cert_pem (rms.back ().certificate);
+ shared_ptr<const certificate> 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<package_manifests, string/*checksum*/> 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 <bpkg/types>
+#include <bpkg/options-types>
namespace bpkg
{
@@ -32,6 +33,13 @@ namespace bpkg
static void
parse (dir_path&, bool&, scanner&);
};
+
+ template <>
+ struct parser<auth>
+ {
+ 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<auth>::
+ 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).
//