Support checking signature for pkg bootstrap.

If the pkg.conf is configured with SIGNATURE_TYPE: FINGERPRINTS,
and FINGERPRINTS: /etc/keys/pkg then a pkg.sig file is fetched along
with pkg.txz. The signature contains the signature provided by the
signing server, and the public key. The .sig is the exact output
from the signing server in the following format:

  SIGNATURE
  <openssl signed>
  CERT
  <rsa public key>
  END

The signature is verified with the following logic:

 - If the .sig file is missing, it fails.
 - If the .sig doesn't validate, it fails.
 - If the public key in the .sig is not in the known trusted fingerprints,
   it fails.
 - If the public key is in the revoked key list, it fails.

Approved by:	bapt
MFC after:	2 days
Discussed by:	bapt with des, jonathan, gavin
This commit is contained in:
Bryan Drewery 2013-10-26 03:43:02 +00:00
parent 95073c2d2e
commit f12db248e7
Notes: svn2git 2020-12-20 02:59:44 +00:00
svn path=/head/; revision=257147
5 changed files with 615 additions and 45 deletions

View File

@ -2,5 +2,7 @@
FreeBSD: {
url: "pkg+http://pkg.freebsd.org/${ABI}/latest",
mirror_type: "srv",
signature_type: "fingerprints",
fingerprints: "/etc/keys/pkg",
enabled: "yes"
}

View File

@ -7,7 +7,7 @@ NO_MAN= yes
CFLAGS+=-I${.CURDIR}/../../contrib/libyaml/include
.PATH: ${.CURDIR}/../../contrib/libyaml/include
DPADD= ${LIBARCHIVE} ${LIBELF} ${LIBFETCH} ${LIBYAML} ${LIBSBUF}
LDADD= -larchive -lelf -lfetch -lyaml -lsbuf
LDADD= -larchive -lelf -lfetch -lyaml -lsbuf -lssl
USEPRIVATELIB= yaml
.include <bsd.prog.mk>

View File

@ -1,5 +1,6 @@
/*-
* Copyright (c) 2013 Baptiste Daroussin <bapt@FreeBSD.org>
* Copyright (c) 2013 Bryan Drewery <bdrewery@FreeBSD.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@ -86,7 +87,21 @@ static struct config_entry c[] = {
"NO",
NULL,
false,
}
},
[SIGNATURE_TYPE] = {
PKG_CONFIG_STRING,
"SIGNATURE_TYPE",
NULL,
NULL,
false,
},
[FINGERPRINTS] = {
PKG_CONFIG_STRING,
"FINGERPRINTS",
NULL,
NULL,
false,
},
};
static const char *
@ -509,6 +524,12 @@ config_parse(yaml_document_t *doc, yaml_node_t *node, pkg_conf_file_t conftype)
else if (strcasecmp(key->data.scalar.value,
"mirror_type") == 0)
sbuf_cpy(buf, "MIRROR_TYPE");
else if (strcasecmp(key->data.scalar.value,
"signature_type") == 0)
sbuf_cpy(buf, "SIGNATURE_TYPE");
else if (strcasecmp(key->data.scalar.value,
"fingerprints") == 0)
sbuf_cpy(buf, "FINGERPRINTS");
else { /* Skip unknown entries for future use. */
++pair;
continue;

View File

@ -37,6 +37,8 @@ typedef enum {
ABI,
MIRROR_TYPE,
ASSUME_ALWAYS_YES,
SIGNATURE_TYPE,
FINGERPRINTS,
CONFIG_SIZE
} pkg_config_key;

View File

