20151212 Reference implementation “Time over HTTPS”

This is my reference implementation for the “time over HTTPS” specification.

I’ve tested it on FreeBSD and a random Linux.

The two portability issues I’m aware of are fmtcheck(3) and asprintf(3) and they should be trivial to work around, if necessary.

Obviously you must have the openssl(1) program installed, since that’s what the program uses for all the crypto-mumbo-jumbo.

To run it, simply give it the IP or FQDN of a friendly HTTPS server:

$ httpstime www.example.com
*RESULT 0 www.example.com -0.054 0.091 0.145 Verify return code: 0 (ok)

This means that time time on the local system is between 54 milliseconds behind and 91 msec ahead of the remote server, and the width of that interval is 145 msec.

If you specify the ‘-v’ option, you get tons of debug on stderr.

The ‘-C’ option ignores (but reports) certificate validation errors, allowing you to test against self signed certificates etc.

With the ‘-s’ argument you can specify the openssl command line to use if you want custom arguments, for instance root-cert list etc.

See the usage for the rest of the options.

The protocol specification is at:

Discussions, ideas, patches, observations, data etc:

NTP Hackers <hackers@lists.ntp.org>

Enjoy,

phk

Source code:

/*-
 * Copyright (c) 2015 Poul-Henning Kamp
 * All rights reserved.
 *
 * Author: Poul-Henning Kamp <phk@phk.freebsd.dk>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 * Compile on FreeBSD:
 *      cc -o httpstime -Wall -Werror httpstime.c -lm
 *
 * Compile on Linux:
 *      cc -o httpstime -Wall -Werror httpstime.c -lm -lbsd
 */

#ifdef __linux__
#define _GNU_SOURCE             // For asprintf(3)
#endif

#include <sys/time.h>

#include <assert.h>
#include <errno.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#ifdef __linux__
#include <bsd/stdio.h>          // For fmtcheck(3)
#endif

#define AZ(foo)         do { assert((foo) == 0); } while (0)
#define AN(foo)         do { assert((foo) != 0); } while (0)

/**********************************************************************
 * Imported from Varnish-Cache:
 *
 * Semi-trivial functions to handle HTTP header timestamps according to
 * RFC 2616 section 3.3.
 *
 * We must parse four different formats:
 *       000000000011111111112222222222
 *       012345678901234567890123456789
 *       ------------------------------
 *      "Sun, 06 Nov 1994 08:49:37 GMT"         RFC822 & RFC1123
 *      "Sunday, 06-Nov-94 08:49:37 GMT"        RFC850
 *      "Sun Nov  6 08:49:37 1994"              ANSI-C asctime()
 *      "1994-11-06T08:49:37"                   ISO 8601
 *
 * And always output the RFC1123 format.
 *
 * So why are these functions hand-built ?
 *
 * Because the people behind POSIX were short-sighted morons who didn't
 * think anybody would ever need to deal with timestamps in multiple
 * different timezones at the same time -- or for that matter, convert
 * timestamps to broken down UTC/GMT time.
 *
 * We could, and used to, get by by smashing our TZ variable to "UTC" but
 * that ruins the LOCALE for for other parts of the program.
 *
 */

static const char * const weekday_name[] = {
        "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
};

static const char * const more_weekday[] = {
        "day", "day", "sday", "nesday", "rsday", "day", "urday"
};

