You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

302 lines
8.0 KiB
C

#include "http_sign.h"
#include "sha256/sha256.h"
#include "http/server/header.h"
#include "util/format.h"
#include "model/account.h"
#include "model/crypto/keys.h"
#include "model/crypto/base64.h"
#include "model/inbox_envelope.h"
#include <stdlib.h>
#include <stdio.h>
#include <strings.h>
#include <string.h>
#include <time.h>
bool http_signature_make( const char* inbox, struct crypto_keys* keys, struct http_signature* sign, const char* postdata )
{
memset(sign,0,sizeof(*sign));
if( 0 != strncmp( "https://", inbox, 8 ) ) {
printf( "! Invalid inbox: %s\n", inbox );
return false;
}
// Separate host and path from inbox
char* path = index( &inbox[8], '/' );
char* host = sign->host = strndup( &inbox[8], path - &inbox[8] );
// Build HTTP date - TODO: move to separate file
time_t utc_time = time(NULL);
struct tm gmtime_data;
gmtime_r( &utc_time, &gmtime_data );
static const char* day_of_week[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
static const char* month_of_year[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
char date[32];
snprintf( date, sizeof(date),
"%s, %02d %s %d %02d:%02d:%02d GMT",
day_of_week[ gmtime_data.tm_wday ],
gmtime_data.tm_mday,
month_of_year[ gmtime_data.tm_mon ],
gmtime_data.tm_year + 1900,
gmtime_data.tm_hour,
gmtime_data.tm_min,
gmtime_data.tm_sec
);
sign->date = strdup(date);
// Calculate Digest
unsigned char raw_hash[32];
sign->content_length = strlen(postdata);
sha256_easy_hash( postdata, sign->content_length, raw_hash );
char* base64_hash = base64_strict_encode(raw_hash,32);
sign->digest = aformat( "SHA-256=%s", base64_hash );
free(base64_hash);
// Build hash line
char hash_line[512];
snprintf( hash_line, 512,
"(request-target): post %s\nhost: %s\ndate: %s\ncontent-length: %d\ndigest: %s",
path, host, date, sign->content_length, sign->digest
);
printf( "\nbuilding hash_line = %s|\n\n", hash_line );
// sign
sign->signature = crypto_keys_sign( keys, hash_line, strlen(hash_line) );
printf( "Signature: %s\n", sign->signature );
return true;
}
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;
}
}
}
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
unsigned 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);
free(content_length_header);
return result;
failed:
result = false;
goto cleanup;
}
bool http_signature_validate( struct ap_envelope* env, const char* request_target, const char* expected_actor )
{
char* signature_header = NULL;
char* date_header = NULL;
struct crypto_keys* keys = NULL;
bool result = false;
char* hash_line = NULL;
struct account* actor = NULL;
// 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;
//char* algorithm = NULL;
char* key_id = NULL;
char* headers = NULL;
char* signature = 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 ) { printf( "! No signature\n" ); goto failed; }
if( !headers ) { printf( "! No headers\n" ); goto failed; }
if( !key_id ) { printf( "! no key_id\n" ); goto failed; }
// Get actor URI
if( 0 == strcmp(key_id,"Test") ) {
keys = crypto_keys_new();
if( !crypto_keys_load_public( keys, "assets/test.public.pem" ) ) {
printf( "! Failed to load public key\n" );
goto failed;
}
} else {
char* key_name = NULL;
char* actor_uri = strtok_r( key_id, "#", &key_name );
if( !actor_uri ) {
printf( "! Failed to get actor\n" );
goto failed;
}
if( 0 != strcmp( actor_uri, expected_actor ) ) {
printf( "! Signature doesn't match actor (Expected %s, got %s)\n", expected_actor, actor_uri );
goto failed;
}
// Get the account
actor = account_from_uri_or_fetch(actor_uri);
if( !actor ) {
printf( "! failed to load account for %s\n", actor_uri );
goto failed;
}
// Get the public key
keys = account_get_public_key( actor, key_name );
if( !keys ) { goto failed; }
}
// Create the hash line
size_t hash_line_size = 0;
FILE* hl = open_memstream( &hash_line, &hash_line_size );
bool is_first = true;
for( char* part = strtok_r( headers, " ", &rem ); part; part = strtok_r(NULL," ",&rem) ) {
//printf( "header: %s\n", part );
if( !is_first ) {
fprintf( hl, "\n" );
}
if( 0 == strcmp(part,"(request-target)") ) {
fprintf( hl, "(request-target): %s", request_target );
} else if( 0 == strcmp(part,"digest") ) {
if( !validate_body_digest( env ) ) {
printf( "! Digest validation failed" );
goto failed;
}
print_header_line( hl, env, part );
} else {
print_header_line( hl, env, part );
}
is_first = false;
}
fclose(hl);
hash_line = realloc(hash_line, hash_line_size+1 );
hash_line[hash_line_size] = '\0';
//printf( "\nhash_line:\n%s|\n\n", hash_line );
//result = crypto_keys_verify( keys, raw_hash, 32, signature );
result = crypto_keys_verify( keys, hash_line, hash_line_size, signature );
if( !result ) {
printf( "! Signature is not valid\n" );
goto failed;
}
result = true;
cleanup:
crypto_keys_free(keys);
account_free(actor);
free(signature_header);
free(date_header);
free(hash_line);
return result;
failed:
result = false;
goto cleanup;
}
void http_signature_free( struct http_signature* sign )
{
free(sign->host);
free(sign->date);
free(sign->signature);
free(sign->digest);
}