/* * Copyright (C) 2006--2007 American Registry for Internet Numbers ("ARIN") * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND ARIN DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS. IN NO EVENT SHALL ARIN BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. */ /* $Id$ */ /* * "Cynical rsync": Recursively walk RPKI tree using rsync to pull * data from remote sites, validating certificates and CRLs as we go. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define SYSLOG_NAMES /* defines CODE prioritynames[], facilitynames[] */ #include #include #include #include #include #include #include #include #include #ifndef FILENAME_MAX #define FILENAME_MAX 1024 #endif #define SIZEOF_RSYNC (sizeof("rsync://") - 1) #define URI_MAX (FILENAME_MAX + SIZEOF_RSYNC) #define KILL_MAX 10 #ifndef HOST_NAME_MAX #define HOST_NAME_MAX 256 #endif #define XML_SUMMARY_VERSION 1 /* * Logging levels. Same general idea as syslog(), but our own * catagories based on what makes sense for this program. Default * mappings to syslog() priorities are here because it's the easiest * way to make sure that we assign a syslog level to each of ours. */ #define LOG_LEVELS \ QQ(log_sys_err, LOG_ERR) /* Error from OS or library */ \ QQ(log_usage_err, LOG_ERR) /* Bad usage (local error) */ \ QQ(log_data_err, LOG_NOTICE) /* Bad data, no biscuit */ \ QQ(log_telemetry, LOG_INFO) /* Normal progress chatter */ \ QQ(log_verbose, LOG_INFO) /* Extra chatter */ \ QQ(log_debug, LOG_DEBUG) /* Only useful when debugging */ #define QQ(x,y) x , typedef enum log_level { LOG_LEVELS LOG_LEVEL_T_MAX } log_level_t; #undef QQ #define QQ(x,y) { #x , x }, static const struct { const char *name; log_level_t value; } log_levels[] = { LOG_LEVELS }; #undef QQ /* * MIB counters. We import a long list of validation failure codes * from OpenSSL (crypto/x509/x509_vfy.h), but we also have codes * specific to rcynic. */ #define MIB_COUNTERS_FROM_OPENSSL \ QV(X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT) \ QV(X509_V_ERR_UNABLE_TO_GET_CRL) \ QV(X509_V_ERR_UNABLE_TO_DECRYPT_CERT_SIGNATURE) \ QV(X509_V_ERR_UNABLE_TO_DECRYPT_CRL_SIGNATURE) \ QV(X509_V_ERR_UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY) \ QV(X509_V_ERR_CERT_SIGNATURE_FAILURE) \ QV(X509_V_ERR_CRL_SIGNATURE_FAILURE) \ QV(X509_V_ERR_CERT_NOT_YET_VALID) \ QV(X509_V_ERR_CERT_HAS_EXPIRED) \ QV(X509_V_ERR_CRL_NOT_YET_VALID) \ QV(X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD) \ QV(X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD) \ QV(X509_V_ERR_ERROR_IN_CRL_LAST_UPDATE_FIELD) \ QV(X509_V_ERR_ERROR_IN_CRL_NEXT_UPDATE_FIELD) \ QV(X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT) \ QV(X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN) \ QV(X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY) \ QV(X509_V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE) \ QV(X509_V_ERR_CERT_CHAIN_TOO_LONG) \ QV(X509_V_ERR_CERT_REVOKED) \ QV(X509_V_ERR_INVALID_CA) \ QV(X509_V_ERR_PATH_LENGTH_EXCEEDED) \ QV(X509_V_ERR_INVALID_PURPOSE) \ QV(X509_V_ERR_CERT_UNTRUSTED) \ QV(X509_V_ERR_CERT_REJECTED) \ QV(X509_V_ERR_AKID_SKID_MISMATCH) \ QV(X509_V_ERR_AKID_ISSUER_SERIAL_MISMATCH) \ QV(X509_V_ERR_KEYUSAGE_NO_CERTSIGN) \ QV(X509_V_ERR_UNABLE_TO_GET_CRL_ISSUER) \ QV(X509_V_ERR_UNHANDLED_CRITICAL_EXTENSION) \ QV(X509_V_ERR_KEYUSAGE_NO_CRL_SIGN) \ QV(X509_V_ERR_UNHANDLED_CRITICAL_CRL_EXTENSION) \ QV(X509_V_ERR_INVALID_NON_CA) \ QV(X509_V_ERR_PROXY_PATH_LENGTH_EXCEEDED) \ QV(X509_V_ERR_KEYUSAGE_NO_DIGITAL_SIGNATURE) \ QV(X509_V_ERR_PROXY_CERTIFICATES_NOT_ALLOWED) \ QV(X509_V_ERR_INVALID_EXTENSION) \ QV(X509_V_ERR_INVALID_POLICY_EXTENSION) \ QV(X509_V_ERR_NO_EXPLICIT_POLICY) \ QV(X509_V_ERR_UNNESTED_RESOURCE) #define MIB_COUNTERS \ QQ(backup_cert_accepted, "Backup certificates accepted") \ QQ(backup_cert_rejected, "Backup certificates rejected") \ QQ(backup_crl_accepted, "Backup CRLs accepted") \ QQ(backup_crl_rejected, "Backup CRLs rejected") \ QQ(current_cert_accepted, "Current certificates accepted") \ QQ(current_cert_rejected, "Current certificates rejected") \ QQ(current_crl_accepted, "Current CRLs accepted") \ QQ(current_crl_rejected, "Current CRLs rejected") \ QQ(rsync_failed, "rsync transfers failed") \ QQ(rsync_succeeded, "rsync transfers succeeded") \ QQ(rsync_timed_out, "rsync transfers timed out") \ QQ(stale_crl, "Stale CRLs") \ QQ(malformed_sia, "Malformed SIA extensions") \ QQ(sia_missing, "SIA extensions missing") \ QQ(aia_missing, "AIA extensions missing") \ QQ(crldp_missing, "CRLDP extensions missing") \
// $URL$
// $Id$
//
// {arrowhead,arrowtail} shapes indicate database object relationships:
//   1-  none
//   m-  crow
//
// Color code:
//   Blue:  visible in left-right protocol
//   Green: created on the fly

