From b5a229267b4e494fac3e9702c71a5ce5a758bae2 Mon Sep 17 00:00:00 2001 From: teknomunk Date: Tue, 27 Dec 2022 07:28:16 -0600 Subject: [PATCH] Start test suite, implement base64_decode, validate http signatures --- assets/README.md | 7 +++ assets/test.private.pem | 15 +++++ assets/test.public.pem | 6 ++ src/controller/inbox.c | 2 +- src/controller/test.c | 10 ++++ src/controller/test.h | 4 ++ src/controller/test/.crypt-f203bfe5.d | 8 +++ src/controller/test/crypto.c | 79 ++++++++++++++++++++++++++ src/controller/test/crypto.h | 6 ++ src/json | 2 +- src/main.c | 7 ++- src/model/crypto/base64.c | 56 +++++++++++++++++- src/model/crypto/http_sign.c | 81 +++++++++++++++++---------- src/model/crypto/http_sign.h | 2 +- src/model/crypto/keys.c | 62 +++++++++++++++++++- src/model/crypto/keys.h | 2 +- 16 files changed, 307 insertions(+), 42 deletions(-) create mode 100644 assets/README.md create mode 100644 assets/test.private.pem create mode 100644 assets/test.public.pem create mode 100644 src/controller/test.c create mode 100644 src/controller/test.h create mode 100644 src/controller/test/.crypt-f203bfe5.d create mode 100644 src/controller/test/crypto.c create mode 100644 src/controller/test/crypto.h diff --git a/assets/README.md b/assets/README.md new file mode 100644 index 0000000..b2f3876 --- /dev/null +++ b/assets/README.md @@ -0,0 +1,7 @@ +The private/public key pair included here is for the test suite, and is publicly available +at the following location: + + https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-08#appendix-C + +Messages about these two keys being checked into the source repository will be ignored. + diff --git a/assets/test.private.pem b/assets/test.private.pem new file mode 100644 index 0000000..425518a --- /dev/null +++ b/assets/test.private.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF +NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F +UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB +AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA +QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK +kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg +f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u +412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc +mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7 +kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA +gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW +G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI +7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA== +-----END RSA PRIVATE KEY----- diff --git a/assets/test.public.pem b/assets/test.public.pem new file mode 100644 index 0000000..b3bbf6c --- /dev/null +++ b/assets/test.public.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDCFENGw33yGihy92pDjZQhl0C3 +6rPJj+CvfSC8+q28hxA161QFNUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6 +Z4UMR7EOcpfdUE9Hf3m/hs+FUR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJw +oYi+1hqp1fIekaxsyQIDAQAB +-----END PUBLIC KEY----- diff --git a/src/controller/inbox.c b/src/controller/inbox.c index 60dd1c0..fed7027 100644 --- a/src/controller/inbox.c +++ b/src/controller/inbox.c @@ -157,7 +157,7 @@ bool process_one() } // Validate signature - env->validated = http_signature_validate( env ); + env->validated = http_signature_validate( env, "post /inbox" ); // Load activity FILE* f = fmemopen( env->body, strlen(env->body), "r" ); diff --git a/src/controller/test.c b/src/controller/test.c new file mode 100644 index 0000000..cd731ff --- /dev/null +++ b/src/controller/test.c @@ -0,0 +1,10 @@ +#include "test.h" +#include "test/crypto.h" + +#include + +void built_in_test() +{ + if( !test_crypto() ) { printf( "[FAIL] test_crypto\n" ); } +} + diff --git a/src/controller/test.h b/src/controller/test.h new file mode 100644 index 0000000..3d953e9 --- /dev/null +++ b/src/controller/test.h @@ -0,0 +1,4 @@ +#pragma once + +void built_in_test(); + diff --git a/src/controller/test/.crypt-f203bfe5.d b/src/controller/test/.crypt-f203bfe5.d new file mode 100644 index 0000000..705c76f --- /dev/null +++ b/src/controller/test/.crypt-f203bfe5.d @@ -0,0 +1,8 @@ +src/controller/test/.crypt-f203bfe5.o: src/controller/test/crypt.c \ + /usr/include/stdc-predef.h src/controller/test/crypto.h \ + /usr/lib/gcc/x86_64-pc-linux-gnu/12.2.0/include/stdbool.h \ + src/model/crypto/http_sign.h +/usr/include/stdc-predef.h: +src/controller/test/crypto.h: +/usr/lib/gcc/x86_64-pc-linux-gnu/12.2.0/include/stdbool.h: +src/model/crypto/http_sign.h: diff --git a/src/controller/test/crypto.c b/src/controller/test/crypto.c new file mode 100644 index 0000000..4878b82 --- /dev/null +++ b/src/controller/test/crypto.c @@ -0,0 +1,79 @@ +#include "crypto.h" + +#include "http_server/header.h" +#include "model/ap/inbox_envelope.h" +#include "model/crypto/keys.h" +#include "model/crypto/http_sign.h" + +#include +#include +#include + +static bool test_signatures() +{ + struct crypto_keys* keys = crypto_keys_new(); + + if( !crypto_keys_load_public( keys, "assets/test.public.pem" ) ) { + printf( "[FAIL] unable to load assets/test.public.pem\n" ); + return false; + } + + if( !crypto_keys_load_private( keys, "assets/test.private.pem" ) ) { + printf( "[FAIL] unable to load assets/test.private.pem\n" ); + return false; + } + + char* data = "This is a test of the emergency broadcast system."; + char* sign = crypto_keys_sign( keys, data, strlen(data) ); + + if( !sign ) { + printf( "[FAIL] unable to sign data\n" ); + return false; + } + free(sign); + + crypto_keys_free(keys); + + return true; +} + +static bool test_http_signature() +{ + // https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-08#C.3 All Headers Test + struct http_header headers[] = { + { .key = "Host", .value = "example.com" }, + { .key = "Date", .value = "Thu, 05 Jan 2014 21:31:40 GMT" }, + { .key = "Content-Type", .value = "application/json" }, + { .key = "Digest", .value = "SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=" }, + { .key = "Content-Length", .value = "18" }, + { .key = "Signature", .value = + "keyId=\"Test\",algorithm=\"rsa-sha256\"," + "headers=\"(request-target) host date content-type digest content-length\"," + "signature=\"Ef7MlxLXoBovhil3AlyjtBwAL9g4TN3tibLj7uuNB3CROat/9Kae" + "Q4hW2NiJ+pZ6HQEOx9vYZAyi+7cmIkmJszJCut5kQLAwuX+Ms/mUFvpKlSo9StS" + "2bMXDBNjOh4Auj774GFj4gwjS+3NhFeoqyr/MuN6HsEnkvn6zdgfE2i0=\"" + }, + }; + + struct ap_envelope env = { + .when = "1388957500000000000", + .headers = { + .items = headers, + .count = sizeof(headers) / sizeof(headers[0]), + }, + .validated = false, + .body = "{\"hello\": \"world\"}", + }; + + return http_signature_validate( &env, "post /foo?param=value&pet=dog" ); +} + +bool test_crypto() +{ + bool result = true; + if( !test_signatures() ) { printf( "[FAIL] test_signatures()\n" ); return false; } + if( !test_http_signature() ) { printf( "[FAIL] test_http_signature()\n" ); return false; } + + return true; +} + diff --git a/src/controller/test/crypto.h b/src/controller/test/crypto.h new file mode 100644 index 0000000..2408593 --- /dev/null +++ b/src/controller/test/crypto.h @@ -0,0 +1,6 @@ +#pragma once + +#include + +bool test_crypto(); + diff --git a/src/json b/src/json index 84141b7..5205ad4 160000 --- a/src/json +++ b/src/json @@ -1 +1 @@ -Subproject commit 84141b7260faf667d733d571ea0e81ac42fb0907 +Subproject commit 5205ad49d932341983ef5c8ef5b0c09433f8e06f diff --git a/src/main.c b/src/main.c index 6894801..76548c6 100644 --- a/src/main.c +++ b/src/main.c @@ -13,6 +13,7 @@ #include "controller/main.h" #include "controller/inbox.h" #include "controller/outbox.h" +#include "controller/test.h" #include @@ -75,8 +76,9 @@ int main( int argc, char* argv[] ) int section = 0; sscanf(argv[1],"--section=%d",§ion); switch( section ) { - case 1: process_inbox(); return 0; - case 2: process_outbox(); return 0; + case 1: process_inbox(); goto exit; + case 2: process_outbox(); goto exit; + case 3: built_in_test(); goto exit; case 100: develop(); return 0; } @@ -100,6 +102,7 @@ int main( int argc, char* argv[] ) int code = 0; if( !run_webserver(args) ) { code = 1; } +exit: app_args_release(args); return code; diff --git a/src/model/crypto/base64.c b/src/model/crypto/base64.c index ba95131..7c5cd59 100644 --- a/src/model/crypto/base64.c +++ b/src/model/crypto/base64.c @@ -2,11 +2,14 @@ #include #include +#include +#include + +static const char* alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 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; @@ -52,6 +55,55 @@ char* base64_strict_encode( void* v_binary, size_t len ) } bool base64_decode( const char* str, void** v_binary, size_t* len ) { - return false; + int input_length = strlen(str); + + uint8_t* data = malloc( ( input_length * 6 + 7 ) / 8 ); + *v_binary = data; + *len = 0; + + uint32_t buffer = 0; + int bits = 0; + uint8_t part; + + for(;;) { + //printf( " - Input %02X (%c)\n", *str, *str ); + switch( *str ) { + case 'A' ... 'Z': part = *str - 'A'; break; + case 'a' ... 'z': part = *str - 'a' + 26; break; + case '0' ... '9': part = *str - '0' + 52; break; + case '+': part = 62; break; + case '/': part = 63; break; + case '=': goto finish; + case '\0': + goto done; + default: + free( *v_binary ); + *len = 0; + return false; + }; + ++str; + + buffer = ( buffer << 6 ) | part; + bits += 6; + //printf( "buffer=%08X, bits=%d\n", buffer, bits ); + + if( bits > 8 ) { + uint8_t byte = buffer >> ( bits % 8 ); + *data = byte; + //printf( " - Output: %02X (%c)\n", byte, byte ); + buffer &= ( 1 << (bits%8) ) - 1; + *len += 1; + ++data; + bits -= 8; + } + } +done: + *len += 1; +finish: + uint8_t byte = buffer >> ( bits % 8 ); + *data = byte; + ++data; + bits = 0; + return true; } diff --git a/src/model/crypto/http_sign.c b/src/model/crypto/http_sign.c index 0790705..db46f8b 100644 --- a/src/model/crypto/http_sign.c +++ b/src/model/crypto/http_sign.c @@ -17,7 +17,7 @@ bool http_signature_make( const char* inbox, struct crypto_keys* keys, struct ht memset(sign,0,sizeof(*sign)); if( 0 != strncmp( "https://", inbox, 8 ) ) { - printf( "Invalid inbox: %s\n", inbox ); + printf( "! Invalid inbox: %s\n", inbox ); return false; } @@ -85,8 +85,6 @@ void print_header_line( FILE* hl, struct ap_envelope* env, const char* header_na first = false; } } - - fprintf( hl, "\n" ); } static bool validate_body_digest( struct ap_envelope* env ) @@ -137,20 +135,18 @@ static bool validate_body_digest( struct ap_envelope* env ) 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 ) +bool http_signature_validate( struct ap_envelope* env, const char* request_target ) { char* signature_header = NULL; char* date_header = NULL; - char* algorithm = NULL; - char* key_id = NULL; - char* headers = NULL; - char* signature = NULL; + struct crypto_keys* keys = NULL; bool result = false; // Get the Signature header @@ -166,7 +162,7 @@ bool http_signature_validate( struct ap_envelope* env ) } } if( !signature_header ) { return false; } - printf( "Found Signature: %s\n", signature_header ); + //printf( "Found Signature: %s\n", signature_header ); // Validate time if( date_header ) { @@ -175,6 +171,10 @@ bool http_signature_validate( struct ap_envelope* env ) // 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; @@ -199,65 +199,84 @@ bool http_signature_validate( struct ap_envelope* env ) signature = value; } - printf( "\t%s = %s\n", key, 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; } + 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 - char* key_name = NULL; - char* actor_uri = strtok_r( key_id, "#", &key_name ); - if( !actor_uri ) { goto failed; } + 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; + } - // 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 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; } + // Get the public key + 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 ); + bool is_first = true; for( char* part = strtok_r( headers, " ", &rem ); part; part = strtok_r(NULL," ",&rem) ) { - printf( "header: %s\n", part ); + //printf( "header: %s\n", part ); + if( !is_first ) { + fprintf( hl, "\n" ); + } if( 0 == strcmp(part,"(request-target)") ) { - fprintf( hl, "(request-target): post /inbox\n" ); + fprintf( hl, "(request-target): %s", request_target ); } else if( 0 == strcmp(part,"digest") ) { if( !validate_body_digest( env ) ) { - printf( "Digest validation failed\n" ); + 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", hash_line ); + //printf( "\nhash_line:\n%s|\n\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" ); + //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; } - printf( "TODO: validate HTTP signature - so far, so good\n" ); + result = true; cleanup: + crypto_keys_free(keys); free(signature_header); + free(date_header); + free(hash_line); return result; failed: result = false; diff --git a/src/model/crypto/http_sign.h b/src/model/crypto/http_sign.h index 9197b7a..f94283e 100644 --- a/src/model/crypto/http_sign.h +++ b/src/model/crypto/http_sign.h @@ -14,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 ap_envelope* env ); +bool http_signature_validate( struct ap_envelope* env, const char* request_target ); diff --git a/src/model/crypto/keys.c b/src/model/crypto/keys.c index 709a29f..8fa558f 100644 --- a/src/model/crypto/keys.c +++ b/src/model/crypto/keys.c @@ -1,6 +1,7 @@ #include "keys.h" #include "model/crypto/base64.h" +#include "sha256/sha256.h" #include #include @@ -44,6 +45,7 @@ bool crypto_keys_load_private( struct crypto_keys* keys, const char* filename ) } FILE* f = fopen(filename,"r"); + if( !f ) { return false; } keys->privkey = PEM_read_PrivateKey( f, NULL, NULL, NULL ); fclose(f); @@ -57,6 +59,7 @@ bool crypto_keys_load_public( struct crypto_keys* keys, const char* filename ) keys->pubkey = NULL; } FILE* f = fopen(filename,"r"); + if( !f ) { return false; } keys->pubkey = PEM_read_PUBKEY( f, NULL, NULL, NULL ); fclose(f); @@ -66,13 +69,16 @@ bool crypto_keys_load_public( struct crypto_keys* keys, const char* filename ) char* crypto_keys_sign( struct crypto_keys* keys, void* data, unsigned int size ) { - char buffer[512]; char* result = NULL; char* sign_binary = NULL; EVP_PKEY_CTX* ctx = NULL; if( !keys->privkey ) { return NULL; } + // hash data with SHA-256 + char hash[32]; + sha256_easy_hash( data, size, hash ); + // Setup for signature // Code based on https://www.openssl.org/docs/man3.1/man3/EVP_PKEY_sign.html ctx = EVP_PKEY_CTX_new(keys->privkey, NULL /* no engine */); @@ -88,10 +94,11 @@ char* crypto_keys_sign( struct crypto_keys* keys, void* data, unsigned int size sign_binary = malloc(siglen); if( !sign_binary ) { goto failed; } - int retcode = EVP_PKEY_sign(ctx, sign_binary, &siglen, data, size); + int retcode = EVP_PKEY_sign(ctx, sign_binary, &siglen, hash, 32); if( retcode <= 0 ) { + char buffer[512]; ERR_error_string_n( retcode, buffer, sizeof(buffer) ); - printf( "Failed to create signature: %s\n", buffer ); + printf( "! Failed to create signature: %s\n", buffer ); goto failed; } @@ -105,3 +112,52 @@ failed: goto cleanup; } +bool crypto_keys_verify( struct crypto_keys* keys, void* data, unsigned int size, char* signature ) +{ + bool result = false; + char* sign_binary = NULL; + EVP_PKEY_CTX* ctx = NULL; + char* signature_bin = NULL; + + if( !keys->pubkey ) { goto failed; } + + // hash data with SHA-256 + char hash[32]; + sha256_easy_hash( data, size, hash ); + + // Decode the signature + size_t signature_len = 0; + if( !base64_decode( signature, (void**)&signature_bin, &signature_len ) ) { + printf( "! Failed to decode base64 signature\n" ); + goto failed; + } + + // Setup for signature + // Code based on https://www.openssl.org/docs/man3.1/man3/EVP_PKEY_verify.html + ctx = EVP_PKEY_CTX_new(keys->pubkey, NULL /* no engine */); + if( !ctx ) { goto failed; } + + if( EVP_PKEY_verify_init(ctx) <= 0 ) { goto failed; } + if( EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PADDING) <= 0 ) { goto failed; } + if( EVP_PKEY_CTX_set_signature_md(ctx, EVP_sha256()) <= 0 ) { goto failed; } + + int retcode = EVP_PKEY_verify(ctx, signature_bin, signature_len, hash, 32 ); + if( retcode < 0 ) { + char buffer[512]; + ERR_error_string_n( retcode, buffer, sizeof(buffer) ); + printf( "! Failed to verify signature: %s\n", buffer ); + goto failed; + } + + if( retcode == 1 ) { + result = true; + } +cleanup: + free(signature_bin); + EVP_PKEY_CTX_free(ctx); + return result; +failed: + result = false; + goto cleanup; +} + diff --git a/src/model/crypto/keys.h b/src/model/crypto/keys.h index 774abd8..d8fb63c 100644 --- a/src/model/crypto/keys.h +++ b/src/model/crypto/keys.h @@ -12,7 +12,7 @@ bool crypto_keys_load_public ( struct crypto_keys* keys, const char* filename ); bool crypto_keys_load_private( struct crypto_keys* keys, const char* filename ); char* crypto_keys_sign( struct crypto_keys* keys, void* data, unsigned int size ); -bool crypto_keys_verify( struct crypto_keys* keys, void* data, unsigned int size ); +bool crypto_keys_verify( struct crypto_keys* keys, void* data, unsigned int size, char* signature ); char* base64_strict_encode( void* v_binary, size_t len );