/* $Cambridge: hermes/src/prayer/lib/raven_wls_response.c,v 1.2 2008/09/16 09:59:57 dpc22 Exp $ */

/* Support for Ucam Web single sign on environment "Raven". Of no interest
 * to anyone else.
 *
 * Raven WLS response tokens are thirteen fields glued together using !.
 * First eleven parameters are RSA signed by last two
 */

#include <stdio.h>
#include <string.h>
#include <time.h>

#include <openssl/bio.h>
#include <openssl/rsa.h>
#include <openssl/pem.h>
#include <openssl/hmac.h>
#include <openssl/err.h>
#include <openssl/evp.h>

#include "lib.h"

/* ====================================================================== */

char *
raven_wls_response_status_message(struct raven_wls_response *wls)
{
    int err = atoi(wls->status);

    switch (err) {
    case 200 :
        return "OK";
    case 410 :
        return "Authentication cancelled at user's request";
    case 510 :
        return "No mutually acceptable types of authentication available";
    case 520 :
        return "Unsupported authentication protocol version";
    case 530 :
        return "Parameter error in authentication request";
    case 540 :
        return "Interaction with the user would be required";
    case 550 :
        return "Web server and authentication server clocks out of sync";
    case 560 :
        return "Web server not authorised to use "
            "the authentication service";
    case 570 :
        return "Operation declined by the authentication service";
    }
    return "Unrecognised error code";
}

/* ====================================================================== */

/* Verify wls signature */

static int
raven_wls_response_sig_decode(char *string, unsigned char **result)
{
    unsigned long len;
    int i;  
    char *res, *d = strdup(string);

    for (i = 0; i < strlen(d); i++) {
        if (d[i] == '-') d[i] = '+';
        else if (d[i] == '.') d[i] = '/';
        else if (d[i] == '_') d[i] = '=';
    }

    res = d;
    len = strlen(res);
    string_base64_decode((unsigned char *)res, strlen(res), &len);

    res[len] = '\0'; /* for safety if nothing else */
    *result = (unsigned char *)res;
    return len;
}

#ifdef SSL_ENABLE
static int 
raven_wls_response_sig_verify(char *data, char *sig,
                              char *key_path, char *key_id) 
{
    char key_full_path[1024];    /* key_path comes from sysadmin */          
    char digest[21];
    unsigned char* decoded_sig;
    int sig_length;
    int result;
    FILE *key_file;
    RSA *public_key;

    snprintf(key_full_path, sizeof(key_full_path),
             "%s/pubkey%s", key_path, key_id);

    SHA1((const unsigned char *)data, strlen(data), (unsigned char *)digest);
  
    if ((key_file = (FILE *)fopen(key_full_path, "r")) == NULL)
        return(0);

    public_key = (RSA *)PEM_read_RSAPublicKey(key_file, NULL, NULL, NULL);
    fclose(key_file);
 
    if (public_key == NULL)
        return(0);

    sig_length = raven_wls_response_sig_decode(sig, &decoded_sig);

    result = RSA_verify(NID_sha1, (unsigned char *)digest, 20, 
                        decoded_sig, sig_length, public_key);

    RSA_free(public_key);
    return ((result == 1) ? 1 : 0);
}
#else
static int 
raven_wls_response_sig_verify(char *data, char *sig,
                              char *key_path, char *key_id) 
{
    /* Need SSL libraries for SSL verify */
    return(0);
}
#endif

/* ====================================================================== */

/* Utility routines for splitting WLS response token into sections */

static int
raven_wls_response_count_fields(char *s)
{
    int result = 1;

    while (*s) {
        if (*s == '!')
            result++;

        s++;
    }
    return(result);
}

static char *
raven_wls_response_get_single(char **sp)
{
    char *s = *sp;
    char *t = strchr(s, '!');

    if (t)
        *t++ = '\0';
    else
        t = s + strlen(s);

    *sp = t;

    return(s);
}

/* ====================================================================== */

/* ...and then putting them back together with correct separator and quoting */

static int
raven_wls_response_field_len(char *s)
{
    int result = 0;

    while (*s) {
        if ((*s == '!') || (*s == '%'))
            result += 3;
        else
            result++;
        s++;
    }
    return(result);
}

static void
raven_wls_response_field_print(char **dp, char *s)
{
    char *d = *dp;

    while (*s) {
        if ((*s == '!') || (*s == '%')) {
            sprintf(d, "%%%02x", (int)(*s));
            d += 3;
        } else
            *d++ = *s;

        s++;
    }
    *d = '\0';

    *dp = d;
}

static char *raven_wls_response_field_quote(char **fields, int count)
{
    char *result, *t;
    int len = 0, i;

    for (i=0; i < count; i++)
        len += raven_wls_response_field_len(fields[i]);

    result = t = xmalloc(len + (count-1) + 1); /* (count-1) separators, \0 */
    for (i=0; i < 11; i++) {
        raven_wls_response_field_print(&t, fields[i]);
        *t++ = '!';
    }
    t[-1] = '\0';
    return(result);
}

/* ====================================================================== */

/* Main interface to raven_wls_response:
 *
 * Given a token of the aappropriate form, split it into its component
 * parts and then verify the RSA signature tagged on the end
 */