@ -1,5 +1,6 @@
/*-
* Copyright (c) 2012-2013 Baptiste Daroussin <bapt@FreeBSD.org>
* Copyright (c) 2013 Bryan Drewery <bdrewery@FreeBSD.org>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@ -28,10 +29,15 @@
__FBSDID("$FreeBSD$");
#include <sys/param.h>
#include <sys/queue.h>
#include <sys/types.h>
#include <sys/sbuf.h>
#include <sys/wait.h>
#define _WITH_GETLINE
#include <archive.h>
#include <archive_entry.h>
#include <dirent.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
@ -43,10 +49,35 @@ __FBSDID("$FreeBSD$");
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <yaml.h>
#include <openssl/err.h>
#include <openssl/ssl.h>
#include "dns_utils.h"
#include "config.h"
struct sig_cert {
unsigned char *sig;
int siglen;
unsigned char *cert;
int certlen;
bool trusted;
};
typedef enum {
HASH_UNKNOWN,
HASH_SHA256,
} hash_t;
struct fingerprint {
hash_t type;
char hash[BUFSIZ];
STAILQ_ENTRY(fingerprint) next;
};
STAILQ_HEAD(fingerprint_list, fingerprint);
static int
extract_pkg_static(int fd, char *p, int sz)
{
@ -129,59 +160,34 @@ install_pkg_static(char *path, char *pkgpath)
}
static int
bootstrap_pkg(void)
fetch_to_fd(const char *url, char *path)
{
struct url *u;
FILE *remote;
FILE *config;
char *site;
struct dns_srvinfo *mirrors, *current;
/* To store _https._tcp. + hostname + \0 */
char zone[MAXHOSTNAMELEN + 13];
char url[MAXPATHLEN];
char conf[MAXPATHLEN];
char tmppkg[MAXPATHLEN];
const char *packagesite, *mirror_type;
char buf[10240];
char pkgstatic[MAXPATHLEN];
int fd, retry, ret, max_retry;
struct url_stat st;
FILE *remote;
/* To store _https._tcp. + hostname + \0 */
int fd;
int retry, max_retry;
off_t done, r;
time_t now;
time_t last;
time_t now, last;
char buf[10240];
char zone[MAXHOSTNAMELEN + 13];
static const char *mirror_type = NULL;
done = 0;
last = 0;
max_retry = 3;
ret = -1;
remote = NULL;
config = NULL;
current = mirrors = NULL;
remote = NULL;
if (config_string(PACKAGESITE, &packagesite) != 0) {
warnx("No PACKAGESITE defined");
return (-1);
}
if (config_string(MIRROR_TYPE, &mirror_type) != 0) {
if (mirror_type == NULL && config_string(MIRROR_TYPE, &mirror_type)
!= 0) {
warnx("No MIRROR_TYPE defined");
return (-1);
}
printf("Bootstrapping pkg from %s, please wait...\n", packagesite);
/* Support pkg+http:// for PACKAGESITE which is the new format
in 1.2 to avoid confusion on why http://pkg.FreeBSD.org has
no A record. */
if (strncmp(URL_SCHEME_PREFIX, packagesite,
strlen(URL_SCHEME_PREFIX)) == 0)
packagesite += strlen(URL_SCHEME_PREFIX);
snprintf(url, MAXPATHLEN, "%s/Latest/pkg.txz", packagesite);
snprintf(tmppkg, MAXPATHLEN, "%s/pkg.txz.XXXXXX",
getenv("TMPDIR") ? getenv("TMPDIR") : _PATH_TMP);
if ((fd = mkstemp(tmppkg)) == -1) {
if ((fd = mkstemp(path)) == -1) {
warn("mkstemp()");
return (-1);
}
@ -229,7 +235,7 @@ bootstrap_pkg(void)
if (write(fd, buf, r) != r) {
warn("write()");
goto cleanup;
goto fetchfail;
}
done += r;
@ -241,7 +247,544 @@ bootstrap_pkg(void)
if (ferror(remote))
goto fetchfail;
if ((ret = extract_pkg_static(fd, pkgstatic, MAXPATHLEN)) == 0)
goto cleanup;
fetchfail:
if (fd != -1) {
close(fd);
fd = -1;
unlink(path);
}
cleanup:
if (remote != NULL)
fclose(remote);
return fd;
}
static struct fingerprint *
parse_fingerprint(yaml_document_t *doc, yaml_node_t *node)
{
yaml_node_pair_t *pair;
yaml_char_t *function, *fp;
struct fingerprint *f;
hash_t fct = HASH_UNKNOWN;
function = fp = NULL;
pair = node->data.mapping.pairs.start;
while (pair < node->data.mapping.pairs.top) {
yaml_node_t *key = yaml_document_get_node(doc, pair->key);
yaml_node_t *val = yaml_document_get_node(doc, pair->value);
if (key->data.scalar.length <= 0) {
++pair;
continue;
}
if (val->type != YAML_SCALAR_NODE) {
++pair;
continue;
}
if (strcasecmp(key->data.scalar.value, "function") == 0)
function = val->data.scalar.value;
else if (strcasecmp(key->data.scalar.value, "fingerprint")
== 0)
fp = val->data.scalar.value;
++pair;
continue;
}
if (fp == NULL || function == NULL)
return (NULL);
if (strcasecmp(function, "sha256") == 0)
fct = HASH_SHA256;
if (fct == HASH_UNKNOWN) {
fprintf(stderr, "Unsupported hashing function: %s\n", function);
return (NULL);
}
f = calloc(1, sizeof(struct fingerprint));
f->type = fct;
strlcpy(f->hash, fp, sizeof(f->hash));
return (f);
}
static struct fingerprint *
load_fingerprint(const char *dir, const char *filename)
{
yaml_parser_t parser;
yaml_document_t doc;
yaml_node_t *node;
FILE *fp;
struct fingerprint *f;
char path[MAXPATHLEN];
f = NULL;
snprintf(path, MAXPATHLEN, "%s/%s", dir, filename);
if ((fp = fopen(path, "r")) == NULL)
return (NULL);
yaml_parser_initialize(&parser);
yaml_parser_set_input_file(&parser, fp);
yaml_parser_load(&parser, &doc);
node = yaml_document_get_root_node(&doc);
if (node == NULL || node->type != YAML_MAPPING_NODE)
goto out;
f = parse_fingerprint(&doc, node);
out:
yaml_document_delete(&doc);
yaml_parser_delete(&parser);
fclose(fp);
return (f);
}
static struct fingerprint_list *
load_fingerprints(const char *path, int *count)
{
DIR *d;
struct dirent *ent;
struct fingerprint *finger;
struct fingerprint_list *fingerprints;
*count = 0;
fingerprints = calloc(1, sizeof(struct fingerprint_list));
if (fingerprints == NULL)
return (NULL);
STAILQ_INIT(fingerprints);
if ((d = opendir(path)) == NULL)
return (NULL);
while ((ent = readdir(d))) {
if (strcmp(ent->d_name, ".") == 0 ||
strcmp(ent->d_name, "..") == 0)
continue;
finger = load_fingerprint(path, ent->d_name);
if (finger != NULL) {
STAILQ_INSERT_TAIL(fingerprints, finger, next);
++(*count);
}
}
closedir(d);
return (fingerprints);
}
static void
sha256_hash(unsigned char hash[SHA256_DIGEST_LENGTH],
char out[SHA256_DIGEST_LENGTH * 2 + 1])
{
int i;
for (i = 0; i < SHA256_DIGEST_LENGTH; i++)
sprintf(out + (i * 2), "%02x", hash[i]);
out[SHA256_DIGEST_LENGTH * 2] = '\0';
}
static void
sha256_buf(char *buf, size_t len, char out[SHA256_DIGEST_LENGTH * 2 + 1])
{
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256_CTX sha256;
out[0] = '\0';
SHA256_Init(&sha256);
SHA256_Update(&sha256, buf, len);
SHA256_Final(hash, &sha256);
sha256_hash(hash, out);
}
static int
sha256_fd(int fd, char out[SHA256_DIGEST_LENGTH * 2 + 1])
{
int my_fd;
FILE *fp;
char buffer[BUFSIZ];
unsigned char hash[SHA256_DIGEST_LENGTH];
size_t r;
int ret;
SHA256_CTX sha256;
my_fd = -1;
fp = NULL;
r = 0;
ret = 1;
out[0] = '\0';
/* Duplicate the fd so that fclose(3) does not close it. */
if ((my_fd = dup(fd)) == -1) {
warnx("dup");
goto cleanup;
}
if ((fp = fdopen(my_fd, "rb")) == NULL) {
warnx("fdopen");
goto cleanup;
}
SHA256_Init(&sha256);
while ((r = fread(buffer, 1, BUFSIZ, fp)) > 0)
SHA256_Update(&sha256, buffer, r);
if (ferror(fp) != 0) {
warnx("fread");
goto cleanup;
}
SHA256_Final(hash, &sha256);
sha256_hash(hash, out);
ret = 0;
cleanup:
if (fp != NULL)
fclose(fp);
else if (my_fd != -1)
close(my_fd);
(void)lseek(fd, 0, SEEK_SET);
return (ret);
}
static EVP_PKEY *
load_public_key_buf(const unsigned char *cert, int certlen)
{
EVP_PKEY *pkey;
BIO *bp;
char errbuf[1024];
bp = BIO_new_mem_buf((void *)cert, certlen);
if ((pkey = PEM_read_bio_PUBKEY(bp, NULL, NULL, NULL)) == NULL)
warnx("%s", ERR_error_string(ERR_get_error(), errbuf));
BIO_free(bp);
return (pkey);
}
static bool
rsa_verify_cert(int fd, const unsigned char *key, int keylen,
unsigned char *sig, int siglen)
{
EVP_MD_CTX *mdctx;
EVP_PKEY *pkey;
char sha256[(SHA256_DIGEST_LENGTH * 2) + 2];
char errbuf[1024];
bool ret;
pkey = NULL;
mdctx = NULL;
ret = false;
/* Compute SHA256 of the package. */
if (lseek(fd, 0, 0) == -1) {
warn("lseek");
goto cleanup;
}
if ((sha256_fd(fd, sha256)) == -1) {
warnx("Error creating SHA256 hash for package");
goto cleanup;
}
if ((pkey = load_public_key_buf(key, keylen)) == NULL) {
warnx("Error reading public key");
goto cleanup;
}
/* Verify signature of the SHA256(pkg) is valid. */
printf("Verifying signature... ");
if ((mdctx = EVP_MD_CTX_create()) == NULL) {
warnx("%s", ERR_error_string(ERR_get_error(), errbuf));
goto error;
}
if (EVP_DigestVerifyInit(mdctx, NULL, EVP_sha256(), NULL, pkey) != 1) {
warnx("%s", ERR_error_string(ERR_get_error(), errbuf));
goto error;
}
if (EVP_DigestVerifyUpdate(mdctx, sha256, strlen(sha256)) != 1) {
warnx("%s", ERR_error_string(ERR_get_error(), errbuf));
goto error;
}
if (EVP_DigestVerifyFinal(mdctx, sig, siglen) != 1) {
warnx("%s", ERR_error_string(ERR_get_error(), errbuf));
goto error;
}
ret = true;
printf("done\n");
goto cleanup;
error:
printf("failed\n");
cleanup:
if (pkey)
EVP_PKEY_free(pkey);
if (mdctx)
EVP_MD_CTX_destroy(mdctx);
ERR_free_strings();
return (ret);
}
static struct sig_cert *
parse_cert(int fd) {
int my_fd;
struct sig_cert *sc;
FILE *fp;
struct sbuf *buf, *sig, *cert;
char *line;
size_t linecap;
ssize_t linelen;
my_fd = -1;
sc = NULL;
line = NULL;
linecap = 0;
if (lseek(fd, 0, 0) == -1) {
warn("lseek");
return (NULL);
}
/* Duplicate the fd so that fclose(3) does not close it. */
if ((my_fd = dup(fd)) == -1) {
warnx("dup");
return (NULL);
}
if ((fp = fdopen(my_fd, "rb")) == NULL) {
warn("fdopen");
close(my_fd);
return (NULL);
}
sig = sbuf_new_auto();
cert = sbuf_new_auto();
while ((linelen = getline(&line, &linecap, fp)) > 0) {
if (strcmp(line, "SIGNATURE\n") == 0) {
buf = sig;
continue;
} else if (strcmp(line, "CERT\n") == 0) {
buf = cert;
continue;
} else if (strcmp(line, "END\n") == 0) {
break;
}
if (buf != NULL)
sbuf_bcat(buf, line, linelen);
}
fclose(fp);
/* Trim out unrelated trailing newline */
sbuf_setpos(sig, sbuf_len(sig) - 1);
sbuf_finish(sig);
sbuf_finish(cert);
sc = calloc(1, sizeof(struct sig_cert));
sc->siglen = sbuf_len(sig);
sc->sig = calloc(1, sc->siglen);
memcpy(sc->sig, sbuf_data(sig), sc->siglen);
sc->certlen = sbuf_len(cert);
sc->cert = strdup(sbuf_data(cert));
sbuf_delete(sig);
sbuf_delete(cert);
return (sc);
}
static bool
verify_signature(int fd_pkg, int fd_sig)
{
struct fingerprint_list *trusted, *revoked;
struct fingerprint *fingerprint;
struct sig_cert *sc;
bool ret;
int trusted_count, revoked_count;
const char *fingerprints;
char path[MAXPATHLEN];
char hash[SHA256_DIGEST_LENGTH * 2 + 1];
trusted = revoked = NULL;
ret = false;
/* Read and parse fingerprints. */
if (config_string(FINGERPRINTS, &fingerprints) != 0) {
warnx("No CONFIG_FINGERPRINTS defined");
goto cleanup;
}
snprintf(path, MAXPATHLEN, "%s/trusted", fingerprints);
if ((trusted = load_fingerprints(path, &trusted_count)) == NULL) {
warnx("Error loading trusted certificates");
goto cleanup;
}
if (trusted_count == 0 || trusted == NULL) {
fprintf(stderr, "No trusted certificates found.\n");
goto cleanup;
}
snprintf(path, MAXPATHLEN, "%s/revoked", fingerprints);
if ((revoked = load_fingerprints(path, &revoked_count)) == NULL) {
warnx("Error loading revoked certificates");
goto cleanup;
}
/* Read certificate and signature in. */
if ((sc = parse_cert(fd_sig)) == NULL) {
warnx("Error parsing certificate");
goto cleanup;
}
/* Explicitly mark as non-trusted until proven otherwise. */
sc->trusted = false;
/* Parse signature and pubkey out of the certificate */
sha256_buf(sc->cert, sc->certlen, hash);
/* Check if this hash is revoked */
if (revoked != NULL) {
STAILQ_FOREACH(fingerprint, revoked, next) {
if (strcasecmp(fingerprint->hash, hash) == 0) {
fprintf(stderr, "The certificate has been "
"revoked\n");
goto cleanup;
}
}
}
STAILQ_FOREACH(fingerprint, trusted, next) {
if (strcasecmp(fingerprint->hash, hash) == 0) {
sc->trusted = true;
break;
}
}
if (sc->trusted == false) {
fprintf(stderr, "No trusted certificate found matching "
"package's certificate\n");
goto cleanup;
}
/* Verify the signature. */
if (rsa_verify_cert(fd_pkg, sc->cert, sc->certlen, sc->sig,
sc->siglen) == false) {
fprintf(stderr, "Signature is not valid\n");
goto cleanup;
}
ret = true;
cleanup:
if (trusted) {
STAILQ_FOREACH(fingerprint, trusted, next)
free(fingerprint);
free(trusted);
}
if (revoked) {
STAILQ_FOREACH(fingerprint, revoked, next)
free(fingerprint);
free(revoked);
}
if (sc) {
if (sc->cert)
free(sc->cert);
if (sc->sig)
free(sc->sig);
free(sc);
}
return (ret);
}
static int
bootstrap_pkg(void)
{
FILE *config;
int fd_pkg, fd_sig;
int ret;
char *site;
char url[MAXPATHLEN];
char conf[MAXPATHLEN];
char tmppkg[MAXPATHLEN];
char tmpsig[MAXPATHLEN];
const char *packagesite;
const char *signature_type;
char pkgstatic[MAXPATHLEN];
fd_sig = -1;
ret = -1;
config = NULL;
if (config_string(PACKAGESITE, &packagesite) != 0) {
warnx("No PACKAGESITE defined");
return (-1);
}
if (config_string(SIGNATURE_TYPE, &signature_type) != 0) {
warnx("Error looking up SIGNATURE_TYPE");
return (-1);
}
printf("Bootstrapping pkg from %s, please wait...\n", packagesite);
/* Support pkg+http:// for PACKAGESITE which is the new format
in 1.2 to avoid confusion on why http://pkg.FreeBSD.org has
no A record. */
if (strncmp(URL_SCHEME_PREFIX, packagesite,
strlen(URL_SCHEME_PREFIX)) == 0)
packagesite += strlen(URL_SCHEME_PREFIX);
snprintf(url, MAXPATHLEN, "%s/Latest/pkg.txz", packagesite);
snprintf(tmppkg, MAXPATHLEN, "%s/pkg.txz.XXXXXX",
getenv("TMPDIR") ? getenv("TMPDIR") : _PATH_TMP);
if ((fd_pkg = fetch_to_fd(url, tmppkg)) == -1)
goto fetchfail;
if (signature_type != NULL &&
strcasecmp(signature_type, "FINGERPRINTS") == 0) {
snprintf(tmpsig, MAXPATHLEN, "%s/pkg.txz.sig.XXXXXX",
getenv("TMPDIR") ? getenv("TMPDIR") : _PATH_TMP);
snprintf(url, MAXPATHLEN, "%s/Latest/pkg.txz.sig",
packagesite);
if ((fd_sig = fetch_to_fd(url, tmpsig)) == -1) {
fprintf(stderr, "Signature for pkg not available.\n");
goto fetchfail;
}
if (verify_signature(fd_pkg, fd_sig) == false)
goto cleanup;
}
if ((ret = extract_pkg_static(fd_pkg, pkgstatic, MAXPATHLEN)) == 0)
ret = install_pkg_static(pkgstatic, tmppkg);
snprintf(conf, MAXPATHLEN, "%s/etc/pkg.conf",
@ -274,9 +817,11 @@ bootstrap_pkg(void)
"ports: 'ports-mgmt/pkg'.\n");
cleanup:
if (remote != NULL)
fclose(remote);
close(fd);
if (fd_sig != -1) {
close(fd_sig);
unlink(tmpsig);
}
close(fd_pkg);
unlink(tmppkg);
return (ret);