diff --git a/src/controller/inbox.c b/src/controller/inbox.c index 9d39fee..60dd1c0 100644 --- a/src/controller/inbox.c +++ b/src/controller/inbox.c @@ -132,14 +132,6 @@ bool route_activity( struct ap_activity* act ) return false; } -bool validate_signature( struct ap_envelope* env ) -{ - // TODO: validate signature - printf( "TODO: validate signature\n" ); - - return false; -} - bool process_one() { // Items requiring cleanup @@ -163,13 +155,13 @@ bool process_one() printf( "Failed to parse envelope+activity for data/inbox/%d.json\n", id ); return false; } - validate_signature(env); + + // Validate signature + env->validated = http_signature_validate( env ); // Load activity FILE* f = fmemopen( env->body, strlen(env->body), "r" ); act = ap_activity_from_FILE(f); - fclose(f); - if( !act ) { goto failed; } // Discard delete requests diff --git a/src/model/account.c b/src/model/account.c index bd62b33..65122ab 100644 --- a/src/model/account.c +++ b/src/model/account.c @@ -10,6 +10,7 @@ // Model #include "model/server.h" #include "model/ap/account.h" +#include "model/crypto/keys.h" // Stdlib #include @@ -127,11 +128,44 @@ void account_sync_from_acitvity_pub( unsigned int account_id ) a->server = server_name; } + // Extract out the public key + char* id = strdup(ap->public_key.id); + char* key_id = NULL; + strtok_r( id, "#", &key_id ); + snprintf( filename, sizeof(filename), "data/accounts/%d/%s.pem", a->id, key_id ); + FILE* key_pem = fopen(filename,"w"); + fprintf( key_pem, "%s", ap->public_key.pem ); + fclose(key_pem); + account_save(a); ap_account_free(ap); account_free(a); } +struct crypto_keys* account_get_public_key( struct account* a, const char* key_name ) +{ + char filename[512]; + snprintf( filename, sizeof(filename), "data/accounts/%d/%s.pem", a->id, key_name ); + FILE* f = fopen( filename, "r" ); + if( !f ) { + printf( "Failed to open file %s\n", filename ); + return NULL; + } + fclose(f); + + struct crypto_keys* keys = crypto_keys_new(); + if( crypto_keys_load_public( keys, filename ) ) { + return keys; + } + + printf( "Failed to load public key from %s\n", filename ); + crypto_keys_free(keys); + return NULL; +} +struct crypto_keys* account_get_private_key( struct account* a ) +{ + return NULL; +} static void create_account_skeleton( int account_id ) { diff --git a/src/model/account.h b/src/model/account.h index a00ca7e..d0c6674 100644 --- a/src/model/account.h +++ b/src/model/account.h @@ -3,6 +3,8 @@ #include #include +struct crypto_keys; + enum account_type { at_owner = 1, @@ -38,6 +40,8 @@ struct account* account_new(); void account_free( struct account* a ); void account_save( struct account* a ); void account_sync_from_acitvity_pub( unsigned int id ); +struct crypto_keys* account_get_public_key( struct account* a, const char* key_name ); +struct crypto_keys* account_get_private_key( struct account* a ); void account_add_follower( struct account* a, struct account* follower ); void account_remove_follower( struct account* a, struct account* follower ); diff --git a/src/model/crypto/base64.c b/src/model/crypto/base64.c new file mode 100644 index 0000000..ba95131 --- /dev/null +++ b/src/model/crypto/base64.c @@ -0,0 +1,57 @@ +#include "base64.h" + +#include +#include + +char* base64_strict_encode( void* v_binary, size_t len ) +{ + // RFC 4648 - https://www.rfc-editor.org/rfc/rfc4648.html + static const char* alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + enum { pad = '=' }; + + uint8_t* binary = v_binary; + const int groups = ( len + 2 ) / 3; + const int padding = len % 3; + + char* result = malloc( groups * 4 + 1 ); + result[groups*4] = '\0'; + + uint32_t word; + char* out = result; + for( int remain = len; remain > 0; remain -= 3, binary += 3, out += 4 ) { + word = 0; + switch( remain ) { + default: + word |= binary[2] << (8*0); + case 2: + word |= binary[1] << (8*1); + case 1: + word |= binary[0] << (8*2); + } + switch( remain ) { + default: + out[3] = alphabet[ ( word >> (6*0) ) % 64 ]; + case 2: + out[2] = alphabet[ ( word >> (6*1) ) % 64 ]; + case 1: + out[1] = alphabet[ ( word >> (6*2) ) % 64 ]; + out[0] = alphabet[ ( word >> (6*3) ) % 64 ]; + } + switch( remain ) { + case 1: + out[3] = '='; + out[2] = '='; + break; + case 2: + out[3] = '='; + break; + } + } + + return result; +} +bool base64_decode( const char* str, void** v_binary, size_t* len ) +{ + return false; +} + diff --git a/src/model/crypto/base64.h b/src/model/crypto/base64.h new file mode 100644 index 0000000..68b4a3b --- /dev/null +++ b/src/model/crypto/base64.h @@ -0,0 +1,8 @@ +#pragma once + +#include +#include + +char* base64_strict_encode( void* v_binary, size_t len ); +bool base64_decode( const char* str, void** v_binary, size_t* len ); + diff --git a/src/model/crypto/http_sign.c b/src/model/crypto/http_sign.c index 5c87672..0790705 100644 --- a/src/model/crypto/http_sign.c +++ b/src/model/crypto/http_sign.c @@ -1,7 +1,10 @@ #include "http_sign.h" #include "sha256/sha256.h" +#include "model/account.h" #include "model/crypto/keys.h" +#include "model/crypto/base64.h" +#include "model/ap/inbox_envelope.h" #include #include @@ -54,6 +57,212 @@ bool http_signature_make( const char* inbox, struct crypto_keys* keys, struct ht return true; } +static void print_downcase( FILE* f, const char* str ) +{ + for(;*str;++str) { + switch( *str ) { + case 'A' ... 'Z': + fputc( *str - 'A' + 'a', f ); + break; + default: + fputc( *str, f ); + break; + } + } +} + +void print_header_line( FILE* hl, struct ap_envelope* env, const char* header_name ) +{ + fprintf( hl, "%s: ", header_name ); + + bool first = true; + for( int i = 0; i < env->headers.count; ++i ) { + if( 0 == strcasecmp(header_name,env->headers.items[i].key ) ) { + if( !first ) { + fprintf( hl, "," ); + } + fprintf( hl, "%s", env->headers.items[i].value ); + first = false; + } + } + + fprintf( hl, "\n" ); +} + +static bool validate_body_digest( struct ap_envelope* env ) +{ + // Per: https://datatracker.ietf.org/doc/html/rfc3230 + + char* digest_header = NULL; + char* content_length_header = NULL; + char* hash = NULL; + bool result = false; + + // Get the Digest header + for( int i = 0; i < env->headers.count; ++i ) { + char* key = env->headers.items[i].key; + char* value = env->headers.items[i].value; + if( 0 == strcasecmp( "Digest", key ) ) { + free(digest_header); + digest_header = strdup(value); + } else if( 0 == strcasecmp( "Content-Length", key ) ) { + free(content_length_header); + content_length_header = strdup(value); + } + } + if( !digest_header ) { goto failed; } + if( !content_length_header ) { goto failed; } + + // Validate the body length against the Content-Length header + int content_length = -1; + sscanf( content_length_header, "%d", &content_length ); + if( strlen(env->body) != content_length ) { + printf( "Content-Length mismatch\n" ); + goto failed; + } + + char* digest = NULL; + char* algo = strtok_r(digest_header, "=", &digest ); + + // Only implementing SHA-256 + if( 0 != strcmp( algo, "SHA-256") ) { goto failed; } + + // Calculate sha-256 hash + char raw_hash[32]; + sha256_easy_hash( env->body, content_length, raw_hash ); + hash = base64_strict_encode(raw_hash,32); + + result = ( 0 == strcmp(digest,hash) ); + +cleanup: + free(hash); + free(digest_header); + return result; +failed: + result = false; + goto cleanup; +} + +bool http_signature_validate( struct ap_envelope* env ) +{ + char* signature_header = NULL; + char* date_header = NULL; + char* algorithm = NULL; + char* key_id = NULL; + char* headers = NULL; + char* signature = NULL; + bool result = false; + + // Get the Signature header + for( int i = 0; i < env->headers.count; ++i ) { + char* key = env->headers.items[i].key; + char* value = env->headers.items[i].value; + if( 0 == strcasecmp( "Signature", key ) ) { + free(signature_header); + signature_header = strdup(value); + } else if( 0 == strcasecmp( "Date", key ) ) { + free(date_header); + date_header = strdup(value); + } + } + if( !signature_header ) { return false; } + printf( "Found Signature: %s\n", signature_header ); + + // Validate time + if( date_header ) { + printf( "TODO: validate received within window\n" ); + } + + // Break apart the header + char* rem = NULL; + for( char* part = strtok_r(signature_header,",",&rem); part; part = strtok_r(NULL,",",&rem) ) { + // Split apart the key="value" pair + char* key; + char* value; + key = strtok_r( part, "=", &value ); + + // Strip the "" off + if( value[0] != '\"' ) { goto failed; } + value = &value[1]; + int len = strlen(value); + if( value[len-1] != '\"' ) { goto failed; } + value[len-1] = '\0'; + + // Handle parts + if( 0 == strcmp(key,"keyId") ) { + key_id = value; + } else if( 0 == strcmp(key,"algorithm") ) { + algorithm = value; + } else if( 0 == strcmp(key,"headers") ) { + headers = value; + } else if( 0 == strcmp(key,"signature") ) { + signature = value; + } + + printf( "\t%s = %s\n", key, value ); + } + + // Make sure we got all the required information + if( !signature ) { goto failed; } + if( !headers ) { goto failed; } + if( !key_id ) { goto failed; } + + // Get actor URI + char* key_name = NULL; + char* actor_uri = strtok_r( key_id, "#", &key_name ); + if( !actor_uri ) { goto failed; } + + // Get the account + struct account* actor = account_fetch_from_uri(actor_uri); + if( !actor ) { printf( "failed to load account for %s\n", actor_uri ); goto failed; } + + // Get the public key + struct crypto_keys* keys = account_get_public_key( actor, key_name ); + if( !keys ) { goto failed; } + + // Create the hash line + char* hash_line = NULL; + size_t hash_line_size = 0; + FILE* hl = open_memstream( &hash_line, &hash_line_size ); + + for( char* part = strtok_r( headers, " ", &rem ); part; part = strtok_r(NULL," ",&rem) ) { + printf( "header: %s\n", part ); + if( 0 == strcmp(part,"(request-target)") ) { + fprintf( hl, "(request-target): post /inbox\n" ); + } else if( 0 == strcmp(part,"digest") ) { + if( !validate_body_digest( env ) ) { + printf( "Digest validation failed\n" ); + goto failed; + } + print_header_line( hl, env, part ); + } else { + print_header_line( hl, env, part ); + } + } + fclose(hl); + + hash_line = realloc(hash_line, hash_line_size+1 ); + hash_line[hash_line_size] = '\0'; + printf( "\nhash_line:\n%s\n", hash_line ); + + char raw_hash[32]; + sha256_easy_hash( hash_line, hash_line_size, raw_hash ); + + char* signature_bin = NULL; + size_t signature_len = 0; + if( !base64_decode( signature, (void**)&signature_bin, &signature_len ) ) { + printf( "Failed to decode base64 signature\n" ); + goto failed; + } + + printf( "TODO: validate HTTP signature - so far, so good\n" ); +cleanup: + free(signature_header); + return result; +failed: + result = false; + goto cleanup; +} void http_signature_free( struct http_signature* sign ) { diff --git a/src/model/crypto/http_sign.h b/src/model/crypto/http_sign.h index 8a26655..9197b7a 100644 --- a/src/model/crypto/http_sign.h +++ b/src/model/crypto/http_sign.h @@ -4,6 +4,7 @@ struct crypto_keys; +struct ap_envelope; struct http_signature { char* host; @@ -13,5 +14,5 @@ struct http_signature bool http_signature_make( const char* inbox, struct crypto_keys* keys, struct http_signature* sign ); void http_signature_free( struct http_signature* sign ); -bool http_signature_validate( struct http_signature* sign ); +bool http_signature_validate( struct ap_envelope* env ); diff --git a/src/model/crypto/keys.c b/src/model/crypto/keys.c index 76e3963..709a29f 100644 --- a/src/model/crypto/keys.c +++ b/src/model/crypto/keys.c @@ -1,5 +1,7 @@ #include "keys.h" +#include "model/crypto/base64.h" + #include #include #include @@ -48,54 +50,20 @@ bool crypto_keys_load_private( struct crypto_keys* keys, const char* filename ) return !!keys->privkey; } - -char* base64_strict_encode( void* v_binary, size_t len ) +bool crypto_keys_load_public( struct crypto_keys* keys, const char* filename ) { - // RFC 4648 - https://www.rfc-editor.org/rfc/rfc4648.html - static const char* alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - enum { pad = '=' }; - - uint8_t* binary = v_binary; - const int groups = ( len + 2 ) / 3; - const int padding = len % 3; - - char* result = malloc( groups * 4 + 1 ); - result[groups*4] = '\0'; - - uint32_t word; - char* out = result; - for( int remain = len; remain > 0; remain -= 3, binary += 3, out += 4 ) { - word = 0; - switch( remain ) { - default: - word |= binary[2] << (8*0); - case 2: - word |= binary[1] << (8*1); - case 1: - word |= binary[0] << (8*2); - } - switch( remain ) { - default: - out[3] = alphabet[ ( word >> (6*0) ) % 64 ]; - case 2: - out[2] = alphabet[ ( word >> (6*1) ) % 64 ]; - case 1: - out[1] = alphabet[ ( word >> (6*2) ) % 64 ]; - out[0] = alphabet[ ( word >> (6*3) ) % 64 ]; - } - switch( remain ) { - case 1: - out[3] = '='; - out[2] = '='; - break; - case 2: - out[3] = '='; - break; - } + if( keys->pubkey ) { + EVP_PKEY_free( keys->pubkey ); + keys->pubkey = NULL; } + FILE* f = fopen(filename,"r"); - return result; + keys->pubkey = PEM_read_PUBKEY( f, NULL, NULL, NULL ); + fclose(f); + + return !!keys->pubkey; } + char* crypto_keys_sign( struct crypto_keys* keys, void* data, unsigned int size ) { char buffer[512];