struct raven_wls_response *
raven_wls_response_parse(char *wls_string, char *sig_path, char **error)
{
    char *fields[13], *s;
    struct raven_wls_response *wls;
    int i;

    /* Expecting WLS version 1 response with 13 parameters */
    if (strncmp(wls_string, "1!", 2) != 0) {
        if (error)
            *error = "Not a WLS version 1 response";
        return(NULL);
    }

    if (raven_wls_response_count_fields(wls_string) != 13) {
        if (error)
            *error = "Malformed WLS response";
        return(NULL);
    }

    wls = xmalloc(sizeof(struct raven_wls_response));

    /* Split wls_string into its component parts */
    wls->data = s = strdup(wls_string);
    for (i=0; i < 13; i++) {
        fields[i] = raven_wls_response_get_single(&s);
        string_url_decode(fields[i]);
    }

    wls->ver       = fields[0];
    wls->status    = fields[1]; 
    wls->msg       = fields[2]; 
    wls->issue     = fields[3]; 
    wls->id        = fields[4]; 
    wls->url       = fields[5]; 
    wls->principal = fields[6]; 
    wls->auth      = fields[7]; 
    wls->sso       = fields[8]; 
    wls->life      = fields[9]; 
    wls->params    = fields[10]; 
    wls->kid       = fields[11]; 
    wls->sig       = fields[12]; 

    /* Combine first eleven fields quoting '|' and '%' characters */
    wls->lhs = raven_wls_response_field_quote(fields, 11);

    if (!string_isnumber(wls->kid)) {
        if (error)
            *error = "KID parameter not a number";
        raven_wls_response_free(wls);
        return(NULL);
    }

    /* Verify first eleven field using the last two */
    if (!raven_wls_response_sig_verify(wls->lhs, wls->sig,
                                       sig_path, wls->kid)) {
        if (error)
            *error = "WLS signature verify failed";
        raven_wls_response_free(wls);
        return(NULL);
    }

    /* Check fields which must be integers. Do in client for better errors? */
    if (!string_isnumber(wls->ver)) {
        if (error)
            *error = "Bad WLS response: ver field not integer";
        raven_wls_response_free(wls);
        return(NULL);
    }

    if (!string_isnumber(wls->status)) {
        if (error)
            *error = "Bad WLS response: status field not integer";
        raven_wls_response_free(wls);
        return(NULL);
    }

    return(wls);
}

/* Free data structures allocated by raven_wls_response_parse() */

void
raven_wls_response_free(struct raven_wls_response *wls)
{
    if (!wls)
        return;

    if (wls->data) free(wls->data);
    if (wls->lhs)  free(wls->lhs);
}

/* ====================================================================== */

/* Convert wls->issue field (which has format: 20080804T134807Z) into
 * time_t timestamp. Returns (time_t)(-1) if input has invalid format. */

time_t
raven_wls_response_timestamp(struct raven_wls_response *wls)
{
    struct tm tm;

    memset(&tm, 0, sizeof(struct tm));

    if (!(wls && wls->issue && (strlen(wls->issue) == 16)))
        return((time_t)(-1));

    if (sscanf(wls->issue, "%04u%02u%02uT%02u%02u%02uZ",
               &tm.tm_year, &tm.tm_mon, &tm.tm_mday,
               &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 6)
        return((time_t)(-1));

    if ((tm.tm_year <  1900) || (tm.tm_year > 2060) ||  /* Need to adjust */
        (tm.tm_mon  <=    0) || (tm.tm_mon  > 12)   ||  /* Need to adjust */
        (tm.tm_mday <=    0) || (tm.tm_mday > 31)   ||
        (tm.tm_hour <     0) || (tm.tm_hour > 23)   ||
        (tm.tm_min  <     0) || (tm.tm_min  > 59)   ||
        (tm.tm_sec  <     0) || (tm.tm_sec  > 60))      /* Leap second */
        return((time_t)(-1));

    tm.tm_isdst = 0;         /* Timestamps always GMT/Zulu */
    tm.tm_year -= 1900;      /* years since 1900 */
    tm.tm_mon  -= 1;         /* Range 0..11, just to be helpful */

    return (mktime(&tm));
}

int
raven_wls_response_check_timestamp(struct raven_wls_response *wls,
                                   int maxskew, int timeout)
{
    time_t time_now, time_raven;

    time_raven = raven_wls_response_timestamp(wls);
    if (time_raven == (time_t)(-1))
        return(0);

    time_now   = time(NULL);
    if (time_now == (time_t)(-1))
        return(0);

    if (time_raven > time_now) {
        if ((time_raven - time_now) <= maxskew)
            return(1);
    } else {
        if ((time_now - time_raven) <= (maxskew+timeout))
            return(1);
    }

    return(0);
}

char *
raven_wls_response_now()
{
    static char buf[64];
    time_t now    = time(NULL);
    struct tm *tm = gmtime(&now);

    snprintf(buf, sizeof(buf), "%04u%02u%02uT%02u%02u%02uZ",
             1900+tm->tm_year, 1+tm->tm_mon, tm->tm_mday,
             tm->tm_hour, tm->tm_min, tm->tm_sec);

    return(buf);
}

