From: Denis A. Altoé Falqueto <denisfalqueto@gmail.com> The packages and database files will have the possibility of being digitally signed, using gpg2 as the tool to sign and verify the signatures. makepkg and repo-add were changed so they can receive a key to be used in the signing process and use gpg2 instead of gpg. The case for gpg2 is that it allows user agents to be used for passphrase entry. For example, in KDE, it will open a dialog to ask for the passphrase and it will be cached for some time. It is also possible with Gnome. In simpler environments, the passphrase will be asked everytime in the command line. repo-add will store the signature of the packages added, if it exists. The signature will be stored in the section %PGPSIG% of the desc file and will have a variable length. The reason is that different key sizes generate different signature sizes. So we must be prepared for any size. libalpm was changed to check the signature in the database update and package installation (not in -U, for now). Each repository in pacman.conf can be configured to use the signature feature according to three options: Always, Optional and None. Always indicates that the signature must exists and be valid for the repository to be updated. Optional doesn't demnds the signature, but if it is present, it must be valid. None doesn't bother with signatures at all. Signed-off-by: Denis A. Altoé Falqueto <denisfalqueto@gmail.com> --- configure.ac | 4 - doc/makepkg.8.txt | 4 + doc/makepkg.conf.5.txt | 6 +- doc/repo-add.8.txt | 7 +- lib/libalpm/alpm.h | 2 +- lib/libalpm/be_files.c | 96 ++++++++++++++-- lib/libalpm/error.c | 2 - lib/libalpm/signing.c | 282 ++++++++++++++++++++++------------------------ lib/libalpm/signing.h | 3 +- lib/libalpm/sync.c | 2 +- scripts/.gitignore | 1 + scripts/Makefile.am | 3 + scripts/makepkg.sh.in | 32 ++++-- scripts/pacman-key.sh.in | 278 +++++++++++++++++++++++++++++++++++++++++++++ scripts/repo-add.sh.in | 63 ++++++++--- 15 files changed, 588 insertions(+), 197 deletions(-) create mode 100644 scripts/pacman-key.sh.in diff --git a/configure.ac b/configure.ac index b6ce68b..008975b 100644 --- a/configure.ac +++ b/configure.ac @@ -154,10 +154,6 @@ else fi AM_CONDITIONAL(INTERNAL_DOWNLOAD, test "x$internaldownload" = "xyes") -# Check for gpgme -AC_CHECK_LIB([gpgme], [gpgme_check_version], , - AC_MSG_ERROR([gpgme is needed to compile pacman!])) - # Checks for header files. AC_CHECK_HEADERS([fcntl.h glob.h libintl.h limits.h locale.h string.h strings.h sys/ioctl.h sys/param.h sys/statvfs.h sys/syslimits.h sys/time.h syslog.h wchar.h]) diff --git a/doc/makepkg.8.txt b/doc/makepkg.8.txt index a2fdb3f..4d8f26b 100644 --- a/doc/makepkg.8.txt +++ b/doc/makepkg.8.txt @@ -161,6 +161,10 @@ Options (Passed to pacman) Prevent pacman from displaying a progress bar; useful if you are redirecting makepkg output to file. +*\--signwithkey*:: + Select a specific key to be used to sign the package. If absent, + the default from the keyring key will be used. + Additional Features ------------------- diff --git a/doc/makepkg.conf.5.txt b/doc/makepkg.conf.5.txt index a565bd6..f82bc19 100644 --- a/doc/makepkg.conf.5.txt +++ b/doc/makepkg.conf.5.txt @@ -94,9 +94,9 @@ Options PKGBUILD options array. *sign*;; - Generate a PGP signature file using GnuPG. This will execute `gpg - --detach-sign --use-agent` on the built package to generate a detached - signature file, using the GPG agent if it is available. The signature + Generate a PGP signature file using GnuPG. This will execute `gpg2 + --detach-sign` on the built package to generate a detached signature + file, using the GPG agent if it is available. The signature file will be the entire filename of the package with a ``.sig'' extension. diff --git a/doc/repo-add.8.txt b/doc/repo-add.8.txt index e6cc940..6b8c0ef 100644 --- a/doc/repo-add.8.txt +++ b/doc/repo-add.8.txt @@ -10,9 +10,9 @@ repo-add - package database maintenance utility Synopsis -------- -repo-add [-q] <path-to-db> <package1> [<package2> ...] +repo-add [-q] [-s [-k key]] <path-to-db> <package1> [<package2> ...] -repo-remove [-q] <path-to-db> <packagename> [<packagename2> ...] +repo-remove [-q] [-s [-k key]] <path-to-db> <packagename> [<packagename2> ...] Description @@ -40,6 +40,9 @@ Options signature file, using the GPG agent if it is available. The signature file will be the entire filename of the database with a ``.sig'' extension. +*-k key*:: + Select a specific key to be used for the signing of the database file. + If absent, the default key from the default keyring will be used. See Also -------- diff --git a/lib/libalpm/alpm.h b/lib/libalpm/alpm.h index 2ce049d..7eeb734 100644 --- a/lib/libalpm/alpm.h +++ b/lib/libalpm/alpm.h @@ -512,6 +512,7 @@ enum _pmerrno_t { PM_ERR_DB_NOT_FOUND, PM_ERR_DB_WRITE, PM_ERR_DB_REMOVE, + PM_ERR_DB_SIG, /* Servers */ PM_ERR_SERVER_BAD_URL, PM_ERR_SERVER_NONE, @@ -550,7 +551,6 @@ enum _pmerrno_t { /* External library errors */ PM_ERR_LIBARCHIVE, PM_ERR_LIBFETCH, - PM_ERR_GPGME, PM_ERR_EXTERNAL_DOWNLOAD }; diff --git a/lib/libalpm/be_files.c b/lib/libalpm/be_files.c index 1235165..9751b4e 100644 --- a/lib/libalpm/be_files.c +++ b/lib/libalpm/be_files.c @@ -49,7 +49,9 @@ #include "delta.h" #include "deps.h" #include "dload.h" +#include "signing.h" +#define DBSIGEXT ".sig" static int checkdbdir(pmdb_t *db) { @@ -208,7 +210,8 @@ static int remove_olddir(const char *syncdbpath, alpm_list_t *dirlist) */ int SYMEXPORT alpm_db_update(int force, pmdb_t *db) { - char *dbfile, *dbfilepath; + char *dbfile = NULL, *dbfilepath = NULL, *dbsigfile = NULL, + *fulldbfile = NULL, *fulldbsigfile = NULL; const char *dbpath, *syncdbpath; alpm_list_t *newdirlist = NULL, *olddirlist = NULL; alpm_list_t *onlynew = NULL, *onlyold = NULL; @@ -238,17 +241,62 @@ int SYMEXPORT alpm_db_update(int force, pmdb_t *db) dbpath = alpm_option_get_dbpath(); ret = _alpm_download_single_file(dbfile, db->servers, dbpath, force); - free(dbfile); - if(ret == 1) { /* files match, do nothing */ pm_errno = 0; + free(dbfile); return(1); } else if(ret == -1) { /* pm_errno was set by the download code */ _alpm_log(PM_LOG_DEBUG, "failed to sync db: %s\n", alpm_strerrorlast()); + free(dbfile); return(-1); } + + /* Check the signature of the database, if it is marked as signed */ + if(db->pgp_verify != PM_PGP_VERIFY_NEVER) { + /* Assemble the signature file name */ + len = strlen(dbfile) + strlen(DBSIGEXT) + 1; + MALLOC(dbsigfile, len, RET_ERR(PM_ERR_MEMORY, -1)); + sprintf(dbsigfile, "%s" DBSIGEXT, dbfile); + + /* Try to download the signature file */ + ret = _alpm_download_single_file(dbsigfile, db->servers, dbpath, force); + if (ret == -1 && db->pgp_verify == PM_PGP_VERIFY_ALWAYS) { + _alpm_log(PM_LOG_DEBUG, "failed to download signature for db: %s\n", alpm_strerrorlast()); + free(dbfile); + free(dbsigfile); + return (-1); + } + else if (ret != -1) { + /* Assemble the full db and signature file names */ + MALLOC(fulldbfile, strlen(dbpath) + strlen(dbfile) + 2, RET_ERR(PM_ERR_MEMORY, -1)); + MALLOC(fulldbsigfile, strlen(dbpath) + strlen(dbsigfile) + 2, RET_ERR(PM_ERR_MEMORY, -1)); + sprintf(fulldbfile, "%s/%s", dbpath, dbfile); + sprintf(fulldbsigfile, "%s/%s", dbpath, dbsigfile); + + /* Check the signature */ + int ret = _alpm_gpg_checksig_file(fulldbfile, fulldbsigfile); + _alpm_log(PM_LOG_DEBUG, "return from _alpm_gpg_checksig_file = %d\n", ret); + + FREE(fulldbfile); + FREE(fulldbsigfile); + + /* VerifSig = Always -> we will only accept 0 as a correct value + * (missing or invalid signatures are errors) + * VerifSig = Optional -> we will accept 0 or -1 as correct values + * (missing signature is ok, but if it present, it must be valid) */ + if((db->pgp_verify == PM_PGP_VERIFY_ALWAYS && ret != 0) || + (db->pgp_verify == PM_PGP_VERIFY_OPTIONAL && ret == 1)) { + _alpm_log(PM_LOG_ERROR, "the signature doesn't match the repository database.\n"); + free(dbfile); + free(dbsigfile); + RET_ERR(PM_ERR_DB_SIG, -1); + } + } + FREE(dbsigfile); + } + FREE(dbfile); syncdbpath = _alpm_db_path(db); @@ -256,7 +304,7 @@ int SYMEXPORT alpm_db_update(int force, pmdb_t *db) len = strlen(dbpath) + strlen(db->treename) + strlen(DBEXT) + 1; MALLOC(dbfilepath, len, RET_ERR(PM_ERR_MEMORY, -1)); sprintf(dbfilepath, "%s%s" DBEXT, dbpath, db->treename); - + if(force) { /* if forcing update, remove the old dir and extract the db */ if(_alpm_rmrf(syncdbpath) != 0) { @@ -291,6 +339,10 @@ int SYMEXPORT alpm_db_update(int force, pmdb_t *db) ret = _alpm_unpack(dbfilepath, syncdbpath, onlynew, 0); cleanup: + if (dbsigfile != NULL) + FREE(dbsigfile); + if (dbfile != NULL) + FREE(dbfile); FREELIST(newdirlist); FREELIST(olddirlist); alpm_list_free(onlynew); @@ -437,7 +489,8 @@ int _alpm_db_read(pmdb_t *db, pmpkg_t *info, pmdbinfrq_t inforeq) char line[513]; int sline = sizeof(line)-1; char *pkgpath = NULL; - + alpm_list_t *pgpsig_lines = NULL; + ALPM_LOG_FUNC; if(db == NULL) { @@ -610,11 +663,33 @@ int _alpm_db_read(pmdb_t *db, pmpkg_t *info, pmdbinfrq_t inforeq) STRDUP(info->md5sum, _alpm_strtrim(line), goto error); } else if(strcmp(line, "%PGPSIG%") == 0) { /* PGPSIG tag only appears in sync repositories, - * not the local one. */ - if(fgets(line, 512, fp) == NULL) { - goto error; + * not the local one. The size must not be fixed, + * because the key used will affect the final size */ + pgpsig_lines = NULL; + int len = 0; + /* Create a list of strings to store the signature for now */ + while(fgets(line, sline, fp) && strlen(_alpm_strtrim(line))) { + char *linedup; + STRDUP(linedup, _alpm_strtrim(line), goto error); + pgpsig_lines = alpm_list_add(pgpsig_lines, linedup); + len += strlen(linedup); } - STRDUP(info->pgpsig.encdata, _alpm_strtrim(line), goto error); + + MALLOC(info->pgpsig.encdata, (size_t)(len + 1), goto error); + info->pgpsig.encdata[0] = '\0'; + + /* Assemble the signature from the list of strings */ + while (pgpsig_lines != NULL) { + alpm_list_t *pgpsig_line = pgpsig_lines; + pgpsig_lines = pgpsig_lines->next; + + strcat(info->pgpsig.encdata, (char *)pgpsig_line->data); + + /* Free the current node, as it is not needed anymore */ + free(pgpsig_line->data); + free(pgpsig_line); + } + pgpsig_lines = NULL; } else if(strcmp(line, "%REPLACES%") == 0) { while(fgets(line, sline, fp) && strlen(_alpm_strtrim(line))) { char *linedup; @@ -731,6 +806,9 @@ int _alpm_db_read(pmdb_t *db, pmpkg_t *info, pmdbinfrq_t inforeq) return(0); error: + if (pgpsig_lines) { + FREELIST(pgpsig_lines); + } free(pkgpath); if(fp) { fclose(fp); diff --git a/lib/libalpm/error.c b/lib/libalpm/error.c index 907a9f2..1fccc46 100644 --- a/lib/libalpm/error.c +++ b/lib/libalpm/error.c @@ -158,8 +158,6 @@ const char SYMEXPORT *alpm_strerror(int err) /* obviously shouldn't get here... */ return _("download library error"); #endif - case PM_ERR_GPGME: - return _("gpgme error"); case PM_ERR_EXTERNAL_DOWNLOAD: return _("error invoking external downloader"); /* Unknown error! */ diff --git a/lib/libalpm/signing.c b/lib/libalpm/signing.c index 2b15528..80eba15 100644 --- a/lib/libalpm/signing.c +++ b/lib/libalpm/signing.c @@ -17,13 +17,8 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -#include "config.h" - -#include <stdlib.h> #include <stdio.h> -#include <string.h> -#include <locale.h> /* setlocale() */ -#include <gpgme.h> +#include <errno.h> /* libalpm */ #include "signing.h" @@ -32,169 +27,158 @@ #include "log.h" #include "alpm.h" -#define CHECK_ERR(void) do { \ - if(err != GPG_ERR_NO_ERROR) { goto error; } \ - } while(0) +// Max length of the command line string. I believe that it is enough... +#define MAX_COMMAND 2048 +#define MAX_LINE 1024 +#define TRUST_MARGINAL "[GNUPG:] TRUST_MARGINAL" +#define TRUST_FULLY "[GNUPG:] TRUST_FULLY" +#define TRUST_ULTIMATE "[GNUPG:] TRUST_ULTIMATE" + +// Names of files related to the keyring +#define PACMAN_KEYRING_DIR "/etc/pacman.d/gnupg" +#define PACMAN_SECRET "secret.gpg" +#define PACMAN_TRUSTDB "trustdb.gpg" +#define PACMAN_KEYRING "pacman.gpg" +#define PACMAN_SHARE_DIR "/usr/share/pacman" + +// Commands and common parameters +#define GPG_PROGRAM "gpg2" +#define GPG GPG_PROGRAM " --ignore-time-conflict --no-options --no-default-keyring" +#define GPG_PACMAN GPG " --status-fd 1 --homedir " PACMAN_KEYRING_DIR " --secret-keyring " \ + PACMAN_KEYRING_DIR "/" PACMAN_SECRET " --trustdb-name " PACMAN_KEYRING_DIR \ + "/" PACMAN_TRUSTDB " --keyring " PACMAN_KEYRING_DIR "/" PACMAN_KEYRING \ + " --primary-keyring " PACMAN_KEYRING_DIR "/" PACMAN_KEYRING " 2> /dev/null" -static int gpgme_init(void) +/** + * Check the PGP package signature for the given package file. + * The signature is stored on memory, so it needs to be saved to + * a temporary file. + * @param pkgpath the full path to a package file + * @param sig PGP signature data in raw form (already decoded) + * @return a int value : 0 (valid), 1 (invalid), -1 (an error occured) + */ +int _alpm_gpg_checksig_memory(const char *pkgpath, const pmpgpsig_t *sig) { - static int init = 0; - const char *version; - gpgme_error_t err; - gpgme_engine_info_t enginfo; - - ALPM_LOG_FUNC; - - if(init) { - /* we already successfully initialized the library */ - return(0); + int status = 1, i; + char *tmpName = NULL; + FILE *tmpSig = NULL; + + // Get a temporary file, which will store the signature. + while (tmpSig == NULL) { + tmpName = tempnam(NULL, "pacman"); + if (tmpName == NULL) { + _alpm_log(PM_LOG_ERROR, _("Unable to create a temporary file name.\n")); + status = -1; + goto cleanup; + } + _alpm_log(PM_LOG_DEBUG, "tmpName: \'%s\'\n", tmpName); + + // Write the signature to the temporary file + tmpSig = fopen(tmpName, "w+x"); + if (tmpSig == NULL && errno == EEXIST) { + _alpm_log(PM_LOG_DEBUG, "the temporary file already exists\n"); + FREE(tmpName); + } else if (tmpSig == NULL) { + _alpm_log(PM_LOG_ERROR, _("Unable to create a temporary file.\n")); + status = -1; + goto cleanup; + } } - if(!alpm_option_get_signaturedir()) { - RET_ERR(PM_ERR_SIG_MISSINGDIR, 1); + // Copy the signature into the temp file + // Write the bytes at once, to make it more efficient + for (i = 0; i < sig->rawlen; i++) { + if (fputc(sig->rawdata[i], tmpSig) == EOF) { + _alpm_log(PM_LOG_ERROR, _("Error writing signature to temporary file\n")); + status = -1; + goto cleanup; + } } - /* calling gpgme_check_version() returns the current version and runs - * some internal library setup code */ - version = gpgme_check_version(NULL); - _alpm_log(PM_LOG_DEBUG, "GPGME version: %s\n", version); - gpgme_set_locale(NULL, LC_CTYPE, setlocale(LC_CTYPE, NULL)); -#ifdef LC_MESSAGES - gpgme_set_locale(NULL, LC_MESSAGES, setlocale(LC_MESSAGES, NULL)); -#endif - /* NOTE: - * The GPGME library installs a SIGPIPE signal handler automatically if - * the default signal hander is in use. The only time we set a handler - * for SIGPIPE is in dload.c, and we reset it when we are done. Given that - * we do this, we can let GPGME do its automagic. However, if we install - * a library-wide SIGPIPE handler, we will have to be careful. - */ - - /* check for OpenPGP support (should be a no-brainer, but be safe) */ - err = gpgme_engine_check_version(GPGME_PROTOCOL_OpenPGP); - CHECK_ERR(); - - /* set and check engine information */ - err = gpgme_set_engine_info(GPGME_PROTOCOL_OpenPGP, NULL, - alpm_option_get_signaturedir()); - CHECK_ERR(); - err = gpgme_get_engine_info(&enginfo); - CHECK_ERR(); - _alpm_log(PM_LOG_DEBUG, "GPGME engine info: file=%s, home=%s\n", - enginfo->file_name, enginfo->home_dir); - - init = 1; - return(0); - -error: - _alpm_log(PM_LOG_ERROR, _("GPGME error: %s\n"), gpgme_strerror(err)); - RET_ERR(PM_ERR_GPGME, 1); + // Close the temporary file so that gpg can use it + fclose(tmpSig); + tmpSig = NULL; + + // Calls the function that checks the signature in a file + status = _alpm_gpg_checksig_file(pkgpath, tmpName); +cleanup: + if (tmpSig != NULL) { + fclose(tmpSig); + } + if (tmpName != NULL) { + // Delete the temporary file, if it exists + if (access(tmpName, F_OK) != -1) { + remove(tmpName); + } + + FREE(tmpName); + } + return status; } /** * Check the PGP package signature for the given package file. + * The signature is stored in a file, which is passed to gpg. * @param pkgpath the full path to a package file - * @param sig PGP signature data in raw form (already decoded) + * @param sigpath path to the PGP Signature file. * @return a int value : 0 (valid), 1 (invalid), -1 (an error occured) */ -int _alpm_gpgme_checksig(const char *pkgpath, const pmpgpsig_t *sig) +int _alpm_gpg_checksig_file(const char *pkgpath, const char *sigpath) { - int ret = 0; - gpgme_error_t err; - gpgme_ctx_t ctx; - gpgme_data_t pkgdata, sigdata; - gpgme_verify_result_t result; - gpgme_signature_t gpgsig; - FILE *pkgfile = NULL, *sigfile = NULL; - - ALPM_LOG_FUNC; - - if(!sig || !sig->rawdata) { - RET_ERR(PM_ERR_SIG_UNKNOWN, -1); - } - if(!pkgpath || access(pkgpath, R_OK) != 0) { - RET_ERR(PM_ERR_PKG_NOT_FOUND, -1); + int status = 1; + char *command = NULL, *line = NULL; + FILE *output = NULL; + int resultCommand = 0, statusCommand = 0, trusted = 0; + + CALLOC(command, MAX_COMMAND, sizeof(char), RET_ERR(PM_ERR_MEMORY, NULL)); + + if (snprintf(command, MAX_COMMAND, GPG_PACMAN " --verify %s %s", sigpath, pkgpath) >= MAX_COMMAND) { + _alpm_log(PM_LOG_ERROR, _("The gpg command string is too long\n")); + status = -1; + goto cleanup; } - if(gpgme_init()) { - /* pm_errno was set in gpgme_init() */ - return(-1); + _alpm_log(PM_LOG_DEBUG, "gpg command is: \'%s\'\n", command); + + output = popen(command, "r"); + if (output == NULL) { + _alpm_log(PM_LOG_ERROR, _("Error calling gpg2 external program\n")); + status = -1; + goto cleanup; } - err = gpgme_new(&ctx); - CHECK_ERR(); - - /* create our necessary data objects to verify the signature */ - /* first the package itself */ - pkgfile = fopen(pkgpath, "rb"); - if(pkgfile == NULL) { - pm_errno = PM_ERR_PKG_OPEN; - ret = -1; - goto error; - } - err = gpgme_data_new_from_stream(&pkgdata, pkgfile); - CHECK_ERR(); - - /* next create data object for the signature */ - err = gpgme_data_new_from_mem(&sigdata, (char*)sig->rawdata, sig->rawlen, 0); - CHECK_ERR(); - - /* here's where the magic happens */ - err = gpgme_op_verify(ctx, sigdata, pkgdata, NULL); - CHECK_ERR(); - result = gpgme_op_verify_result(ctx); - gpgsig = result->signatures; - if (!gpgsig || gpgsig->next) { - _alpm_log(PM_LOG_ERROR, _("Unexpected number of signatures\n")); - ret = -1; - goto error; - } - fprintf(stdout, "\nsummary=%x\n", gpgsig->summary); - fprintf(stdout, "fpr=%s\n", gpgsig->fpr); - fprintf(stdout, "status=%d\n", gpgsig->status); - fprintf(stdout, "timestamp=%lu\n", gpgsig->timestamp); - fprintf(stdout, "wrong_key_usage=%u\n", gpgsig->wrong_key_usage); - fprintf(stdout, "pka_trust=%u\n", gpgsig->pka_trust); - fprintf(stdout, "chain_model=%u\n", gpgsig->chain_model); - fprintf(stdout, "validity=%d\n", gpgsig->validity); - fprintf(stdout, "validity_reason=%d\n", gpgsig->validity_reason); - fprintf(stdout, "key=%d\n", gpgsig->pubkey_algo); - fprintf(stdout, "hash=%d\n", gpgsig->hash_algo); - - if(gpgsig->summary & GPGME_SIGSUM_VALID) { - /* good signature, continue */ - _alpm_log(PM_LOG_DEBUG, _("Package %s has a valid signature.\n"), - pkgpath); - } else if(gpgsig->summary & GPGME_SIGSUM_GREEN) { - /* 'green' signature, not sure what to do here */ - _alpm_log(PM_LOG_WARNING, _("Package %s has a green signature.\n"), - pkgpath); - } else if(gpgsig->summary & GPGME_SIGSUM_KEY_MISSING) { - pm_errno = PM_ERR_SIG_UNKNOWN; - _alpm_log(PM_LOG_WARNING, _("Package %s has a signature from an unknown key.\n"), - pkgpath); - ret = -1; - } else { - /* we'll capture everything else here */ - pm_errno = PM_ERR_SIG_INVALID; - _alpm_log(PM_LOG_ERROR, _("Package %s has an invalid signature.\n"), - pkgpath); - ret = 1; + + // Read the output of the command to see if there are lines with the pattern + // TRUST_MARGINAL, TRUST_FULLY or TRUST_ULTIMATE. These results indicate that + // the level of trust of the key are at leas MARGINAL, which means that the + // users trusts the signer or there is a path in the web of trust that + // assigns at least the MARGINAL level + CALLOC(line, MAX_LINE, sizeof(char), RET_ERR(PM_ERR_MEMORY, NULL)); + while (fgets(line, MAX_LINE, output)) { + // Debug of status messages from gpg + if (strncmp(line, "[GNUPG:]", (size_t)strlen("[GNUPG:]")) == 0) { + _alpm_log(PM_LOG_DEBUG, "%s\n", line); + } + if (strncmp(line, TRUST_MARGINAL, (size_t)strlen(TRUST_MARGINAL)) == 0 || + strncmp(line, TRUST_FULLY, (size_t)strlen(TRUST_FULLY)) == 0 || + strncmp(line, TRUST_ULTIMATE, (size_t)strlen(TRUST_ULTIMATE)) == 0) { + trusted = 1; + } } + + resultCommand = pclose(output); -error: - gpgme_data_release(sigdata); - gpgme_data_release(pkgdata); - gpgme_release(ctx); - if(sigfile) { - fclose(sigfile); + // We don't need to check the return status of the command, because the + // trusted variable only is 1 if the signature is valid and trusted. + if (trusted == 1) { + status = 0; } - if(pkgfile) { - fclose(pkgfile); +cleanup: + if (line) { + FREE(line); } - if(err != GPG_ERR_NO_ERROR) { - _alpm_log(PM_LOG_ERROR, _("GPGME error: %s\n"), gpgme_strerror(err)); - RET_ERR(PM_ERR_GPGME, -1); + if (command) { + FREE(command); } - return(ret); + return status; } /** @@ -207,7 +191,7 @@ int SYMEXPORT alpm_pkg_check_pgp_signature(pmpkg_t *pkg) ALPM_LOG_FUNC; ASSERT(pkg != NULL, return(0)); - return(_alpm_gpgme_checksig(alpm_pkg_get_filename(pkg), + return(_alpm_gpg_checksig_memory(alpm_pkg_get_filename(pkg), alpm_pkg_get_pgpsig(pkg))); } diff --git a/lib/libalpm/signing.h b/lib/libalpm/signing.h index c004697..fe28a22 100644 --- a/lib/libalpm/signing.h +++ b/lib/libalpm/signing.h @@ -21,7 +21,8 @@ #include "alpm.h" -int _alpm_gpgme_checksig(const char *pkgpath, const pmpgpsig_t *sig); +int _alpm_gpg_checksig_memory(const char *pkgpath, const pmpgpsig_t *sig); +int _alpm_gpg_checksig_file(const char *pkgpath, const char *sigpath); #endif /* _ALPM_SIGNING_H */ diff --git a/lib/libalpm/sync.c b/lib/libalpm/sync.c index 3994d5d..aa1fffe 100644 --- a/lib/libalpm/sync.c +++ b/lib/libalpm/sync.c @@ -938,7 +938,7 @@ int _alpm_sync_commit(pmtrans_t *trans, pmdb_t *db_local, alpm_list_t **data) pmdb_t *sdb = alpm_pkg_get_db(spkg); if(sdb->pgp_verify != PM_PGP_VERIFY_NEVER) { - int ret = _alpm_gpgme_checksig(filepath, pgpsig); + int ret = _alpm_gpg_checksig_memory(filepath, pgpsig); if((sdb->pgp_verify == PM_PGP_VERIFY_ALWAYS && ret != 0) || (sdb->pgp_verify == PM_PGP_VERIFY_OPTIONAL && ret == 1)) { errors++; diff --git a/scripts/.gitignore b/scripts/.gitignore index eafc493..1c662de 100644 --- a/scripts/.gitignore +++ b/scripts/.gitignore @@ -4,3 +4,4 @@ rankmirrors repo-add repo-remove pkgdelta +pacman-key diff --git a/scripts/Makefile.am b/scripts/Makefile.am index 330acb9..d4f96de 100644 --- a/scripts/Makefile.am +++ b/scripts/Makefile.am @@ -7,6 +7,7 @@ bin_SCRIPTS = \ OURSCRIPTS = \ makepkg \ + pacman-key \ pacman-optimize \ pkgdelta \ rankmirrors \ @@ -14,6 +15,7 @@ OURSCRIPTS = \ EXTRA_DIST = \ makepkg.sh.in \ + pacman-key.sh.in \ pacman-optimize.sh.in \ pkgdelta.sh.in \ rankmirrors.sh.in \ @@ -60,6 +62,7 @@ $(OURSCRIPTS): Makefile @mv $@.tmp $@ makepkg: $(srcdir)/makepkg.sh.in +pacman-key: ${srcdir}/pacman-key.sh.in pacman-optimize: $(srcdir)/pacman-optimize.sh.in pkgdelta: $(srcdir)/pkgdelta.sh.in rankmirrors: $(srcdir)/rankmirrors.sh.in diff --git a/scripts/makepkg.sh.in b/scripts/makepkg.sh.in index 223bf12..cb25312 100644 --- a/scripts/makepkg.sh.in +++ b/scripts/makepkg.sh.in @@ -28,7 +28,7 @@ # makepkg uses quite a few external programs during its execution. You # need to have at least the following installed for makepkg to function: # bsdtar (libarchive), bzip2, coreutils, fakeroot, find (findutils), -# gettext, grep, gzip, openssl, sed, tput (ncurses), xz +# gettext, grep, gzip, openssl, sed, tput (ncurses), xz, gpg # gettext initialization export TEXTDOMAIN='pacman' @@ -43,6 +43,8 @@ BUILDSCRIPT='@BUILDSCRIPT@' startdir="$PWD" srcdir="$startdir/src" pkgdir="$startdir/pkg" +GPG="gpg2" +SIG_EXT=".sig" packaging_options=('strip' 'docs' 'libtool' 'emptydirs' 'zipman' 'purge') other_options=('ccache' 'distcc' 'makeflags' 'force') @@ -74,6 +76,7 @@ BUILDFUNC=0 PKGFUNC=0 SPLITPKG=0 PKGLIST="" +SIGNKEY="" # Forces the pkgver of the current PKGBUILD. Used by the fakeroot call # when dealing with svn/cvs/etc PKGBUILDs. @@ -1028,7 +1031,7 @@ create_package() { local ret=0 [[ -f $pkg_file ]] && rm -f "$pkg_file" - [[ -f $pkg_file.sig ]] && rm -f "$pkg_file.sig" + [[ -f ${pkg_file}${SIG_EXT} ]] && rm -f "${pkg_file}${SIG_EXT}" # when fileglobbing, we want * in an empty directory to expand to # the null string rather than itself @@ -1055,7 +1058,7 @@ create_package() { if (( ! ret )) && [[ "$PKGDEST" != "${startdir}" ]]; then ln -sf "${pkg_file}" "${pkg_file/$PKGDEST/$startdir}" ret=$? - [[ -f $pkg_file.sig ]] && ln -sf "$pkg_file.sig" "${pkg_file/$PKGDEST/$startdir}.sig" + [[ -f ${pkg_file}${SIG_EXT} ]] && ln -sf "${pkg_file}${SIG_EXT}" "${pkg_file/$PKGDEST/$startdir}${SIG_EXT}" fi if (( ret )); then @@ -1070,13 +1073,24 @@ create_signature() { local ret=0 local filename="$1" msg "$(gettext "Signing package...")" - if [ ! $(type -p "gpg") ]; then - error "$(gettext "Cannot find the gpg binary! Is gnupg installed?")" + if [[ ! $(type -p "${GPG}") ]]; then + error "$(gettext "Cannot find the ${GPG} binary! Is gnupg installed?")" exit 1 # $E_MISSING_PROGRAM fi - gpg --detach-sign --use-agent "$filename" || ret=$? + + # Check if SIGNKEY is valid. + local SIGNWITHKEY="" + if [[ "${SIGNKEY}" ]]; then + if ! ${GPG} --list-key "${SIGNKEY}" 1>/dev/null 2>&1; then + error "$(gettext "The key ${SIGNKEY} doesn\'t exist.")" + exit 1 + fi + SIGNWITHKEY="-u ${SIGNKEY}" + fi + # The signature will be generated directly in ascii-friendly format + ${GPG} --detach-sign ${SIGNWITHKEY} "$filename" || ret=$? if (( ! ret )); then - msg2 "$(gettext "Created signature file %s.")" "$filename.sig" + msg2 "$(gettext "Created signature file %s.")" "${filename}${SIG_EXT}" else warning "$(gettext "Failed to sign package file.")" fi @@ -1522,6 +1536,7 @@ usage() { echo "$(gettext " --pkg <list> Only build listed packages from a split package")" echo "$(gettext " --skipinteg Do not fail when integrity checks are missing")" echo "$(gettext " --source Generate a source-only tarball without downloaded sources")" + echo "$(gettext " --signwithkey Selects an specific key to use for signing")" echo echo "$(gettext "These options can be passed to pacman:")" echo @@ -1557,7 +1572,7 @@ OPT_SHORT="AcCdefFghiLmop:rRsV" OPT_LONG="allsource,asroot,ignorearch,clean,cleancache,nodeps" OPT_LONG="$OPT_LONG,noextract,force,forcever:,geninteg,help,holdver" OPT_LONG="$OPT_LONG,install,log,nocolor,nobuild,pkg:,rmdeps,repackage,skipinteg" -OPT_LONG="$OPT_LONG,source,syncdeps,version,config:" +OPT_LONG="$OPT_LONG,source,syncdeps,version,config:,signwithkey" # Pacman Options OPT_LONG="$OPT_LONG,noconfirm,noprogressbar" OPT_TEMP="$(parse_options $OPT_SHORT $OPT_LONG "$@" || echo 'PARSE_OPTIONS FAILED')" @@ -1600,6 +1615,7 @@ while true; do --skipinteg) SKIPINTEG=1 ;; --source) SOURCEONLY=1 ;; -s|--syncdeps) DEP_BIN=1 ;; + --signwithkey) shift; SIGNKEY=$1 ;; -h|--help) usage; exit 0 ;; # E_OK -V|--version) version; exit 0 ;; # E_OK diff --git a/scripts/pacman-key.sh.in b/scripts/pacman-key.sh.in new file mode 100644 index 0000000..a33c684 --- /dev/null +++ b/scripts/pacman-key.sh.in @@ -0,0 +1,278 @@ +#!/bin/bash -e +# +# pacman-key - manages pacman's keyring +# @configure_input@ +# +# Copyright (c) 2010 - Denis A. Altoé Falqueto <denisfalqueto@gmail.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +# gettext initialization +export TEXTDOMAIN='pacman' +export TEXTDOMAINDIR='@localedir@' + +# Based on apt-key, from Debian +# Author: Denis A. Altoé Falqueto <denisfalqueto at gmail dot com> + +PACMAN_KEY_VERSION="0.1" + +# According to apt-key, gpg doesn't like to be called without a secret keyring. +# We will not really need one, because pacman will not sign packages, just verify +# their integrities. +PACMAN_KEYRING_DIR="/etc/pacman.d/gnupg" +PACMAN_SECRET="secret.gpg" +PACMAN_TRUSTDB="trustdb.gpg" +PACMAN_KEYRING="pacman.gpg" +PACMAN_SHARE_DIR="/usr/share/pacman" + +# Default parameters for the command gpg. Some more will be added when needed +GPG_PROGRAM="gpg2" +GPG="${GPG_PROGRAM} --ignore-time-conflict --no-options --no-default-keyring" +GPG_PACMAN="${GPG} --homedir ${PACMAN_KEYRING_DIR} --secret-keyring ${PACMAN_KEYRING_DIR}/${PACMAN_SECRET} \ + --trustdb-name ${PACMAN_KEYRING_DIR}/${PACMAN_TRUSTDB} --keyring ${PACMAN_KEYRING_DIR}/${PACMAN_KEYRING} \ + --primary-keyring ${PACMAN_KEYRING_DIR}/${PACMAN_KEYRING}" +SIG_EXT=".sig" + +# Read-only keyring with keys to be added to the keyring +ADDED_KEYS="${PACMAN_SHARE_DIR}/addedkeys.gpg" + +# Read-only keyring with keys removed from the keyring. They need to be removed before +# the keys from the added keyring be really imported +REMOVED_KEYS="${PACMAN_SHARE_DIR}/removedkeys.gpg" + +usage() { + echo "pacman-key - Pacman's keyring management utility" + echo "Usage: $(basename $0) command [arguments]" + echo + echo "Manage pacman's list of trusted keys" + echo + echo " pacman-key add <file> ... - add the key contained in <file> ('-' for stdin)" + echo " pacman-key del <keyid> ... - remove the key <keyid>" + echo " pacman-key export <keyid> ... - output the key <keyid>" + echo " pacman-key exportall - output all trusted keys" + echo " pacman-key receive <keyserver> <keyid> ... - fetch the keyids from the specified keyserver URL" + echo " pacman-key trust <keyid> ... - set the truslevel of the given key" + echo " pacman-key updatedb - update the trustdb of pacman" + echo " pacman-key reload - reloads the keys from the keyring package" + echo " pacman-key list - list keys" + echo " pacman-key finger <keyid> ... - list fingerprints" + echo " pacman-key adv <params> - pass advanced options to gpg" + echo " pacman-key help - displays this message" + echo " pacman-key version - displays the current version" + echo + echo "If no specific keyring file is given the command applies to the default keyring." +} + +prepare_homedir() { + if [[ ! -d ${PACMAN_KEYRING_DIR} ]] ; then + mkdir -p "${PACMAN_KEYRING_DIR}" + [[ ! -f "${PACMAN_KEYRING_DIR}/${PACMAN_SECRET}" ]] && touch "${PACMAN_KEYRING_DIR}/${PACMAN_SECRET}" + [[ ! -f "${PACMAN_KEYRING_DIR}/${PACMAN_KEYRING}" ]] && touch "${PACMAN_KEYRING_DIR}/${PACMAN_KEYRING}" + chmod 700 "${PACMAN_KEYRING_DIR}" + chmod 600 "${PACMAN_KEYRING_DIR}"/* + fi +} + +add_key() { + ${GPG_PACMAN} --quiet --batch --import "$1" +} + +remove_key() { + ${GPG_PACMAN} --quiet --batch --delete-key --yes "$1" +} + +update_trustdb() { + ${GPG_PACMAN} --batch --check-trustdb +} + +list_sigs() { + ${GPG_PACMAN} --batch --list-sigs +} + +list_fingerprints() { + ${GPG_PACMAN} --batch --fingerprint $* +} + +export_key() { + ${GPG_PACMAN} --armor --export "$1" +} + +export_all() { + ${GPG_PACMAN} --armor --export +} + +trust_key() { + # Verify if the key exists in pacman's keyring + if ${GPG_PACMAN} --list-key "$1" > /dev/null 2>&1 ; then + ${GPG_PACMAN} --fingerprint "$1" + ${GPG_PACMAN} --edit-key "$1" + else + echo "The key identified by $1 doesn't exist" + exit 1 + fi +} + +reload_keyring() { + # Verify the signature of removed keys file + if [[ -f ${REMOVED_KEYS} ]] && ! ${GPG_PACMAN} --quiet --verify ${REMOVED_KEYS}${SIG_EXT} ; then + echo "The signature of file ${REMOVED_KEYS} is not valid." + exit 1 + fi + + # Verify the signature of the added keys file + if [[ -f ${ADDED_KEYS} ]] && ! ${GPG_PACMAN} --quiet --verify ${ADDED_KEYS}${SIG_EXT} ; then + echo "The signature of file ${ADDED_KEYS} is not valid." + exit 1 + fi + + # Remove the keys from REMOVED_KEYS keyring + [[ -r ${REMOVED_KEYS} ]] && cat "${REMOVED_KEYS}" | while read key ; do + ${GPG_PACMAN} --quiet --batch --yes --delete-keys ${key} + done + + # Add keys from the current set of keys from pacman-keyring package. The web of trust will + # be updated automatically. + if [[ -r ${ADDED_KEYS} ]] ; then + add_keys=$(${GPG} --keyring ${ADDED_KEYS} --with-colons --list-keys | grep ^pub | cut -d: -f5) + for add_key in $add_keys; do + echo "Chave $add_key" + ${GPG} --quiet --batch --keyring $ADDED_KEYS --export $add_key | ${GPG_PACMAN} --import + ADDED=1 + done + fi + + # Update trustdb, just to be sure + update_trustdb +} + +receive() { + keyserver="$1" + shift + ${GPG_PACMAN} --keyserver ${keyserver} $* +} + +# PROGRAM START + +if ! type gettext &>/dev/null; then + gettext() { + echo "$@" + } +fi + +command="$1" +if [[ -z "$command" ]]; then + usage + exit 1 +fi +shift + +if [[ "$command" != "version" && "$command" != "help" ]] && ! which "${GPG_PROGRAM}" >/dev/null 2>&1; then + echo >&2 "Warning: gnupg does not seem to be installed." + echo >&2 "Warning: pacman-key requires gnupg for most operations." + echo >&2 +fi + +prepare_homedir + +case "$command" in + add) + if (( $# == 0 )) ; then + echo "You need to specify at least one key identifier" + usage + exit 1 + fi + while (( $# > 0 )) ; do + add_key $1 + shift + done + ;; + del|rm|remove) + if (( $# == 0 )) ; then + echo "You need to specify at least one key identifier" + usage + exit 1 + fi + while (( $# > 0 )) ; do + remove_key $1 + shift + done + ;; + updatedb) + update_trustdb + ;; + reload) + reload_keyring + ;; + list) + list_sigs + ;; + finger*) + if (( $# == 0 )) ; then + echo "You need to specify at least one key identifier" + usage + exit 1 + fi + list_fingerprints $* + ;; + export) + if (( $# == 0 )) ; then + echo "You need to specify at least one key identifier" + usage + exit 1 + fi + while (( $# > 0 )) ; do + export_key $1 + shift + done + ;; + exportall) + export_all + ;; + receive) + if (( $# < 2 )) ; then + echo "You need to specify the keyserver and at least one key identifier" + usage + exit 1 + fi + receive $* + ;; + trust) + if (( $# == 0 )) ; then + echo "You need to specify at least one key identifier" + usage + exit 1 + fi + while (( $# > 0 )) ; do + trust_key $1 + shift + done + ;; + adv*) + echo "Executing: ${GPG_PACMAN} $*" + ${GPG_PACMAN} $* || ret=$? + exit $ret + ;; + --help) + usage + ;; + --version) + echo "pacman-key v${PACMAN_KEY_VERSION}" + echo " This program can be freely distributed under the GPL v2" + ;; + *) + usage + exit 1 + ;; +esac diff --git a/scripts/repo-add.sh.in b/scripts/repo-add.sh.in index 9b04be9..ad9b524 100644 --- a/scripts/repo-add.sh.in +++ b/scripts/repo-add.sh.in @@ -26,6 +26,8 @@ export TEXTDOMAINDIR='@localedir@' myver='@PACKAGE_VERSION@' confdir='@sysconfdir@' +GPG="gpg2" +SIG_EXT=".sig" QUIET=0 SIGN=0 @@ -62,8 +64,8 @@ error() { # print usage instructions usage() { printf "repo-add, repo-remove (pacman) %s\n\n" "$myver" - printf "$(gettext "Usage: repo-add [-q] [-s] [-v] <path-to-db> <package|delta> ...\n")" - printf "$(gettext "Usage: repo-remove [-q] <path-to-db> <packagename|delta> ...\n\n")" + printf "$(gettext "Usage: repo-add [-q] [-s [-k key]] [-v] <path-to-db> <package|delta> ...\n")" + printf "$(gettext "Usage: repo-remove [-q] [-s [-k key]] <path-to-db> <packagename|delta> ...\n\n")" printf "$(gettext "\ repo-add will update a package database by reading a package file.\n\ Multiple packages to add can be specified on the command line.\n\n")" @@ -185,13 +187,24 @@ create_signature() { local dbfile="$1" local ret=0 msg "$(gettext "Signing database...")" - if [ ! $(type -p "gpg") ]; then - error "$(gettext "Cannot find the gpg binary! Is gnupg installed?")" + if [ ! $(type -p "${GPG}") ]; then + error "$(gettext "Cannot find the ${GPG} binary! Is gnupg installed?")" exit 1 # $E_MISSING_PROGRAM fi - gpg --detach-sign --use-agent "$dbfile" || ret=$? + + # Check if SIGNKEY is valid. + local SIGNWITHKEY="" + if [[ "${SIGNKEY}" ]]; then + if ! "${GPG}" --list-key "${SIGNKEY}" 1>/dev/null 2>&1; then + error "$(gettext "The key ${SIGNKEY} doesnn't exist.")" + exit 1 + fi + SIGNWITHKEY="-u ${SIGNKEY}" + fi + echo "${GPG} --detach-sign ${SIGNWITHKEY} $dbfile" + ${GPG} --detach-sign ${SIGNWITHKEY} "$dbfile" || ret=$? if (( ! ret )); then - msg2 "$(gettext "Created signature file %s.")" "$dbfile.sig" + msg2 "$(gettext "Created signature file %s.")" "${dbfile}${SIG_EXT}" else warning "$(gettext "Failed to sign package database.")" fi @@ -203,15 +216,15 @@ verify_signature() { local dbfile="$1" local ret=0 msg "$(gettext "Verifying database signature...")" - if [ ! $(type -p "gpg") ]; then - error "$(gettext "Cannot find the gpg binary! Is gnupg installed?")" + if [ ! $(type -p "${GPG}") ]; then + error "$(gettext "Cannot find the ${GPG} binary! Is gnupg installed?")" exit 1 # $E_MISSING_PROGRAM fi - if [[ ! -f $dbfile.sig ]]; then + if [[ ! -f ${dbfile}${SIG_EXT} ]]; then warning "$(gettext "No existing signature found, skipping verification.")" return fi - gpg --verify "$dbfile.sig" || ret=$? + ${GPG} --verify "${dbfile}${SIG_EXT}" || ret=$? if (( ! ret )); then msg2 "$(gettext "Database signature file verified.")" else @@ -298,9 +311,9 @@ db_write_entry() echo -e "%MD5SUM%\n$md5sum\n" >>desc # add base64'd PGP signature - if [[ -f $startdir/$pkgfile.sig ]]; then + if [[ -f $startdir/$pkgfile${SIG_EXT} ]]; then echo -e "%PGPSIG%" >>desc - echo -e "$(openssl base64 -in "$startdir/$pkgfile.sig" | tr -d '\n')\n" >>desc + echo -e "$(openssl enc -base64 -in "$startdir/$pkgfile${SIG_EXT}")\n" >>desc fi [[ -n $url ]] && echo -e "%URL%\n$url\n" >>desc @@ -492,10 +505,24 @@ trap 'trap_exit "$(gettext "An unknown error has occured. Exiting...")"' ERR success=0 # parse arguments -for arg in "$@"; do +while [[ $# > 0 ]] ; do + arg="$1" case "$arg" in -q|--quiet) QUIET=1;; - -s|--sign) SIGN=1;; + -s|--sign) + SIGN=1 + # The signature will be made, even if there are no operations + success=1 + ;; + -k) + shift + SIGNKEY="$1" + # Check if key really exists + if ! ${GPG} --list-key ${SIGNKEY} 1> /dev/null 2>&1; then + error "$(gettext "Cannot find key $SIGNKEY.")" + exit 1 + fi + ;; -v|--verify) VERIFY=1;; *) if [[ -z $REPO_DB_FILE ]]; then @@ -510,6 +537,7 @@ for arg in "$@"; do fi ;; esac + shift done # if at least one operation was a success, re-zip database @@ -529,18 +557,19 @@ if (( success )); then cd "$tmpdir" if [[ -n $(ls) ]]; then bsdtar -c${TAR_OPT}f "$filename" * - create_signature "$filename" else # we have no packages remaining? zip up some emptyness warning "$(gettext "No packages remain, creating empty database.")" bsdtar -c${TAR_OPT}f "$filename" -T /dev/null fi + # The signature must be dealt with in both cases, empty repo or not. + create_signature "$filename" cd "$startdir" [[ -f $REPO_DB_FILE ]] && mv -f "$REPO_DB_FILE" "${REPO_DB_FILE}.old" - [[ -f $REPO_DB_FILE.sig ]] && rm -f "$REPO_DB_FILE.sig" + [[ -f $REPO_DB_FILE${SIG_EXT} ]] && rm -f "$REPO_DB_FILE${SIG_EXT}" [[ -f $tmpdir/$filename ]] && mv "$tmpdir/$filename" "$REPO_DB_FILE" - [[ -f $tmpdir/$filename.sig ]] && mv "$tmpdir/$filename.sig" "$REPO_DB_FILE.sig" + [[ -f $tmpdir/$filename${SIG_EXT} ]] && mv "$tmpdir/$filename${SIG_EXT}" "$REPO_DB_FILE${SIG_EXT}" ln -sf "$REPO_DB_FILE" "${REPO_DB_FILE%.tar.*}" else msg "$(gettext "No packages modified, nothing to do.")" -- 1.7.1