digraph rpki_engine_objects {
	rotate=90;	size="11,8.5";	splines=true;	ratio=fill;
	node		[ shape=record ];

	// Objects visible in left-to-right protocol
	node		[ color=blue ];
	self		[ label="Self|{Preferences}" ];
	parent		[ label="Parent|{URI|TA|SIA Base}" ];
	repo		[ label="Repository|{URI|TA}" ];
	child		[ label="Child|{TA}" ];
	biz_sign	[ label="Business\nSigning Context|{Keypair|CertChain}" ];
	route_origin	[ label="Route\nOrigin|{AS Number}" ];

	// Objects which left-right protocol sees as part of other
	// objects but which SQL needs to be separate for
	// normalization.

	addr_set	[ label="Address\nPrefix", color=purple ];

	// Objects created on the fly by the RPKI engine
	node		[ color=green ];
	ca		[ label="CA|{Last CRL #|Next CRL Date|Last Issued Serial #|Last Manifest #|Next Manifest Date|SIA URI}" ];
	ca_detail	[ label="CA Detail|{CA Private Key Handle|CA Public Key|Latest CA Certificate|Manifest EE Private Key Handle|Manifest EE Public Key|Latest Manifest EE Certificate|Latest Manifest|Latest CRL}" ];

	// Some question whether these objects need to be in database
	// per se or are just properties hanging on some other object
	// like ca or ca_detail.  For manifests, we need last serial,
	// same as for CRL.
	roa		[ label="ROA|{EE Certificate|ROA}" ];

	// This one is a table of everything we have ever issued to
	// this child, not to be confused with what's -currently-
	// issued to this child.  Some question whether this hangs off
	// ca or ca_detail, but we -think- hanging off of ca_detail is
	// correct because certificates are issued by a particular
	// keypair.

	child_cert	[ label="Child CA Certificate" ];

	// One-many mappings
	edge [ color=blue, arrowtail=none, arrowhead=crow ];
	self -> biz_sign;
	biz_sign -> child;
	biz_sign -> parent;
	biz_sign -> repo;
	self -> child;
	self -> parent;
	repo -> parent;
	self -> route_origin;

	route_origin -> addr_set [ color=purple, arrowtail=none, arrowhead=crow ];

	// This is many-many because each child is an entity, each CA
	// can have multiple children, and each child can hold certs
	// from multiple CAs (thanks, RobL).
	//
	ca -> child	[ color=green, arrowtail=crow, arrowhead=crow ];

	// One-many mappings
	edge [ color=green, arrowtail=none, arrowhead=crow ];
	ca -> ca_detail;
	child -> child_cert;
	parent -> ca;
	ca_detail -> child_cert;
	ca_detail -> roa;

	// One-one mapping -- separate object to highlight dynamic nature
	edge [ color=green, arrowtail=none, arrowhead=none, style=solid ];
	route_origin -> roa;

}