static const char * const month_name[] = {
        "Jan", "Feb", "Mar", "Apr", "May", "Jun",
        "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};

static const int days_in_month[] = {
        31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31
};

static const int days_before_month[] = {
        0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334
};


#define FAIL()  \
        do { return (0); } while (0)

#define DIGIT(mult, fld)                                        \
        do {                                                    \
                if (*p < '0' || *p > '9')                       \
                        FAIL();                                 \
                fld += (*p - '0') * mult;                       \
                p++;                                            \
        } while(0)

#define MUSTBE(chr)                                             \
        do {                                                    \
                if (*p != chr)                                  \
                        FAIL();                                 \
                p++;                                            \
        } while(0)

#define WEEKDAY()                                               \
        do {                                                    \
                int i;                                          \
                for(i = 0; i < 7; i++) {                        \
                        if (!memcmp(p, weekday_name[i], 3)) {   \
                                weekday = i;                    \
                                break;                          \
                        }                                       \
                }                                               \
                if (i == 7)                                     \
                        FAIL();                                 \
                p += 3;                                         \
        } while(0)

#define MONTH()                                                 \
        do {                                                    \
                int i;                                          \
                for(i = 0; i < 12; i++) {                       \
                        if (!memcmp(p, month_name[i], 3)) {     \
                                month = i + 1;                  \
                                break;                          \
                        }                                       \
                }                                               \
                if (i == 12)                                    \
                        FAIL();                                 \
                p += 3;                                         \
        } while(0)

#define TIMESTAMP()                                             \
        do {                                                    \
                DIGIT(10, hour);                                \
                DIGIT(1, hour);                                 \
                MUSTBE(':');                                    \
                DIGIT(10, min);                                 \
                DIGIT(1, min);                                  \
                MUSTBE(':');                                    \
                DIGIT(10, sec);                                 \
                DIGIT(1, sec);                                  \
        } while(0)

static double
VTIM_parse(const char *p)
{
        double t;
        int month = 0, year = 0, weekday = -1, mday = 0;
        int hour = 0, min = 0, sec = 0;
        int d, leap;

        while (*p == ' ')
                p++;

        if (*p >= '0' && *p <= '9') {
                /* ISO8601 -- "1994-11-06T08:49:37" */
                DIGIT(1000, year);
                DIGIT(100, year);
                DIGIT(10, year);
                DIGIT(1, year);
                MUSTBE('-');
                DIGIT(10, month);
                DIGIT(1, month);
                MUSTBE('-');
                DIGIT(10, mday);
                DIGIT(1, mday);
                MUSTBE('T');
                TIMESTAMP();
        } else {
                WEEKDAY();
                assert(weekday >= 0 && weekday <= 6);
                if (*p == ',') {
                        /* RFC822 & RFC1123 - "Sun, 06 Nov 1994 08:49:37 GMT" */
                        p++;
                        MUSTBE(' ');
                        DIGIT(10, mday);
                        DIGIT(1, mday);
                        MUSTBE(' ');
                        MONTH();
                        MUSTBE(' ');
                        DIGIT(1000, year);
                        DIGIT(100, year);
                        DIGIT(10, year);
                        DIGIT(1, year);
                        MUSTBE(' ');
                        TIMESTAMP();
                        MUSTBE(' ');
                        MUSTBE('G');
                        MUSTBE('M');
                        MUSTBE('T');
                } else if (*p == ' ') {
                        /* ANSI-C asctime() -- "Sun Nov  6 08:49:37 1994" */
                        p++;
                        MONTH();
                        MUSTBE(' ');
                        if (*p != ' ')
                                DIGIT(10, mday);
                        else
                                p++;
                        DIGIT(1, mday);
                        MUSTBE(' ');
                        TIMESTAMP();
                        MUSTBE(' ');
                        DIGIT(1000, year);
                        DIGIT(100, year);
                        DIGIT(10, year);
                        DIGIT(1, year);
                } else if (!memcmp(p, more_weekday[weekday],
                    strlen(more_weekday[weekday]))) {
                        /* RFC850 -- "Sunday, 06-Nov-94 08:49:37 GMT" */
                        p += strlen(more_weekday[weekday]);
                        MUSTBE(',');
                        MUSTBE(' ');
                        DIGIT(10, mday);
                        DIGIT(1, mday);
                        MUSTBE('-');
                        MONTH();
                        MUSTBE('-');
                        DIGIT(10, year);
                        DIGIT(1, year);
                        year += 1900;
                        if (year < 1969)
                                year += 100;
                        MUSTBE(' ');
                        TIMESTAMP();
                        MUSTBE(' ');
                        MUSTBE('G');
                        MUSTBE('M');
                        MUSTBE('T');
                } else
                        FAIL();
        }

        while (*p == ' ')
                p++;

        if (*p != '\0')
                FAIL();

        if (sec < 0 || sec > 60)        /* Leapseconds! */
                FAIL();
        if (min < 0 || min > 59)
                FAIL();
        if (hour < 0 || hour > 23)
                FAIL();
        if (month < 1 || month > 12)
                FAIL();
        if (mday < 1 || mday > days_in_month[month - 1])
                FAIL();
        if (year < 1899)
                FAIL();

        leap =
            ((year) % 4) == 0 && (((year) % 100) != 0 || ((year) % 400) == 0);

        if (month == 2 && mday > 28 && !leap)
                FAIL();

        if (sec == 60)                  /* Ignore Leapseconds */
                sec--;

        t = ((hour * 60.) + min) * 60. + sec;

        d = (mday - 1) + days_before_month[month - 1];

        if (month > 2 && leap)
                d++;

        d += (year % 100) * 365;        /* There are 365 days in a year */

        if ((year % 100) > 0)           /* And a leap day every four years */
                d += (((year % 100) - 1) / 4);

        d += ((year / 100) - 20) *      /* Days relative to y2000 */
            (100 * 365 + 24);           /* 24 leapdays per year in a century */

        d += ((year - 1) / 400) - 4;    /* And one more every 400 years */

        /*
         * Now check weekday, if we have one.
         * 6 is because 2000-01-01 was a saturday.
         * 10000 is to make sure the modulus argument is always positive
         */
        if (weekday != -1 && (d + 6 + 7 * 10000) % 7 != weekday)
                FAIL();

        t += d * 86400.;

        t += 10957. * 86400.;           /* 10957 days frm UNIX epoch to y2000 */

        return (t);
}

/**********************************************************************
 * Parameters which can be changed.
 */

static const char *httpstime_ssl_cmd =
   "openssl s_client -verify 10 -verify_return_error -connect %s:443 2>&1";

/*
 * Capture line containing this string in the command output before the first
 * HTTP response.  Useful with openssl without '-verify_return_error'
 */

static const char *httpstime_ssl_result =
   "Verify return code";

static const char *httpstime_url =
   "/.well-known/time";

static const char *httpstime_user_agent =
   "Ntimed_https/1.0";

static FILE *httpstime_debug = NULL;

static int httpstime_npoll = 4;

static int httpstime_timeout = 5;

/**********************************************************************
 * This function sends a HTTP request, receives a HTTP response and
 * returns the following information:
 *      ts[0] = Transmit timestamp
 *      ts[1] = Receive timestamp
 *      ts[2] = Date: header timestamp
 *      http = HTTP status code an string
 *      ssl = openssl verification result string, only if 'first' is true.
 */

static int
poll_https(FILE *f, const char *site, int first,
    double *ts, char **http, char **ssl)
{
        char buf[BUFSIZ];
        int retval = 1, j = 0;
        struct timeval tv;
        char *p;

        AN(ts);
        ts[0] = nan("");
        ts[1] = nan("");
        ts[2] = nan("");
        AN(http);
        *http = NULL;
        if (first) {
                AN(ssl);
                *ssl = NULL;
        }

        AZ(gettimeofday(&tv, NULL));
        ts[0] = tv.tv_sec + 1e-6 * tv.tv_usec;
        fprintf(f, "HEAD %s HTTP/1.1\r\nHost: %s\r\nUser-Agent: %s\r\n\r\n",
            httpstime_url, site, httpstime_user_agent);
        fflush(f);

        if (httpstime_debug != NULL)
                fprintf(httpstime_debug,
                    "*WROTE "
                    "HEAD %s HTTP/1.1\r\nHost: %s\r\nUser-Agent: %s\r\n\r\n",
                    httpstime_url, site, httpstime_user_agent);

        while (1) {
                (void)alarm(httpstime_timeout);
                if (fgets(buf, sizeof buf, f) == NULL)
                        break;
                AZ(gettimeofday(&tv, NULL));
                if (httpstime_debug != NULL)
                        fprintf(httpstime_debug, "*READ %s", buf);
                if (j && (buf[0] == '\r' || buf[0] == '\n'))
                        break;
                if (first && !j && strstr(buf, httpstime_ssl_result) != NULL) {
                        *ssl = strdup(buf);
                        p = strchr(*ssl, '\r');
                        if (p != NULL)
                                *p = '\0';
                        p = strchr(*ssl, '\n');
                        if (p != NULL)
                                *p = '\0';
                } else if (!strncmp(buf, "HTTP/1", 6)) {
                        ts[1] = tv.tv_sec + 1e-6 * tv.tv_usec;
                        *http = strdup(buf);
                        p = strchr(*http, '\r');
                        if (p != NULL)
                                *p = '\0';
                        p = strchr(*http, '\n');
                        if (p != NULL)
                                *p = '\0';
                        j++;
                } else if (!strncmp(buf, "Date:", 5)) {
                        p = strchr(buf, '\r');
                        if (p != NULL)
                                *p = '\0';
                        p = strchr(buf, '\n');
                        if (p != NULL)
                                *p = '\0';
                        ts[2] = VTIM_parse(buf + 5);
                        retval = 0;
                }
        }
        (void)alarm(0);
        return (retval);
}

/**********************************************************************
 */

static int
httpstime(const char *site, double *tlow, double *thigh, char **sslr)
{
        FILE *f;
        struct timeval tv;
        double ts[3];
        double dt, b, rtt;
        double t0, t1, x0, x1;
        int i, j;
        char *cmd;
        const char *p;
        char *http, *ssl = NULL;

        AN(site);
        AN(tlow);
        *tlow = nan("");
        AN(thigh);
        *thigh = nan("");
        AN(sslr);
        *sslr = NULL;

        assert(fmtcheck(httpstime_ssl_cmd, "%s") == httpstime_ssl_cmd);

        assert(asprintf(&cmd, httpstime_ssl_cmd, site) > 0);
        AN(cmd);

        if (httpstime_debug != NULL)
                fprintf(httpstime_debug, "*CMD %s\n", cmd);

        f = popen(cmd, "r+");
        if (f == NULL)
                return(2);

        AZ(gettimeofday(&tv, NULL));

        x0 = -9e9;
        x1 = 9e9;
        p = " ";
        for (j = 0; j < httpstime_npoll; j++) {
                i = poll_https(f, site, (j == 0), ts, &http, &ssl);
                if (i)
                        return(2);
                t0 = (ts[0] - 1) - ts[2];
                t1 = ts[1] - ts[2];
                rtt = ts[1] - ts[0];
                if (t0 > x0) {
                        x0 = t0;
                        if (t1 < x1)
                                x1 = t1;
                        p = "B";
                } else if (t1 < x1) {
                        x1 = t1;
                        p = "A";
                } else {
                        p = "C";
                }

                dt = .5 * (x1 + x0) - .5 * rtt;
                while (dt < 0)
                        dt += 1.0;
                if (httpstime_debug != NULL) {
                        fprintf(httpstime_debug,
                            "*TSTAMPS %.3f %.3f %.3f RTT %.3f\n",
                            ts[0], ts[1], ts[2], rtt);
                        fprintf(httpstime_debug,
                            "*THIS_WINDOW %.3f %.3f\n", t0, t1);
                        fprintf(httpstime_debug,
                            "*TOTAL_WINDOW %.3f %.3f WIDTH %.3f CUT %s\n",
                            x0, x1, x1 - x0, p);
                        fprintf(httpstime_debug,
                            "*HTTP_RESULT %s\n", http);
                        if (j == 0 && ssl != NULL)
                                fprintf(httpstime_debug,
                                    "*SSL_RESULT %s\n", ssl);
                        fprintf(httpstime_debug,
                            "*DT %.3f\n", dt);
                }
                free(http);
                fflush(stdout);
                AZ(gettimeofday(&tv, NULL));
                b = dt - tv.tv_usec * 1e-6;
                while (b < 0)
                        b += 1;
                while (b > 1)
                        b -= 1;
                AZ(usleep((useconds_t)(b * 1e6)));
        }

        i = pclose(f);
        free(cmd);
        if (i) {
                if (httpstime_debug != NULL)
                        fprintf(httpstime_debug, "*CMD_EXIT 0x%x\n", i);
                return (-1);
        }
        *sslr = ssl;
        *tlow = x0;
        *thigh = x1;
        return (0);
}

/**********************************************************************
 *
 */

static int
usage(const char *a0)
{
        fprintf(stderr, "%s: Wrong arguments.  Usage:\n", a0);
        fprintf(stderr, "\t-C\t\tIgnore cert validation errors\n");
        fprintf(stderr, "\t-n\t<int>\tnumber_of_polls\n");
        fprintf(stderr, "\t-r\t<str>\tSSL-program-result-pattern\n");
        fprintf(stderr, "\t-s\t<str>\tSSL-program_command_line\n");
        fprintf(stderr, "\t-t\t<int>\tTimeout in seconds\n");
        fprintf(stderr, "\t-u\t<str>\tURL to query\n");
        fprintf(stderr, "\t-v\t\tverbose mode (->stderr)\n");
        return (2);
}

int
main(int argc, char * const *argv)
{
        int i, j;
        double tlow, thigh;
        char *ssl;

        while ((i = getopt(argc, argv, "Cn:r:s:t:u:v")) != -1) {
                switch (i) {
                case 'C':
                        httpstime_ssl_cmd =
                            "openssl s_client -verify 10 -connect %s:443 2>&1";
                        break;
                case 'n':
                        httpstime_npoll = (int)strtoul(optarg, NULL, 0);
                        break;
                case 'r':
                        httpstime_ssl_result = optarg;
                        break;
                case 's':
                        if (fmtcheck(optarg, "%s") != optarg) {
                                fprintf(stderr, "ERROR:\n");
                                fprintf(stderr, "\t-s format error %s\n",
                                    "Must contain exactly one '%%s'");
                                return (usage(argv[0]));
                        }
                        httpstime_ssl_cmd = optarg;
                        break;
                case 't':
                        httpstime_timeout = (int)strtoul(optarg, NULL, 0);
                        break;
                case 'u':
                        httpstime_url = optarg;
                        break;
                case 'v':
                        httpstime_debug = stderr;
                        break;
                default:
                        return(usage(argv[0]));
                }
        }
        argc -= optind;
        argv += optind;

        for (i = 0; i < argc; i++) {
                j = httpstime(argv[i], &tlow, &thigh, &ssl);
                printf("*RESULT %d %s %.3f %.3f %.3f %s\n",
                    j, argv[i], tlow, thigh, thigh - tlow, ssl);
                free(ssl);
        }
        return (0);
}