aboutsummaryrefslogtreecommitdiff
path: root/rp/rcynic/rcynic.c
diff options
context:
space:
mode:
Diffstat (limited to 'rp/rcynic/rcynic.c')
-rw-r--r--rp/rcynic/rcynic.c6070
1 files changed, 6070 insertions, 0 deletions
diff --git a/rp/rcynic/rcynic.c b/rp/rcynic/rcynic.c
new file mode 100644
index 00000000..dea9c48f
--- /dev/null
+++ b/rp/rcynic/rcynic.c
@@ -0,0 +1,6070 @@
+/*
+ * Copyright (C) 2013--2014 Dragon Research Labs ("DRL")
+ * Portions copyright (C) 2009--2012 Internet Systems Consortium ("ISC")
+ * Portions copyright (C) 2006--2008 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 notices and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND DRL, ISC, AND ARIN DISCLAIM ALL
+ * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL DRL,
+ * ISC, OR 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$ */
+
+/**
+ * @mainpage
+ *
+ * "Cynical rsync": Recursively walk RPKI tree using rsync to pull
+ * data from remote sites, validating certificates and CRLs as we go.
+ *
+ * Doxygen doesn't quite know what to make of a one-file C program,
+ * and ends up putting most of the interesting data @link rcynic.c
+ * here. @endlink
+ */
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/time.h>
+#include <sys/file.h>
+#include <errno.h>
+#include <sys/signal.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <dirent.h>
+#include <limits.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <utime.h>
+#include <glob.h>
+#include <sys/param.h>
+#include <getopt.h>
+
+#define SYSLOG_NAMES /* defines CODE prioritynames[], facilitynames[] */
+#include <syslog.h>
+
+#include <openssl/bio.h>
+#include <openssl/pem.h>
+#include <openssl/err.h>
+#include <openssl/x509.h>
+#include <openssl/x509v3.h>
+#include <openssl/safestack.h>
+#include <openssl/conf.h>
+#include <openssl/rand.h>
+#include <openssl/asn1t.h>
+#include <openssl/cms.h>
+
+#include <rpki/roa.h>
+#include <rpki/manifest.h>
+
+#include "bio_f_linebreak.h"
+
+#include "defstack.h"
+
+#if !defined(FILENAME_MAX) && defined(PATH_MAX) && PATH_MAX > 1024
+#define FILENAME_MAX PATH_MAX
+#elif !defined(FILENAME_MAX)
+#define FILENAME_MAX 1024
+#endif
+
+#define SCHEME_RSYNC ("rsync://")
+#define SIZEOF_RSYNC (sizeof(SCHEME_RSYNC) - 1)
+
+/**
+ * Maximum length of a hostname.
+ */
+#ifndef HOSTNAME_MAX
+#define HOSTNAME_MAX 256
+#endif
+
+/**
+ * Maximum length of an URI.
+ */
+#define URI_MAX (SIZEOF_RSYNC + HOSTNAME_MAX + 1 + FILENAME_MAX)
+
+/**
+ * Maximum number of times we try to kill an inferior process before
+ * giving up.
+ */
+#define KILL_MAX 10
+
+/**
+ * Version number of XML summary output.
+ */
+#define XML_SUMMARY_VERSION 1
+
+/**
+ * How much buffer space do we need for a raw address?
+ */
+#define ADDR_RAW_BUF_LEN 16
+
+/**
+ * How many bytes is a SHA256 digest?
+ */
+#define HASH_SHA256_LEN 32
+
+/**
+ * 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 derived from OpenSSL. Long list of validation failure
+ * codes from OpenSSL (crypto/x509/x509_vfy.h).
+ */
+
+#define MIB_COUNTERS_FROM_OPENSSL \
+ 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)
+
+/**
+ * MIB counters specific to rcynic.
+ */
+
+#define MIB_COUNTERS \
+ MIB_COUNTERS_FROM_OPENSSL \
+ QB(aia_extension_missing, "AIA extension missing") \
+ QB(aia_extension_forbidden, "AIA extension forbidden") \
+ QB(aia_uri_missing, "AIA URI missing") \
+ QB(aki_extension_issuer_mismatch, "AKI extension issuer mismatch") \
+ QB(aki_extension_missing, "AKI extension missing") \
+ QB(aki_extension_wrong_format, "AKI extension is wrong format") \
+ QB(bad_asidentifiers, "Bad ASIdentifiers extension") \
+ QB(bad_certificate_policy, "Bad certificate policy") \
+ QB(bad_cms_econtenttype, "Bad CMS eContentType") \
+ QB(bad_cms_si_contenttype, "Bad CMS SI ContentType") \
+ QB(bad_cms_signer, "Bad CMS signer") \
+ QB(bad_cms_signer_infos, "Bad CMS signerInfos") \
+ QB(bad_crl, "Bad CRL") \
+ QB(bad_ipaddrblocks, "Bad IPAddrBlocks extension") \
+ QB(bad_key_usage, "Bad keyUsage") \
+ QB(bad_manifest_digest_length, "Bad manifest digest length") \
+ QB(bad_public_key, "Bad public key") \
+ QB(bad_roa_asID, "Bad ROA asID") \
+ QB(bad_certificate_serial_number, "Bad certificate serialNumber") \
+ QB(bad_manifest_number, "Bad manifestNumber") \
+ QB(certificate_bad_signature, "Bad certificate signature") \
+ QB(certificate_failed_validation, "Certificate failed validation") \
+ QB(cms_econtent_decode_error, "CMS eContent decode error") \
+ QB(cms_includes_crls, "CMS includes CRLs") \
+ QB(cms_signer_missing, "CMS signer missing") \
+ QB(cms_ski_mismatch, "CMS SKI mismatch") \
+ QB(cms_validation_failure, "CMS validation failure") \
+ QB(crl_issuer_name_mismatch, "CRL issuer name mismatch") \
+ QB(crl_not_in_manifest, "CRL not listed in manifest") \
+ QB(crl_not_yet_valid, "CRL not yet valid") \
+ QB(crl_number_extension_missing, "CRL number extension missing") \
+ QB(crl_number_is_negative, "CRL number is negative") \
+ QB(crl_number_out_of_range, "CRL number out of range") \
+ QB(crldp_doesnt_match_issuer_sia, "CRLDP doesn't match issuer's SIA") \
+ QB(crldp_uri_missing, "CRLDP URI missing") \
+ QB(disallowed_x509v3_extension, "Disallowed X.509v3 extension") \
+ QB(duplicate_name_in_manifest, "Duplicate name in manifest") \
+ QB(inappropriate_eku_extension, "Inappropriate EKU extension") \
+ QB(malformed_aia_extension, "Malformed AIA extension") \
+ QB(malformed_sia_extension, "Malformed SIA extension") \
+ QB(malformed_basic_constraints, "Malformed basicConstraints") \
+ QB(malformed_trust_anchor, "Malformed trust anchor") \
+ QB(malformed_cadirectory_uri, "Malformed caDirectory URI") \
+ QB(malformed_crldp_extension, "Malformed CRDLP extension") \
+ QB(malformed_crldp_uri, "Malformed CRDLP URI") \
+ QB(malformed_roa_addressfamily, "Malformed ROA addressFamily") \
+ QB(malformed_tal_uri, "Malformed TAL URI") \
+ QB(manifest_carepository_mismatch, "Manifest caRepository mismatch") \
+ QB(manifest_interval_overruns_cert, "Manifest interval overruns certificate") \
+ QB(manifest_lists_missing_object, "Manifest lists missing object") \
+ QB(manifest_not_yet_valid, "Manifest not yet valid") \
+ QB(missing_resources, "Missing resources") \
+ QB(nonconformant_asn1_time_value, "Nonconformant ASN.1 time value") \
+ QB(nonconformant_public_key_algorithm,"Nonconformant public key algorithm")\
+ QB(nonconformant_signature_algorithm, "Nonconformant signature algorithm")\
+ QB(nonconformant_digest_algorithm, "Nonconformant digest algorithm") \
+ QB(nonconformant_certificate_uid, "Nonconformant certificate UID") \
+ QB(object_rejected, "Object rejected") \
+ QB(rfc3779_inheritance_required, "RFC 3779 inheritance required") \
+ QB(roa_contains_bad_afi_value, "ROA contains bad AFI value") \
+ QB(roa_max_prefixlen_too_short, "ROA maxPrefixlen too short") \
+ QB(roa_resource_not_in_ee, "ROA resource not in EE") \
+ QB(roa_resources_malformed, "ROA resources malformed") \
+ QB(rsync_transfer_failed, "rsync transfer failed") \
+ QB(rsync_transfer_timed_out, "rsync transfer timed out") \
+ QB(safi_not_allowed, "SAFI not allowed") \
+ QB(sia_cadirectory_uri_missing, "SIA caDirectory URI missing") \
+ QB(sia_extension_missing, "SIA extension missing") \
+ QB(sia_manifest_uri_missing, "SIA manifest URI missing") \
+ QB(ski_extension_missing, "SKI extension missing") \
+ QB(ski_public_key_mismatch, "SKI public key mismatch") \
+ QB(trust_anchor_key_mismatch, "Trust anchor key mismatch") \
+ QB(trust_anchor_with_crldp, "Trust anchor can't have CRLDP") \
+ QB(unknown_afi, "Unknown AFI") \
+ QB(unknown_openssl_verify_error, "Unknown OpenSSL verify error") \
+ QB(unreadable_trust_anchor, "Unreadable trust anchor") \
+ QB(unreadable_trust_anchor_locator, "Unreadable trust anchor locator") \
+ QB(wrong_object_version, "Wrong object version") \
+ QW(aia_doesnt_match_issuer, "AIA doesn't match issuer") \
+ QW(backup_thisupdate_newer_than_current, "Backup thisUpdate newer than current") \
+ QW(backup_number_higher_than_current, "Backup number higher than current") \
+ QW(bad_thisupdate, "Bad CRL thisUpdate") \
+ QW(bad_cms_si_signed_attributes, "Bad CMS SI signed attributes") \
+ QW(bad_signed_object_uri, "Bad signedObject URI") \
+ QW(crldp_names_newer_crl, "CRLDP names newer CRL") \
+ QW(digest_mismatch, "Digest mismatch") \
+ QW(ee_certificate_with_1024_bit_key, "EE certificate with 1024 bit key") \
+ QW(issuer_uses_multiple_crldp_values, "Issuer uses multiple CRLDP values")\
+ QW(multiple_rsync_uris_in_extension, "Multiple rsync URIs in extension") \
+ QW(nonconformant_issuer_name, "Nonconformant X.509 issuer name") \
+ QW(nonconformant_subject_name, "Nonconformant X.509 subject name") \
+ QW(policy_qualifier_cps, "Policy Qualifier CPS") \
+ QW(rsync_partial_transfer, "rsync partial transfer") \
+ QW(rsync_transfer_skipped, "rsync transfer skipped") \
+ QW(sia_extension_missing_from_ee, "SIA extension missing from EE") \
+ QW(skipped_because_not_in_manifest, "Skipped because not in manifest") \
+ QW(stale_crl_or_manifest, "Stale CRL or manifest") \
+ QW(tainted_by_stale_crl, "Tainted by stale CRL") \
+ QW(tainted_by_stale_manifest, "Tainted by stale manifest") \
+ QW(tainted_by_not_being_in_manifest, "Tainted by not being in manifest") \
+ QW(trust_anchor_not_self_signed, "Trust anchor not self-signed") \
+ QW(trust_anchor_skipped, "Trust anchor skipped") \
+ QW(unknown_object_type_skipped, "Unknown object type skipped") \
+ QW(uri_too_long, "URI too long") \
+ QW(wrong_cms_si_signature_algorithm, "Wrong CMS SI signature algorithm") \
+ QW(wrong_cms_si_digest_algorithm, "Wrong CMS SI digest algorithm") \
+ QG(non_rsync_uri_in_extension, "Non-rsync URI in extension") \
+ QG(object_accepted, "Object accepted") \
+ QG(rechecking_object, "Rechecking object") \
+ QG(rsync_transfer_succeeded, "rsync transfer succeeded") \
+ QG(validation_ok, "OK")
+
+#define QV(x) QB(mib_openssl_##x, 0)
+
+static const char
+ mib_counter_kind_good[] = "good",
+ mib_counter_kind_warn[] = "warn",
+ mib_counter_kind_bad[] = "bad";
+
+#define QG(x,y) mib_counter_kind_good ,
+#define QW(x,y) mib_counter_kind_warn ,
+#define QB(x,y) mib_counter_kind_bad ,
+static const char * const mib_counter_kind[] = { MIB_COUNTERS NULL };
+#undef QB
+#undef QW
+#undef QG
+
+#define QG(x,y) QQ(x,y)
+#define QW(x,y) QQ(x,y)
+#define QB(x,y) QQ(x,y)
+
+#define QQ(x,y) x ,
+typedef enum mib_counter { MIB_COUNTERS MIB_COUNTER_T_MAX } mib_counter_t;
+#undef QQ
+
+#define QQ(x,y) y ,
+static const char * const mib_counter_desc[] = { MIB_COUNTERS NULL };
+#undef QQ
+
+#define QQ(x,y) #x ,
+static const char * const mib_counter_label[] = { MIB_COUNTERS NULL };
+#undef QQ
+
+#undef QV
+
+#define QQ(x,y) 0 ,
+#define QV(x) x ,
+static const long mib_counter_openssl[] = { MIB_COUNTERS 0 };
+#undef QV
+#undef QQ
+
+/**
+ * Object sources. We always try to get fresh copies of objects using
+ * rsync, but if that fails we try using backup copies from what
+ * worked the last time we were run. This means that a URI
+ * potentially represents two different objects, so we need to
+ * distinguish them for tracking purposes in our validation log.
+ */
+
+#define OBJECT_GENERATIONS \
+ QQ(null) \
+ QQ(current) \
+ QQ(backup)
+
+#define QQ(x) object_generation_##x ,
+typedef enum object_generation { OBJECT_GENERATIONS OBJECT_GENERATION_MAX } object_generation_t;
+#undef QQ
+
+#define QQ(x) #x ,
+static const char * const object_generation_label[] = { OBJECT_GENERATIONS NULL };
+#undef QQ
+
+/**
+ * Type-safe string wrapper for URIs.
+ */
+typedef struct { char s[URI_MAX]; } uri_t;
+
+/**
+ * Type-safe string wrapper for filename paths.
+ */
+typedef struct { char s[FILENAME_MAX]; } path_t;
+
+/**
+ * Type-safe wrapper for hash buffers.
+ */
+typedef struct { unsigned char h[EVP_MAX_MD_SIZE]; } hashbuf_t;
+
+/**
+ * Type-safe wrapper for timestamp strings.
+ */
+typedef struct { char s[sizeof("2001-01-01T00:00:00Z") + 1]; } timestamp_t;
+
+/**
+ * Per-URI validation status object.
+ * uri must be first element.
+ */
+typedef struct validation_status {
+ uri_t uri;
+ object_generation_t generation;
+ time_t timestamp;
+ unsigned char events[(MIB_COUNTER_T_MAX + 7) / 8];
+ short balance;
+ struct validation_status *left_child;
+ struct validation_status *right_child;
+} validation_status_t;
+
+DECLARE_STACK_OF(validation_status_t)
+
+/**
+ * Structure to hold data parsed out of a certificate.
+ */
+typedef struct certinfo {
+ int ca, ta;
+ object_generation_t generation;
+ uri_t uri, sia, aia, crldp, manifest, signedobject;
+} certinfo_t;
+
+typedef struct rcynic_ctx rcynic_ctx_t;
+
+/**
+ * States that a walk_ctx_t can be in.
+ */
+typedef enum {
+ walk_state_initial, /**< Initial state */
+ walk_state_rsync, /**< rsyncing certinfo.sia */
+ walk_state_ready, /**< Ready to traverse outputs */
+ walk_state_current, /**< prefix = rc->unauthenticated */
+ walk_state_backup, /**< prefix = rc->old_authenticated */
+ walk_state_done /**< Done walking this cert's outputs */
+} walk_state_t;
+
+/**
+ * Context for certificate tree walks. This includes all the stuff
+ * that we would keep as automatic variables on the call stack if we
+ * didn't have to use callbacks to support multiple rsync processes.
+ */
+typedef struct walk_ctx {
+ unsigned refcount;
+ certinfo_t certinfo;
+ X509 *cert;
+ Manifest *manifest;
+ object_generation_t manifest_generation;
+ STACK_OF(OPENSSL_STRING) *filenames;
+ int manifest_iteration, filename_iteration, stale_manifest;
+ walk_state_t state;
+ uri_t crldp;
+ STACK_OF(X509) *certs;
+ STACK_OF(X509_CRL) *crls;
+} walk_ctx_t;
+
+DECLARE_STACK_OF(walk_ctx_t)
+
+/**
+ * Return codes from rsync functions.
+ */
+typedef enum {
+ rsync_status_done, /* Request completed */
+ rsync_status_failed, /* Request failed */
+ rsync_status_timed_out, /* Request timed out */
+ rsync_status_pending, /* Request in progress */
+ rsync_status_skipped /* Request not attempted */
+} rsync_status_t;
+
+/**
+ * States for asynchronous rsync.
+ * "initial" must be first.
+ */
+
+#define RSYNC_STATES \
+ QQ(initial) \
+ QQ(running) \
+ QQ(conflict_wait) \
+ QQ(retry_wait) \
+ QQ(closed) \
+ QQ(terminating)
+
+#define QQ(x) rsync_state_##x,
+typedef enum { RSYNC_STATES RSYNC_STATE_T_MAX } rsync_state_t;
+#undef QQ
+
+#define QQ(x) #x ,
+static const char * const rsync_state_label[] = { RSYNC_STATES NULL };
+#undef QQ
+
+/**
+ * Context for asyncronous rsync.
+ */
+typedef struct rsync_ctx {
+ uri_t uri;
+ void (*handler)(rcynic_ctx_t *, const struct rsync_ctx *, const rsync_status_t, const uri_t *, void *);
+ void *cookie;
+ rsync_state_t state;
+ enum {
+ rsync_problem_none, /* Must be first */
+ rsync_problem_timed_out,
+ rsync_problem_refused
+ } problem;
+ unsigned tries;
+ pid_t pid;
+ int fd;
+ time_t started, deadline;
+ char buffer[URI_MAX * 4];
+ size_t buflen;
+} rsync_ctx_t;
+
+DECLARE_STACK_OF(rsync_ctx_t)
+
+/**
+ * Record of rsync attempts.
+ */
+typedef struct rsync_history {
+ uri_t uri;
+ time_t started, finished;
+ rsync_status_t status;
+ int final_slash;
+} rsync_history_t;
+
+DECLARE_STACK_OF(rsync_history_t)
+
+/**
+ * Deferred task.
+ */
+typedef struct task {
+ void (*handler)(rcynic_ctx_t *, void *);
+ void *cookie;
+} task_t;
+
+DECLARE_STACK_OF(task_t)
+
+/**
+ * Trust anchor locator (TAL) fetch context.
+ */
+typedef struct tal_ctx {
+ uri_t uri;
+ path_t path;
+ EVP_PKEY *pkey;
+} tal_ctx_t;
+
+/**
+ * Extended context for verify callbacks. This is a wrapper around
+ * OpenSSL's X509_STORE_CTX, and the embedded X509_STORE_CTX @em must be
+ * the first element of this structure in order for the evil cast to
+ * do the right thing. This is ugly but safe, as the C language
+ * promises us that the address of the first element of a structure is
+ * the same as the address of the structure.
+ */
+typedef struct rcynic_x509_store_ctx {
+ X509_STORE_CTX ctx; /* Must be first */
+ rcynic_ctx_t *rc;
+ const certinfo_t *subject;
+} rcynic_x509_store_ctx_t;
+
+/**
+ * Program context that would otherwise be a mess of global variables.
+ */
+struct rcynic_ctx {
+ path_t authenticated, old_authenticated, new_authenticated, unauthenticated;
+ char *jane, *rsync_program;
+ STACK_OF(validation_status_t) *validation_status;
+ STACK_OF(rsync_history_t) *rsync_history;
+ STACK_OF(rsync_ctx_t) *rsync_queue;
+ STACK_OF(task_t) *task_queue;
+ int use_syslog, allow_stale_crl, allow_stale_manifest, use_links;
+ int require_crl_in_manifest, rsync_timeout, priority[LOG_LEVEL_T_MAX];
+ int allow_non_self_signed_trust_anchor, allow_object_not_in_manifest;
+ int max_parallel_fetches, max_retries, retry_wait_min, run_rsync;
+ int allow_digest_mismatch, allow_crl_digest_mismatch;
+ int allow_nonconformant_name, allow_ee_without_signedObject;
+ int allow_1024_bit_ee_key, allow_wrong_cms_si_attributes;
+ int rsync_early;
+ unsigned max_select_time;
+ validation_status_t *validation_status_in_waiting;
+ validation_status_t *validation_status_root;
+ log_level_t log_level;
+ X509_STORE *x509_store;
+};
+
+
+
+/*
+ * Handle NIDs we wish OpenSSL knew about. This is carefully (we
+ * hope) written to do nothing at all for any NID that OpenSSL knows
+ * about; the intent is just to add definitions for things OpenSSL
+ * doesn't know about yet. Of necessity, this is a bit gross, since
+ * it confounds runtime static variables with predefined macro names,
+ * but we try to put all the magic associated with this in one place.
+ *
+ * In the long run it might be cleaner to generate this with a trivial
+ * script and put the result in a shared .h file, but this will do for
+ * the moment.
+ */
+
+#ifndef NID_ad_rpkiManifest
+static int NID_ad_rpkiManifest;
+#endif
+
+#ifndef NID_ad_signedObject
+static int NID_ad_signedObject;
+#endif
+
+#ifndef NID_ct_ROA
+static int NID_ct_ROA;
+#endif
+
+#ifndef NID_ct_rpkiManifest
+static int NID_ct_rpkiManifest;
+#endif
+
+#ifndef NID_ct_rpkiGhostbusters
+static int NID_ct_rpkiGhostbusters;
+#endif
+
+#ifndef NID_cp_ipAddr_asNumber
+static int NID_cp_ipAddr_asNumber;
+#endif
+
+#ifndef NID_id_kp_bgpsec_router
+static int NID_id_kp_bgpsec_router;
+#endif
+
+/**
+ * Missing NIDs, if any.
+ */
+static const struct {
+ int *nid;
+ const char *oid;
+ const char *sn;
+ const char *ln;
+} missing_nids[] = {
+
+#ifndef NID_ad_rpkiManifest
+ {&NID_ad_rpkiManifest, "1.3.6.1.5.5.7.48.10", "id-ad-rpkiManifest", "RPKI Manifest"},
+#endif
+
+#ifndef NID_ad_signedObject
+ {&NID_ad_signedObject, "1.3.6.1.5.5.7.48.11", "id-ad-signedObject", "Signed Object"},
+#endif
+
+#ifndef NID_ct_ROA
+ {&NID_ct_ROA, "1.2.840.113549.1.9.16.1.24", "id-ct-routeOriginAttestation", "ROA eContent"},
+#endif
+
+#ifndef NID_ct_rpkiManifest
+ {&NID_ct_rpkiManifest, "1.2.840.113549.1.9.16.1.26", "id-ct-rpkiManifest", "RPKI Manifest eContent"},
+#endif
+
+#ifndef NID_ct_rpkiGhostbusters
+ {&NID_ct_rpkiGhostbusters, "1.2.840.113549.1.9.16.1.35", "id-ct-rpkiGhostbusters", "RPKI Ghostbusters eContent"},
+#endif
+
+#ifndef NID_cp_ipAddr_asNumber
+ {&NID_cp_ipAddr_asNumber, "1.3.6.1.5.5.7.14.2", "id-cp-ipAddr-asNumber", "RPKI Certificate Policy"},
+#endif
+
+#ifndef NID_id_kp_bgpsec_router
+ {&NID_id_kp_bgpsec_router, "1.3.6.1.5.5.7.3.30", "id-kp-bgpsec-router", "BGPSEC Router Certificate"},
+#endif
+
+};
+
+
+
+/**
+ * Subversion ID data.
+ */
+static const char svn_id[] = "$Id$";
+
+/**
+ * Suffix we use temporarily during the symlink shuffle. Could be
+ * almost anything, but we want to do the length check early, before
+ * we waste a lot of work we'll just have to throw away, so we just
+ * wire in something short and obvious.
+ */
+static const char authenticated_symlink_suffix[] = ".new";
+
+/**
+ * Constants for comparisions. We can't build these at compile time,
+ * so they can't be const, but treat them as if they were once
+ * allocated.
+ *
+ * We probably need both a better scheme for naming NID_ replacements
+ * and a more comprehensive rewrite of how we handle OIDs OpenSSL
+ * doesn't know about, so that we neither conflict with defined
+ * symbols nor duplicate effort nor explode if and when OpenSSL adds
+ * new OIDs (with or without the names we would have used).
+ */
+
+static const ASN1_INTEGER *asn1_zero, *asn1_four_octets, *asn1_twenty_octets;
+static int NID_binary_signing_time;
+
+
+
+/**
+ * Handle missing NIDs.
+ */
+static int
+create_missing_nids(void)
+{
+ int i;
+
+ for (i = 0; i < (int) (sizeof(missing_nids) / sizeof(*missing_nids)); i++)
+ if ((*missing_nids[i].nid = OBJ_txt2nid(missing_nids[i].oid)) == NID_undef &&
+ (*missing_nids[i].nid = OBJ_create(missing_nids[i].oid,
+ missing_nids[i].sn,
+ missing_nids[i].ln)) == NID_undef)
+ return 0;
+
+ return 1;
+}
+
+
+
+/**
+ * Type-safe wrapper around free() to keep safestack macros happy.
+ */
+static void OPENSSL_STRING_free(OPENSSL_STRING s)
+{
+ if (s)
+ free(s);
+}
+
+/**
+ * Wrapper around an idiom we use with OPENSSL_STRING stacks. There's
+ * a bug in the current sk_OPENSSL_STRING_delete() macro that casts
+ * the return value to the wrong type, so we cast it to something
+ * innocuous here and avoid using that macro elsewhere.
+ */
+static void sk_OPENSSL_STRING_remove(STACK_OF(OPENSSL_STRING) *sk, const char *str)
+{
+ OPENSSL_STRING_free((void *) sk_OPENSSL_STRING_delete(sk, sk_OPENSSL_STRING_find(sk, str)));
+}
+
+/**
+ * Allocate a new validation_status_t object.
+ */
+static validation_status_t *validation_status_t_new(void)
+{
+ validation_status_t *v = malloc(sizeof(*v));
+ if (v)
+ memset(v, 0, sizeof(*v));
+ return v;
+}
+
+/**
+ * Type-safe wrapper around free() to keep safestack macros happy.
+ */
+static void validation_status_t_free(validation_status_t *v)
+{
+ if (v)
+ free(v);
+}
+
+
+
+/**
+ * Allocate a new rsync_history_t object.
+ */
+static rsync_history_t *rsync_history_t_new(void)
+{
+ rsync_history_t *h = malloc(sizeof(*h));
+ if (h)
+ memset(h, 0, sizeof(*h));
+ return h;
+}
+
+/**
+ * Type-safe wrapper around free() to keep safestack macros happy.
+ */
+static void rsync_history_t_free(rsync_history_t *h)
+{
+ if (h)
+ free(h);
+}
+
+/**
+ * Compare two rsync_history_t objects.
+ */
+static int rsync_history_cmp(const rsync_history_t * const *a, const rsync_history_t * const *b)
+{
+ return strcmp((*a)->uri.s, (*b)->uri.s);
+}
+
+
+
+/**
+ * Convert a time_t to a printable string in UTC format.
+ */
+static const char *time_to_string(timestamp_t *ts, const time_t *t)
+{
+ time_t now;
+ size_t n;
+
+ assert(ts != NULL);
+
+ if (t == NULL) {
+ now = time(0);
+ t = &now;
+ }
+
+ n = strftime(ts->s, sizeof(ts->s), "%Y-%m-%dT%H:%M:%SZ", gmtime(t));
+ assert(n > 0);
+
+ return ts->s;
+}
+
+/*
+ * GCC attributes to help catch format string errors.
+ */
+
+#ifdef __GNUC__
+
+static void logmsg(const rcynic_ctx_t *rc,
+ const log_level_t level,
+ const char *fmt, ...)
+ __attribute__ ((format (printf, 3, 4)));
+#endif
+
+/**
+ * Logging.
+ */
+static void vlogmsg(const rcynic_ctx_t *rc,
+ const log_level_t level,
+ const char *fmt,
+ va_list ap)
+{
+ assert(rc && fmt);
+
+ if (rc->log_level < level)
+ return;
+
+ if (rc->use_syslog) {
+ vsyslog(rc->priority[level], fmt, ap);
+ } else {
+ char ts[sizeof("00:00:00")+1];
+ time_t t = time(0);
+ strftime(ts, sizeof(ts), "%H:%M:%S", localtime(&t));
+ fprintf(stderr, "%s: ", ts);
+ if (rc->jane)
+ fprintf(stderr, "%s: ", rc->jane);
+ vfprintf(stderr, fmt, ap);
+ putc('\n', stderr);
+ }
+}
+
+/**
+ * Logging.
+ */
+static void logmsg(const rcynic_ctx_t *rc,
+ const log_level_t level,
+ const char *fmt, ...)
+{
+ va_list ap;
+ va_start(ap, fmt);
+ vlogmsg(rc, level, fmt, ap);
+ va_end(ap);
+}
+
+/**
+ * Print OpenSSL library errors.
+ */
+static void log_openssl_errors(const rcynic_ctx_t *rc)
+{
+ const char *data, *file;
+ unsigned long code;
+ char error[256];
+ int flags, line;
+
+ if (!rc->log_level < log_verbose)
+ return;
+
+ while ((code = ERR_get_error_line_data(&file, &line, &data, &flags))) {
+ ERR_error_string_n(code, error, sizeof(error));
+ if (data && (flags & ERR_TXT_STRING))
+ logmsg(rc, log_sys_err, "OpenSSL error %s:%d: %s: %s", file, line, error, data);
+ else
+ logmsg(rc, log_sys_err, "OpenSSL error %s:%d: %s", file, line, error);
+ }
+}
+
+/**
+ * Configure logging.
+ */
+static int configure_logmsg(rcynic_ctx_t *rc, const char *name)
+{
+ int i;
+
+ assert(rc && name);
+
+ for (i = 0; i < sizeof(log_levels)/sizeof(*log_levels); i++) {
+ if (!strcmp(name, log_levels[i].name)) {
+ rc->log_level = log_levels[i].value;
+ return 1;
+ }
+ }
+
+ logmsg(rc, log_usage_err, "Bad log level %s", name);
+ return 0;
+}
+
+/**
+ * Configure syslog.
+ */
+static int configure_syslog(const rcynic_ctx_t *rc,
+ int *result,
+ const CODE *table,
+ const char *name)
+{
+ assert(result && table && name);
+
+ while (table->c_name && strcmp(table->c_name, name))
+ table++;
+
+ if (table->c_name) {
+ *result = table->c_val;
+ return 1;
+ } else {
+ logmsg(rc, log_usage_err, "Bad syslog code %s", name);
+ return 0;
+ }
+}
+
+/**
+ * Configure boolean variable.
+ */
+static int configure_boolean(const rcynic_ctx_t *rc,
+ int *result,
+ const char *val)
+{
+ assert(rc && result && val);
+
+ switch (*val) {
+ case 'y': case 'Y': case 't': case 'T': case '1':
+ *result = 1;
+ return 1;
+ case 'n': case 'N': case 'f': case 'F': case '0':
+ *result = 0;
+ return 1;
+ default:
+ logmsg(rc, log_usage_err, "Bad boolean value %s", val);
+ return 0;
+ }
+}
+
+/**
+ * Configure integer variable.
+ */
+static int configure_integer(const rcynic_ctx_t *rc,
+ int *result,
+ const char *val)
+{
+ long res;
+ char *p;
+
+ assert(rc && result && val);
+
+ res = strtol(val, &p, 10);
+
+ if (*val != '\0' && *p == '\0') {
+ *result = (int) res;
+ return 1;
+ } else {
+ logmsg(rc, log_usage_err, "Bad integer value %s", val);
+ return 0;
+ }
+}
+
+/**
+ * Configure unsigned integer variable.
+ */
+static int configure_unsigned_integer(const rcynic_ctx_t *rc,
+ unsigned *result,
+ const char *val)
+{
+ unsigned long res;
+ char *p;
+
+ assert(rc && result && val);
+
+ res = strtoul(val, &p, 10);
+
+ if (*val != '\0' && *p == '\0') {
+ *result = (unsigned) res;
+ return 1;
+ } else {
+ logmsg(rc, log_usage_err, "Bad integer value %s", val);
+ return 0;
+ }
+}
+
+
+
+/**
+ * Make a directory if it doesn't already exist.
+ */
+static int mkdir_maybe(const rcynic_ctx_t *rc, const path_t *name)
+{
+ path_t path;
+ char *s;
+
+ assert(name != NULL);
+ if (strlen(name->s) >= sizeof(path.s)) {
+ logmsg(rc, log_data_err, "Pathname %s too long", name->s);
+ return 0;
+ }
+ strcpy(path.s, name->s);
+ s = path.s[0] == '/' ? path.s + 1 : path.s;
+ if ((s = strrchr(s, '/')) == NULL)
+ return 1;
+ *s = '\0';
+ if (!mkdir_maybe(rc, &path)) {
+ logmsg(rc, log_sys_err, "Failed to make directory %s", path.s);
+ return 0;
+ }
+ if (!access(path.s, F_OK))
+ return 1;
+ logmsg(rc, log_verbose, "Creating directory %s", path.s);
+ return mkdir(path.s, 0777) == 0;
+}
+
+/**
+ * strdup() a string and push it onto a stack.
+ */
+static int sk_OPENSSL_STRING_push_strdup(STACK_OF(OPENSSL_STRING) *sk, const char *str)
+{
+ OPENSSL_STRING s = strdup(str);
+
+ if (s && sk_OPENSSL_STRING_push(sk, s))
+ return 1;
+ if (s)
+ free(s);
+ return 0;
+}
+
+/**
+ * Compare two URI strings, for OpenSSL STACK operations.
+ */
+
+static int uri_cmp(const char * const *a, const char * const *b)
+{
+ return strcmp(*a, *b);
+}
+
+/**
+ * Is string an rsync URI?
+ */
+static int is_rsync(const char *uri)
+{
+ return uri && !strncmp(uri, SCHEME_RSYNC, SIZEOF_RSYNC);
+}
+
+/**
+ * Convert an rsync URI to a filename, checking for evil character
+ * sequences. NB: This routine can't call mib_increment(), because
+ * mib_increment() calls it, so errors detected here only go into
+ * the log, not the MIB.
+ */
+static int uri_to_filename(const rcynic_ctx_t *rc,
+ const uri_t *uri,
+ path_t *path,
+ const path_t *prefix)
+{
+ const char *u;
+ size_t n;
+
+ path->s[0] = '\0';
+
+ if (!is_rsync(uri->s)) {
+ logmsg(rc, log_telemetry, "%s is not an rsync URI, not converting to filename", uri->s);
+ return 0;
+ }
+
+ u = uri->s + SIZEOF_RSYNC;
+ n = strlen(u);
+
+ if (u[0] == '/' || u[0] == '.' || strstr(u, "/../") ||
+ (n >= 3 && !strcmp(u + n - 3, "/.."))) {
+ logmsg(rc, log_data_err, "Dangerous URI %s, not converting to filename", uri->s);
+ return 0;
+ }
+
+ if (prefix)
+ n += strlen(prefix->s);
+
+ if (n >= sizeof(path->s)) {
+ logmsg(rc, log_data_err, "URI %s too long, not converting to filename", uri->s);
+ return 0;
+ }
+
+ if (prefix) {
+ strcpy(path->s, prefix->s);
+ strcat(path->s, u);
+ } else {
+ strcpy(path->s, u);
+ }
+
+ return 1;
+}
+
+/**
+ * Compare filename fields of two FileAndHash structures.
+ */
+static int FileAndHash_name_cmp(const FileAndHash * const *a, const FileAndHash * const *b)
+{
+ return strcmp((char *) (*a)->file->data, (char *) (*b)->file->data);
+}
+
+/**
+ * Get value of code in a validation_status_t.
+ */
+static int validation_status_get_code(const validation_status_t *v,
+ const mib_counter_t code)
+{
+ assert(v && code < MIB_COUNTER_T_MAX);
+ return (v->events[code / 8] & (1 << (code % 8))) != 0;
+}
+
+/**
+ * Set value of code in a validation_status_t.
+ */
+static void validation_status_set_code(validation_status_t *v,
+ const mib_counter_t code,
+ int value)
+{
+ assert(v && code < MIB_COUNTER_T_MAX);
+ if (value)
+ v->events[code / 8] |= (1 << (code % 8));
+ else
+ v->events[code / 8] &= ~(1 << (code % 8));
+}
+
+/**
+ * validation_status object comparison, for AVL tree rather than
+ * OpenSSL stacks.
+ */
+static int
+validation_status_cmp(const validation_status_t *node,
+ const uri_t *uri,
+ const object_generation_t generation)
+{
+ int cmp = ((int) node->generation) - ((int) generation);
+ if (cmp)
+ return cmp;
+ else
+ return strcmp(uri->s, node->uri.s);
+}
+
+/**
+ * validation_status AVL tree insertion. Adapted from code written by
+ * Paul Vixie and explictly placed in the public domain using examples
+ * from the book: "Algorithms & Data Structures," Niklaus Wirth,
+ * Prentice-Hall, 1986, ISBN 0-13-022005-1. Thanks, Paul!
+ */
+static validation_status_t *
+validation_status_sprout(validation_status_t **node,
+ int *needs_balancing,
+ validation_status_t *new_node)
+{
+#ifdef AVL_DEBUG
+#define AVL_MSG(msg) sprintf(stderr, "AVL_DEBUG: '%s'\n", msg)
+#else
+#define AVL_MSG(msg)
+#endif
+
+ validation_status_t *p1, *p2, *result;
+ int cmp;
+
+ /*
+ * Are we grounded? If so, add the node "here" and set the
+ * rebalance flag, then exit.
+ */
+ if (*node == NULL) {
+ AVL_MSG("Grounded, adding new node");
+ new_node->left_child = NULL;
+ new_node->right_child = NULL;
+ new_node->balance = 0;
+ *node = new_node;
+ *needs_balancing = 1;
+ return *node;
+ }
+
+ /*
+ * Compare the data.
+ */
+ cmp = validation_status_cmp(*node, &new_node->uri, new_node->generation);
+
+ /*
+ * If LESS, prepare to move to the left.
+ */
+ if (cmp < 0) {
+
+ AVL_MSG("LESS. sprouting left.");
+ result = validation_status_sprout(&(*node)->left_child, needs_balancing, new_node);
+
+ if (*needs_balancing) {
+ AVL_MSG("LESS: left branch has grown longer");
+
+ switch ((*node)->balance) {
+
+ case 1:
+ /*
+ * Right branch WAS longer; balance is ok now.
+ */
+ AVL_MSG("LESS: case 1.. balance restored implicitly");
+ (*node)->balance = 0;
+ *needs_balancing = 0;
+ break;
+
+ case 0:
+ /*
+ * Balance WAS okay; now left branch longer.
+ */
+ AVL_MSG("LESS: case 0.. balnce bad but still ok");
+ (*node)->balance = -1;
+ break;
+
+ case -1:
+ /*
+ * Left branch was already too long. Rebalance.
+ */
+ AVL_MSG("LESS: case -1: rebalancing");
+ p1 = (*node)->left_child;
+
+ if (p1->balance == -1) {
+ AVL_MSG("LESS: single LL");
+ (*node)->left_child = p1->right_child;
+ p1->right_child = *node;
+ (*node)->balance = 0;
+ *node = p1;
+ }
+
+ else {
+ AVL_MSG("LESS: double LR");
+
+ p2 = p1->right_child;
+ p1->right_child = p2->left_child;
+ p2->left_child = p1;
+
+ (*node)->left_child = p2->right_child;
+ p2->right_child = *node;
+
+ if (p2->balance == -1)
+ (*node)->balance = 1;
+ else
+ (*node)->balance = 0;
+
+ if (p2->balance == 1)
+ p1->balance = -1;
+ else
+ p1->balance = 0;
+ *node = p2;
+ }
+
+ (*node)->balance = 0;
+ *needs_balancing = 0;
+ }
+ }
+ return result;
+ }
+
+ /*
+ * If MORE, prepare to move to the right.
+ */
+ if (cmp > 0) {
+
+ AVL_MSG("MORE: sprouting to the right");
+ result = validation_status_sprout(&(*node)->right_child, needs_balancing, new_node);
+
+ if (*needs_balancing) {
+ AVL_MSG("MORE: right branch has grown longer");
+
+ switch ((*node)->balance) {
+
+ case -1:AVL_MSG("MORE: balance was off, fixed implicitly");
+ (*node)->balance = 0;
+ *needs_balancing = 0;
+ break;
+
+ case 0: AVL_MSG("MORE: balance was okay, now off but ok");
+ (*node)->balance = 1;
+ break;
+
+ case 1: AVL_MSG("MORE: balance was off, need to rebalance");
+ p1 = (*node)->right_child;
+
+ if (p1->balance == 1) {
+ AVL_MSG("MORE: single RR");
+ (*node)->right_child = p1->left_child;
+ p1->left_child = *node;
+ (*node)->balance = 0;
+ *node = p1;
+ }
+
+ else {
+ AVL_MSG("MORE: double RL");
+
+ p2 = p1->left_child;
+ p1->left_child = p2->right_child;
+ p2->right_child = p1;
+
+ (*node)->right_child = p2->left_child;
+ p2->left_child = *node;
+
+ if (p2->balance == 1)
+ (*node)->balance = -1;
+ else
+ (*node)->balance = 0;
+
+ if (p2->balance == -1)
+ p1->balance = 1;
+ else
+ p1->balance = 0;
+
+ *node = p2;
+ } /*else*/
+ (*node)->balance = 0;
+ *needs_balancing = 0;
+ }
+ }
+ return result;
+ }
+
+ /*
+ * Neither more nor less, found existing node matching key, return it.
+ */
+ AVL_MSG("I found it!");
+ *needs_balancing = 0;
+ return *node;
+
+#undef AVL_MSG
+}
+
+/**
+ * Add a validation status entry to internal log.
+ */
+static void log_validation_status(rcynic_ctx_t *rc,
+ const uri_t *uri,
+ const mib_counter_t code,
+ const object_generation_t generation)
+{
+ validation_status_t *v = NULL;
+ int needs_balancing = 0;
+
+ assert(rc && uri && code < MIB_COUNTER_T_MAX && generation < OBJECT_GENERATION_MAX);
+
+ if (!rc->validation_status)
+ return;
+
+ if (code == rsync_transfer_skipped && !rc->run_rsync)
+ return;
+
+ if (rc->validation_status_in_waiting == NULL &&
+ (rc->validation_status_in_waiting = validation_status_t_new()) == NULL) {
+ logmsg(rc, log_sys_err, "Couldn't allocate validation status entry for %s", uri->s);
+ return;
+ }
+
+ v = rc->validation_status_in_waiting;
+ memset(v, 0, sizeof(*v));
+ v->uri = *uri;
+ v->generation = generation;
+
+ v = validation_status_sprout(&rc->validation_status_root, &needs_balancing, v);
+ if (v == rc->validation_status_in_waiting)
+ rc->validation_status_in_waiting = NULL;
+
+ if (rc->validation_status_in_waiting == NULL &&
+ !sk_validation_status_t_push(rc->validation_status, v)) {
+ logmsg(rc, log_sys_err, "Couldn't store validation status entry for %s", uri->s);
+ return;
+ }
+
+ v->timestamp = time(0);
+
+ if (validation_status_get_code(v, code))
+ return;
+
+ validation_status_set_code(v, code, 1);
+
+ logmsg(rc, log_verbose, "Recording \"%s\" for %s%s%s",
+ (mib_counter_desc[code]
+ ? mib_counter_desc[code]
+ : X509_verify_cert_error_string(mib_counter_openssl[code])),
+ (generation != object_generation_null ? object_generation_label[generation] : ""),
+ (generation != object_generation_null ? " " : ""),
+ uri->s);
+}
+
+/**
+ * Copy or link a file, as the case may be.
+ */
+static int cp_ln(const rcynic_ctx_t *rc, const path_t *source, const path_t *target)
+{
+ struct stat statbuf;
+ struct utimbuf utimebuf;
+ FILE *in = NULL, *out = NULL;
+ int c, ok = 0;
+
+ if (rc->use_links) {
+ (void) unlink(target->s);
+ ok = link(source->s, target->s) == 0;
+ if (!ok)
+ logmsg(rc, log_sys_err, "Couldn't link %s to %s: %s",
+ source->s, target->s, strerror(errno));
+ return ok;
+ }
+
+ if ((in = fopen(source->s, "rb")) == NULL ||
+ (out = fopen(target->s, "wb")) == NULL)
+ goto done;
+
+ while ((c = getc(in)) != EOF)
+ if (putc(c, out) == EOF)
+ goto done;
+
+ ok = 1;
+
+ done:
+ ok &= !(in != NULL && fclose(in) == EOF);
+ ok &= !(out != NULL && fclose(out) == EOF);
+
+ if (!ok) {
+ logmsg(rc, log_sys_err, "Couldn't copy %s to %s: %s",
+ source->s, target->s, strerror(errno));
+ return ok;
+ }
+
+ /*
+ * Perserve the file modification time to allow for detection of
+ * changed objects in the authenticated directory. Failure to reset
+ * the times is not optimal, but is also not critical, thus no
+ * failure return.
+ */
+ if (stat(source->s, &statbuf) < 0 ||
+ (utimebuf.actime = statbuf.st_atime,
+ utimebuf.modtime = statbuf.st_mtime,
+ utime(target->s, &utimebuf) < 0))
+ logmsg(rc, log_sys_err, "Couldn't copy inode timestamp from %s to %s: %s",
+ source->s, target->s, strerror(errno));
+
+ return ok;
+}
+
+/**
+ * Install an object.
+ */
+static int install_object(rcynic_ctx_t *rc,
+ const uri_t *uri,
+ const path_t *source,
+ const object_generation_t generation)
+{
+ path_t target;
+
+ if (!uri_to_filename(rc, uri, &target, &rc->new_authenticated)) {
+ logmsg(rc, log_data_err, "Couldn't generate installation name for %s", uri->s);
+ return 0;
+ }
+
+ if (!mkdir_maybe(rc, &target)) {
+ logmsg(rc, log_sys_err, "Couldn't create directory for %s", target.s);
+ return 0;
+ }
+
+ if (!cp_ln(rc, source, &target))
+ return 0;
+ log_validation_status(rc, uri, object_accepted, generation);
+ return 1;
+}
+
+/**
+ * AVL tree lookup for validation status objects.
+ */
+static validation_status_t *
+validation_status_find(validation_status_t *node,
+ const uri_t *uri,
+ const object_generation_t generation)
+{
+ int cmp;
+
+ while (node != NULL && (cmp = validation_status_cmp(node, uri, generation)) != 0)
+ node = cmp < 0 ? node->left_child : node->right_child;
+
+ return node;
+}
+
+/**
+ * Check whether we have a validation status entry corresponding to a
+ * given filename. This is intended for use during pruning the
+ * unauthenticated tree, so it only checks the current generation.
+ */
+static int
+validation_status_find_filename(const rcynic_ctx_t *rc,
+ const char *filename)
+{
+ uri_t uri;
+
+ if (strlen(filename) + SIZEOF_RSYNC >= sizeof(uri.s))
+ return 0;
+
+ strcpy(uri.s, SCHEME_RSYNC);
+ strcat(uri.s, filename);
+
+ return validation_status_find(rc->validation_status_root, &uri, object_generation_current) != NULL;
+}
+
+/**
+ * Figure out whether we already have a good copy of an object. This
+ * is a little more complicated than it sounds, because we might have
+ * failed the current generation and accepted the backup due to having
+ * followed the old CA certificate chain first during a key rollover.
+ * So if this check is of the current object and we have not already
+ * accepted the current object for this URI, we need to recheck.
+ *
+ * We also handle logging when we decide that we do need to check, so
+ * that the caller doesn't need to concern itself with why we thought
+ * the check was necessary.
+ */
+static int skip_checking_this_object(rcynic_ctx_t *rc,
+ const uri_t *uri,
+ const object_generation_t generation)
+{
+ validation_status_t *v = NULL;
+ path_t path;
+
+ assert(rc && uri && rc->validation_status);
+
+ if (!uri_to_filename(rc, uri, &path, &rc->new_authenticated))
+ return 1;
+
+ if (access(path.s, R_OK)) {
+ logmsg(rc, log_telemetry, "Checking %s", uri->s);
+ return 0;
+ }
+
+ if (generation != object_generation_current)
+ return 1;
+
+ v = validation_status_find(rc->validation_status_root, uri, generation);
+
+ if (v != NULL && validation_status_get_code(v, object_accepted))
+ return 1;
+
+ log_validation_status(rc, uri, rechecking_object, generation);
+ logmsg(rc, log_telemetry, "Rechecking %s", uri->s);
+ return 0;
+}
+
+
+
+/**
+ * Check str for a suffix.
+ */
+static int endswith(const char *str, const char *suffix)
+{
+ size_t len_str, len_suffix;
+ assert(str != NULL && suffix != NULL);
+ len_str = strlen(str);
+ len_suffix = strlen(suffix);
+ return len_str >= len_suffix && !strcmp(str + len_str - len_suffix, suffix);
+}
+
+/**
+ * Check str for a prefix.
+ */
+static int startswith(const char *str, const char *prefix)
+{
+ size_t len_str, len_prefix;
+ assert(str != NULL && prefix != NULL);
+ len_str = strlen(str);
+ len_prefix = strlen(prefix);
+ return len_str >= len_prefix && !strncmp(str, prefix, len_prefix);
+}
+
+/**
+ * Convert a filename to a file:// URI, for logging.
+ */
+static void filename_to_uri(uri_t *uri,
+ const char *fn)
+{
+ assert(sizeof("file://") < sizeof(uri->s));
+ strcpy(uri->s, "file://");
+ if (*fn != '/') {
+ if (getcwd(uri->s + strlen(uri->s), sizeof(uri->s) - strlen(uri->s)) == NULL ||
+ (!endswith(uri->s, "/") && strlen(uri->s) >= sizeof(uri->s) - 1))
+ uri->s[0] = '\0';
+ else
+ strcat(uri->s, "/");
+ }
+ if (uri->s[0] != '\0' && strlen(uri->s) + strlen(fn) < sizeof(uri->s))
+ strcat(uri->s, fn);
+ else
+ uri->s[0] = '\0';
+}
+
+/**
+ * Set a directory name, adding or stripping trailing slash as needed.
+ */
+static int set_directory(const rcynic_ctx_t *rc, path_t *out, const char *in, const int want_slash)
+{
+ int has_slash, need_slash;
+ size_t n;
+
+ assert(rc && in && out);
+
+ n = strlen(in);
+
+ if (n == 0) {
+ logmsg(rc, log_usage_err, "Empty path");
+ return 0;
+ }
+
+ has_slash = in[n - 1] == '/';
+
+ need_slash = want_slash && !has_slash;
+
+ if (n + need_slash + 1 > sizeof(out->s)) {
+ logmsg(rc, log_usage_err, "Path \"%s\" too long", in);
+ return 0;
+ }
+
+ strcpy(out->s, in);
+ if (need_slash)
+ strcat(out->s, "/");
+ else if (has_slash && !want_slash)
+ out->s[n - 1] = '\0';
+
+ return 1;
+}
+
+/**
+ * Test whether a filesystem path points to a directory.
+ */
+static int is_directory(const path_t *name)
+{
+ struct stat st;
+
+ assert(name);
+ return lstat(name->s, &st) == 0 && S_ISDIR(st.st_mode);
+}
+
+/**
+ * Remove a directory tree, like rm -rf.
+ */
+static int rm_rf(const path_t *name)
+{
+ path_t path;
+ struct dirent *d;
+ DIR *dir;
+ int ret = 0;
+
+ assert(name);
+
+ if (!is_directory(name))
+ return unlink(name->s) == 0;
+
+ if ((dir = opendir(name->s)) == NULL)
+ return 0;
+
+ while ((d = readdir(dir)) != NULL) {
+ if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
+ continue;
+ if (snprintf(path.s, sizeof(path.s), "%s/%s", name->s, d->d_name) >= sizeof(path.s))
+ goto done;
+ if (unlink(path.s) == 0)
+ continue;
+ else if (rm_rf(&path))
+ continue;
+ else
+ goto done;
+ }
+
+ ret = rmdir(name->s) == 0;
+
+ done:
+ closedir(dir);
+ return ret;
+}
+
+/**
+ * Construct names for the directories not directly settable by the
+ * user.
+ *
+ * This function also checks for an old-style rc->authenticated
+ * directory, to simplify upgrade from older versions of rcynic.
+ */
+static int construct_directory_names(rcynic_ctx_t *rc)
+{
+ struct stat st;
+ ssize_t n;
+ path_t p;
+ time_t t = time(0);
+
+ p = rc->authenticated;
+
+ n = strlen(p.s);
+
+ if (n + sizeof(authenticated_symlink_suffix) >= sizeof(p.s)) {
+ logmsg(rc, log_usage_err, "Symlink name would be too long");
+ return 0;
+ }
+
+ if (strftime(p.s + n, sizeof(p.s) - n - 1, ".%Y-%m-%dT%H:%M:%SZ", gmtime(&t)) == 0) {
+ logmsg(rc, log_usage_err, "Generated path with timestamp would be too long");
+ return 0;
+ }
+
+ if (!set_directory(rc, &rc->new_authenticated, p.s, 1))
+ return 0;
+
+ if (!set_directory(rc, &rc->old_authenticated, rc->authenticated.s, 1))
+ return 0;
+
+ if (lstat(rc->authenticated.s, &st) == 0 && S_ISDIR((st.st_mode)) &&
+ strlen(rc->authenticated.s) + sizeof(".old") < sizeof(p.s)) {
+ p = rc->authenticated;
+ strcat(p.s, ".old");
+ rm_rf(&p);
+ (void) rename(rc->authenticated.s, p.s);
+ }
+
+ if (lstat(rc->authenticated.s, &st) == 0 && S_ISDIR(st.st_mode)) {
+ logmsg(rc, log_usage_err,
+ "Existing %s directory is in the way, please remove it",
+ rc->authenticated.s);
+ return 0;
+ }
+
+ return 1;
+}
+
+/**
+ * Do final symlink shuffle and cleanup of output directories.
+ */
+static int finalize_directories(const rcynic_ctx_t *rc)
+{
+ path_t path, real_old, real_new;
+ const char *dir;
+ glob_t g;
+ int i;
+
+ if (!realpath(rc->old_authenticated.s, real_old.s))
+ real_old.s[0] = '\0';
+
+ if (!realpath(rc->new_authenticated.s, real_new.s))
+ real_new.s[0] = '\0';
+
+ assert(real_new.s[0] && real_new.s[strlen(real_new.s) - 1] != '/');
+
+ if ((dir = strrchr(real_new.s, '/')) == NULL)
+ dir = real_new.s;
+ else
+ dir++;
+
+ path = rc->authenticated;
+
+ if (strlen(path.s) + sizeof(authenticated_symlink_suffix) >= sizeof(path.s))
+ return 0;
+ strcat(path.s, authenticated_symlink_suffix);
+
+ (void) unlink(path.s);
+
+ if (symlink(dir, path.s) < 0) {
+ logmsg(rc, log_sys_err, "Couldn't link %s to %s: %s",
+ path.s, dir, strerror(errno));
+ return 0;
+ }
+
+ if (rename(path.s, rc->authenticated.s) < 0) {
+ logmsg(rc, log_sys_err, "Couldn't rename %s to %s: %s",
+ path.s, rc->authenticated.s, strerror(errno));
+ return 0;
+ }
+
+ if (real_old.s[0] && strlen(rc->authenticated.s) + sizeof(".old") < sizeof(path.s)) {
+ assert(real_old.s[strlen(real_old.s) - 1] != '/');
+
+ path = rc->authenticated;
+ strcat(path.s, ".old");
+
+ (void) unlink(path.s);
+
+ if ((dir = strrchr(real_old.s, '/')) == NULL)
+ dir = real_old.s;
+ else
+ dir++;
+
+ (void) symlink(dir, path.s);
+ }
+
+ path = rc->authenticated;
+ assert(strlen(path.s) + sizeof(".*") < sizeof(path.s));
+ strcat(path.s, ".*");
+
+ memset(&g, 0, sizeof(g));
+
+ if (real_new.s[0] && glob(path.s, 0, 0, &g) == 0) {
+ for (i = 0; i < g.gl_pathc; i++)
+ if (realpath(g.gl_pathv[i], path.s) &&
+ strcmp(path.s, real_old.s) &&
+ strcmp(path.s, real_new.s))
+ rm_rf(&path);
+ globfree(&g);
+ }
+
+ return 1;
+}
+
+
+
+/**
+ * Test whether a pair of URIs "conflict", that is, whether attempting
+ * to rsync both of them at the same time in parallel might cause
+ * unpredictable behavior. Might need a better name for this test.
+ *
+ * Returns non-zero iff the two URIs "conflict".
+ */
+static int conflicting_uris(const uri_t *a, const uri_t *b)
+{
+ size_t len_a, len_b;
+
+ assert(a && is_rsync(a->s) && b && is_rsync(b->s));
+
+ len_a = strlen(a->s);
+ len_b = strlen(b->s);
+
+ assert(len_a < sizeof(a->s) && len_b < sizeof(b->s));
+
+ return !strncmp(a->s, b->s, len_a < len_b ? len_a : len_b);
+}
+
+
+
+/**
+ * Read non-directory filenames from a directory, so we can check to
+ * see what's missing from a manifest.
+ */
+static STACK_OF(OPENSSL_STRING) *directory_filenames(const rcynic_ctx_t *rc,
+ const walk_state_t state,
+ const uri_t *uri)
+{
+ STACK_OF(OPENSSL_STRING) *result = NULL;
+ path_t dpath, fpath;
+ const path_t *prefix = NULL;
+ DIR *dir = NULL;
+ struct dirent *d;
+ int ok = 0;
+
+ assert(rc && uri);
+
+ switch (state) {
+ case walk_state_current:
+ prefix = &rc->unauthenticated;
+ break;
+ case walk_state_backup:
+ prefix = &rc->old_authenticated;
+ break;
+ default:
+ goto done;
+ }
+
+ if (!uri_to_filename(rc, uri, &dpath, prefix) ||
+ (dir = opendir(dpath.s)) == NULL ||
+ (result = sk_OPENSSL_STRING_new(uri_cmp)) == NULL)
+ goto done;
+
+ while ((d = readdir(dir)) != NULL)
+ if (snprintf(fpath.s, sizeof(fpath.s), "%s/%s", dpath.s, d->d_name) >= sizeof(fpath.s)) {
+ logmsg(rc, log_data_err, "Local path name %s/%s too long", dpath.s, d->d_name);
+ goto done;
+ }
+ else if (!is_directory(&fpath) && !sk_OPENSSL_STRING_push_strdup(result, d->d_name)) {
+ logmsg(rc, log_sys_err, "sk_OPENSSL_STRING_push_strdup() failed, probably memory exhaustion");
+ goto done;
+ }
+
+ ok = 1;
+
+ done:
+ if (dir != NULL)
+ closedir(dir);
+
+ if (ok)
+ return result;
+
+ sk_OPENSSL_STRING_pop_free(result, OPENSSL_STRING_free);
+ return NULL;
+}
+
+
+
+/**
+ * Increment walk context reference count.
+ */
+static void walk_ctx_attach(walk_ctx_t *w)
+{
+ if (w != NULL) {
+ w->refcount++;
+ assert(w->refcount != 0);
+ }
+}
+
+/**
+ * Decrement walk context reference count; freeing the context if the
+ * reference count is now zero.
+ */
+static void walk_ctx_detach(walk_ctx_t *w)
+{
+ if (w != NULL && --(w->refcount) == 0) {
+ assert(w->refcount == 0);
+ X509_free(w->cert);
+ Manifest_free(w->manifest);
+ sk_X509_free(w->certs);
+ sk_X509_CRL_pop_free(w->crls, X509_CRL_free);
+ sk_OPENSSL_STRING_pop_free(w->filenames, OPENSSL_STRING_free);
+ free(w);
+ }
+}
+
+/**
+ * Return top context of a walk context stack.
+ */
+static walk_ctx_t *walk_ctx_stack_head(STACK_OF(walk_ctx_t) *wsk)
+{
+ return sk_walk_ctx_t_value(wsk, sk_walk_ctx_t_num(wsk) - 1);
+}
+
+/**
+ * Whether we're done iterating over a walk context. Think of this as
+ * the thing you call (negated) in the second clause of a conceptual
+ * "for" loop.
+ */
+static int walk_ctx_loop_done(STACK_OF(walk_ctx_t) *wsk)
+{
+ walk_ctx_t *w = walk_ctx_stack_head(wsk);
+ return wsk == NULL || w == NULL || w->state >= walk_state_done;
+}
+
+/**
+ * Walk context iterator. Think of this as the thing you call in the
+ * third clause of a conceptual "for" loop: this reinitializes as
+ * necessary for the next pass through the loop.
+ *
+ * General idea here is that we have several state variables in a walk
+ * context which collectively define the current pass, product URI,
+ * etc, and we want to be able to iterate through this sequence via
+ * the event system. So this function steps to the next state.
+ *
+ * Conceptually, w->manifest->fileList and w->filenames form a single
+ * array with index w->manifest_iteration + w->filename_iteration.
+ * Beware of fencepost errors, I've gotten this wrong once already.
+ * Slightly odd coding here is to make it easier to check this.
+ */
+static void walk_ctx_loop_next(const rcynic_ctx_t *rc, STACK_OF(walk_ctx_t) *wsk)
+{
+ walk_ctx_t *w = walk_ctx_stack_head(wsk);
+ int n_manifest, n_filenames;
+
+ assert(rc && wsk && w);
+
+ assert(w->manifest_iteration >= 0 && w->filename_iteration >= 0);
+
+ n_manifest = w->manifest ? sk_FileAndHash_num(w->manifest->fileList) : 0;
+ n_filenames = w->filenames ? sk_OPENSSL_STRING_num(w->filenames) : 0;
+
+ if (w->manifest_iteration + w->filename_iteration < n_manifest + n_filenames) {
+ if (w->manifest_iteration < n_manifest)
+ w->manifest_iteration++;
+ else
+ w->filename_iteration++;
+ }
+
+ assert(w->manifest_iteration <= n_manifest && w->filename_iteration <= n_filenames);
+
+ if (w->manifest_iteration + w->filename_iteration < n_manifest + n_filenames)
+ return;
+
+ while (!walk_ctx_loop_done(wsk)) {
+ w->state++;
+ w->manifest_iteration = 0;
+ w->filename_iteration = 0;
+ sk_OPENSSL_STRING_pop_free(w->filenames, OPENSSL_STRING_free);
+ w->filenames = directory_filenames(rc, w->state, &w->certinfo.sia);
+ if (w->manifest != NULL || w->filenames != NULL)
+ return;
+ }
+}
+
+static int check_manifest(rcynic_ctx_t *rc, STACK_OF(walk_ctx_t) *wsk);
+
+/**
+ * Loop initializer for walk context. Think of this as the thing you
+ * call in the first clause of a conceptual "for" loop.
+ */
+static void walk_ctx_loop_init(rcynic_ctx_t *rc, STACK_OF(walk_ctx_t) *wsk)
+{
+ walk_ctx_t *w = walk_ctx_stack_head(wsk);
+
+ assert(rc && wsk && w && w->state == walk_state_ready);
+
+ if (!w->manifest && !check_manifest(rc, wsk)) {
+ /*
+ * Simple failure to find a manifest doesn't get here. This is
+ * for manifest failures that cause us to reject all of this
+ * certificate's products due to policy knob settings.
+ */
+ w->state = walk_state_done;
+ return;
+ }
+
+ if (!w->manifest)
+ logmsg(rc, log_telemetry, "Couldn't get manifest %s, blundering onward", w->certinfo.manifest.s);
+
+ w->manifest_iteration = 0;
+ w->filename_iteration = 0;
+ w->state++;
+ assert(w->state == walk_state_current);
+
+ assert(w->filenames == NULL);
+ w->filenames = directory_filenames(rc, w->state, &w->certinfo.sia);
+
+ w->stale_manifest = w->manifest != NULL && X509_cmp_current_time(w->manifest->nextUpdate) < 0;
+
+ while (!walk_ctx_loop_done(wsk) &&
+ (w->manifest == NULL || w->manifest_iteration >= sk_FileAndHash_num(w->manifest->fileList)) &&
+ (w->filenames == NULL || w->filename_iteration >= sk_OPENSSL_STRING_num(w->filenames)))
+ walk_ctx_loop_next(rc, wsk);
+}
+
+/**
+ * Extract URI and hash values from walk context.
+ */
+static int walk_ctx_loop_this(const rcynic_ctx_t *rc,
+ STACK_OF(walk_ctx_t) *wsk,
+ uri_t *uri,
+ const unsigned char **hash,
+ size_t *hashlen)
+{
+ const walk_ctx_t *w = walk_ctx_stack_head(wsk);
+ const char *name = NULL;
+ FileAndHash *fah = NULL;
+
+ assert(rc && wsk && w && uri && hash && hashlen);
+
+ if (w->manifest != NULL && w->manifest_iteration < sk_FileAndHash_num(w->manifest->fileList)) {
+ fah = sk_FileAndHash_value(w->manifest->fileList, w->manifest_iteration);
+ name = (const char *) fah->file->data;
+ } else if (w->filenames != NULL && w->filename_iteration < sk_OPENSSL_STRING_num(w->filenames)) {
+ name = sk_OPENSSL_STRING_value(w->filenames, w->filename_iteration);
+ }
+
+ if (name == NULL) {
+ logmsg(rc, log_sys_err, "Can't find a URI in walk context, this shouldn't happen: state %d, manifest_iteration %d, filename_iteration %d",
+ (int) w->state, w->manifest_iteration, w->filename_iteration);
+ return 0;
+ }
+
+ if (strlen(w->certinfo.sia.s) + strlen(name) >= sizeof(uri->s)) {
+ logmsg(rc, log_data_err, "URI %s%s too long, skipping", w->certinfo.sia.s, uri->s);
+ return 0;
+ }
+
+ strcpy(uri->s, w->certinfo.sia.s);
+ strcat(uri->s, name);
+
+ if (fah != NULL) {
+ sk_OPENSSL_STRING_remove(w->filenames, name);
+ *hash = fah->hash->data;
+ *hashlen = fah->hash->length;
+ } else {
+ *hash = NULL;
+ *hashlen = 0;
+ }
+
+ return 1;
+}
+
+/**
+ * Create a new walk context stack.
+ */
+static STACK_OF(walk_ctx_t) *walk_ctx_stack_new(void)
+{
+ return sk_walk_ctx_t_new_null();
+}
+
+/**
+ * Push a walk context onto a walk context stack, return the new context.
+ */
+static walk_ctx_t *walk_ctx_stack_push(STACK_OF(walk_ctx_t) *wsk,
+ X509 *x,
+ const certinfo_t *certinfo)
+{
+ walk_ctx_t *w;
+
+ if (x == NULL ||
+ (certinfo == NULL) != (sk_walk_ctx_t_num(wsk) == 0) ||
+ (w = malloc(sizeof(*w))) == NULL)
+ return NULL;
+
+ memset(w, 0, sizeof(*w));
+ w->cert = x;
+ if (certinfo != NULL)
+ w->certinfo = *certinfo;
+ else
+ memset(&w->certinfo, 0, sizeof(w->certinfo));
+
+ if (!sk_walk_ctx_t_push(wsk, w)) {
+ free(w);
+ return NULL;
+ }
+
+ walk_ctx_attach(w);
+ return w;
+}
+
+/**
+ * Pop and discard a walk context from a walk context stack.
+ */
+static void walk_ctx_stack_pop(STACK_OF(walk_ctx_t) *wsk)
+{
+ walk_ctx_detach(sk_walk_ctx_t_pop(wsk));
+}
+
+/**
+ * Clone a stack of walk contexts.
+ */
+static STACK_OF(walk_ctx_t) *walk_ctx_stack_clone(STACK_OF(walk_ctx_t) *old_wsk)
+{
+ STACK_OF(walk_ctx_t) *new_wsk;
+ int i;
+ if (old_wsk == NULL || (new_wsk = sk_walk_ctx_t_dup(old_wsk)) == NULL)
+ return NULL;
+ for (i = 0; i < sk_walk_ctx_t_num(new_wsk); i++)
+ walk_ctx_attach(sk_walk_ctx_t_value(new_wsk, i));
+ return new_wsk;
+}
+
+/**
+ * Extract certificate stack from walk context stack. Returns a newly
+ * created STACK_OF(X509) pointing to the existing cert objects.
+ *
+ * NB: This is a shallow copy, so use sk_X509_free() to free it, not
+ * sk_X509_pop_free().
+ */
+static STACK_OF(X509) *walk_ctx_stack_certs(const rcynic_ctx_t *rc,
+ STACK_OF(walk_ctx_t) *wsk)
+{
+ STACK_OF(X509) *xsk = sk_X509_new_null();
+ walk_ctx_t *w;
+ int i;
+
+ assert(rc);
+
+ for (i = 0; i < sk_walk_ctx_t_num(wsk); i++)
+ if ((w = sk_walk_ctx_t_value(wsk, i)) == NULL ||
+ (w->cert != NULL && !sk_X509_push(xsk, w->cert)))
+ goto fail;
+
+ return xsk;
+
+ fail:
+ logmsg(rc, log_sys_err, "Couldn't clone walk_ctx_stack, memory exhausted?");
+ sk_X509_free(xsk);
+ return NULL;
+}
+
+/**
+ * Free a walk context stack, decrementing reference counts of each
+ * frame on it.
+ */
+static void walk_ctx_stack_free(STACK_OF(walk_ctx_t) *wsk)
+{
+ sk_walk_ctx_t_pop_free(wsk, walk_ctx_detach);
+}
+
+
+
+static int rsync_count_running(const rcynic_ctx_t *);
+
+/**
+ * Add a task to the task queue.
+ */
+static int task_add(const rcynic_ctx_t *rc,
+ void (*handler)(rcynic_ctx_t *, void *),
+ void *cookie)
+{
+ task_t *t = malloc(sizeof(*t));
+
+ assert(rc && rc->task_queue && handler);
+
+ assert(rsync_count_running(rc) <= rc->max_parallel_fetches);
+
+ if (!t)
+ return 0;
+
+ t->handler = handler;
+ t->cookie = cookie;
+
+ if (sk_task_t_push(rc->task_queue, t))
+ return 1;
+
+ free(t);
+ return 0;
+}
+
+/**
+ * Run tasks until queue is empty.
+ */
+static void task_run_q(rcynic_ctx_t *rc)
+{
+ task_t *t;
+ assert(rc && rc->task_queue);
+ while ((t = sk_task_t_shift(rc->task_queue)) != NULL) {
+ t->handler(rc, t->cookie);
+ free(t);
+ }
+}
+
+
+
+/**
+ * Check cache of whether we've already fetched a particular URI.
+ */
+static rsync_history_t *rsync_history_uri(const rcynic_ctx_t *rc,
+ const uri_t *uri)
+{
+ rsync_history_t h;
+ char *s;
+ int i;
+
+ assert(rc && uri && rc->rsync_history);
+
+ if (!is_rsync(uri->s))
+ return NULL;
+
+ h.uri = *uri;
+
+ while ((s = strrchr(h.uri.s, '/')) != NULL && s[1] == '\0')
+ *s = '\0';
+
+ while ((i = sk_rsync_history_t_find(rc->rsync_history, &h)) < 0) {
+ if ((s = strrchr(h.uri.s, '/')) == NULL ||
+ (s - h.uri.s) < SIZEOF_RSYNC)
+ return NULL;
+ *s = '\0';
+ }
+
+ return sk_rsync_history_t_value(rc->rsync_history, i);
+}
+
+/**
+ * Record that we've already attempted to synchronize a particular
+ * rsync URI.
+ */
+static void rsync_history_add(const rcynic_ctx_t *rc,
+ const rsync_ctx_t *ctx,
+ const rsync_status_t status)
+{
+ int final_slash = 0;
+ rsync_history_t *h;
+ uri_t uri;
+ size_t n;
+ char *s;
+
+ assert(rc && ctx && rc->rsync_history && is_rsync(ctx->uri.s));
+
+ uri = ctx->uri;
+
+ while ((s = strrchr(uri.s, '/')) != NULL && s[1] == '\0') {
+ final_slash = 1;
+ *s = '\0';
+ }
+
+ if (status != rsync_status_done) {
+
+ n = SIZEOF_RSYNC + strcspn(uri.s + SIZEOF_RSYNC, "/");
+ assert(n < sizeof(uri.s));
+ uri.s[n] = '\0';
+ final_slash = 1;
+
+ if ((h = rsync_history_uri(rc, &uri)) != NULL) {
+ assert(h->status != rsync_status_done);
+ return;
+ }
+ }
+
+ if ((h = rsync_history_t_new()) != NULL) {
+ h->uri = uri;
+ h->status = status;
+ h->started = ctx->started;
+ h->finished = time(0);
+ h->final_slash = final_slash;
+ }
+
+ if (h == NULL || !sk_rsync_history_t_push(rc->rsync_history, h)) {
+ rsync_history_t_free(h);
+ logmsg(rc, log_sys_err,
+ "Couldn't add %s to rsync_history, blundering onwards", uri.s);
+ }
+}
+
+
+
+/**
+ * Return count of how many rsync contexts are in running.
+ */
+static int rsync_count_running(const rcynic_ctx_t *rc)
+{
+ const rsync_ctx_t *ctx;
+ int i, n = 0;
+
+ assert(rc && rc->rsync_queue);
+
+ for (i = 0; (ctx = sk_rsync_ctx_t_value(rc->rsync_queue, i)) != NULL; ++i) {
+ switch (ctx->state) {
+ case rsync_state_running:
+ case rsync_state_closed:
+ case rsync_state_terminating:
+ n++;
+ default:
+ continue;
+ }
+ }
+
+ return n;
+}
+
+/**
+ * Test whether an rsync context conflicts with anything that's
+ * currently runable.
+ */
+static int rsync_conflicts(const rcynic_ctx_t *rc,
+ const rsync_ctx_t *ctx)
+{
+ const rsync_ctx_t *c;
+ int i;
+
+ assert(rc && ctx && rc->rsync_queue);
+
+ for (i = 0; (c = sk_rsync_ctx_t_value(rc->rsync_queue, i)) != NULL; ++i)
+ if (c != ctx &&
+ (c->state == rsync_state_initial ||
+ c->state == rsync_state_running) &&
+ conflicting_uris(&c->uri, &ctx->uri))
+ return 1;
+
+ return 0;
+}
+
+/**
+ * Test whether a rsync context is runable at this time.
+ */
+static int rsync_runable(const rcynic_ctx_t *rc,
+ const rsync_ctx_t *ctx)
+{
+ assert(rc && ctx);
+
+ switch (ctx->state) {
+
+ case rsync_state_initial:
+ case rsync_state_running:
+ return 1;
+
+ case rsync_state_retry_wait:
+ return ctx->deadline <= time(0);
+
+ case rsync_state_closed:
+ case rsync_state_terminating:
+ return 0;
+
+ case rsync_state_conflict_wait:
+ return !rsync_conflicts(rc, ctx);
+
+ default:
+ break;
+ }
+
+ return 0;
+}
+
+/**
+ * Return count of runable rsync contexts.
+ */
+static int rsync_count_runable(const rcynic_ctx_t *rc)
+{
+ const rsync_ctx_t *ctx;
+ int i, n = 0;
+
+ assert(rc && rc->rsync_queue);
+
+ for (i = 0; (ctx = sk_rsync_ctx_t_value(rc->rsync_queue, i)) != NULL; ++i)
+ if (rsync_runable(rc, ctx))
+ n++;
+
+ return n;
+}
+
+/**
+ * Call rsync context handler, if one is set.
+ */
+static void rsync_call_handler(rcynic_ctx_t *rc,
+ rsync_ctx_t *ctx,
+ const rsync_status_t status)
+{
+ if (!ctx)
+ return;
+
+ switch (status) {
+
+ case rsync_status_pending:
+ case rsync_status_done:
+ break;
+
+ case rsync_status_failed:
+ log_validation_status(rc, &ctx->uri, rsync_transfer_failed, object_generation_null);
+ break;
+
+ case rsync_status_timed_out:
+ log_validation_status(rc, &ctx->uri, rsync_transfer_timed_out, object_generation_null);
+ break;
+
+ case rsync_status_skipped:
+ log_validation_status(rc, &ctx->uri, rsync_transfer_skipped, object_generation_null);
+ break;
+ }
+
+ if (ctx->handler)
+ ctx->handler(rc, ctx, status, &ctx->uri, ctx->cookie);
+}
+
+/**
+ * Run an rsync process.
+ */
+static void rsync_run(rcynic_ctx_t *rc,
+ rsync_ctx_t *ctx)
+{
+ static const char * const rsync_cmd[] = {
+ "rsync", "--update", "--times", "--copy-links", "--itemize-changes"
+ };
+ static const char * const rsync_tree_args[] = {
+ "--recursive", "--delete"
+ };
+
+ const char *argv[10];
+ path_t path;
+ int i, argc = 0, flags, pipe_fds[2];
+
+ pipe_fds[0] = pipe_fds[1] = -1;
+
+ assert(rc && ctx && ctx->pid == 0 && ctx->state != rsync_state_running && rsync_runable(rc, ctx));
+
+ if (rsync_history_uri(rc, &ctx->uri)) {
+ logmsg(rc, log_verbose, "Late rsync cache hit for %s", ctx->uri.s);
+ rsync_call_handler(rc, ctx, rsync_status_done);
+ (void) sk_rsync_ctx_t_delete_ptr(rc->rsync_queue, ctx);
+ free(ctx);
+ return;
+ }
+
+ assert(rsync_count_running(rc) < rc->max_parallel_fetches);
+
+ logmsg(rc, log_telemetry, "Fetching %s", ctx->uri.s);
+
+ memset(argv, 0, sizeof(argv));
+
+ for (i = 0; i < sizeof(rsync_cmd)/sizeof(*rsync_cmd); i++) {
+ assert(argc < sizeof(argv)/sizeof(*argv));
+ argv[argc++] = rsync_cmd[i];
+ }
+ if (endswith(ctx->uri.s, "/")) {
+ for (i = 0; i < sizeof(rsync_tree_args)/sizeof(*rsync_tree_args); i++) {
+ assert(argc < sizeof(argv)/sizeof(*argv));
+ argv[argc++] = rsync_tree_args[i];
+ }
+ }
+
+ if (rc->rsync_program)
+ argv[0] = rc->rsync_program;
+
+ if (!uri_to_filename(rc, &ctx->uri, &path, &rc->unauthenticated)) {
+ logmsg(rc, log_data_err, "Couldn't extract filename from URI: %s", ctx->uri.s);
+ goto lose;
+ }
+
+ assert(argc < sizeof(argv)/sizeof(*argv));
+ argv[argc++] = ctx->uri.s;
+
+ assert(argc < sizeof(argv)/sizeof(*argv));
+ argv[argc++] = path.s;
+
+ if (!mkdir_maybe(rc, &path)) {
+ logmsg(rc, log_sys_err, "Couldn't make target directory: %s", path.s);
+ goto lose;
+ }
+
+ for (i = 0; i < argc; i++)
+ logmsg(rc, log_debug, "rsync argv[%d]: %s", i, argv[i]);
+
+ if (pipe(pipe_fds) < 0) {
+ logmsg(rc, log_sys_err, "pipe() failed: %s", strerror(errno));
+ goto lose;
+ }
+
+ switch ((ctx->pid = vfork())) {
+
+ case -1:
+ logmsg(rc, log_sys_err, "vfork() failed: %s", strerror(errno));
+ goto lose;
+
+ case 0:
+ /*
+ * Child
+ */
+#define whine(msg) ((void) write(2, msg, sizeof(msg) - 1))
+ if (close(pipe_fds[0]) < 0)
+ whine("close(pipe_fds[0]) failed\n");
+ else if (dup2(pipe_fds[1], 1) < 0)
+ whine("dup2(pipe_fds[1], 1) failed\n");
+ else if (dup2(pipe_fds[1], 2) < 0)
+ whine("dup2(pipe_fds[1], 2) failed\n");
+ else if (close(pipe_fds[1]) < 0)
+ whine("close(pipe_fds[1]) failed\n");
+ else if (execvp(argv[0], (char * const *) argv) < 0)
+ whine("execvp(argv[0], (char * const *) argv) failed\n");
+ whine("last system error: ");
+ write(2, strerror(errno), strlen(strerror(errno)));
+ whine("\n");
+ _exit(1);
+#undef whine
+
+ default:
+ /*
+ * Parent
+ */
+ ctx->fd = pipe_fds[0];
+ if ((flags = fcntl(ctx->fd, F_GETFL, 0)) == -1 ||
+ fcntl(ctx->fd, F_SETFL, flags | O_NONBLOCK) == -1) {
+ logmsg(rc, log_sys_err, "fcntl(ctx->fd, F_[GS]ETFL, O_NONBLOCK) failed: %s",
+ strerror(errno));
+ goto lose;
+ }
+ (void) close(pipe_fds[1]);
+ ctx->state = rsync_state_running;
+ ctx->problem = rsync_problem_none;
+ if (!ctx->started)
+ ctx->started = time(0);
+ if (rc->rsync_timeout)
+ ctx->deadline = time(0) + rc->rsync_timeout;
+ logmsg(rc, log_verbose, "Subprocess %u started, queued %d, runable %d, running %d, max %d, URI %s",
+ (unsigned) ctx->pid, sk_rsync_ctx_t_num(rc->rsync_queue), rsync_count_runable(rc), rsync_count_running(rc), rc->max_parallel_fetches, ctx->uri.s);
+ rsync_call_handler(rc, ctx, rsync_status_pending);
+ return;
+
+ }
+
+ lose:
+ if (pipe_fds[0] != -1)
+ (void) close(pipe_fds[0]);
+ if (pipe_fds[1] != -1)
+ (void) close(pipe_fds[1]);
+ if (rc->rsync_queue && ctx)
+ (void) sk_rsync_ctx_t_delete_ptr(rc->rsync_queue, ctx);
+ rsync_call_handler(rc, ctx, rsync_status_failed);
+ if (ctx->pid > 0) {
+ (void) kill(ctx->pid, SIGKILL);
+ ctx->pid = 0;
+ }
+}
+
+/**
+ * Process one line of rsync's output. This is a separate function
+ * primarily to centralize scraping for magic error strings.
+ */
+static void do_one_rsync_log_line(const rcynic_ctx_t *rc,
+ rsync_ctx_t *ctx)
+{
+ unsigned u;
+ char *s;
+
+ /*
+ * Send line to our log unless it's empty.
+ */
+ if (ctx->buffer[strspn(ctx->buffer, " \t\n\r")] != '\0')
+ logmsg(rc, log_telemetry, "rsync[%u]: %s", ctx->pid, ctx->buffer);
+
+ /*
+ * Check for magic error strings
+ */
+ if ((s = strstr(ctx->buffer, "@ERROR: max connections")) != NULL) {
+ ctx->problem = rsync_problem_refused;
+ if (sscanf(s, "@ERROR: max connections (%u) reached -- try again later", &u) == 1)
+ logmsg(rc, log_verbose, "Subprocess %u reported limit of %u for %s", ctx->pid, u, ctx->uri.s);
+ }
+}
+
+/**
+ * Construct select() arguments.
+ */
+static int rsync_construct_select(const rcynic_ctx_t *rc,
+ const time_t now,
+ fd_set *rfds,
+ struct timeval *tv)
+{
+ rsync_ctx_t *ctx;
+ time_t when = 0;
+ int i, n = 0;
+
+ assert(rc && rc->rsync_queue && rfds && tv && rc->max_select_time >= 0);
+
+ FD_ZERO(rfds);
+
+ for (i = 0; (ctx = sk_rsync_ctx_t_value(rc->rsync_queue, i)) != NULL; ++i) {
+
+#if 0
+ logmsg(rc, log_debug, "+++ ctx[%d] pid %d fd %d state %s started %lu deadline %lu",
+ i, ctx->pid, ctx->fd, rsync_state_label[ctx->state],
+ (unsigned long) ctx->started, (unsigned long) ctx->deadline);
+#endif
+
+ switch (ctx->state) {
+
+ case rsync_state_running:
+ assert(ctx->fd >= 0);
+ FD_SET(ctx->fd, rfds);
+ if (ctx->fd > n)
+ n = ctx->fd;
+ if (!rc->rsync_timeout)
+ continue;
+ /* Fall through */
+
+ case rsync_state_retry_wait:
+ if (when == 0 || ctx->deadline < when)
+ when = ctx->deadline;
+ /* Fall through */
+
+ default:
+ continue;
+ }
+ }
+
+ if (!when)
+ tv->tv_sec = rc->max_select_time;
+ else if (when < now)
+ tv->tv_sec = 0;
+ else if (when < now + rc->max_select_time)
+ tv->tv_sec = when - now;
+ else
+ tv->tv_sec = rc->max_select_time;
+ tv->tv_usec = 0;
+ return n;
+}
+
+/**
+ * Convert rsync_status_t to mib_counter_t.
+ *
+ * Maybe some day this will go away and we won't be carrying
+ * essentially the same information in two different databases, but
+ * for now I'll settle for cleaning up the duplicate code logic.
+ */
+static mib_counter_t rsync_status_to_mib_counter(rsync_status_t status)
+{
+ switch (status) {
+ case rsync_status_done: return rsync_transfer_succeeded;
+ case rsync_status_timed_out: return rsync_transfer_timed_out;
+ case rsync_status_failed: return rsync_transfer_failed;
+ case rsync_status_skipped: return rsync_transfer_skipped;
+ default:
+ /*
+ * Keep GCC from whining about untested cases.
+ */
+ assert(status == rsync_status_done ||
+ status == rsync_status_timed_out ||
+ status == rsync_status_failed ||
+ status == rsync_status_skipped);
+ return rsync_transfer_failed;
+ }
+}
+
+/**
+ * Manager for queue of rsync tasks in progress.
+ *
+ * General plan here is to process one completed child, or output
+ * accumulated from children, or block if there is absolutely nothing
+ * to do, on the theory that caller had nothing to do either or would
+ * not have called us. Once we've done something allegedly useful, we
+ * return, because this is not the event loop; if and when the event
+ * loop has nothing more important to do, we'll be called again.
+ *
+ * So this is the only place where the program blocks waiting for
+ * children, but we only do it when we know there's nothing else
+ * useful that we could be doing while we wait.
+ */
+static void rsync_mgr(rcynic_ctx_t *rc)
+{
+ rsync_status_t rsync_status;
+ int i, n, pid_status = -1;
+ rsync_ctx_t *ctx = NULL;
+ time_t now = time(0);
+ struct timeval tv;
+ fd_set rfds;
+ pid_t pid;
+ char *s;
+
+ assert(rc && rc->rsync_queue);
+
+ /*
+ * Check for exited subprocesses.
+ */
+
+ while ((pid = waitpid(-1, &pid_status, WNOHANG)) > 0) {
+
+ /*
+ * Child exited, handle it.
+ */
+
+ logmsg(rc, log_verbose, "Subprocess %u exited with status %d",
+ (unsigned) pid, WEXITSTATUS(pid_status));
+
+ for (i = 0; (ctx = sk_rsync_ctx_t_value(rc->rsync_queue, i)) != NULL; ++i)
+ if (ctx->pid == pid)
+ break;
+ if (ctx == NULL) {
+ assert(i == sk_rsync_ctx_t_num(rc->rsync_queue));
+ logmsg(rc, log_sys_err, "Couldn't find rsync context for pid %d", pid);
+ continue;
+ }
+
+ close(ctx->fd);
+ ctx->fd = -1;
+
+ if (ctx->buflen > 0) {
+ assert(ctx->buflen < sizeof(ctx->buffer));
+ ctx->buffer[ctx->buflen] = '\0';
+ do_one_rsync_log_line(rc, ctx);
+ ctx->buflen = 0;
+ }
+
+ switch (WEXITSTATUS(pid_status)) {
+
+ case 0:
+ rsync_status = rsync_status_done;
+ break;
+
+ case 5: /* "Error starting client-server protocol" */
+ /*
+ * Handle remote rsyncd refusing to talk to us because we've
+ * exceeded its connection limit. Back off for a short
+ * interval, then retry.
+ */
+ if (ctx->problem == rsync_problem_refused && ctx->tries < rc->max_retries) {
+ unsigned char r;
+ if (!RAND_bytes(&r, sizeof(r)))
+ r = 60;
+ ctx->deadline = time(0) + rc->retry_wait_min + r;
+ ctx->state = rsync_state_retry_wait;
+ ctx->problem = rsync_problem_none;
+ ctx->pid = 0;
+ ctx->tries++;
+ logmsg(rc, log_telemetry, "Scheduling retry for %s", ctx->uri.s);
+ continue;
+ }
+ goto failure;
+
+ case 23: /* "Partial transfer due to error" */
+ /*
+ * This appears to be a catch-all for "something bad happened
+ * trying to do what you asked me to do". In the cases I've
+ * seen to date, this is things like "the directory you
+ * requested isn't there" or "NFS exploded when I tried to touch
+ * the directory". These aren't network layer failures, so we
+ * (probably) shouldn't give up on the repository host.
+ */
+ rsync_status = rsync_status_done;
+ log_validation_status(rc, &ctx->uri, rsync_partial_transfer, object_generation_null);
+ break;
+
+ default:
+ failure:
+ rsync_status = rsync_status_failed;
+ logmsg(rc, log_data_err, "rsync %u exited with status %d fetching %s",
+ (unsigned) pid, WEXITSTATUS(pid_status), ctx->uri.s);
+ break;
+ }
+
+ if (rc->rsync_timeout && now >= ctx->deadline)
+ rsync_status = rsync_status_timed_out;
+ log_validation_status(rc, &ctx->uri,
+ rsync_status_to_mib_counter(rsync_status),
+ object_generation_null);
+ rsync_history_add(rc, ctx, rsync_status);
+ rsync_call_handler(rc, ctx, rsync_status);
+ (void) sk_rsync_ctx_t_delete_ptr(rc->rsync_queue, ctx);
+ free(ctx);
+ ctx = NULL;
+ }
+
+ if (pid == -1 && errno != EINTR && errno != ECHILD)
+ logmsg(rc, log_sys_err, "waitpid() returned error: %s", strerror(errno));
+
+ assert(rsync_count_running(rc) <= rc->max_parallel_fetches);
+
+ /*
+ * Look for rsync contexts that have become runable. Odd loop
+ * structure is because rsync_run() might decide to remove the
+ * specified rsync task from the queue instead of running it.
+ */
+ for (i = 0; (ctx = sk_rsync_ctx_t_value(rc->rsync_queue, i)) != NULL; i++) {
+ n = sk_rsync_ctx_t_num(rc->rsync_queue);
+ if (ctx->state != rsync_state_running &&
+ rsync_runable(rc, ctx) &&
+ rsync_count_running(rc) < rc->max_parallel_fetches)
+ rsync_run(rc, ctx);
+ if (n > sk_rsync_ctx_t_num(rc->rsync_queue))
+ i--;
+ }
+
+ assert(rsync_count_running(rc) <= rc->max_parallel_fetches);
+
+ /*
+ * Check for log text from subprocesses.
+ */
+
+ n = rsync_construct_select(rc, now, &rfds, &tv);
+
+ if (n > 0 && tv.tv_sec)
+ logmsg(rc, log_verbose, "Waiting up to %u seconds for rsync, queued %d, runable %d, running %d, max %d",
+ (unsigned) tv.tv_sec, sk_rsync_ctx_t_num(rc->rsync_queue), rsync_count_runable(rc),
+ rsync_count_running(rc), rc->max_parallel_fetches);
+
+ if (n > 0) {
+#if 0
+ logmsg(rc, log_debug, "++ select(%d, %u)", n, tv.tv_sec);
+#endif
+ n = select(n + 1, &rfds, NULL, NULL, &tv);
+ }
+
+ if (n > 0) {
+
+ for (i = 0; (ctx = sk_rsync_ctx_t_value(rc->rsync_queue, i)) != NULL; ++i) {
+ if (ctx->fd <= 0 || !FD_ISSET(ctx->fd, &rfds))
+ continue;
+
+ assert(ctx->buflen < sizeof(ctx->buffer) - 1);
+
+ while ((n = read(ctx->fd, ctx->buffer + ctx->buflen, sizeof(ctx->buffer) - 1 - ctx->buflen)) > 0) {
+ ctx->buflen += n;
+ assert(ctx->buflen < sizeof(ctx->buffer));
+ ctx->buffer[ctx->buflen] = '\0';
+
+ while ((s = strchr(ctx->buffer, '\n')) != NULL) {
+ *s++ = '\0';
+ do_one_rsync_log_line(rc, ctx);
+ assert(s > ctx->buffer && s < ctx->buffer + sizeof(ctx->buffer));
+ ctx->buflen -= s - ctx->buffer;
+ assert(ctx->buflen < sizeof(ctx->buffer));
+ if (ctx->buflen > 0)
+ memmove(ctx->buffer, s, ctx->buflen);
+ ctx->buffer[ctx->buflen] = '\0';
+ }
+
+ if (ctx->buflen == sizeof(ctx->buffer) - 1) {
+ ctx->buffer[sizeof(ctx->buffer) - 1] = '\0';
+ do_one_rsync_log_line(rc, ctx);
+ ctx->buflen = 0;
+ }
+ }
+
+ if (n == 0) {
+ (void) close(ctx->fd);
+ ctx->fd = -1;
+ ctx->state = rsync_state_closed;
+ }
+ }
+ }
+
+ assert(rsync_count_running(rc) <= rc->max_parallel_fetches);
+
+ /*
+ * Deal with children that have been running too long.
+ */
+ if (rc->rsync_timeout) {
+ for (i = 0; (ctx = sk_rsync_ctx_t_value(rc->rsync_queue, i)) != NULL; ++i) {
+ int sig;
+ if (ctx->pid <= 0 || now < ctx->deadline)
+ continue;
+ sig = ctx->tries++ < KILL_MAX ? SIGTERM : SIGKILL;
+ if (ctx->state != rsync_state_terminating) {
+ ctx->problem = rsync_problem_timed_out;
+ ctx->state = rsync_state_terminating;
+ ctx->tries = 0;
+ logmsg(rc, log_telemetry, "Subprocess %u is taking too long fetching %s, whacking it", (unsigned) ctx->pid, ctx->uri.s);
+ rsync_history_add(rc, ctx, rsync_status_timed_out);
+ } else if (sig == SIGTERM) {
+ logmsg(rc, log_verbose, "Whacking subprocess %u again", (unsigned) ctx->pid);
+ } else {
+ logmsg(rc, log_verbose, "Whacking subprocess %u with big hammer", (unsigned) ctx->pid);
+ }
+ (void) kill(ctx->pid, sig);
+ ctx->deadline = now + 1;
+ }
+ }
+}
+
+/**
+ * Set up rsync context and attempt to start it.
+ */
+static void rsync_init(rcynic_ctx_t *rc,
+ const uri_t *uri,
+ void *cookie,
+ void (*handler)(rcynic_ctx_t *, const rsync_ctx_t *, const rsync_status_t, const uri_t *, void *))
+{
+ rsync_ctx_t *ctx = NULL;
+
+ assert(rc && uri && strlen(uri->s) > SIZEOF_RSYNC);
+
+ if (!rc->run_rsync) {
+ logmsg(rc, log_verbose, "rsync disabled, skipping %s", uri->s);
+ if (handler)
+ handler(rc, NULL, rsync_status_skipped, uri, cookie);
+ return;
+ }
+
+ if (rsync_history_uri(rc, uri)) {
+ logmsg(rc, log_verbose, "rsync cache hit for %s", uri->s);
+ if (handler)
+ handler(rc, NULL, rsync_status_done, uri, cookie);
+ return;
+ }
+
+ if ((ctx = malloc(sizeof(*ctx))) == NULL) {
+ logmsg(rc, log_sys_err, "malloc(rsync_ctxt_t) failed");
+ if (handler)
+ handler(rc, NULL, rsync_status_failed, uri, cookie);
+ return;
+ }
+
+ memset(ctx, 0, sizeof(*ctx));
+ ctx->uri = *uri;
+ ctx->handler = handler;
+ ctx->cookie = cookie;
+ ctx->fd = -1;
+
+ if (!sk_rsync_ctx_t_push(rc->rsync_queue, ctx)) {
+ logmsg(rc, log_sys_err, "Couldn't push rsync state object onto queue, punting %s", ctx->uri.s);
+ rsync_call_handler(rc, ctx, rsync_status_failed);
+ free(ctx);
+ return;
+ }
+
+ if (rsync_conflicts(rc, ctx)) {
+ logmsg(rc, log_debug, "New rsync context %s is feeling conflicted", ctx->uri.s);
+ ctx->state = rsync_state_conflict_wait;
+ }
+}
+
+/**
+ * rsync a trust anchor.
+ */
+static void rsync_ta(rcynic_ctx_t *rc,
+ const uri_t *uri,
+ tal_ctx_t *tctx,
+ void (*handler)(rcynic_ctx_t *, const rsync_ctx_t *,
+ const rsync_status_t, const uri_t *, void *))
+{
+ assert(endswith(uri->s, ".cer"));
+ rsync_init(rc, uri, tctx, handler);
+}
+
+/**
+ * rsync an entire subtree, generally rooted at a SIA collection.
+ */
+static void rsync_tree(rcynic_ctx_t *rc,
+ const uri_t *uri,
+ STACK_OF(walk_ctx_t) *wsk,
+ void (*handler)(rcynic_ctx_t *, const rsync_ctx_t *,
+ const rsync_status_t, const uri_t *, void *))
+{
+ assert(endswith(uri->s, "/"));
+ rsync_init(rc, uri, wsk, handler);
+}
+
+
+
+/**
+ * Clean up old stuff from previous rsync runs. --delete doesn't help
+ * if the URI changes and we never visit the old URI again.
+ */
+static int prune_unauthenticated(const rcynic_ctx_t *rc,
+ const path_t *name,
+ const size_t baselen)
+{
+ path_t path;
+ struct dirent *d;
+ DIR *dir;
+ const char *slash;
+
+ assert(rc && name && baselen > 0 && strlen(name->s) >= baselen);
+
+ if (!is_directory(name)) {
+ logmsg(rc, log_usage_err, "prune: %s is not a directory", name->s);
+ return 0;
+ }
+
+ if ((dir = opendir(name->s)) == NULL) {
+ logmsg(rc, log_sys_err, "prune: opendir() failed on %s: %s", name->s, strerror(errno));
+ return 0;
+ }
+
+ slash = endswith(name->s, "/") ? "" : "/";
+
+ while ((d = readdir(dir)) != NULL) {
+ if (!strcmp(d->d_name, ".") || !strcmp(d->d_name, ".."))
+ continue;
+
+ if (snprintf(path.s, sizeof(path.s), "%s%s%s", name->s, slash, d->d_name) >= sizeof(path.s)) {
+ logmsg(rc, log_debug, "prune: %s%s%s too long", name->s, slash, d->d_name);
+ goto done;
+ }
+
+ if (validation_status_find_filename(rc, path.s + baselen)) {
+ logmsg(rc, log_debug, "prune: cache hit %s", path.s);
+ continue;
+ }
+
+ if (unlink(path.s) == 0) {
+ logmsg(rc, log_debug, "prune: removed %s", path.s);
+ continue;
+ }
+
+ if (prune_unauthenticated(rc, &path, baselen))
+ continue;
+
+ logmsg(rc, log_sys_err, "prune: removing %s failed: %s", path.s, strerror(errno));
+ goto done;
+ }
+
+ if (rmdir(name->s) == 0)
+ logmsg(rc, log_debug, "prune: removed %s", name->s);
+ else if (errno != ENOTEMPTY)
+ logmsg(rc, log_sys_err, "prune: couldn't remove %s: %s", name->s, strerror(errno));
+
+ done:
+ closedir(dir);
+ return !d;
+}
+
+
+
+/**
+ * Read a DER object using a BIO pipeline that hashes the file content
+ * as we read it. Returns the internal form of the parsed DER object,
+ * sets the hash buffer (if specified) as a side effect. The default
+ * hash algorithm is SHA-256.
+ */
+static void *read_file_with_hash(const path_t *filename,
+ const ASN1_ITEM *it,
+ const EVP_MD *md,
+ hashbuf_t *hash)
+{
+ void *result = NULL;
+ BIO *b;
+
+ if ((b = BIO_new_file(filename->s, "rb")) == NULL)
+ goto error;
+
+ if (hash != NULL) {
+ BIO *b2 = BIO_new(BIO_f_md());
+ if (b2 == NULL)
+ goto error;
+ if (md == NULL)
+ md = EVP_sha256();
+ if (!BIO_set_md(b2, md)) {
+ BIO_free(b2);
+ goto error;
+ }
+ BIO_push(b2, b);
+ b = b2;
+ }
+
+ if ((result = ASN1_item_d2i_bio(it, b, NULL)) == NULL)
+ goto error;
+
+ if (hash != NULL) {
+ memset(hash, 0, sizeof(*hash));
+ BIO_gets(b, (char *) hash, sizeof(hash->h));
+ }
+
+ error:
+ BIO_free_all(b);
+ return result;
+}
+
+/**
+ * Read and hash a certificate.
+ */
+static X509 *read_cert(const path_t *filename, hashbuf_t *hash)
+{
+ return read_file_with_hash(filename, ASN1_ITEM_rptr(X509), NULL, hash);
+}
+
+/**
+ * Read and hash a CRL.
+ */
+static X509_CRL *read_crl(const path_t *filename, hashbuf_t *hash)
+{
+ return read_file_with_hash(filename, ASN1_ITEM_rptr(X509_CRL), NULL, hash);
+}
+
+/**
+ * Read and hash a CMS message.
+ */
+static CMS_ContentInfo *read_cms(const path_t *filename, hashbuf_t *hash)
+{
+ return read_file_with_hash(filename, ASN1_ITEM_rptr(CMS_ContentInfo), NULL, hash);
+}
+
+
+
+/**
+ * Extract CRLDP data from a certificate. Stops looking after finding
+ * the first rsync URI.
+ */
+static int extract_crldp_uri(rcynic_ctx_t *rc,
+ const uri_t *uri,
+ const object_generation_t generation,
+ const STACK_OF(DIST_POINT) *crldp,
+ uri_t *result)
+{
+ DIST_POINT *d;
+ int i;
+
+ assert(rc && uri && crldp && result);
+
+ if (sk_DIST_POINT_num(crldp) != 1)
+ goto bad;
+
+ d = sk_DIST_POINT_value(crldp, 0);
+
+ if (d->reasons || d->CRLissuer || !d->distpoint || d->distpoint->type != 0)
+ goto bad;
+
+ 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);
+ if (n == NULL || n->type != GEN_URI)
+ goto bad;
+ if (!is_rsync((char *) n->d.uniformResourceIdentifier->data))
+ log_validation_status(rc, uri, non_rsync_uri_in_extension, generation);
+ else if (sizeof(result->s) <= n->d.uniformResourceIdentifier->length)
+ log_validation_status(rc, uri, uri_too_long, generation);
+ else if (result->s[0])
+ log_validation_status(rc, uri, multiple_rsync_uris_in_extension, generation);
+ else
+ strcpy(result->s, (char *) n->d.uniformResourceIdentifier->data);
+ }
+
+ return result->s[0];
+
+ bad:
+ log_validation_status(rc, uri, malformed_crldp_extension, generation);
+ return 0;
+}
+
+/**
+ * Extract SIA or AIA data from a certificate.
+ */
+static int extract_access_uri(rcynic_ctx_t *rc,
+ const uri_t *uri,
+ const object_generation_t generation,
+ const AUTHORITY_INFO_ACCESS *xia,
+ const int nid,
+ uri_t *result,
+ int *count)
+{
+ int i;
+
+ assert(rc && uri && xia && result && count);
+
+ for (i = 0; i < sk_ACCESS_DESCRIPTION_num(xia); i++) {
+ ACCESS_DESCRIPTION *a = sk_ACCESS_DESCRIPTION_value(xia, i);
+ if (a == NULL || a->location->type != GEN_URI)
+ return 0;
+ if (OBJ_obj2nid(a->method) != nid)
+ continue;
+ ++*count;
+ if (!is_rsync((char *) a->location->d.uniformResourceIdentifier->data))
+ log_validation_status(rc, uri, non_rsync_uri_in_extension, generation);
+ else if (sizeof(result->s) <= a->location->d.uniformResourceIdentifier->length)
+ log_validation_status(rc, uri, uri_too_long, generation);
+ else if (result->s[0])
+ log_validation_status(rc, uri, multiple_rsync_uris_in_extension, generation);
+ else
+ strcpy(result->s, (char *) a->location->d.uniformResourceIdentifier->data);
+ }
+ return 1;
+}
+
+
+
+/**
+ * Check to see whether an AKI extension is present, is of the right
+ * form, and matches the issuer.
+ */
+static int check_aki(rcynic_ctx_t *rc,
+ const uri_t *uri,
+ const X509 *issuer,
+ const AUTHORITY_KEYID *aki,
+ const object_generation_t generation)
+{
+ assert(rc && uri && issuer && issuer->skid);
+
+ if (aki == NULL) {
+ log_validation_status(rc, uri, aki_extension_missing, generation);
+ return 0;
+ }
+
+ if (!aki->keyid || aki->serial || aki->issuer) {
+ log_validation_status(rc, uri, aki_extension_wrong_format, generation);
+ return 0;
+ }
+
+ if (ASN1_OCTET_STRING_cmp(aki->keyid, issuer->skid)) {
+ log_validation_status(rc, uri, aki_extension_issuer_mismatch, generation);
+ return 0;
+ }
+
+ return 1;
+}
+
+
+
+/**
+ * Check whether a Distinguished Name conforms to the rescert profile.
+ * The profile is very restrictive: it only allows one mandatory
+ * CommonName field and one optional SerialNumber field, both of which
+ * must be of type PrintableString.
+ */
+static int check_allowed_dn(X509_NAME *dn)
+{
+ X509_NAME_ENTRY *ne;
+ ASN1_STRING *s;
+ int loc;
+
+ if (dn == NULL)
+ return 0;
+
+ switch (X509_NAME_entry_count(dn)) {
+
+ case 2:
+ if ((loc = X509_NAME_get_index_by_NID(dn, NID_serialNumber, -1)) < 0 ||
+ (ne = X509_NAME_get_entry(dn, loc)) == NULL ||
+ (s = X509_NAME_ENTRY_get_data(ne)) == NULL ||
+ ASN1_STRING_type(s) != V_ASN1_PRINTABLESTRING)
+ return 0;
+
+ /* Fall through */
+
+ case 1:
+ if ((loc = X509_NAME_get_index_by_NID(dn, NID_commonName, -1)) < 0 ||
+ (ne = X509_NAME_get_entry(dn, loc)) == NULL ||
+ (s = X509_NAME_ENTRY_get_data(ne)) == NULL ||
+ ASN1_STRING_type(s) != V_ASN1_PRINTABLESTRING)
+ return 0;
+
+ return 1;
+
+ default:
+ return 0;
+ }
+}
+
+/**
+ * Check whether an ASN.1 TIME value conforms to RFC 5280 4.1.2.5.
+ */
+static int check_allowed_time_encoding(ASN1_TIME *t)
+{
+ switch (t->type) {
+
+ case V_ASN1_UTCTIME:
+ return t->length == sizeof("yymmddHHMMSSZ") - 1;
+
+ case V_ASN1_GENERALIZEDTIME:
+ return (t->length == sizeof("yyyymmddHHMMSSZ") - 1 &&
+ strcmp("205", (char *) t->data) <= 0);
+
+ }
+ return 0;
+}
+
+/**
+ * Compare ASN1_TIME values.
+ */
+static int asn1_time_cmp(ASN1_TIME *t1, ASN1_TIME *t2)
+{
+ ASN1_GENERALIZEDTIME *g1 = ASN1_TIME_to_generalizedtime(t1, NULL);
+ ASN1_GENERALIZEDTIME *g2 = ASN1_TIME_to_generalizedtime(t2, NULL);
+
+ int cmp = ASN1_STRING_cmp(g1, g2);
+
+ ASN1_GENERALIZEDTIME_free(g1);
+ ASN1_GENERALIZEDTIME_free(g2);
+
+ return cmp;
+}
+
+
+
+/**
+ * Attempt to read and check one CRL from disk.
+ */
+
+static X509_CRL *check_crl_1(rcynic_ctx_t *rc,
+ const uri_t *uri,
+ path_t *path,
+ const path_t *prefix,
+ X509 *issuer,
+ const object_generation_t generation)
+{
+ STACK_OF(X509_REVOKED) *revoked;
+ X509_CRL *crl = NULL;
+ EVP_PKEY *pkey;
+ int i, ret;
+
+ assert(uri && path && issuer);
+
+ if (!uri_to_filename(rc, uri, path, prefix) ||
+ (crl = read_crl(path, NULL)) == NULL)
+ goto punt;
+
+ if (X509_CRL_get_version(crl) != 1) {
+ log_validation_status(rc, uri, wrong_object_version, generation);
+ goto punt;
+ }
+
+ if (!crl->crl || !crl->crl->sig_alg || !crl->crl->sig_alg->algorithm ||
+ OBJ_obj2nid(crl->crl->sig_alg->algorithm) != NID_sha256WithRSAEncryption) {
+ log_validation_status(rc, uri, nonconformant_signature_algorithm, generation);
+ goto punt;
+ }
+
+ if (!check_allowed_time_encoding(X509_CRL_get_lastUpdate(crl)) ||
+ !check_allowed_time_encoding(X509_CRL_get_nextUpdate(crl))) {
+ log_validation_status(rc, uri, nonconformant_asn1_time_value, generation);
+ goto punt;
+ }
+
+ if (X509_cmp_current_time(X509_CRL_get_lastUpdate(crl)) > 0) {
+ log_validation_status(rc, uri, crl_not_yet_valid, generation);
+ goto punt;
+ }
+
+ if (X509_cmp_current_time(X509_CRL_get_nextUpdate(crl)) < 0) {
+ log_validation_status(rc, uri, stale_crl_or_manifest, generation);
+ if (!rc->allow_stale_crl)
+ goto punt;
+ }
+
+ if (!check_aki(rc, uri, issuer, crl->akid, generation))
+ goto punt;
+
+ if (crl->crl_number == NULL) {
+ log_validation_status(rc, uri, crl_number_extension_missing, generation);
+ goto punt;
+ }
+
+ if (ASN1_INTEGER_cmp(crl->crl_number, asn1_zero) < 0) {
+ log_validation_status(rc, uri, crl_number_is_negative, generation);
+ goto punt;
+ }
+
+ if (ASN1_INTEGER_cmp(crl->crl_number, asn1_twenty_octets) > 0) {
+ log_validation_status(rc, uri, crl_number_out_of_range, generation);
+ goto punt;
+ }
+
+ if (X509_CRL_get_ext_count(crl) != 2) {
+ log_validation_status(rc, uri, disallowed_x509v3_extension, generation);
+ goto punt;
+ }
+
+ if (X509_NAME_cmp(X509_CRL_get_issuer(crl), X509_get_subject_name(issuer))) {
+ log_validation_status(rc, uri, crl_issuer_name_mismatch, generation);
+ goto punt;
+ }
+
+ if (!check_allowed_dn(X509_CRL_get_issuer(crl))) {
+ log_validation_status(rc, uri, nonconformant_issuer_name, generation);
+ if (!rc->allow_nonconformant_name)
+ goto punt;
+ }
+
+ if ((revoked = X509_CRL_get_REVOKED(crl)) != NULL) {
+ for (i = sk_X509_REVOKED_num(revoked) - 1; i >= 0; --i) {
+ if (X509_REVOKED_get_ext_count(sk_X509_REVOKED_value(revoked, i)) > 0) {
+ log_validation_status(rc, uri, disallowed_x509v3_extension, generation);
+ goto punt;
+ }
+ }
+ }
+
+ 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;
+}
+
+/**
+ * Check whether we already have a particular CRL, attempt to fetch it
+ * and check issuer's signature if we don't.
+ *
+ * General plan here is to do basic checks on both current and backup
+ * generation CRLs, then, if both generations pass all of our other
+ * tests, pick the generation with the highest CRL number, to protect
+ * against replay attacks.
+ */
+static X509_CRL *check_crl(rcynic_ctx_t *rc,
+ const uri_t *uri,
+ X509 *issuer)
+{
+ X509_CRL *old_crl, *new_crl, *result = NULL;
+ path_t old_path, new_path;
+
+ if (uri_to_filename(rc, uri, &new_path, &rc->new_authenticated) &&
+ (new_crl = read_crl(&new_path, NULL)) != NULL)
+ return new_crl;
+
+ logmsg(rc, log_telemetry, "Checking CRL %s", uri->s);
+
+ new_crl = check_crl_1(rc, uri, &new_path, &rc->unauthenticated,
+ issuer, object_generation_current);
+
+ old_crl = check_crl_1(rc, uri, &old_path, &rc->old_authenticated,
+ issuer, object_generation_backup);
+
+ if (!new_crl)
+ result = old_crl;
+
+ else if (!old_crl)
+ result = new_crl;
+
+ else {
+ ASN1_GENERALIZEDTIME *g_old = ASN1_TIME_to_generalizedtime(X509_CRL_get_lastUpdate(old_crl), NULL);
+ ASN1_GENERALIZEDTIME *g_new = ASN1_TIME_to_generalizedtime(X509_CRL_get_lastUpdate(new_crl), NULL);
+ int num_cmp = ASN1_INTEGER_cmp(old_crl->crl_number, new_crl->crl_number);
+ int date_cmp = (!g_old || !g_new) ? 0 : ASN1_STRING_cmp(g_old, g_new);
+
+ if (!g_old)
+ log_validation_status(rc, uri, bad_thisupdate, object_generation_backup);
+ if (!g_new)
+ log_validation_status(rc, uri, bad_thisupdate, object_generation_current);
+ if (num_cmp > 0)
+ log_validation_status(rc, uri, backup_number_higher_than_current, object_generation_current);
+ if (g_old && g_new && date_cmp > 0)
+ log_validation_status(rc, uri, backup_thisupdate_newer_than_current, object_generation_current);
+
+ if (num_cmp > 0 && (!g_old || !g_new || date_cmp > 0))
+ result = old_crl;
+ else
+ result = new_crl;
+
+ ASN1_GENERALIZEDTIME_free(g_old);
+ ASN1_GENERALIZEDTIME_free(g_new);
+ }
+
+ if (result && result == new_crl)
+ install_object(rc, uri, &new_path, object_generation_current);
+ else if (!access(new_path.s, F_OK))
+ log_validation_status(rc, uri, object_rejected, object_generation_current);
+
+ if (result && result == old_crl)
+ install_object(rc, uri, &old_path, object_generation_backup);
+ else if (!result && !access(old_path.s, F_OK))
+ log_validation_status(rc, uri, object_rejected, object_generation_backup);
+
+ if (result != new_crl)
+ X509_CRL_free(new_crl);
+
+ if (result != old_crl)
+ X509_CRL_free(old_crl);
+
+ return result;
+}
+
+
+/**
+ * Check digest of a CRL we've already accepted.
+ */
+static int check_crl_digest(const rcynic_ctx_t *rc,
+ const uri_t *uri,
+ const unsigned char *hash,
+ const size_t hashlen)
+{
+ X509_CRL *crl = NULL;
+ hashbuf_t hashbuf;
+ path_t path;
+ int result;
+
+ assert(rc && uri && hash);
+
+ if (!uri_to_filename(rc, uri, &path, &rc->new_authenticated) ||
+ (crl = read_crl(&path, &hashbuf)) == NULL)
+ return 0;
+
+ result = hashlen <= sizeof(hashbuf.h) && !memcmp(hashbuf.h, hash, hashlen);
+
+ X509_CRL_free(crl);
+
+ return result;
+}
+
+
+
+/**
+ * Validation callback function for use with x509_verify_cert().
+ */
+static int check_x509_cb(int ok, X509_STORE_CTX *ctx)
+{
+ rcynic_x509_store_ctx_t *rctx = (rcynic_x509_store_ctx_t *) ctx;
+ mib_counter_t code;
+
+ 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 isn't really an error, exactly. CRLs don't really
+ * "expire". What OpenSSL really means by this error is just
+ * "it's now later than the issuer said it intended to publish a
+ * new CRL". Whether we treat this as an error or not is
+ * configurable, see the allow_stale_crl parameter.
+ *
+ * Deciding whether to allow stale CRLs is check_crl_1()'s job,
+ * not ours. By the time this callback occurs, we've already
+ * accepted the CRL; this callback is just notifying us that the
+ * object being checked is tainted by a stale CRL. So we mark the
+ * object as tainted and carry on.
+ */
+ log_validation_status(rctx->rc, &rctx->subject->uri, tainted_by_stale_crl, rctx->subject->generation);
+ ok = 1;
+ return ok;
+
+ case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT:
+ /*
+ * This is another error that's only an error in the strange world
+ * of OpenSSL, but a more serious one. By default, OpenSSL
+ * expects all trust anchors to be self-signed. This is not a
+ * PKIX requirement, it's just an OpenSSL thing, but one violates
+ * it at one's peril, because the only way to convince OpenSSL to
+ * allow a non-self-signed trust anchor is to intercept this
+ * "error" in the verify callback handler.
+ *
+ * So this program supports non-self-signed trust anchors, but be
+ * warned that enabling this feature may cause this program's
+ * output not to work with other OpenSSL-based applications.
+ */
+ if (rctx->rc->allow_non_self_signed_trust_anchor)
+ ok = 1;
+ log_validation_status(rctx->rc, &rctx->subject->uri, trust_anchor_not_self_signed, rctx->subject->generation);
+ return ok;
+
+ /*
+ * Select correct MIB counter for every known OpenSSL verify errors
+ * except the ones we handle explicitly above, then fall through to
+ * common handling for all of these.
+ */
+#define QV(x) \
+ case x: \
+ code = mib_openssl_##x; \
+ break;
+ MIB_COUNTERS_FROM_OPENSSL;
+#undef QV
+
+ default:
+ code = unknown_openssl_verify_error;
+ break;
+ }
+
+ log_validation_status(rctx->rc, &rctx->subject->uri, code, rctx->subject->generation);
+ return ok;
+}
+
+/**
+ * Check crypto aspects of a certificate, policy OID, RFC 3779 path
+ * validation, and conformance to the RPKI certificate profile.
+ */
+static int check_x509(rcynic_ctx_t *rc,
+ STACK_OF(walk_ctx_t) *wsk,
+ const uri_t *uri,
+ X509 *x,
+ certinfo_t *certinfo,
+ const object_generation_t generation)
+{
+ walk_ctx_t *w = walk_ctx_stack_head(wsk);
+ rcynic_x509_store_ctx_t rctx;
+ EVP_PKEY *issuer_pkey = NULL, *subject_pkey = NULL;
+ unsigned long flags = (X509_V_FLAG_POLICY_CHECK | X509_V_FLAG_EXPLICIT_POLICY | X509_V_FLAG_X509_STRICT);
+ AUTHORITY_INFO_ACCESS *sia = NULL, *aia = NULL;
+ STACK_OF(POLICYINFO) *policies = NULL;
+ ASN1_BIT_STRING *ski_pubkey = NULL;
+ STACK_OF(DIST_POINT) *crldp = NULL;
+ EXTENDED_KEY_USAGE *eku = NULL;
+ BASIC_CONSTRAINTS *bc = NULL;
+ hashbuf_t ski_hashbuf;
+ unsigned ski_hashlen, afi;
+ int i, ok, crit, loc, ex_count, routercert = 0, ret = 0;
+
+ assert(rc && wsk && w && uri && x && w->cert);
+
+ /*
+ * Cleanup logic will explode if rctx.ctx hasn't been initialized,
+ * so we need to do this before running any test that can fail.
+ */
+ if (!X509_STORE_CTX_init(&rctx.ctx, rc->x509_store, x, NULL))
+ return 0;
+
+ /*
+ * certinfo == NULL means x is a self-signed trust anchor.
+ */
+ if (certinfo == NULL)
+ certinfo = &w->certinfo;
+
+ memset(certinfo, 0, sizeof(*certinfo));
+
+ certinfo->uri = *uri;
+ certinfo->generation = generation;
+
+ if (ASN1_INTEGER_cmp(X509_get_serialNumber(x), asn1_zero) <= 0 ||
+ ASN1_INTEGER_cmp(X509_get_serialNumber(x), asn1_twenty_octets) > 0) {
+ log_validation_status(rc, uri, bad_certificate_serial_number, generation);
+ goto done;
+ }
+
+ if (!check_allowed_time_encoding(X509_get_notBefore(x)) ||
+ !check_allowed_time_encoding(X509_get_notAfter(x))) {
+ log_validation_status(rc, uri, nonconformant_asn1_time_value, generation);
+ goto done;
+ }
+
+ /*
+ * Apparently nothing ever looks at these fields, so there are no
+ * API functions for them. We wouldn't bother either if they
+ * weren't forbidden by the RPKI certificate profile.
+ */
+ if (!x->cert_info || x->cert_info->issuerUID || x->cert_info->subjectUID) {
+ log_validation_status(rc, uri, nonconformant_certificate_uid, generation);
+ goto done;
+ }
+
+ /*
+ * Keep track of allowed extensions we've seen. Once we've
+ * processed all the ones we expect, anything left is an error.
+ */
+ ex_count = X509_get_ext_count(x);
+
+ /*
+ * We don't use X509_check_ca() to set certinfo->ca anymore, because
+ * it's not paranoid enough to enforce the RPKI certificate profile,
+ * but we still call it because we need it (or something) to invoke
+ * x509v3_cache_extensions() for us.
+ */
+ (void) X509_check_ca(x);
+
+ if ((bc = X509_get_ext_d2i(x, NID_basic_constraints, &crit, NULL)) != NULL) {
+ ex_count--;
+ if (!crit || bc->ca <= 0 || bc->pathlen != NULL) {
+ log_validation_status(rc, uri, malformed_basic_constraints, generation);
+ goto done;
+ }
+ }
+
+ certinfo->ca = bc != NULL;
+
+ if (certinfo == &w->certinfo) {
+ certinfo->ta = 1;
+ if (!certinfo->ca) {
+ log_validation_status(rc, uri, malformed_trust_anchor, generation);
+ goto done;
+ }
+ }
+
+ if ((aia = X509_get_ext_d2i(x, NID_info_access, NULL, NULL)) != NULL) {
+ int n_caIssuers = 0;
+ ex_count--;
+ if (!extract_access_uri(rc, uri, generation, aia, NID_ad_ca_issuers,
+ &certinfo->aia, &n_caIssuers) ||
+ !certinfo->aia.s[0] ||
+ sk_ACCESS_DESCRIPTION_num(aia) != n_caIssuers) {
+ log_validation_status(rc, uri, malformed_aia_extension, generation);
+ goto done;
+ }
+ }
+
+ if (certinfo->ta && aia) {
+ log_validation_status(rc, uri, aia_extension_forbidden, generation);
+ goto done;
+ }
+
+ if (!certinfo->ta && !aia) {
+ log_validation_status(rc, uri, aia_extension_missing, generation);
+ goto done;
+ }
+
+ if ((eku = X509_get_ext_d2i(x, NID_ext_key_usage, &crit, NULL)) != NULL) {
+ ex_count--;
+ if (crit || certinfo->ca || !endswith(uri->s, ".cer") || sk_ASN1_OBJECT_num(eku) == 0) {
+ log_validation_status(rc, uri, inappropriate_eku_extension, generation);
+ goto done;
+ }
+ for (i = 0; i < sk_ASN1_OBJECT_num(eku); i++)
+ routercert |= OBJ_obj2nid(sk_ASN1_OBJECT_value(eku, i)) == NID_id_kp_bgpsec_router;
+ }
+
+ if ((sia = X509_get_ext_d2i(x, NID_sinfo_access, NULL, NULL)) != NULL) {
+ int got_caDirectory, got_rpkiManifest, got_signedObject;
+ int n_caDirectory = 0, n_rpkiManifest = 0, n_signedObject = 0;
+ ex_count--;
+ ok = (extract_access_uri(rc, uri, generation, sia, NID_caRepository,
+ &certinfo->sia, &n_caDirectory) &&
+ extract_access_uri(rc, uri, generation, sia, NID_ad_rpkiManifest,
+ &certinfo->manifest, &n_rpkiManifest) &&
+ extract_access_uri(rc, uri, generation, sia, NID_ad_signedObject,
+ &certinfo->signedobject, &n_signedObject));
+ got_caDirectory = certinfo->sia.s[0] != '\0';
+ got_rpkiManifest = certinfo->manifest.s[0] != '\0';
+ got_signedObject = certinfo->signedobject.s[0] != '\0';
+ ok &= sk_ACCESS_DESCRIPTION_num(sia) == n_caDirectory + n_rpkiManifest + n_signedObject;
+ if (certinfo->ca)
+ ok &= got_caDirectory && got_rpkiManifest && !got_signedObject;
+ else if (rc->allow_ee_without_signedObject)
+ ok &= !got_caDirectory && !got_rpkiManifest;
+ else
+ ok &= !got_caDirectory && !got_rpkiManifest && got_signedObject;
+ if (!ok) {
+ log_validation_status(rc, uri, malformed_sia_extension, generation);
+ goto done;
+ }
+ } else if (certinfo->ca || !rc->allow_ee_without_signedObject) {
+ log_validation_status(rc, uri, sia_extension_missing, generation);
+ goto done;
+ } else if (!routercert) {
+ log_validation_status(rc, uri, sia_extension_missing_from_ee, generation);
+ }
+
+ if (certinfo->signedobject.s[0] && strcmp(uri->s, certinfo->signedobject.s))
+ log_validation_status(rc, uri, bad_signed_object_uri, generation);
+
+ if ((crldp = X509_get_ext_d2i(x, NID_crl_distribution_points, NULL, NULL)) != NULL) {
+ ex_count--;
+ if (!extract_crldp_uri(rc, uri, generation, crldp, &certinfo->crldp))
+ goto done;
+ }
+
+ rctx.rc = rc;
+ rctx.subject = certinfo;
+
+ if (w->certs == NULL && (w->certs = walk_ctx_stack_certs(rc, wsk)) == NULL)
+ goto done;
+
+ if (X509_get_version(x) != 2) {
+ log_validation_status(rc, uri, wrong_object_version, generation);
+ goto done;
+ }
+
+ if (!x->cert_info || !x->cert_info->signature || !x->cert_info->signature->algorithm ||
+ OBJ_obj2nid(x->cert_info->signature->algorithm) != NID_sha256WithRSAEncryption) {
+ log_validation_status(rc, uri, nonconformant_signature_algorithm, generation);
+ goto done;
+ }
+
+ if (certinfo->sia.s[0] && certinfo->sia.s[strlen(certinfo->sia.s) - 1] != '/') {
+ log_validation_status(rc, uri, malformed_cadirectory_uri, generation);
+ goto done;
+ }
+
+ if (!w->certinfo.ta && strcmp(w->certinfo.uri.s, certinfo->aia.s))
+ log_validation_status(rc, uri, aia_doesnt_match_issuer, generation);
+
+ if (certinfo->ca && !certinfo->sia.s[0]) {
+ log_validation_status(rc, uri, sia_cadirectory_uri_missing, generation);
+ goto done;
+ }
+
+ if (certinfo->ca && !certinfo->manifest.s[0]) {
+ log_validation_status(rc, uri, sia_manifest_uri_missing, generation);
+ goto done;
+ }
+
+ if (certinfo->ca && !startswith(certinfo->manifest.s, certinfo->sia.s)) {
+ log_validation_status(rc, uri, manifest_carepository_mismatch, generation);
+ goto done;
+ }
+
+ if (x->skid) {
+ ex_count--;
+ } else {
+ log_validation_status(rc, uri, ski_extension_missing, generation);
+ goto done;
+ }
+
+ if (!check_allowed_dn(X509_get_subject_name(x))) {
+ log_validation_status(rc, uri, nonconformant_subject_name, generation);
+ if (!rc->allow_nonconformant_name)
+ goto done;
+ }
+
+ if (!check_allowed_dn(X509_get_issuer_name(x))) {
+ log_validation_status(rc, uri, nonconformant_issuer_name, generation);
+ if (!rc->allow_nonconformant_name)
+ goto done;
+ }
+
+ if ((policies = X509_get_ext_d2i(x, NID_certificate_policies, &crit, NULL)) != NULL) {
+ POLICYQUALINFO *qualifier = NULL;
+ POLICYINFO *policy = NULL;
+ ex_count--;
+ if (!crit || sk_POLICYINFO_num(policies) != 1 ||
+ (policy = sk_POLICYINFO_value(policies, 0)) == NULL ||
+ OBJ_obj2nid(policy->policyid) != NID_cp_ipAddr_asNumber ||
+ sk_POLICYQUALINFO_num(policy->qualifiers) > 1 ||
+ (sk_POLICYQUALINFO_num(policy->qualifiers) == 1 &&
+ ((qualifier = sk_POLICYQUALINFO_value(policy->qualifiers, 0)) == NULL ||
+ OBJ_obj2nid(qualifier->pqualid) != NID_id_qt_cps))) {
+ log_validation_status(rc, uri, bad_certificate_policy, generation);
+ goto done;
+ }
+ if (qualifier)
+ log_validation_status(rc, uri, policy_qualifier_cps, generation);
+ }
+
+ if (!X509_EXTENSION_get_critical(X509_get_ext(x, X509_get_ext_by_NID(x, NID_key_usage, -1))) ||
+ (x->ex_flags & EXFLAG_KUSAGE) == 0 ||
+ x->ex_kusage != (certinfo->ca ? KU_KEY_CERT_SIGN | KU_CRL_SIGN : KU_DIGITAL_SIGNATURE)) {
+ log_validation_status(rc, uri, bad_key_usage, generation);
+ goto done;
+ }
+ ex_count--;
+
+ if (x->rfc3779_addr) {
+ ex_count--;
+ if (routercert ||
+ (loc = X509_get_ext_by_NID(x, NID_sbgp_ipAddrBlock, -1)) < 0 ||
+ !X509_EXTENSION_get_critical(X509_get_ext(x, loc)) ||
+ !v3_addr_is_canonical(x->rfc3779_addr) ||
+ sk_IPAddressFamily_num(x->rfc3779_addr) == 0) {
+ log_validation_status(rc, uri, bad_ipaddrblocks, generation);
+ goto done;
+ }
+ for (i = 0; i < sk_IPAddressFamily_num(x->rfc3779_addr); i++) {
+ IPAddressFamily *f = sk_IPAddressFamily_value(x->rfc3779_addr, i);
+ afi = v3_addr_get_afi(f);
+ if (afi != IANA_AFI_IPV4 && afi != IANA_AFI_IPV6) {
+ log_validation_status(rc, uri, unknown_afi, generation);
+ goto done;
+ }
+ if (f->addressFamily->length != 2) {
+ log_validation_status(rc, uri, safi_not_allowed, generation);
+ goto done;
+ }
+ }
+ }
+
+ if (x->rfc3779_asid) {
+ ex_count--;
+ if ((loc = X509_get_ext_by_NID(x, NID_sbgp_autonomousSysNum, -1)) < 0 ||
+ !X509_EXTENSION_get_critical(X509_get_ext(x, loc)) ||
+ !v3_asid_is_canonical(x->rfc3779_asid) ||
+ x->rfc3779_asid->asnum == NULL ||
+ x->rfc3779_asid->rdi != NULL ||
+ (routercert && x->rfc3779_asid->asnum->type == ASIdentifierChoice_inherit)) {
+ log_validation_status(rc, uri, bad_asidentifiers, generation);
+ goto done;
+ }
+ }
+
+ if (!x->rfc3779_addr && !x->rfc3779_asid) {
+ log_validation_status(rc, uri, missing_resources, generation);
+ goto done;
+ }
+
+ subject_pkey = X509_get_pubkey(x);
+ ok = subject_pkey != NULL;
+ if (ok) {
+ ASN1_OBJECT *algorithm;
+
+ (void) X509_PUBKEY_get0_param(&algorithm, NULL, NULL, NULL, X509_get_X509_PUBKEY(x));
+
+ switch (OBJ_obj2nid(algorithm)) {
+
+ case NID_rsaEncryption:
+ ok = (EVP_PKEY_type(subject_pkey->type) == EVP_PKEY_RSA &&
+ BN_get_word(subject_pkey->pkey.rsa->e) == 65537);
+ if (!ok)
+ break;
+ if (!certinfo->ca && rc->allow_1024_bit_ee_key &&
+ BN_num_bits(subject_pkey->pkey.rsa->n) == 1024)
+ log_validation_status(rc, uri, ee_certificate_with_1024_bit_key, generation);
+ else
+ ok = BN_num_bits(subject_pkey->pkey.rsa->n) == 2048;
+ break;
+
+ case NID_X9_62_id_ecPublicKey:
+ ok = !certinfo->ca && routercert;
+ break;
+
+ default:
+ ok = 0;
+ }
+ }
+ if (!ok) {
+ log_validation_status(rc, uri, bad_public_key, generation);
+ goto done;
+ }
+
+ if (x->skid == NULL ||
+ (ski_pubkey = X509_get0_pubkey_bitstr(x)) == NULL ||
+ !EVP_Digest(ski_pubkey->data, ski_pubkey->length,
+ ski_hashbuf.h, &ski_hashlen, EVP_sha1(), NULL) ||
+ ski_hashlen != 20 ||
+ ski_hashlen != x->skid->length ||
+ memcmp(ski_hashbuf.h, x->skid->data, ski_hashlen)) {
+ log_validation_status(rc, uri, ski_public_key_mismatch, generation);
+ goto done;
+ }
+
+ if (x->akid) {
+ ex_count--;
+ if (!check_aki(rc, uri, w->cert, x->akid, generation))
+ goto done;
+ }
+
+ if (!x->akid && !certinfo->ta) {
+ log_validation_status(rc, uri, aki_extension_missing, generation);
+ goto done;
+ }
+
+ if ((issuer_pkey = X509_get_pubkey(w->cert)) == NULL || X509_verify(x, issuer_pkey) <= 0) {
+ log_validation_status(rc, uri, certificate_bad_signature, generation);
+ goto done;
+ }
+
+ if (certinfo->ta) {
+
+ if (certinfo->crldp.s[0]) {
+ log_validation_status(rc, uri, trust_anchor_with_crldp, generation);
+ goto done;
+ }
+
+ } else {
+
+ if (!certinfo->crldp.s[0]) {
+ log_validation_status(rc, uri, crldp_uri_missing, generation);
+ goto done;
+ }
+
+ if (!certinfo->ca && !startswith(certinfo->crldp.s, w->certinfo.sia.s)) {
+ log_validation_status(rc, uri, crldp_doesnt_match_issuer_sia, generation);
+ goto done;
+ }
+
+ if (w->crls == NULL && ((w->crls = sk_X509_CRL_new_null()) == NULL ||
+ !sk_X509_CRL_push(w->crls, NULL))) {
+ logmsg(rc, log_sys_err, "Internal allocation error setting up CRL for validation");
+ goto done;
+ }
+
+ assert(sk_X509_CRL_num(w->crls) == 1);
+ assert((w->crldp.s[0] == '\0') == (sk_X509_CRL_value(w->crls, 0) == NULL));
+
+ if (strcmp(w->crldp.s, certinfo->crldp.s)) {
+ X509_CRL *old_crl = sk_X509_CRL_value(w->crls, 0);
+ X509_CRL *new_crl = check_crl(rc, &certinfo->crldp, w->cert);
+
+ if (w->crldp.s[0])
+ log_validation_status(rc, uri, issuer_uses_multiple_crldp_values, generation);
+
+ if (new_crl == NULL) {
+ log_validation_status(rc, uri, bad_crl, generation);
+ goto done;
+ }
+
+ if (old_crl && new_crl && ASN1_INTEGER_cmp(old_crl->crl_number, new_crl->crl_number) < 0) {
+ log_validation_status(rc, uri, crldp_names_newer_crl, generation);
+ X509_CRL_free(old_crl);
+ old_crl = NULL;
+ }
+
+ if (old_crl == NULL) {
+ sk_X509_CRL_set(w->crls, 0, new_crl);
+ w->crldp = certinfo->crldp;
+ } else {
+ X509_CRL_free(new_crl);
+ }
+ }
+
+ assert(sk_X509_CRL_value(w->crls, 0));
+ flags |= X509_V_FLAG_CRL_CHECK;
+ X509_STORE_CTX_set0_crls(&rctx.ctx, w->crls);
+ }
+
+ if (ex_count > 0) {
+ log_validation_status(rc, uri, disallowed_x509v3_extension, generation);
+ goto done;
+ }
+
+ assert(w->certs != NULL);
+ X509_STORE_CTX_trusted_stack(&rctx.ctx, w->certs);
+ X509_STORE_CTX_set_verify_cb(&rctx.ctx, check_x509_cb);
+
+ X509_VERIFY_PARAM_set_flags(rctx.ctx.param, flags);
+
+ X509_VERIFY_PARAM_add0_policy(rctx.ctx.param, OBJ_nid2obj(NID_cp_ipAddr_asNumber));
+
+ if (X509_verify_cert(&rctx.ctx) <= 0) {
+ log_validation_status(rc, uri, certificate_failed_validation, generation);
+ goto done;
+ }
+
+ ret = 1;
+
+ done:
+ X509_STORE_CTX_cleanup(&rctx.ctx);
+ EVP_PKEY_free(issuer_pkey);
+ EVP_PKEY_free(subject_pkey);
+ BASIC_CONSTRAINTS_free(bc);
+ sk_ACCESS_DESCRIPTION_pop_free(sia, ACCESS_DESCRIPTION_free);
+ sk_ACCESS_DESCRIPTION_pop_free(aia, ACCESS_DESCRIPTION_free);
+ sk_DIST_POINT_pop_free(crldp, DIST_POINT_free);
+ sk_POLICYINFO_pop_free(policies, POLICYINFO_free);
+ sk_ASN1_OBJECT_pop_free(eku, ASN1_OBJECT_free);
+
+ return ret;
+}
+
+/**
+ * Extract one datum from a CMS_SignerInfo.
+ */
+static void *extract_si_datum(CMS_SignerInfo *si,
+ int *n,
+ const int optional,
+ const int nid,
+ const int asn1_type)
+{
+ int i = CMS_signed_get_attr_by_NID(si, nid, -1);
+ void *result = NULL;
+ X509_ATTRIBUTE *a;
+
+ assert(si && n);
+
+ if (i < 0 && optional)
+ return NULL;
+
+ if (i >= 0 &&
+ CMS_signed_get_attr_by_NID(si, nid, i) < 0 &&
+ (a = CMS_signed_get_attr(si, i)) != NULL &&
+ X509_ATTRIBUTE_count(a) == 1 &&
+ (result = X509_ATTRIBUTE_get0_data(a, 0, asn1_type, NULL)) != NULL)
+ --*n;
+ else
+ *n = -1;
+
+ return result;
+}
+
+/**
+ * Check a signed CMS object.
+ */
+static int check_cms(rcynic_ctx_t *rc,
+ STACK_OF(walk_ctx_t) *wsk,
+ const uri_t *uri,
+ path_t *path,
+ const path_t *prefix,
+ CMS_ContentInfo **pcms,
+ X509 **px,
+ certinfo_t *certinfo,
+ BIO *bio,
+ const unsigned char *hash,
+ const size_t hashlen,
+ const int expected_eContentType_nid,
+ const int require_inheritance,
+ const object_generation_t generation)
+{
+ STACK_OF(CMS_SignerInfo) *signer_infos = NULL;
+ CMS_ContentInfo *cms = NULL;
+ CMS_SignerInfo *si = NULL;
+ ASN1_OCTET_STRING *sid = NULL;
+ X509_NAME *si_issuer = NULL;
+ ASN1_INTEGER *si_serial = NULL;
+ STACK_OF(X509_CRL) *crls = NULL;
+ STACK_OF(X509) *certs = NULL;
+ X509_ALGOR *signature_alg = NULL, *digest_alg = NULL;
+ ASN1_OBJECT *oid = NULL;
+ hashbuf_t hashbuf;
+ X509 *x = NULL;
+ certinfo_t certinfo_;
+ int i, result = 0;
+
+ assert(rc && wsk && uri && path && prefix);
+
+ if (!certinfo)
+ certinfo = &certinfo_;
+
+ if (!uri_to_filename(rc, uri, path, prefix))
+ goto error;
+
+ if (hash)
+ cms = read_cms(path, &hashbuf);
+ else
+ cms = read_cms(path, NULL);
+
+ if (!cms)
+ goto error;
+
+ if (hash && (hashlen > sizeof(hashbuf.h) ||
+ memcmp(hashbuf.h, hash, hashlen))) {
+ log_validation_status(rc, uri, digest_mismatch, generation);
+ if (!rc->allow_digest_mismatch)
+ goto error;
+ }
+
+ if (OBJ_obj2nid(CMS_get0_eContentType(cms)) != expected_eContentType_nid) {
+ log_validation_status(rc, uri, bad_cms_econtenttype, generation);
+ goto error;
+ }
+
+ if (CMS_verify(cms, NULL, NULL, NULL, bio, CMS_NO_SIGNER_CERT_VERIFY) <= 0) {
+ log_validation_status(rc, uri, cms_validation_failure, generation);
+ goto error;
+ }
+
+ if ((crls = CMS_get1_crls(cms)) != NULL) {
+ log_validation_status(rc, uri, cms_includes_crls, generation);
+ goto error;
+ }
+
+ if ((signer_infos = CMS_get0_SignerInfos(cms)) == NULL ||
+ sk_CMS_SignerInfo_num(signer_infos) != 1 ||
+ (si = sk_CMS_SignerInfo_value(signer_infos, 0)) == NULL ||
+ !CMS_SignerInfo_get0_signer_id(si, &sid, &si_issuer, &si_serial) ||
+ sid == NULL || si_issuer != NULL || si_serial != NULL ||
+ CMS_unsigned_get_attr_count(si) != -1) {
+ log_validation_status(rc, uri, bad_cms_signer_infos, generation);
+ goto error;
+ }
+
+ CMS_SignerInfo_get0_algs(si, NULL, &x, &digest_alg, &signature_alg);
+
+ if (x == NULL) {
+ log_validation_status(rc, uri, cms_signer_missing, generation);
+ goto error;
+ }
+
+ if ((certs = CMS_get1_certs(cms)) == NULL ||
+ sk_X509_num(certs) != 1 ||
+ X509_cmp(x, sk_X509_value(certs, 0))) {
+ log_validation_status(rc, uri, bad_cms_signer, generation);
+ goto error;
+ }
+
+ X509_ALGOR_get0(&oid, NULL, NULL, signature_alg);
+ i = OBJ_obj2nid(oid);
+ if (i != NID_sha256WithRSAEncryption && i != NID_rsaEncryption) {
+ log_validation_status(rc, uri, wrong_cms_si_signature_algorithm, generation);
+ goto error;
+ }
+
+ X509_ALGOR_get0(&oid, NULL, NULL, digest_alg);
+ if (OBJ_obj2nid(oid) != NID_sha256) {
+ log_validation_status(rc, uri, wrong_cms_si_digest_algorithm, generation);
+ goto error;
+ }
+
+ i = CMS_signed_get_attr_count(si);
+
+ (void) extract_si_datum(si, &i, 1, NID_pkcs9_signingTime, V_ASN1_UTCTIME);
+ (void) extract_si_datum(si, &i, 1, NID_binary_signing_time, V_ASN1_INTEGER);
+ oid = extract_si_datum(si, &i, 0, NID_pkcs9_contentType, V_ASN1_OBJECT);
+ (void) extract_si_datum(si, &i, 0, NID_pkcs9_messageDigest, V_ASN1_OCTET_STRING);
+
+ if (i != 0) {
+ log_validation_status(rc, uri, bad_cms_si_signed_attributes, generation);
+ if (!rc->allow_wrong_cms_si_attributes)
+ goto error;
+ }
+
+ if (OBJ_obj2nid(oid) != expected_eContentType_nid) {
+ log_validation_status(rc, uri, bad_cms_si_contenttype, generation);
+ goto error;
+ }
+
+ if (CMS_SignerInfo_cert_cmp(si, x)) {
+ log_validation_status(rc, uri, cms_ski_mismatch, generation);
+ goto error;
+ }
+
+ if (!check_x509(rc, wsk, uri, x, certinfo, generation))
+ goto error;
+
+ if (require_inheritance && x->rfc3779_addr) {
+ for (i = 0; i < sk_IPAddressFamily_num(x->rfc3779_addr); i++) {
+ IPAddressFamily *f = sk_IPAddressFamily_value(x->rfc3779_addr, i);
+ if (f->ipAddressChoice->type != IPAddressChoice_inherit) {
+ log_validation_status(rc, uri, rfc3779_inheritance_required, generation);
+ goto error;
+ }
+ }
+ }
+
+ if (require_inheritance && x->rfc3779_asid && x->rfc3779_asid->asnum &&
+ x->rfc3779_asid->asnum->type != ASIdentifierChoice_inherit) {
+ log_validation_status(rc, uri, rfc3779_inheritance_required, generation);
+ goto error;
+ }
+
+ if (pcms) {
+ *pcms = cms;
+ cms = NULL;
+ }
+
+ if (px)
+ *px = x;
+
+ result = 1;
+
+ error:
+ CMS_ContentInfo_free(cms);
+ sk_X509_CRL_pop_free(crls, X509_CRL_free);
+ sk_X509_pop_free(certs, X509_free);
+
+ return result;
+}
+
+
+
+/**
+ * Load certificate, check against manifest, then run it through all
+ * the check_x509() tests.
+ */
+static X509 *check_cert_1(rcynic_ctx_t *rc,
+ STACK_OF(walk_ctx_t) *wsk,
+ const uri_t *uri,
+ path_t *path,
+ const path_t *prefix,
+ certinfo_t *certinfo,
+ const unsigned char *hash,
+ const size_t hashlen,
+ object_generation_t generation)
+{
+ hashbuf_t hashbuf;
+ X509 *x = NULL;
+
+ assert(uri && path && wsk && certinfo);
+
+ if (!uri_to_filename(rc, uri, path, prefix))
+ return NULL;
+
+ if (access(path->s, R_OK))
+ return NULL;
+
+ if (hash)
+ x = read_cert(path, &hashbuf);
+ else
+ x = read_cert(path, NULL);
+
+ if (!x) {
+ logmsg(rc, log_sys_err, "Can't read certificate %s", path->s);
+ goto punt;
+ }
+
+ if (hash && (hashlen > sizeof(hashbuf.h) ||
+ memcmp(hashbuf.h, hash, hashlen))) {
+ log_validation_status(rc, uri, digest_mismatch, generation);
+ if (!rc->allow_digest_mismatch)
+ goto punt;
+ }
+
+ if (check_x509(rc, wsk, uri, x, certinfo, generation))
+ return x;
+
+ punt:
+ X509_free(x);
+ return NULL;
+}
+
+/**
+ * Try to find a good copy of a certificate either in fresh data or in
+ * backup data from a previous run of this program.
+ */
+static X509 *check_cert(rcynic_ctx_t *rc,
+ STACK_OF(walk_ctx_t) *wsk,
+ uri_t *uri,
+ certinfo_t *certinfo,
+ const unsigned char *hash,
+ const size_t hashlen)
+{
+ walk_ctx_t *w = walk_ctx_stack_head(wsk);
+ object_generation_t generation;
+ const path_t *prefix = NULL;
+ path_t path;
+ X509 *x;
+
+ assert(rc && uri && wsk && w && certinfo);
+
+ switch (w->state) {
+ case walk_state_current:
+ prefix = &rc->unauthenticated;
+ generation = object_generation_current;
+ break;
+ case walk_state_backup:
+ prefix = &rc->old_authenticated;
+ generation = object_generation_backup;
+ break;
+ default:
+ return NULL;
+ }
+
+ if (skip_checking_this_object(rc, uri, generation))
+ return NULL;
+
+ if ((x = check_cert_1(rc, wsk, uri, &path, prefix, certinfo,
+ hash, hashlen, generation)) != NULL)
+ install_object(rc, uri, &path, generation);
+ else if (!access(path.s, F_OK))
+ log_validation_status(rc, uri, object_rejected, generation);
+ else if (hash && generation == w->manifest_generation)
+ log_validation_status(rc, uri, manifest_lists_missing_object, generation);
+
+ return x;
+}
+
+
+
+/**
+ * Read and check one manifest from disk.
+ */
+static Manifest *check_manifest_1(rcynic_ctx_t *rc,
+ STACK_OF(walk_ctx_t) *wsk,
+ const uri_t *uri,
+ path_t *path,
+ const path_t *prefix,
+ certinfo_t *certinfo,
+ const object_generation_t generation)
+{
+ STACK_OF(FileAndHash) *sorted_fileList = NULL;
+ Manifest *manifest = NULL, *result = NULL;
+ CMS_ContentInfo *cms = NULL;
+ FileAndHash *fah = NULL, *fah2 = NULL;
+ BIO *bio = NULL;
+ X509 *x;
+ int i;
+
+ assert(rc && wsk && uri && path && prefix);
+
+ if ((bio = BIO_new(BIO_s_mem())) == NULL) {
+ logmsg(rc, log_sys_err, "Couldn't allocate BIO for manifest %s", uri->s);
+ goto done;
+ }
+
+ if (!check_cms(rc, wsk, uri, path, prefix, &cms, &x, certinfo, bio, NULL, 0,
+ NID_ct_rpkiManifest, 1, generation))
+ goto done;
+
+ if ((manifest = ASN1_item_d2i_bio(ASN1_ITEM_rptr(Manifest), bio, NULL)) == NULL) {
+ log_validation_status(rc, uri, cms_econtent_decode_error, generation);
+ goto done;
+ }
+
+ if (manifest->version) {
+ log_validation_status(rc, uri, wrong_object_version, generation);
+ goto done;
+ }
+
+ if (X509_cmp_current_time(manifest->thisUpdate) > 0) {
+ log_validation_status(rc, uri, manifest_not_yet_valid, generation);
+ goto done;
+ }
+
+ if (X509_cmp_current_time(manifest->nextUpdate) < 0) {
+ log_validation_status(rc, uri, stale_crl_or_manifest, generation);
+ if (!rc->allow_stale_manifest)
+ goto done;
+ }
+
+ if (asn1_time_cmp(manifest->thisUpdate, X509_get_notBefore(x)) < 0 ||
+ asn1_time_cmp(manifest->nextUpdate, X509_get_notAfter(x)) > 0) {
+ log_validation_status(rc, uri, manifest_interval_overruns_cert, generation);
+ goto done;
+ }
+
+ if (ASN1_INTEGER_cmp(manifest->manifestNumber, asn1_zero) < 0 ||
+ ASN1_INTEGER_cmp(manifest->manifestNumber, asn1_twenty_octets) > 0) {
+ log_validation_status(rc, uri, bad_manifest_number, generation);
+ goto done;
+ }
+
+ if (OBJ_obj2nid(manifest->fileHashAlg) != NID_sha256) {
+ log_validation_status(rc, uri, nonconformant_digest_algorithm, generation);
+ goto done;
+ }
+
+ if ((sorted_fileList = sk_FileAndHash_dup(manifest->fileList)) == NULL) {
+ logmsg(rc, log_sys_err, "Couldn't allocate shallow copy of fileList for manifest %s", uri->s);
+ goto done;
+ }
+
+ (void) sk_FileAndHash_set_cmp_func(sorted_fileList, FileAndHash_name_cmp);
+ sk_FileAndHash_sort(sorted_fileList);
+
+ for (i = 0; (fah = sk_FileAndHash_value(sorted_fileList, i)) != NULL && (fah2 = sk_FileAndHash_value(sorted_fileList, i + 1)) != NULL; i++) {
+ if (!strcmp((char *) fah->file->data, (char *) fah2->file->data)) {
+ log_validation_status(rc, uri, duplicate_name_in_manifest, generation);
+ goto done;
+ }
+ }
+
+ for (i = 0; (fah = sk_FileAndHash_value(manifest->fileList, i)) != NULL; i++) {
+ if (fah->hash->length != HASH_SHA256_LEN ||
+ (fah->hash->flags & (ASN1_STRING_FLAG_BITS_LEFT | 7)) > ASN1_STRING_FLAG_BITS_LEFT) {
+ log_validation_status(rc, uri, bad_manifest_digest_length, generation);
+ goto done;
+ }
+ }
+
+ result = manifest;
+ manifest = NULL;
+
+ done:
+ BIO_free(bio);
+ Manifest_free(manifest);
+ CMS_ContentInfo_free(cms);
+ sk_FileAndHash_free(sorted_fileList);
+ return result;
+}
+
+/**
+ * Check whether we already have a particular manifest, attempt to fetch it
+ * and check issuer's signature if we don't.
+ *
+ * General plan here is to do basic checks on both current and backup
+ * generation manifests, then, if both generations pass all of our
+ * other tests, pick the generation with the highest manifest number,
+ * to protect against replay attacks.
+ *
+ * Once we've picked the manifest we're going to use, we need to check
+ * it against the CRL we've chosen. Not much we can do if they don't
+ * match besides whine about it, but we do need to whine in this case.
+ */
+static int check_manifest(rcynic_ctx_t *rc,
+ STACK_OF(walk_ctx_t) *wsk)
+{
+ walk_ctx_t *w = walk_ctx_stack_head(wsk);
+ Manifest *old_manifest, *new_manifest, *result = NULL;
+ certinfo_t old_certinfo, new_certinfo;
+ const uri_t *uri, *crldp = NULL;
+ object_generation_t generation = object_generation_null;
+ path_t old_path, new_path;
+ FileAndHash *fah = NULL;
+ const char *crl_tail;
+ int i, ok = 1;
+
+ assert(rc && wsk && w && !w->manifest);
+
+ uri = &w->certinfo.manifest;
+
+ logmsg(rc, log_telemetry, "Checking manifest %s", uri->s);
+
+ new_manifest = check_manifest_1(rc, wsk, uri, &new_path,
+ &rc->unauthenticated, &new_certinfo,
+ object_generation_current);
+
+ old_manifest = check_manifest_1(rc, wsk, uri, &old_path,
+ &rc->old_authenticated, &old_certinfo,
+ object_generation_backup);
+
+ if (!new_manifest)
+ result = old_manifest;
+
+ else if (!old_manifest)
+ result = new_manifest;
+
+ else {
+ int num_cmp = ASN1_INTEGER_cmp(old_manifest->manifestNumber, new_manifest->manifestNumber);
+ int date_cmp = ASN1_STRING_cmp(old_manifest->thisUpdate, new_manifest->thisUpdate);
+
+ if (num_cmp > 0)
+ log_validation_status(rc, uri, backup_number_higher_than_current, object_generation_current);
+ if (date_cmp > 0)
+ log_validation_status(rc, uri, backup_thisupdate_newer_than_current, object_generation_current);
+
+ if (num_cmp > 0 && date_cmp > 0)
+ result = old_manifest;
+ else
+ result = new_manifest;
+ }
+
+ if (result && result == new_manifest) {
+ generation = object_generation_current;
+ install_object(rc, uri, &new_path, generation);
+ crldp = &new_certinfo.crldp;
+ }
+
+ if (result && result == old_manifest) {
+ generation = object_generation_backup;
+ install_object(rc, uri, &old_path, generation);
+ crldp = &old_certinfo.crldp;
+ }
+
+ if (result) {
+ crl_tail = strrchr(crldp->s, '/');
+ assert(crl_tail != NULL);
+ crl_tail++;
+
+ for (i = 0; (fah = sk_FileAndHash_value(result->fileList, i)) != NULL; i++)
+ if (!strcmp((char *) fah->file->data, crl_tail))
+ break;
+
+ if (!fah) {
+ log_validation_status(rc, uri, crl_not_in_manifest, generation);
+ if (rc->require_crl_in_manifest)
+ ok = 0;
+ }
+
+ else if (!check_crl_digest(rc, crldp, fah->hash->data, fah->hash->length)) {
+ log_validation_status(rc, uri, digest_mismatch, generation);
+ if (!rc->allow_crl_digest_mismatch)
+ ok = 0;
+ }
+ }
+
+ if ((!result || result != new_manifest) && !access(new_path.s, F_OK))
+ log_validation_status(rc, uri, object_rejected, object_generation_current);
+
+ if (!result && !access(old_path.s, F_OK))
+ log_validation_status(rc, uri, object_rejected, object_generation_backup);
+
+ if (result != new_manifest)
+ Manifest_free(new_manifest);
+
+ if (result != old_manifest)
+ Manifest_free(old_manifest);
+
+ w->manifest = result;
+ if (crldp)
+ w->crldp = *crldp;
+ w->manifest_generation = generation;
+
+ return ok;
+}
+
+
+
+/**
+ * Mark CRL or manifest that we're rechecking so XML report makes more sense.
+ */
+static void rsync_needed_mark_recheck(rcynic_ctx_t *rc,
+ const uri_t *uri)
+{
+ validation_status_t *v = NULL;
+
+ if (uri->s[0] != '\0')
+ v = validation_status_find(rc->validation_status_root,
+ uri, object_generation_current);
+
+ if (v) {
+ validation_status_set_code(v, stale_crl_or_manifest, 0);
+ log_validation_status(rc, uri, rechecking_object,
+ object_generation_current);
+ }
+}
+
+/**
+ * Check whether we need to rsync a particular tree. This depends on
+ * the setting of rc->rsync_early, whether we have a valid manifest on
+ * file, and whether that manifest is stale yet.
+ */
+static int rsync_needed(rcynic_ctx_t *rc,
+ STACK_OF(walk_ctx_t) *wsk)
+{
+ walk_ctx_t *w = walk_ctx_stack_head(wsk);
+ int needed;
+
+ assert(rc && wsk && w);
+
+ needed = (rc->rsync_early ||
+ !check_manifest(rc, wsk) ||
+ w->manifest == NULL ||
+ X509_cmp_current_time(w->manifest->nextUpdate) < 0);
+
+ if (needed && w->manifest != NULL) {
+ rsync_needed_mark_recheck(rc, &w->certinfo.manifest);
+ rsync_needed_mark_recheck(rc, &w->certinfo.crldp);
+ Manifest_free(w->manifest);
+ w->manifest = NULL;
+ }
+
+ return needed;
+}
+
+
+
+/**
+ * Extract a ROA prefix from the ASN.1 bitstring encoding.
+ */
+static int extract_roa_prefix(const ROAIPAddress *ra,
+ const unsigned afi,
+ unsigned char *addr,
+ unsigned *prefixlen,
+ unsigned *max_prefixlen)
+{
+ unsigned length;
+ long maxlen;
+
+ assert(ra && addr && prefixlen && max_prefixlen);
+
+ maxlen = ASN1_INTEGER_get(ra->maxLength);
+
+ switch (afi) {
+ case IANA_AFI_IPV4: length = 4; break;
+ case IANA_AFI_IPV6: length = 16; break;
+ default: return 0;
+ }
+
+ if (ra->IPAddress->length < 0 || ra->IPAddress->length > length ||
+ maxlen < 0 || maxlen > (long) length * 8)
+ return 0;
+
+ if (ra->IPAddress->length > 0) {
+ memcpy(addr, ra->IPAddress->data, ra->IPAddress->length);
+ if ((ra->IPAddress->flags & 7) != 0) {
+ unsigned char mask = 0xFF >> (8 - (ra->IPAddress->flags & 7));
+ addr[ra->IPAddress->length - 1] &= ~mask;
+ }
+ }
+
+ memset(addr + ra->IPAddress->length, 0, length - ra->IPAddress->length);
+ *prefixlen = (ra->IPAddress->length * 8) - (ra->IPAddress->flags & 7);
+ *max_prefixlen = ra->maxLength ? (unsigned) maxlen : *prefixlen;
+
+ return 1;
+}
+
+/**
+ * Read and check one ROA from disk.
+ */
+static int check_roa_1(rcynic_ctx_t *rc,
+ STACK_OF(walk_ctx_t) *wsk,
+ const uri_t *uri,
+ path_t *path,
+ const path_t *prefix,
+ const unsigned char *hash,
+ const size_t hashlen,
+ const object_generation_t generation)
+{
+ STACK_OF(IPAddressFamily) *roa_resources = NULL, *ee_resources = NULL;
+ unsigned char addrbuf[ADDR_RAW_BUF_LEN];
+ CMS_ContentInfo *cms = NULL;
+ BIO *bio = NULL;
+ ROA *roa = NULL;
+ X509 *x = NULL;
+ int i, j, result = 0;
+ unsigned afi, *safi = NULL, safi_, prefixlen, max_prefixlen;
+ ROAIPAddressFamily *rf;
+ ROAIPAddress *ra;
+
+ assert(rc && wsk && uri && path && prefix);
+
+ if ((bio = BIO_new(BIO_s_mem())) == NULL) {
+ logmsg(rc, log_sys_err, "Couldn't allocate BIO for ROA %s", uri->s);
+ goto error;
+ }
+
+ if (!check_cms(rc, wsk, uri, path, prefix, &cms, &x, NULL, bio, NULL, 0,
+ NID_ct_ROA, 0, generation))
+ goto error;
+
+ if (!(roa = ASN1_item_d2i_bio(ASN1_ITEM_rptr(ROA), bio, NULL))) {
+ log_validation_status(rc, uri, cms_econtent_decode_error, generation);
+ goto error;
+ }
+
+ if (roa->version) {
+ log_validation_status(rc, uri, wrong_object_version, generation);
+ goto error;
+ }
+
+ if (ASN1_INTEGER_cmp(roa->asID, asn1_zero) < 0 ||
+ ASN1_INTEGER_cmp(roa->asID, asn1_four_octets) > 0) {
+ log_validation_status(rc, uri, bad_roa_asID, generation);
+ goto error;
+ }
+
+ ee_resources = X509_get_ext_d2i(x, NID_sbgp_ipAddrBlock, NULL, NULL);
+
+ /*
+ * Extract prefixes from ROA and convert them into a resource set.
+ */
+
+ if (!(roa_resources = sk_IPAddressFamily_new_null()))
+ goto error;
+
+ for (i = 0; i < sk_ROAIPAddressFamily_num(roa->ipAddrBlocks); i++) {
+ rf = sk_ROAIPAddressFamily_value(roa->ipAddrBlocks, i);
+ if (!rf || !rf->addressFamily || rf->addressFamily->length < 2 || rf->addressFamily->length > 3) {
+ log_validation_status(rc, uri, malformed_roa_addressfamily, generation);
+ goto error;
+ }
+ afi = (rf->addressFamily->data[0] << 8) | (rf->addressFamily->data[1]);
+ if (rf->addressFamily->length == 3)
+ *(safi = &safi_) = rf->addressFamily->data[2];
+ for (j = 0; j < sk_ROAIPAddress_num(rf->addresses); j++) {
+ ra = sk_ROAIPAddress_value(rf->addresses, j);
+ if (!ra ||
+ !extract_roa_prefix(ra, afi, addrbuf, &prefixlen, &max_prefixlen) ||
+ !v3_addr_add_prefix(roa_resources, afi, safi, addrbuf, prefixlen)) {
+ log_validation_status(rc, uri, roa_resources_malformed, generation);
+ goto error;
+ }
+ if (max_prefixlen < prefixlen) {
+ log_validation_status(rc, uri, roa_max_prefixlen_too_short, generation);
+ goto error;
+ }
+ }
+ }
+
+ /*
+ * ROAs can include nested prefixes, so direct translation to
+ * resource sets could include overlapping ranges, which is illegal.
+ * So we have to remove nested stuff before whacking into canonical
+ * form. Fortunately, this is relatively easy, since we know these
+ * are just prefixes, not ranges: in a list of prefixes sorted by
+ * the RFC 3779 rules, the first element of a set of nested prefixes
+ * will always be the least specific.
+ */
+
+ for (i = 0; i < sk_IPAddressFamily_num(roa_resources); i++) {
+ IPAddressFamily *f = sk_IPAddressFamily_value(roa_resources, i);
+
+ if ((afi = v3_addr_get_afi(f)) == 0) {
+ log_validation_status(rc, uri, roa_contains_bad_afi_value, generation);
+ goto error;
+ }
+
+ if (f->ipAddressChoice->type == IPAddressChoice_addressesOrRanges) {
+ IPAddressOrRanges *aors = f->ipAddressChoice->u.addressesOrRanges;
+
+ sk_IPAddressOrRange_sort(aors);
+
+ for (j = 0; j < sk_IPAddressOrRange_num(aors) - 1; j++) {
+ IPAddressOrRange *a = sk_IPAddressOrRange_value(aors, j);
+ IPAddressOrRange *b = sk_IPAddressOrRange_value(aors, j + 1);
+ unsigned char a_min[ADDR_RAW_BUF_LEN], a_max[ADDR_RAW_BUF_LEN];
+ unsigned char b_min[ADDR_RAW_BUF_LEN], b_max[ADDR_RAW_BUF_LEN];
+ int length;
+
+ if ((length = v3_addr_get_range(a, afi, a_min, a_max, ADDR_RAW_BUF_LEN)) == 0 ||
+ (length = v3_addr_get_range(b, afi, b_min, b_max, ADDR_RAW_BUF_LEN)) == 0) {
+ log_validation_status(rc, uri, roa_resources_malformed, generation);
+ goto error;
+ }
+
+ if (memcmp(a_max, b_max, length) >= 0) {
+ (void) sk_IPAddressOrRange_delete(aors, j + 1);
+ IPAddressOrRange_free(b);
+ --j;
+ }
+ }
+ }
+ }
+
+ if (!v3_addr_canonize(roa_resources)) {
+ log_validation_status(rc, uri, roa_resources_malformed, generation);
+ goto error;
+ }
+
+ if (!v3_addr_subset(roa_resources, ee_resources)) {
+ log_validation_status(rc, uri, roa_resource_not_in_ee, generation);
+ goto error;
+ }
+
+ result = 1;
+
+ error:
+ BIO_free(bio);
+ ROA_free(roa);
+ CMS_ContentInfo_free(cms);
+ sk_IPAddressFamily_pop_free(roa_resources, IPAddressFamily_free);
+ sk_IPAddressFamily_pop_free(ee_resources, IPAddressFamily_free);
+
+ return result;
+}
+
+/**
+ * Check whether we already have a particular ROA, attempt to fetch it
+ * and check issuer's signature if we don't.
+ */
+static void check_roa(rcynic_ctx_t *rc,
+ STACK_OF(walk_ctx_t) *wsk,
+ const uri_t *uri,
+ const unsigned char *hash,
+ const size_t hashlen)
+{
+ walk_ctx_t *w = walk_ctx_stack_head(wsk);
+ path_t path;
+
+ assert(rc && wsk && w && uri);
+
+ if (uri_to_filename(rc, uri, &path, &rc->new_authenticated) &&
+ !access(path.s, F_OK))
+ return;
+
+ logmsg(rc, log_telemetry, "Checking ROA %s", uri->s);
+
+ if (check_roa_1(rc, wsk, uri, &path, &rc->unauthenticated,
+ hash, hashlen, object_generation_current)) {
+ install_object(rc, uri, &path, object_generation_current);
+ return;
+ }
+
+ if (!access(path.s, F_OK))
+ log_validation_status(rc, uri, object_rejected, object_generation_current);
+ else if (hash)
+ log_validation_status(rc, uri, manifest_lists_missing_object, object_generation_current);
+
+ if (check_roa_1(rc, wsk, uri, &path, &rc->old_authenticated,
+ hash, hashlen, object_generation_backup)) {
+ install_object(rc, uri, &path, object_generation_backup);
+ return;
+ }
+
+ if (!access(path.s, F_OK))
+ log_validation_status(rc, uri, object_rejected, object_generation_backup);
+ else if (hash && w->manifest_generation == object_generation_backup)
+ log_validation_status(rc, uri, manifest_lists_missing_object, object_generation_backup);
+}
+
+
+
+/**
+ * Read and check one Ghostbuster record from disk.
+ */
+static int check_ghostbuster_1(rcynic_ctx_t *rc,
+ STACK_OF(walk_ctx_t) *wsk,
+ const uri_t *uri,
+ path_t *path,
+ const path_t *prefix,
+ const unsigned char *hash,
+ const size_t hashlen,
+ const object_generation_t generation)
+{
+ CMS_ContentInfo *cms = NULL;
+ BIO *bio = NULL;
+ X509 *x;
+ int result = 0;
+
+ assert(rc && wsk && uri && path && prefix);
+
+#if 0
+ /*
+ * May want this later if we're going to inspect the VCard. For now,
+ * just leave this NULL and the right thing should happen.
+ */
+ if ((bio = BIO_new(BIO_s_mem())) == NULL) {
+ logmsg(rc, log_sys_err, "Couldn't allocate BIO for Ghostbuster record %s", uri->s);
+ goto error;
+ }
+#endif
+
+ if (!check_cms(rc, wsk, uri, path, prefix, &cms, &x, NULL, bio, NULL, 0,
+ NID_ct_rpkiGhostbusters, 1, generation))
+ goto error;
+
+#if 0
+ /*
+ * Here is where we would read the VCard from the bio returned by
+ * CMS_verify() so that we could check the VCard.
+ */
+#endif
+
+ result = 1;
+
+ error:
+ BIO_free(bio);
+ CMS_ContentInfo_free(cms);
+
+ return result;
+}
+
+/**
+ * Check whether we already have a particular Ghostbuster record,
+ * attempt to fetch it and check issuer's signature if we don't.
+ */
+static void check_ghostbuster(rcynic_ctx_t *rc,
+ STACK_OF(walk_ctx_t) *wsk,
+ const uri_t *uri,
+ const unsigned char *hash,
+ const size_t hashlen)
+{
+ walk_ctx_t *w = walk_ctx_stack_head(wsk);
+ path_t path;
+
+ assert(rc && wsk && w && uri);
+
+ if (uri_to_filename(rc, uri, &path, &rc->new_authenticated) &&
+ !access(path.s, F_OK))
+ return;
+
+ logmsg(rc, log_telemetry, "Checking Ghostbuster record %s", uri->s);
+
+ if (check_ghostbuster_1(rc, wsk, uri, &path, &rc->unauthenticated,
+ hash, hashlen, object_generation_current)) {
+ install_object(rc, uri, &path, object_generation_current);
+ return;
+ }
+
+ if (!access(path.s, F_OK))
+ log_validation_status(rc, uri, object_rejected, object_generation_current);
+ else if (hash)
+ log_validation_status(rc, uri, manifest_lists_missing_object, object_generation_current);
+
+ if (check_ghostbuster_1(rc, wsk, uri, &path, &rc->old_authenticated,
+ hash, hashlen, object_generation_backup)) {
+ install_object(rc, uri, &path, object_generation_backup);
+ return;
+ }
+
+ if (!access(path.s, F_OK))
+ log_validation_status(rc, uri, object_rejected, object_generation_backup);
+ else if (hash && w->manifest_generation == object_generation_backup)
+ log_validation_status(rc, uri, manifest_lists_missing_object, object_generation_backup);
+}
+
+
+
+static void walk_cert(rcynic_ctx_t *, void *);
+
+/**
+ * rsync callback for fetching SIA tree.
+ */
+static void rsync_sia_callback(rcynic_ctx_t *rc,
+ const rsync_ctx_t *ctx,
+ const rsync_status_t status,
+ const uri_t *uri,
+ void *cookie)
+{
+ STACK_OF(walk_ctx_t) *wsk = cookie;
+ walk_ctx_t *w = walk_ctx_stack_head(wsk);
+
+ assert(rc && wsk);
+
+ if (status != rsync_status_pending) {
+ w->state++;
+ task_add(rc, walk_cert, wsk);
+ return;
+ }
+
+ if (rsync_count_runable(rc) >= rc->max_parallel_fetches)
+ return;
+
+ if ((wsk = walk_ctx_stack_clone(wsk)) == NULL) {
+ logmsg(rc, log_sys_err,
+ "walk_ctx_stack_clone() failed, probably memory exhaustion, blundering onwards without forking stack");
+ return;
+ }
+
+ walk_ctx_stack_pop(wsk);
+ task_add(rc, walk_cert, wsk);
+}
+
+/**
+ * Recursive walk of certificate hierarchy (core of the program).
+ *
+ * Walk all products of the current certificate, starting with the
+ * ones named in the manifest and continuing with any that we find in
+ * the publication directory but which are not named in the manifest.
+ *
+ * Dispatch to correct checking code for the object named by URI,
+ * based on the filename extension in the uri. CRLs are a special
+ * case because we've already checked them by the time we get here, so
+ * we just ignore them. Other objects are either certificates or
+ * CMS-signed objects of one kind or another.
+ */
+static void walk_cert(rcynic_ctx_t *rc, void *cookie)
+{
+ STACK_OF(walk_ctx_t) *wsk = cookie;
+ const unsigned char *hash = NULL;
+ object_generation_t generation;
+ size_t hashlen;
+ walk_ctx_t *w;
+ uri_t uri;
+
+ assert(rc && wsk);
+
+ while ((w = walk_ctx_stack_head(wsk)) != NULL) {
+
+ switch (w->state) {
+ case walk_state_current:
+ generation = object_generation_current;
+ break;
+ case walk_state_backup:
+ generation = object_generation_backup;
+ break;
+ default:
+ generation = object_generation_null;
+ break;
+ }
+
+ switch (w->state) {
+
+ case walk_state_initial:
+
+ if (!w->certinfo.sia.s[0] || !w->certinfo.ca) {
+ w->state = walk_state_done;
+ continue;
+ }
+
+ if (!w->certinfo.manifest.s[0]) {
+ log_validation_status(rc, &w->certinfo.uri, sia_manifest_uri_missing, w->certinfo.generation);
+ w->state = walk_state_done;
+ continue;
+ }
+
+ w->state++;
+ continue;
+
+ case walk_state_rsync:
+
+ if (rsync_needed(rc, wsk)) {
+ rsync_tree(rc, &w->certinfo.sia, wsk, rsync_sia_callback);
+ return;
+ }
+ log_validation_status(rc, &w->certinfo.sia, rsync_transfer_skipped, object_generation_null);
+ w->state++;
+ continue;
+
+ case walk_state_ready:
+
+ walk_ctx_loop_init(rc, wsk); /* sets w->state */
+ continue;
+
+ case walk_state_current:
+ case walk_state_backup:
+
+ if (!walk_ctx_loop_this(rc, wsk, &uri, &hash, &hashlen)) {
+ walk_ctx_loop_next(rc, wsk);
+ continue;
+ }
+
+ if (endswith(uri.s, ".crl") || endswith(uri.s, ".mft") || endswith(uri.s, ".mnf")) {
+ walk_ctx_loop_next(rc, wsk);
+ continue; /* CRLs and manifests checked elsewhere */
+ }
+
+ if (hash == NULL && !rc->allow_object_not_in_manifest) {
+ log_validation_status(rc, &uri, skipped_because_not_in_manifest, generation);
+ walk_ctx_loop_next(rc, wsk);
+ continue;
+ }
+
+ if (hash == NULL)
+ log_validation_status(rc, &uri, tainted_by_not_being_in_manifest, generation);
+ else if (w->stale_manifest)
+ log_validation_status(rc, &uri, tainted_by_stale_manifest, generation);
+
+ if (endswith(uri.s, ".roa")) {
+ check_roa(rc, wsk, &uri, hash, hashlen);
+ walk_ctx_loop_next(rc, wsk);
+ continue;
+ }
+
+ if (endswith(uri.s, ".gbr")) {
+ check_ghostbuster(rc, wsk, &uri, hash, hashlen);
+ walk_ctx_loop_next(rc, wsk);
+ continue;
+ }
+
+ if (endswith(uri.s, ".cer")) {
+ certinfo_t certinfo;
+ X509 *x = check_cert(rc, wsk, &uri, &certinfo, hash, hashlen);
+ if (!walk_ctx_stack_push(wsk, x, &certinfo))
+ walk_ctx_loop_next(rc, wsk);
+ continue;
+ }
+
+ log_validation_status(rc, &uri, unknown_object_type_skipped, object_generation_null);
+ walk_ctx_loop_next(rc, wsk);
+ continue;
+
+ case walk_state_done:
+
+ walk_ctx_stack_pop(wsk); /* Resume our issuer's state */
+ continue;
+
+ }
+ }
+
+ assert(walk_ctx_stack_head(wsk) == NULL);
+ walk_ctx_stack_free(wsk);
+}
+
+/**
+ * Check a trust anchor. Yes, we trust it, by definition, but it
+ * still needs to conform to the certificate profile, the
+ * self-signature must be correct, etcetera.
+ *
+ * Ownership of the TA certificate object passes to this function when
+ * called (ie, freeing "x" is our responsibility).
+ */
+static int check_ta(rcynic_ctx_t *rc, X509 *x, const uri_t *uri,
+ const path_t *path1, const path_t *path2,
+ const object_generation_t generation)
+{
+ STACK_OF(walk_ctx_t) *wsk = NULL;
+ walk_ctx_t *w = NULL;
+
+ assert(rc && x && uri && path1 && path2);
+
+ if (x == NULL)
+ return 1;
+
+ if ((wsk = walk_ctx_stack_new()) == NULL) {
+ logmsg(rc, log_sys_err, "Couldn't allocate walk context stack");
+ X509_free(x);
+ return 0;
+ }
+
+ if ((w = walk_ctx_stack_push(wsk, x, NULL)) == NULL) {
+ logmsg(rc, log_sys_err, "Couldn't push walk context stack");
+ walk_ctx_stack_free(wsk);
+ X509_free(x);
+ return 0;
+ }
+
+ if (!check_x509(rc, wsk, uri, x, NULL, generation)) {
+ log_validation_status(rc, uri, object_rejected, generation);
+ walk_ctx_stack_free(wsk);
+ return 1;
+ }
+
+ logmsg(rc, log_telemetry, "Copying trust anchor %s to %s", path1->s, path2->s);
+
+ if (!mkdir_maybe(rc, path2) || !cp_ln(rc, path1, path2)) {
+ walk_ctx_stack_free(wsk);
+ return 0;
+ }
+
+ log_validation_status(rc, uri, object_accepted, generation);
+ task_add(rc, walk_cert, wsk);
+ return 1;
+}
+
+
+
+/**
+ * Check a trust anchor read from a local file.
+ */
+static int check_ta_cer(rcynic_ctx_t *rc,
+ const char *fn)
+
+{
+ path_t path1, path2;
+ unsigned long hash;
+ X509 *x = NULL;
+ uri_t uri;
+ int i;
+
+ assert(rc && fn);
+
+ logmsg(rc, log_telemetry, "Processing trust anchor from file %s", fn);
+
+ if (strlen(fn) >= sizeof(path1.s)) {
+ logmsg(rc, log_usage_err, "Trust anchor path name too long %s", fn);
+ return 0;
+ }
+ strcpy(path1.s, fn);
+ filename_to_uri(&uri, path1.s);
+
+ if ((x = read_cert(&path1, NULL)) == NULL) {
+ logmsg(rc, log_usage_err, "Couldn't read trust anchor from file %s", fn);
+ log_validation_status(rc, &uri, unreadable_trust_anchor, object_generation_null);
+ goto lose;
+ }
+
+ hash = X509_subject_name_hash(x);
+
+ for (i = 0; i < INT_MAX; i++) {
+ if (snprintf(path2.s, sizeof(path2.s), "%s%lx.%d.cer",
+ rc->new_authenticated.s, hash, i) >= sizeof(path2.s)) {
+ logmsg(rc, log_sys_err,
+ "Couldn't construct path name for trust anchor %s", path1.s);
+ goto lose;
+ }
+ if (access(path2.s, F_OK))
+ break;
+ }
+ if (i == INT_MAX) {
+ logmsg(rc, log_sys_err, "Couldn't find a free name for trust anchor %s", path1.s);
+ goto lose;
+ }
+
+ return check_ta(rc, x, &uri, &path1, &path2, object_generation_null);
+
+ lose:
+ log_validation_status(rc, &uri, trust_anchor_skipped, object_generation_null);
+ X509_free(x);
+ return 0;
+}
+
+
+
+/**
+ * Allocate a new tal_ctx_t.
+ */
+static tal_ctx_t *tal_ctx_t_new(void)
+{
+ tal_ctx_t *tctx = malloc(sizeof(*tctx));
+ if (tctx)
+ memset(tctx, 0, sizeof(*tctx));
+ return tctx;
+}
+
+/**
+ * Free a tal_ctx_t.
+ */
+static void tal_ctx_t_free(tal_ctx_t *tctx)
+{
+ if (tctx) {
+ EVP_PKEY_free(tctx->pkey);
+ free(tctx);
+ }
+}
+
+/**
+ * Read a trust anchor from disk and compare with known public key.
+ *
+ * NB: EVP_PKEY_cmp() returns 1 for match, not 0 like every other
+ * xyz_cmp() function in the entire OpenSSL library. Go figure.
+ */
+static int check_ta_tal_callback_1(rcynic_ctx_t *rc,
+ const tal_ctx_t *tctx,
+ object_generation_t generation)
+
+{
+ const path_t *prefix = NULL;
+ EVP_PKEY *pkey = NULL;
+ X509 *x = NULL;
+ path_t path;
+ int ret = 0;
+
+ switch (generation) {
+ case object_generation_current:
+ prefix = &rc->unauthenticated;
+ break;
+ case object_generation_backup:
+ prefix = &rc->old_authenticated;
+ break;
+ default:
+ goto done;
+ }
+
+ if (!uri_to_filename(rc, &tctx->uri, &path, prefix)) {
+ log_validation_status(rc, &tctx->uri, unreadable_trust_anchor_locator, generation);
+ goto done;
+ }
+
+ if ((x = read_cert(&path, NULL)) == NULL || (pkey = X509_get_pubkey(x)) == NULL) {
+ log_validation_status(rc, &tctx->uri, unreadable_trust_anchor, generation);
+ goto done;
+ }
+
+ if (EVP_PKEY_cmp(tctx->pkey, pkey) != 1) {
+ log_validation_status(rc, &tctx->uri, trust_anchor_key_mismatch, generation);
+ goto done;
+ }
+
+ ret = check_ta(rc, x, &tctx->uri, &path, &tctx->path, generation);
+ x = NULL;
+
+ done:
+ if (!ret)
+ log_validation_status(rc, &tctx->uri, object_rejected, generation);
+ EVP_PKEY_free(pkey);
+ X509_free(x);
+ return ret;
+}
+
+/**
+ * rsync callback for fetching a TAL.
+ */
+static void rsync_tal_callback(rcynic_ctx_t *rc,
+ const rsync_ctx_t *ctx,
+ const rsync_status_t status,
+ const uri_t *uri,
+ void *cookie)
+{
+ tal_ctx_t *tctx = cookie;
+
+ assert(rc && tctx);
+
+ if (status == rsync_status_pending)
+ return;
+
+ if (!check_ta_tal_callback_1(rc, tctx, object_generation_current) &&
+ !check_ta_tal_callback_1(rc, tctx, object_generation_backup))
+ log_validation_status(rc, &tctx->uri, trust_anchor_skipped, object_generation_null);
+
+ tal_ctx_t_free(tctx);
+}
+
+/**
+ * Check a trust anchor read from a trust anchor locator (TAL).
+ */
+static int check_ta_tal(rcynic_ctx_t *rc,
+ const char *fn)
+
+{
+ tal_ctx_t *tctx = NULL;
+ BIO *bio = NULL;
+ int ret = 1;
+
+ assert(rc && fn);
+
+ logmsg(rc, log_telemetry, "Processing trust anchor locator from file %s", fn);
+
+ if ((tctx = tal_ctx_t_new()) == NULL) {
+ logmsg(rc, log_sys_err, "malloc(tal_ctxt_t) failed");
+ goto done;
+ }
+
+ bio = BIO_new_file(fn, "r");
+
+ if (!bio)
+ logmsg(rc, log_usage_err, "Couldn't open trust anchor locator file %s", fn);
+
+ if (!bio || BIO_gets(bio, tctx->uri.s, sizeof(tctx->uri.s)) <= 0) {
+ uri_t furi;
+ filename_to_uri(&furi, fn);
+ log_validation_status(rc, &furi, unreadable_trust_anchor_locator, object_generation_null);
+ goto done;
+ }
+
+ tctx->uri.s[strcspn(tctx->uri.s, " \t\r\n")] = '\0';
+
+ if (!uri_to_filename(rc, &tctx->uri, &tctx->path, &rc->new_authenticated)) {
+ log_validation_status(rc, &tctx->uri, unreadable_trust_anchor_locator, object_generation_null);
+ goto done;
+ }
+
+ if (!endswith(tctx->uri.s, ".cer")) {
+ log_validation_status(rc, &tctx->uri, malformed_tal_uri, object_generation_null);
+ goto done;
+ }
+
+ bio = BIO_push(BIO_new(BIO_f_linebreak()), bio);
+ bio = BIO_push(BIO_new(BIO_f_base64()), bio);
+ if (bio)
+ tctx->pkey = d2i_PUBKEY_bio(bio, NULL);
+ if (!tctx->pkey) {
+ log_validation_status(rc, &tctx->uri, unreadable_trust_anchor_locator, object_generation_null);
+ goto done;
+ }
+
+ logmsg(rc, log_telemetry, "Processing trust anchor from URI %s", tctx->uri.s);
+
+ rsync_ta(rc, &tctx->uri, tctx, rsync_tal_callback);
+ tctx = NULL; /* Control has passed */
+
+ done:
+ tal_ctx_t_free(tctx);
+ BIO_free_all(bio);
+ return ret;
+}
+
+/**
+ * Check a directory of trust anchors and trust anchor locators.
+ */
+static int check_ta_dir(rcynic_ctx_t *rc,
+ const char *dn)
+{
+ DIR *dir = NULL;
+ struct dirent *d;
+ path_t path;
+ int is_cer, is_tal;
+
+ assert(rc && dn);
+
+ if ((dir = opendir(dn)) == NULL) {
+ logmsg(rc, log_sys_err, "Couldn't open trust anchor directory %s: %s",
+ dn, strerror(errno));
+ return 0;
+ }
+
+ while ((d = readdir(dir)) != NULL) {
+ if (snprintf(path.s, sizeof(path.s), "%s/%s", dn, d->d_name) >= sizeof(path.s)) {
+ logmsg(rc, log_data_err, "Pathname %s/%s too long", dn, d->d_name);
+ break;
+ }
+ is_cer = endswith(path.s, ".cer");
+ is_tal = endswith(path.s, ".tal");
+ if (is_cer && !check_ta_cer(rc, path.s))
+ break;
+ if (is_tal && !check_ta_tal(rc, path.s))
+ break;
+ if (!is_cer && !is_tal)
+ logmsg(rc, log_verbose, "Skipping non-trust-anchor %s", path.s);
+ }
+
+ if (dir != NULL)
+ closedir(dir);
+
+ return !d;;
+}
+
+
+
+/**
+ * Write detailed log of what we've done as an XML file.
+ */
+static int write_xml_file(const rcynic_ctx_t *rc,
+ const char *xmlfile)
+{
+ int i, j, use_stdout, ok;
+ char hostname[HOSTNAME_MAX];
+ mib_counter_t code;
+ timestamp_t ts;
+ FILE *f = NULL;
+ path_t xmltemp;
+
+ if (xmlfile == NULL)
+ return 1;
+
+ use_stdout = !strcmp(xmlfile, "-");
+
+ logmsg(rc, log_telemetry, "Writing XML summary to %s",
+ (use_stdout ? "standard output" : xmlfile));
+
+ if (use_stdout) {
+ f = stdout;
+ ok = 1;
+ } else if (snprintf(xmltemp.s, sizeof(xmltemp.s), "%s.%u.tmp", xmlfile, (unsigned) getpid()) >= sizeof(xmltemp.s)) {
+ logmsg(rc, log_usage_err, "Filename \"%s\" is too long, not writing XML", xmlfile);
+ return 0;
+ } else {
+ ok = (f = fopen(xmltemp.s, "w")) != NULL;
+ }
+
+ ok &= gethostname(hostname, sizeof(hostname)) == 0;
+
+ if (ok)
+ ok &= fprintf(f, "<?xml version=\"1.0\" ?>\n"
+ "<rcynic-summary date=\"%s\" rcynic-version=\"%s\""
+ " summary-version=\"%d\" reporting-hostname=\"%s\">\n"
+ " <labels>\n",
+ time_to_string(&ts, NULL),
+ svn_id, XML_SUMMARY_VERSION, hostname) != EOF;
+
+ for (j = 0; ok && j < MIB_COUNTER_T_MAX; ++j)
+ ok &= fprintf(f, " <%s kind=\"%s\">%s</%s>\n",
+ mib_counter_label[j], mib_counter_kind[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, " </labels>\n") != EOF;
+
+ for (i = 0; ok && i < sk_validation_status_t_num(rc->validation_status); i++) {
+ validation_status_t *v = sk_validation_status_t_value(rc->validation_status, i);
+ assert(v);
+
+ (void) time_to_string(&ts, &v->timestamp);
+
+ for (code = (mib_counter_t) 0; ok && code < MIB_COUNTER_T_MAX; code++) {
+ if (validation_status_get_code(v, code)) {
+ if (ok)
+ ok &= fprintf(f, " <validation_status timestamp=\"%s\" status=\"%s\"",
+ ts.s, mib_counter_label[code]) != EOF;
+ if (ok && (v->generation == object_generation_current ||
+ v->generation == object_generation_backup))
+ ok &= fprintf(f, " generation=\"%s\"",
+ object_generation_label[v->generation]) != EOF;
+ if (ok)
+ ok &= fprintf(f, ">%s</validation_status>\n", v->uri.s) != EOF;
+ }
+ }
+ }
+
+ for (i = 0; ok && i < sk_rsync_history_t_num(rc->rsync_history); i++) {
+ rsync_history_t *h = sk_rsync_history_t_value(rc->rsync_history, i);
+ assert(h);
+
+ if (ok)
+ ok &= fprintf(f, " <rsync_history") != EOF;
+ if (ok && h->started)
+ ok &= fprintf(f, " started=\"%s\"",
+ time_to_string(&ts, &h->started)) != EOF;
+ if (ok && h->finished)
+ ok &= fprintf(f, " finished=\"%s\"",
+ time_to_string(&ts, &h->finished)) != EOF;
+ if (ok && h->status != rsync_status_done)
+ ok &= fprintf(f, " error=\"%u\"", (unsigned) h->status) != EOF;
+ if (ok)
+ ok &= fprintf(f, ">%s%s</rsync_history>\n",
+ h->uri.s, (h->final_slash ? "/" : "")) != EOF;
+ }
+
+ if (ok)
+ ok &= fprintf(f, "</rcynic-summary>\n") != EOF;
+
+ if (f && !use_stdout)
+ ok &= fclose(f) != EOF;
+
+ if (ok && !use_stdout)
+ ok &= rename(xmltemp.s, xmlfile) == 0;
+
+ if (!ok)
+ logmsg(rc, log_sys_err, "Couldn't write XML summary to %s: %s",
+ (use_stdout ? "standard output" : xmlfile), strerror(errno));
+
+ if (!ok && !use_stdout)
+ (void) unlink(xmltemp.s);
+
+ return ok;
+}
+
+
+
+/**
+ * Long options.
+ */
+#define OPTIONS \
+ QA('a', "authenticated", "root of authenticated data tree") \
+ QA('c', "config", "override default name of config file") \
+ QF('h', "help", "print this help message") \
+ QA('j', "jitter", "set jitter value") \
+ QA('l', "log-level", "set log level") \
+ QA('u', "unauthenticated", "root of unauthenticated data tree") \
+ QF('e', "use-stderr", "log to syslog") \
+ QF('s', "use-syslog", "log to stderr") \
+ QF('V', "version", "print program version") \
+ QA('x', "xml-file", "set XML output file location")
+
+const static struct option longopts[] = {
+ { "authenticated", required_argument, NULL, 'a' },
+ { "config", required_argument, NULL, 'c' },
+ { "help", no_argument, NULL, 'h' },
+ { "jitter", required_argument, NULL, 'j' },
+ { "log-level", required_argument, NULL, 'l' },
+ { "unauthenticated", required_argument, NULL, 'u' },
+ { "use-stderr", no_argument, NULL, 'e' },
+ { "use-syslog", no_argument, NULL, 's' },
+ { "version", no_argument, NULL, 'V' },
+ { "xml-file", required_argument, NULL, 'x' },
+ { NULL }
+};
+
+/**
+ * Wrapper around printf() to take arguments like logmsg().
+ * If C had closures, usage() would use them instead of this silliness.
+ */
+static void logmsg_printf(const rcynic_ctx_t *rc,
+ const log_level_t level,
+ const char *fmt, ...)
+{
+ va_list ap;
+ va_start(ap, fmt);
+ vprintf(fmt, ap);
+ putchar('\n');
+ va_end(ap);
+}
+
+/**
+ * Log usage message, either to stdout (for --help) or via logmsg().
+ */
+static void usage (const rcynic_ctx_t *rc, const char *jane)
+{
+ void (*log)(const rcynic_ctx_t *, const log_level_t, const char *, ...) = rc ? logmsg : logmsg_printf;
+ char left[80];
+
+ if (rc && !jane)
+ jane = rc->jane;
+
+ log(rc, log_usage_err, "usage: %s [options]", jane);
+ log(rc, log_usage_err, "options:");
+
+#define QF(_s_, _l_, _d_) \
+ (void) snprintf(left, sizeof(left), "-%c --%-32s", _s_, _l_); \
+ log(rc, log_usage_err, " %s%s", left, _d_);
+
+#define QA(_s_, _l_, _d_) \
+ (void) snprintf(left, sizeof(left), "-%c ARG --%-32s", _s_, _l_ " ARG"); \
+ log(rc, log_usage_err, " %s%s", left, _d_);
+
+ OPTIONS;
+
+#undef QA
+#undef QF
+}
+
+/**
+ * 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;
+ int opt_auth = 0, opt_unauth = 0, keep_lockfile = 0;
+ char *lockfile = NULL, *xmlfile = NULL;
+ char *cfg_file = "rcynic.conf";
+ int c, i, ret = 1, jitter = 600, lockfd = -1;
+ STACK_OF(CONF_VALUE) *cfg_section = NULL;
+ CONF *cfg_handle = NULL;
+ time_t start = 0, finish;
+ rcynic_ctx_t rc;
+ unsigned delay;
+ long eline = 0;
+ path_t ta_dir;
+
+#define QF(_s_, _l_, _d_) _s_,
+#define QA(_s_, _l_, _d_) _s_, ':',
+
+ const static char short_opts[] = { OPTIONS '\0' };
+
+#undef QA
+#undef QF
+
+#define QF(_s_, _l_, _d_) { _l_, no_argument, NULL, _s_ },
+#define QA(_s_, _l_, _d_) { _l_, required_argument, NULL, _s_ },
+
+ static struct option long_opts[] = { OPTIONS { NULL } };
+
+#undef QA
+#undef QF
+
+ memset(&rc, 0, sizeof(rc));
+
+ if ((rc.jane = strrchr(argv[0], '/')) == NULL)
+ rc.jane = argv[0];
+ else
+ rc.jane++;
+
+ rc.log_level = log_data_err;
+ rc.allow_stale_crl = 1;
+ rc.allow_stale_manifest = 1;
+ rc.allow_digest_mismatch = 1;
+ rc.allow_crl_digest_mismatch = 1;
+ rc.allow_nonconformant_name = 1;
+ rc.allow_ee_without_signedObject = 1;
+ rc.allow_1024_bit_ee_key = 1;
+ rc.allow_wrong_cms_si_attributes = 1;
+ rc.max_parallel_fetches = 1;
+ rc.max_retries = 3;
+ rc.retry_wait_min = 30;
+ rc.run_rsync = 1;
+ rc.rsync_timeout = 300;
+ rc.max_select_time = 30;
+ rc.rsync_early = 1;
+
+#define QQ(x,y) rc.priority[x] = y;
+ LOG_LEVELS;
+#undef QQ
+
+ if (!set_directory(&rc, &rc.authenticated, "rcynic-data/authenticated", 0) ||
+ !set_directory(&rc, &rc.unauthenticated, "rcynic-data/unauthenticated/", 1))
+ goto done;
+
+ OpenSSL_add_all_algorithms();
+ ERR_load_crypto_strings();
+
+ if (!create_missing_nids()) {
+ logmsg(&rc, log_sys_err, "Couldn't initialize missing OIDs!");
+ goto done;
+ }
+
+ memset(&ta_dir, 0, sizeof(ta_dir));
+
+ opterr = 0;
+
+ while ((c = getopt_long(argc, argv, short_opts, long_opts, NULL)) > 0) {
+ switch (c) {
+ case 'a':
+ opt_auth = 1;
+ if (!set_directory(&rc, &rc.authenticated, optarg, 0))
+ goto done;
+ break;
+ 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 'h':
+ usage(NULL, rc.jane);
+ ret = 0;
+ goto done;
+ case 'j':
+ if (!configure_integer(&rc, &jitter, optarg))
+ goto done;
+ opt_jitter = 1;
+ break;
+ case 'u':
+ opt_unauth = 1;
+ if (!set_directory(&rc, &rc.unauthenticated, optarg, 1))
+ goto done;
+ break;
+ case 'V':
+ puts(svn_id);
+ ret = 0;
+ goto done;
+ case 'x':
+ xmlfile = strdup(optarg);
+ break;
+ default:
+ usage(&rc, NULL);
+ goto done;
+ }
+ }
+
+ if (!(asn1_zero = s2i_ASN1_INTEGER(NULL, "0x0")) ||
+ !(asn1_four_octets = s2i_ASN1_INTEGER(NULL, "0xFFFFFFFF")) ||
+ !(asn1_twenty_octets = s2i_ASN1_INTEGER(NULL, "0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF")) ||
+ !(NID_binary_signing_time = OBJ_create("1.2.840.113549.1.9.16.2.46",
+ "id-aa-binarySigningTime",
+ "id-aa-binarySigningTime"))) {
+ logmsg(&rc, log_sys_err, "Couldn't initialize ASN.1 constants!");
+ 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 (!opt_auth &&
+ !name_cmp(val->name, "authenticated") &&
+ !set_directory(&rc, &rc.authenticated, val->value, 0))
+ goto done;
+
+ else if (!opt_unauth &&
+ !name_cmp(val->name, "unauthenticated") &&
+ !set_directory(&rc, &rc.unauthenticated, val->value, 1))
+ goto done;
+
+ else if (!name_cmp(val->name, "trust-anchor-directory") &&
+ !set_directory(&rc, &ta_dir, val->value, 0))
+ goto done;
+
+ else if (!name_cmp(val->name, "rsync-timeout") &&
+ !configure_integer(&rc, &rc.rsync_timeout, val->value))
+ goto done;
+
+ else if (!name_cmp(val->name, "max-parallel-fetches") &&
+ !configure_integer(&rc, &rc.max_parallel_fetches, val->value))
+ goto done;
+
+ else if (!name_cmp(val->name, "max-select-time") &&
+ !configure_unsigned_integer(&rc, &rc.max_select_time, 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 (!name_cmp(val->name, "keep-lockfile") &&
+ !configure_boolean(&rc, &keep_lockfile, val->value))
+ goto done;
+
+ 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 (!xmlfile &&
+ (!name_cmp(val->name, "xml-file") ||
+ !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, "allow-stale-manifest") &&
+ !configure_boolean(&rc, &rc.allow_stale_manifest, val->value))
+ goto done;
+
+ else if (!name_cmp(val->name, "allow-non-self-signed-trust-anchor") &&
+ !configure_boolean(&rc, &rc.allow_non_self_signed_trust_anchor, val->value))
+ goto done;
+
+ else if (!name_cmp(val->name, "require-crl-in-manifest") &&
+ !configure_boolean(&rc, &rc.require_crl_in_manifest, val->value))
+ goto done;
+
+ else if (!name_cmp(val->name, "allow-object-not-in-manifest") &&
+ !configure_boolean(&rc, &rc.allow_object_not_in_manifest, val->value))
+ goto done;
+
+ else if (!name_cmp(val->name, "allow-digest-mismatch") &&
+ !configure_boolean(&rc, &rc.allow_digest_mismatch, val->value))
+ goto done;
+
+ else if (!name_cmp(val->name, "allow-crl-digest-mismatch") &&
+ !configure_boolean(&rc, &rc.allow_crl_digest_mismatch, 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;
+
+ else if (!name_cmp(val->name, "run-rsync") &&
+ !configure_boolean(&rc, &rc.run_rsync, val->value))
+ goto done;
+
+ else if (!name_cmp(val->name, "allow-nonconformant-name") &&
+ !configure_boolean(&rc, &rc.allow_nonconformant_name, val->value))
+ goto done;
+
+ else if (!name_cmp(val->name, "allow-ee-without-signedObject") &&
+ !configure_boolean(&rc, &rc.allow_ee_without_signedObject, val->value))
+ goto done;
+
+ else if (!name_cmp(val->name, "allow-1024-bit-ee-key") &&
+ !configure_boolean(&rc, &rc.allow_1024_bit_ee_key, val->value))
+ goto done;
+
+ else if (!name_cmp(val->name, "allow-wrong-cms-si-attributes") &&
+ !configure_boolean(&rc, &rc.allow_wrong_cms_si_attributes, val->value))
+ goto done;
+
+ else if (!name_cmp(val->name, "rsync-early") &&
+ !configure_boolean(&rc, &rc.rsync_early, 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_history = sk_rsync_history_t_new(rsync_history_cmp)) == NULL) {
+ logmsg(&rc, log_sys_err, "Couldn't allocate rsync_history stack");
+ goto done;
+ }
+
+ if ((rc.validation_status = sk_validation_status_t_new_null()) == NULL) {
+ logmsg(&rc, log_sys_err, "Couldn't allocate validation_status stack");
+ goto done;
+ }
+
+ if ((rc.x509_store = X509_STORE_new()) == NULL) {
+ logmsg(&rc, log_sys_err, "Couldn't allocate X509_STORE");
+ goto done;
+ }
+
+ if ((rc.rsync_queue = sk_rsync_ctx_t_new_null()) == NULL) {
+ logmsg(&rc, log_sys_err, "Couldn't allocate rsync_queue");
+ goto done;
+ }
+
+ if ((rc.task_queue = sk_task_t_new_null()) == NULL) {
+ logmsg(&rc, log_sys_err, "Couldn't allocate task_queue");
+ 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));
+ lockfd = -1;
+ goto done;
+ }
+
+ start = time(0);
+ logmsg(&rc, log_telemetry, "Starting");
+
+ if (!construct_directory_names(&rc))
+ goto done;
+
+ if (!access(rc.new_authenticated.s, F_OK)) {
+ logmsg(&rc, log_sys_err,
+ "Timestamped output directory %s already exists! Clock went backwards?",
+ rc.new_authenticated.s);
+ goto done;
+ }
+
+ if (!mkdir_maybe(&rc, &rc.new_authenticated)) {
+ logmsg(&rc, log_sys_err, "Couldn't prepare directory %s: %s",
+ rc.new_authenticated.s, 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);
+
+ assert(val && val->name && val->value);
+
+ if (!name_cmp(val->name, "trust-anchor-uri-with-key") ||
+ !name_cmp(val->name, "indirect-trust-anchor")) {
+ logmsg(&rc, log_usage_err,
+ "Directive \"%s\" is obsolete -- please use \"trust-anchor-locator\" instead",
+ val->name);
+ goto done;
+ }
+
+ if ((!name_cmp(val->name, "trust-anchor") && !check_ta_cer(&rc, val->value)) ||
+ (!name_cmp(val->name, "trust-anchor-locator") && !check_ta_tal(&rc, val->value)))
+ goto done;
+ }
+
+ if (*ta_dir.s != '\0' && !check_ta_dir(&rc, ta_dir.s))
+ goto done;
+
+ while (sk_task_t_num(rc.task_queue) > 0 || sk_rsync_ctx_t_num(rc.rsync_queue) > 0) {
+ task_run_q(&rc);
+ rsync_mgr(&rc);
+ }
+
+ logmsg(&rc, log_telemetry, "Event loop done, beginning final output and cleanup");
+
+ if (!finalize_directories(&rc))
+ goto done;
+
+ if (prune && rc.run_rsync &&
+ !prune_unauthenticated(&rc, &rc.unauthenticated,
+ strlen(rc.unauthenticated.s))) {
+ logmsg(&rc, log_sys_err, "Trouble pruning old unauthenticated data");
+ goto done;
+ }
+
+ if (!write_xml_file(&rc, xmlfile))
+ goto done;
+
+ ret = 0;
+
+ done:
+ log_openssl_errors(&rc);
+
+ /*
+ * Do NOT free cfg_section, NCONF_free() takes care of that
+ */
+ sk_validation_status_t_pop_free(rc.validation_status, validation_status_t_free);
+ sk_rsync_history_t_pop_free(rc.rsync_history, rsync_history_t_free);
+ validation_status_t_free(rc.validation_status_in_waiting);
+ X509_STORE_free(rc.x509_store);
+ NCONF_free(cfg_handle);
+ CONF_modules_free();
+ EVP_cleanup();
+ ERR_free_strings();
+ if (rc.rsync_program)
+ free(rc.rsync_program);
+ if (lockfile && lockfd >= 0 && !keep_lockfile)
+ unlink(lockfile);
+ if (lockfile)
+ free(lockfile);
+ if (xmlfile)
+ free(xmlfile);
+
+ if (start) {
+ finish = time(0);
+ logmsg(&rc, log_telemetry,
+ "Finished, elapsed time %u:%02u:%02u",
+ (unsigned) ((finish - start) / 3600),
+ (unsigned) ((finish - start) / 60 % 60),
+ (unsigned) ((finish - start) % 60));
+ }
+
+ return ret;
+}