// Local Variables:
// compile-command: "dot -Tps2 repository-engine-objects.dot | ps2pdf - repository-engine-objects.pdf"
// End:
me[len - 1] != '/'; if (rsync_cached(rc, name + baselen)) { logmsg(rc, log_debug, "prune: cache hit for %s, not cleaning", name); return 1; } if (rmdir(name) == 0) { logmsg(rc, log_debug, "prune: removed %s", name); return 1; } switch (errno) { case ENOENT: logmsg(rc, log_debug, "prune: nonexistant %s", name); return 1; case ENOTEMPTY: break; default: logmsg(rc, log_debug, "prune: other error %s: %s", name, strerror(errno)); return 0; } if ((dir = opendir(name)) == NULL) return 0; while ((d = readdir(dir)) != NULL) { if (d->d_name[0] == '.' && (d->d_name[1] == '\0' || (d->d_name[1] == '.' && d->d_name[2] == '\0'))) continue; if (len + strlen(d->d_name) + need_slash >= sizeof(path)) { logmsg(rc, log_debug, "prune: %s%s%s too long", name, (need_slash ? "/" : ""), d->d_name); goto done; } strcpy(path, name); if (need_slash) strcat(path, "/"); strcat(path, d->d_name); switch (d->d_type) { case DT_DIR: if (!prune_unauthenticated(rc, path, baselen)) goto done; continue; default: if (rsync_cached(rc, path + baselen)) { logmsg(rc, log_debug, "prune: cache hit %s", path); continue; } if (unlink(path) < 0) { logmsg(rc, log_debug, "prune: removing %s failed: %s", path, strerror(errno)); goto done; } logmsg(rc, log_debug, "prune: removed %s", path); continue; } } if (rmdir(name) < 0 && errno != ENOTEMPTY) logmsg(rc, log_debug, "prune: couldn't remove %s: %s", name, strerror(errno)); done: closedir(dir); return !d; } /* * Read certificate in DER format. */ static X509 *read_cert(const char *filename) { X509 *x = NULL; BIO *b; if ((b = BIO_new_file(filename, "r")) != NULL) x = d2i_X509_bio(b, NULL); BIO_free(b); return x; } /* * Read CRL in DER format. */ static X509_CRL *read_crl(const char *filename) { X509_CRL *crl = NULL; BIO *b; if ((b = BIO_new_file(filename, "r")) != NULL) crl = d2i_X509_CRL_bio(b, NULL); BIO_free(b); return crl; } /* * Parse interesting stuff from a certificate. */ static void extract_crldp_uri(const STACK_OF(DIST_POINT) *crldp, char *uri, const int urilen) { DIST_POINT *d; int i; if (!crldp || sk_DIST_POINT_num(crldp) != 1) return; d = sk_DIST_POINT_value(crldp, 0); if (d->reasons || d->CRLissuer || !d->distpoint || d->distpoint->type != 0) return; for (i = 0; i < sk_GENERAL_NAME_num(d->distpoint->name.fullname); i++) { GENERAL_NAME *n = sk_GENERAL_NAME_value(d->distpoint->name.fullname, i); assert(n != NULL); if (n->type != GEN_URI) return; if (is_rsync((char *) n->d.uniformResourceIdentifier->data) && urilen > n->d.uniformResourceIdentifier->length) { strcpy(uri, (char *) n->d.uniformResourceIdentifier->data); return; } } } static void extract_access_uri(const AUTHORITY_INFO_ACCESS *xia, const unsigned char *oid, const int oidlen, char *uri, const int urilen) { int i; if (!xia) return; for (i = 0; i < sk_ACCESS_DESCRIPTION_num(xia); i++) { ACCESS_DESCRIPTION *a = sk_ACCESS_DESCRIPTION_value(xia, i); assert(a != NULL); if (a->location->type != GEN_URI) return; if (a->method->length == oidlen && !memcmp(a->method->data, oid, oidlen) && is_rsync((char *) a->location->d.uniformResourceIdentifier->data) && urilen > a->location->d.uniformResourceIdentifier->length) { strcpy(uri, (char *) a->location->d.uniformResourceIdentifier->data); return; } } } static void parse_cert(X509 *x, certinfo_t *c, const char *uri) { static const unsigned char aia_oid[] = {0x2b, 0x6, 0x1, 0x5, 0x5, 0x7, 0x30, 0x2}; static const unsigned char sia_oid[] = {0x2b, 0x6, 0x1, 0x5, 0x5, 0x7, 0x30, 0x5}; STACK_OF(DIST_POINT) *crldp; AUTHORITY_INFO_ACCESS *xia; assert(x != NULL && c != NULL); memset(c, 0, sizeof(*c)); c->ca = X509_check_ca(x) == 1; assert(strlen(uri) < sizeof(c->uri)); strcpy(c->uri, uri); if ((xia = X509_get_ext_d2i(x, NID_info_access, NULL, NULL)) != NULL) { extract_access_uri(xia, aia_oid, sizeof(aia_oid), c->aia, sizeof(c->aia)); sk_ACCESS_DESCRIPTION_pop_free(xia, ACCESS_DESCRIPTION_free); } if ((xia = X509_get_ext_d2i(x, NID_sinfo_access, NULL, NULL)) != NULL) { extract_access_uri(xia, sia_oid, sizeof(sia_oid), c->sia, sizeof(c->sia)); sk_ACCESS_DESCRIPTION_pop_free(xia, ACCESS_DESCRIPTION_free); } if ((crldp = X509_get_ext_d2i(x, NID_crl_distribution_points, NULL, NULL)) != NULL) { extract_crldp_uri(crldp, c->crldp, sizeof(c->crldp)); sk_DIST_POINT_pop_free(crldp, DIST_POINT_free); } } /* * Check whether we already have a particular CRL, attempt to fetch it * and check issuer's signature if we don't. */ static X509_CRL *check_crl_1(const char *uri, char *path, const int pathlen, const char *prefix, X509 *issuer) { X509_CRL *crl = NULL; EVP_PKEY *pkey; int ret; assert(uri && path && issuer); if (!uri_to_filename(uri, path, pathlen, prefix) || (crl = read_crl(path)) == NULL) return NULL; if ((pkey = X509_get_pubkey(issuer)) == NULL) goto punt; ret = X509_CRL_verify(crl, pkey); EVP_PKEY_free(pkey); if (ret > 0) return crl; punt: X509_CRL_free(crl); return NULL; } static X509_CRL *check_crl(const rcynic_ctx_t *rc, const char *uri, X509 *issuer) { char path[FILENAME_MAX]; X509_CRL *crl; if (uri_to_filename(uri, path, sizeof(path), rc->authenticated) && (crl = read_crl(path)) != NULL) return crl; logmsg(rc, log_telemetry, "Checking CRL %s", uri); rsync_crl(rc, uri); if ((crl = check_crl_1(uri, path, sizeof(path), rc->unauthenticated, issuer))) { install_object(rc, uri, path, 5); mib_increment(rc, uri, current_crl_accepted); return crl; } else if (!access(path, F_OK)) { mib_increment(rc, uri, current_crl_rejected); } if ((crl = check_crl_1(uri, path, sizeof(path), rc->old_authenticated, issuer))) { install_object(rc, uri, path, 5); mib_increment(rc, uri, backup_crl_accepted); return crl; } else if (!access(path, F_OK)) { mib_increment(rc, uri, backup_crl_rejected); } return NULL; } /* * Check a certificate, including all the crypto, path validation, * and checks for conformance to the RPKI certificate profile. */ static int check_cert_cb(int ok, X509_STORE_CTX *ctx) { rcynic_x509_store_ctx_t *rctx = (rcynic_x509_store_ctx_t *) ctx; assert(rctx != NULL); switch (ctx->error) { case X509_V_OK: return ok; case X509_V_ERR_SUBJECT_ISSUER_MISMATCH: /* * Informational events, not really errors. ctx->check_issued() * is called in many places where failure to find an issuer is not * a failure for the calling function. Just leave these alone. */ return ok; case X509_V_ERR_CRL_HAS_EXPIRED: /* * This may not be an error at all. CRLs don't really "expire", * although the signatures over them do. What OpenSSL really * means by this error is just "it's now later than this source * said it intended to publish a new CRL. Unclear whether this * should be an error; current theory is that it should not be. */ logmsg(rctx->rc, log_data_err, "Stale CRL %s while checking %s", rctx->subj->crldp, rctx->subj->uri); mib_increment(rctx->rc, rctx->subj->uri, stale_crl); if (rctx->rc->allow_stale_crl) ok = 1; return ok; #define QV(x) \ case x: \ mib_increment(rctx->rc, rctx->subj->uri, mib_openssl_##x); \ break; /* * Increment counters for all known OpenSSL verify errors except * the ones we handle explicitly above. */ MIB_COUNTERS_FROM_OPENSSL; #undef QV default: mib_increment(rctx->rc, rctx->subj->uri, unknown_verify_error); break; } if (!ok) logmsg(rctx->rc, log_data_err, "Callback depth %d error %d cert %p issuer %p crl %p: %s", ctx->error_depth, ctx->error, ctx->current_cert, ctx->current_issuer, ctx->current_crl, X509_verify_cert_error_string(ctx->error)); return ok; } static int check_x509(const rcynic_ctx_t *rc, STACK_OF(X509) *certs, X509 *x, const certinfo_t *subj) { rcynic_x509_store_ctx_t rctx; STACK_OF(X509_CRL) *crls = NULL; EVP_PKEY *pkey = NULL; X509_CRL *crl = NULL; X509 *issuer; int ret = 0; assert(rc && certs && x && subj && subj->crldp[0]); issuer = sk_X509_value(certs, sk_X509_num(certs) - 1); assert(issuer != NULL); if (!X509_STORE_CTX_init(&rctx.ctx, rc->x509_store, x, NULL)) return 0; rctx.rc = rc; rctx.subj = subj; if (!subj->ta && ((pkey = X509_get_pubkey(issuer)) == NULL || X509_verify(x, pkey) <= 0)) { logmsg(rc, log_data_err, "%s failed signature check prior to CRL fetch", subj->uri); goto done; } if ((crl = check_crl(rc, subj->crldp, issuer)) == NULL) { logmsg(rc, log_data_err, "Bad CRL %s for %s", subj->crldp, subj->uri); goto done; } if ((crls = sk_X509_CRL_new_null()) == NULL || !sk_X509_CRL_push(crls, crl)) { logmsg(rc, log_sys_err, "Internal allocation error setting up CRL for validation"); goto done; } crl = NULL; X509_STORE_CTX_trusted_stack(&rctx.ctx, certs); X509_STORE_CTX_set0_crls(&rctx.ctx, crls); X509_STORE_CTX_set_verify_cb(&rctx.ctx, check_cert_cb); X509_VERIFY_PARAM_set_flags(rctx.ctx.param, X509_V_FLAG_CRL_CHECK | X509_V_FLAG_POLICY_CHECK | X509_V_FLAG_EXPLICIT_POLICY | X509_V_FLAG_X509_STRICT); X509_VERIFY_PARAM_add0_policy(rctx.ctx.param, /* {0x2b, 0x6, 0x1, 0x5, 0x5, 0x7, 0xe, 0x2} */ OBJ_txt2obj("1.3.6.1.5.5.7.14.2", 0)); if (X509_verify_cert(&rctx.ctx) <= 0) { logmsg(rc, log_data_err, "Validation failure for %s", subj->uri[0] ? subj->uri : subj->ta ? "[Trust anchor]" : "[???]"); goto done; } ret = 1; done: sk_X509_CRL_pop_free(crls, X509_CRL_free); X509_STORE_CTX_cleanup(&rctx.ctx); EVP_PKEY_free(pkey); X509_CRL_free(crl); return ret; } static X509 *check_cert_1(const rcynic_ctx_t *rc, const char *uri, char *path, const int pathlen, const char *prefix, STACK_OF(X509) *certs, const certinfo_t *issuer, certinfo_t *subj) { X509 *x = NULL; assert(uri && path && certs && issuer && subj); if (!uri_to_filename(uri, path, pathlen, prefix)) { logmsg(rc, log_data_err, "Can't convert URI %s to filename", uri); return NULL; } if (access(path, R_OK)) return NULL; if ((x = read_cert(path)) == NULL) { logmsg(rc, log_sys_err, "Can't read certificate %s", path); return NULL; } parse_cert(x, subj, uri); if (subj->sia[0] && subj->sia[strlen(subj->sia) - 1] != '/') { logmsg(rc, log_data_err, "Malformed SIA %s for %s", subj->sia, uri); mib_increment(rc, uri, malformed_sia); goto punt; } if (!subj->aia[0]) { logmsg(rc, log_data_err, "AIA missing for %s", uri); mib_increment(rc, uri, aia_missing); goto punt; } if (!issuer->ta && strcmp(issuer->uri, subj->aia)) { logmsg(rc, log_data_err, "AIA %s of %s doesn't match parent", subj->aia, uri); mib_increment(rc, uri, aia_mismatch); goto punt; } if (subj->ca && !subj->sia[0]) { logmsg(rc, log_data_err, "CA certificate %s without SIA extension", uri); mib_increment(rc, uri, sia_missing); goto punt; } #if 0 /* * Ongoing discussion about removing this restriction from the profile. */ if (!subj->ca && subj->sia[0]) { logmsg(rc, log_data_err, "EE certificate %s with SIA extension", uri); goto punt; } #endif if (!subj->crldp[0]) { logmsg(rc, log_data_err, "Missing CRLDP extension for %s", uri); mib_increment(rc, uri, crldp_missing); goto punt; } if (!check_x509(rc, certs, x, subj)) { logmsg(rc, log_data_err, "Certificate %s failed validation", uri); goto punt; } return x; punt: X509_free(x); return NULL; } static X509 *check_cert(rcynic_ctx_t *rc, char *uri, STACK_OF(X509) *certs, const certinfo_t *issuer, certinfo_t *subj, const char *prefix, const int backup) { char path[FILENAME_MAX]; X509 *x; assert(certs); if (uri_to_filename(uri, path, sizeof(path), rc->authenticated) && !access(path, R_OK)) return NULL; /* Already seen, don't walk it again */ logmsg(rc, log_telemetry, "Checking cert %s", uri); rc->indent++; if ((x = check_cert_1(rc, uri, path, sizeof(path), prefix, certs, issuer, subj)) != NULL) { install_object(rc, uri, path, 5); mib_increment(rc, uri, (backup ? backup_cert_accepted : current_cert_accepted)); } else if (!access(path, F_OK)) { mib_increment(rc, uri, (backup ? backup_cert_rejected : current_cert_rejected)); } rc->indent--; return x; } /* * Recursive walk of certificate hierarchy (core of the program). The * daisy chain recursion is to avoid having to duplicate the stack * manipulation and error handling. */ static void walk_cert(rcynic_ctx_t *rc, const certinfo_t *parent, STACK_OF(X509) *certs); static void walk_cert_1(rcynic_ctx_t *rc, char *uri, STACK_OF(X509) *certs, const certinfo_t *issuer, certinfo_t *subj, const char *prefix, const int backup) { X509 *x; if ((x = check_cert(rc, uri, certs, issuer, subj, prefix, backup)) == NULL) return; if (!sk_X509_push(certs, x)) { logmsg(rc, log_sys_err, "Internal allocation failure recursing over certificate"); return; } walk_cert(rc, subj, certs); X509_free(sk_X509_pop(certs)); } static void walk_cert(rcynic_ctx_t *rc, const certinfo_t *parent, STACK_OF(X509) *certs) { assert(parent && certs); if (parent->sia[0] && parent->ca) { int n_cert = sk_X509_num(certs); char uri[URI_MAX]; certinfo_t child; DIR *dir = NULL; rc->indent++; rsync_sia(rc, parent->sia); while (next_uri(rc, parent->sia, rc->unauthenticated, uri, sizeof(uri), &dir)) walk_cert_1(rc, uri, certs, parent, &child, rc->unauthenticated, 0); while (next_uri(rc, parent->sia, rc->old_authenticated, uri, sizeof(uri), &dir)) walk_cert_1(rc, uri, certs, parent, &child, rc->old_authenticated, 1); assert(sk_X509_num(certs) == n_cert); rc->indent--; } } /* * Main program. Parse command line, read config file, iterate over * trust anchors found via config file and do a tree walk for each * trust anchor. */ int main(int argc, char *argv[]) { int opt_jitter = 0, use_syslog = 0, use_stderr = 0, syslog_facility = 0; int opt_syslog = 0, opt_stderr = 0, opt_level = 0, prune = 1; char *cfg_file = "rcynic.conf", path[FILENAME_MAX]; char *lockfile = NULL, *xmlfile = NULL; int c, i, j, ret = 1, jitter = 600, lockfd = -1; STACK_OF(CONF_VALUE) *cfg_section = NULL; STACK_OF(X509) *certs = NULL; CONF *cfg_handle = NULL; time_t start = 0, finish; unsigned long hash; rcynic_ctx_t rc; unsigned delay; long eline = 0; memset(&rc, 0, sizeof(rc)); if ((rc.jane = strrchr(argv[0], '/')) == NULL) rc.jane = argv[0]; else rc.jane++; set_directory(&rc.authenticated, "rcynic-data/authenticated/"); set_directory(&rc.old_authenticated, "rcynic-data/authenticated.old/"); set_directory(&rc.unauthenticated, "rcynic-data/unauthenticated/"); rc.log_level = log_telemetry; rc.allow_stale_crl = 1; #define QQ(x,y) rc.priority[x] = y; LOG_LEVELS; #undef QQ OpenSSL_add_all_algorithms(); ERR_load_crypto_strings(); while ((c = getopt(argc, argv, "c:l:stpj:V")) > 0) { switch (c) { case 'c': cfg_file = optarg; break; case 'l': opt_level = 1; if (!configure_logmsg(&rc, optarg)) goto done; break; case 's': use_syslog = opt_syslog = 1; break; case 'e': use_stderr = opt_stderr = 1; break; case 'j': if (!configure_integer(&rc, &jitter, optarg)) goto done; opt_jitter = 1; break; case 'V': puts(svn_id); ret = 0; goto done; default: logmsg(&rc, log_usage_err, "usage: %s [-c configfile] [-s] [-e] [-l loglevel] [-j jitter] [-V]", rc.jane); goto done; } } if ((cfg_handle = NCONF_new(NULL)) == NULL) { logmsg(&rc, log_sys_err, "Couldn't create CONF opbject"); goto done; } if (NCONF_load(cfg_handle, cfg_file, &eline) <= 0) { if (eline <= 0) logmsg(&rc, log_usage_err, "Couldn't load config file %s", cfg_file); else logmsg(&rc, log_usage_err, "Error on line %ld of config file %s", eline, cfg_file); goto done; } if (CONF_modules_load(cfg_handle, NULL, 0) <= 0) { logmsg(&rc, log_sys_err, "Couldn't configure OpenSSL"); goto done; } if ((cfg_section = NCONF_get_section(cfg_handle, "rcynic")) == NULL) { logmsg(&rc, log_usage_err, "Couldn't load rcynic section from config file"); goto done; } for (i = 0; i < sk_CONF_VALUE_num(cfg_section); i++) { CONF_VALUE *val = sk_CONF_VALUE_value(cfg_section, i); assert(val && val->name && val->value); if (!name_cmp(val->name, "authenticated")) set_directory(&rc.authenticated, val->value); else if (!name_cmp(val->name, "old-authenticated")) set_directory(&rc.old_authenticated, val->value); else if (!name_cmp(val->name, "unauthenticated")) set_directory(&rc.unauthenticated, val->value); else if (!name_cmp(val->name, "rsync-timeout") && !configure_integer(&rc, &rc.rsync_timeout, val->value)) goto done; else if (!name_cmp(val->name, "rsync-program")) rc.rsync_program = strdup(val->value); else if (!name_cmp(val->name, "lockfile")) lockfile = strdup(val->value); else if (!opt_jitter && !name_cmp(val->name, "jitter") && !configure_integer(&rc, &jitter, val->value)) goto done; else if (!opt_level && !name_cmp(val->name, "log-level") && !configure_logmsg(&rc, val->value)) goto done; else if (!opt_syslog && !name_cmp(val->name, "use-syslog") && !configure_boolean(&rc, &use_syslog, val->value)) goto done; else if (!opt_stderr && !name_cmp(val->name, "use-stderr") && !configure_boolean(&rc, &use_stderr, val->value)) goto done; else if (!name_cmp(val->name, "syslog-facility") && !configure_syslog(&rc, &syslog_facility, facilitynames, val->value)) goto done; else if (!name_cmp(val->name, "xml-summary")) xmlfile = strdup(val->value); else if (!name_cmp(val->name, "allow-stale-crl") && !configure_boolean(&rc, &rc.allow_stale_crl, val->value)) goto done; else if (!name_cmp(val->name, "use-links") && !configure_boolean(&rc, &rc.use_links, val->value)) goto done; else if (!name_cmp(val->name, "prune") && !configure_boolean(&rc, &prune, val->value)) goto done; /* * Ugly, but the easiest way to handle all these strings. */ #define QQ(x,y) \ else if (!name_cmp(val->name, "syslog-priority-" #x) && \ !configure_syslog(&rc, &rc.priority[x], \ prioritynames, val->value)) \ goto done; LOG_LEVELS; /* the semicolon is for emacs */ #undef QQ } if ((rc.rsync_cache = sk_new(rsync_cmp)) == NULL) { logmsg(&rc, log_sys_err, "Couldn't allocate rsync_cache stack"); goto done; } if ((xmlfile) && (rc.host_counters = sk_new(host_counter_cmp)) == NULL) { logmsg(&rc, log_sys_err, "Couldn't allocate host_counters stack"); goto done; } if ((certs = sk_X509_new_null()) == NULL) { logmsg(&rc, log_sys_err, "Couldn't allocate certificate stack"); goto done; } if ((rc.x509_store = X509_STORE_new()) == NULL) { logmsg(&rc, log_sys_err, "Couldn't allocate X509_STORE"); goto done; } rc.use_syslog = use_syslog; if (use_syslog) openlog(rc.jane, LOG_PID | (use_stderr ? LOG_PERROR : 0), (syslog_facility ? syslog_facility : LOG_LOCAL0)); if (jitter > 0) { if (RAND_bytes((unsigned char *) &delay, sizeof(delay)) <= 0) { logmsg(&rc, log_sys_err, "Couldn't read random bytes"); goto done; } delay %= jitter; logmsg(&rc, log_telemetry, "Delaying %u seconds before startup", delay); while (delay > 0) delay = sleep(delay); } if (lockfile && ((lockfd = open(lockfile, O_RDWR|O_CREAT|O_NONBLOCK, 0666)) < 0 || lockf(lockfd, F_TLOCK, 0) < 0)) { if (lockfd >= 0 && errno == EAGAIN) logmsg(&rc, log_telemetry, "Lock %s held by another process", lockfile); else logmsg(&rc, log_sys_err, "Problem locking %s: %s", lockfile, strerror(errno)); goto done; } start = time(0); logmsg(&rc, log_telemetry, "Starting"); if (!rm_rf(rc.old_authenticated)) { logmsg(&rc, log_sys_err, "Couldn't remove %s: %s", rc.old_authenticated, strerror(errno)); goto done; } if (rename(rc.authenticated, rc.old_authenticated) < 0 && errno != ENOENT) { logmsg(&rc, log_sys_err, "Couldn't rename %s to %s: %s", rc.old_authenticated, rc.authenticated, strerror(errno)); goto done; } if (!access(rc.authenticated, F_OK) || !mkdir_maybe(&rc, rc.authenticated)) { logmsg(&rc, log_sys_err, "Couldn't prepare directory %s: %s", rc.authenticated, strerror(errno)); goto done; } for (i = 0; i < sk_CONF_VALUE_num(cfg_section); i++) { CONF_VALUE *val = sk_CONF_VALUE_value(cfg_section, i); certinfo_t ta_info; X509 *x; assert(val && val->name && val->value); if (name_cmp(val->name, "trust-anchor")) continue; logmsg(&rc, log_telemetry, "Processing trust anchor %s", val->value); if ((x = read_cert(val->value)) == NULL) { logmsg(&rc, log_usage_err, "Couldn't read trust anchor %s", val->value); goto done; } hash = X509_subject_name_hash(x); for (j = 0; j < INT_MAX; j++) { if (snprintf(path, sizeof(path), "%s%lx.%d.cer", rc.authenticated, hash, j) == sizeof(path)) { logmsg(&rc, log_sys_err, "Couldn't construct path name for trust anchor %s", val->value); goto done; } if (access(path, F_OK)) break; } if (j == INT_MAX) { logmsg(&rc, log_sys_err, "Couldn't find a free name for trust anchor %s", val->value); goto done; } logmsg(&rc, log_telemetry, "Copying trust anchor %s to %lx.%d.cer", val->value, hash, j); if (!mkdir_maybe(&rc, rc.authenticated) || !(rc.use_links ? ln(val->value, path) : cp(val->value, path))) { logmsg(&rc, log_sys_err, "Couldn't %s trust anchor %s", (rc.use_links ? "link" : "copy"), val->value); goto done; } parse_cert(x, &ta_info, ""); ta_info.ta = 1; sk_X509_push(certs, x); if (ta_info.crldp[0] && !check_x509(&rc, certs, x, &ta_info)) { logmsg(&rc, log_data_err, "Couldn't get CRL for trust anchor %s", val->value); } else { walk_cert(&rc, &ta_info, certs); } X509_free(sk_X509_pop(certs)); assert(sk_X509_num(certs) == 0); } if (prune && !prune_unauthenticated(&rc, rc.unauthenticated, strlen(rc.unauthenticated))) { logmsg(&rc, log_sys_err, "Trouble pruning old unauthenticated data"); goto done; } ret = 0; done: log_openssl_errors(&rc); if (sk_num(rc.host_counters) > 0) { char tad[sizeof("2006-10-13T11:22:33Z") + 1]; char hostname[HOST_NAME_MAX]; time_t tad_time = time(0); struct tm *tad_tm = gmtime(&tad_time); int ok = 1, use_stdout = !strcmp(xmlfile, "-"); FILE *f; strftime(tad, sizeof(tad), "%Y-%m-%dT%H:%M:%SZ", tad_tm); ok &= gethostname(hostname, sizeof(hostname)) == 0; if (use_stdout) f = stdout; else if (ok) ok &= (f = fopen(xmlfile, "w")) != NULL; if (ok) logmsg(&rc, log_telemetry, "Writing XML summary to %s", (use_stdout ? "standard output" : xmlfile)); if (ok) ok &= fprintf(f, "\n" "\n" " \n" " Hostname\n", tad, svn_id, XML_SUMMARY_VERSION, hostname) != EOF; for (j = 0; ok && j < MIB_COUNTER_T_MAX; ++j) ok &= fprintf(f, " <%s>%s\n", mib_counter_label[j], (mib_counter_desc[j] ? mib_counter_desc[j] : X509_verify_cert_error_string(mib_counter_openssl[j])), mib_counter_label[j]) != EOF; if (ok) ok &= fprintf(f, " \n") != EOF; for (i = 0; ok && i < sk_num(rc.host_counters); i++) { host_mib_counter_t *h = (void *) sk_value(rc.host_counters, i); assert(h); if (ok) ok &= fprintf(f, " \n %s\n", h->hostname) != EOF; for (j = 0; ok && j < MIB_COUNTER_T_MAX; ++j) ok &= fprintf(f, " <%s>%lu\n", mib_counter_label[j], h->counters[j], mib_counter_label[j]) != EOF; if (ok) ok &= fprintf(f, " \n") != EOF; } if (ok) ok &= fprintf(f, "\n") != EOF; if (f && !use_stdout) ok &= fclose(f) != EOF; if (!ok) logmsg(&rc, log_sys_err, "Couldn't write XML summary to %s: %s", xmlfile, strerror(errno)); } /* * Do NOT free cfg_section, NCONF_free() takes care of that */ sk_X509_pop_free(certs, X509_free); sk_pop_free(rc.rsync_cache, free); sk_pop_free(rc.host_counters, free); X509_STORE_free(rc.x509_store); NCONF_free(cfg_handle); CONF_modules_free(); EVP_cleanup(); ERR_free_strings(); free(rc.authenticated); free(rc.old_authenticated); free(rc.unauthenticated); if (rc.rsync_program) free(rc.rsync_program); if (lockfile) free(lockfile); if (xmlfile) free(xmlfile); if (start) { finish = time(0); logmsg(&rc, log_telemetry, "Finished, elapsed time %d:%02d:%02d", (finish - start) / 3600, (finish - start) / 60 % 60, (finish - start) % 60); } return ret; }