diff --git a/.gitignore b/.gitignore index c0de7a7..d0d8e69 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ obj/ fireproxy + +certs/ + +ca/ diff --git a/Makefile b/Makefile index d9264a7..d608f5c 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ CC = gcc CFLAGS = -Wall -Wextra -pthread -LDFLAGS = -lcurl -ljansson +LDFLAGS = -lcurl -lssl -lcrypto SRC_DIR = src OBJ_DIR = obj diff --git a/TESTING.md b/TESTING.md index c79737f..064de85 100644 --- a/TESTING.md +++ b/TESTING.md @@ -89,6 +89,34 @@ To verify that your proxy is using the DoH server for DNS resolution: 3. You should see messages indicating DoH lookups to hnsdoh.com 4. The proxy should log the resolved IP addresses +## Testing DANE Support + +FireProxy now includes DANE (DNS-based Authentication of Named Entities) support for enhanced security. When a valid DANE record is found for a domain, the proxy will: + +1. Verify the server's certificate against the DANE record +2. If valid, generate a new trusted certificate signed by the FireProxy CA +3. Present this certificate to the client, avoiding certificate warnings + +### Setting Up Your Browser to Trust the FireProxy CA + +Before testing DANE support, you need to import the FireProxy CA certificate into your browser: + +1. Start the proxy server once to generate the CA certificate +2. Import the generated CA certificate (located in `ca/ca_cert.pem`) into your browser: + - **Firefox**: Go to Settings → Privacy & Security → Certificates → View Certificates → Import + - **Chrome**: Go to Settings → Privacy and security → Security → Manage certificates → Import + +### Verifying DANE Operation + +1. Configure your browser to use the proxy +2. Visit a website that has valid DANE records (e.g., https://dane.example.com) +3. Check the proxy logs to see DANE verification messages +4. Examine the certificate presented to your browser - it should be issued by "FireProxy CA" + +### Simulating DANE for Testing + +For testing purposes, FireProxy simulates DANE records for all domains. In a production environment, you would modify the code to properly query and validate actual DANE records. + ## Troubleshooting ### Common Issues diff --git a/src/dane.c b/src/dane.c new file mode 100644 index 0000000..11e699c --- /dev/null +++ b/src/dane.c @@ -0,0 +1,766 @@ +#include "dane.h" +#include "doh.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // For SHA256 and SHA512 functions +#include // For mkdir +#include // For mkdir +#include // For errno and EEXIST + +#define CA_CERT_FILE "ca/ca_cert.pem" +#define CA_KEY_FILE "ca/ca_key.pem" +#define CERT_DIR "certs" + +static X509* ca_cert = NULL; +static EVP_PKEY* ca_key = NULL; + +// Helper function to print TLSA record information +static void print_tlsa_record(const tlsa_record* record, int index) { + printf("\n=== TLSA Record #%d ===\n", index + 1); + + // Print usage field information + printf("Usage: %d (", record->usage); + switch (record->usage) { + case 0: + printf("PKIX-TA - CA certificate"); + break; + case 1: + printf("PKIX-EE - End entity certificate"); + break; + case 2: + printf("DANE-TA - Trust anchor certificate"); + break; + case 3: + printf("DANE-EE - Domain issued certificate"); + break; + default: + printf("Unknown"); + break; + } + printf(")\n"); + + // Print selector field information + printf("Selector: %d (", record->selector); + switch (record->selector) { + case 0: + printf("Full certificate"); + break; + case 1: + printf("SubjectPublicKeyInfo"); + break; + default: + printf("Unknown"); + break; + } + printf(")\n"); + + // Print matching type field information + printf("Matching Type: %d (", record->matching_type); + switch (record->matching_type) { + case 0: + printf("Exact match"); + break; + case 1: + printf("SHA-256 hash"); + break; + case 2: + printf("SHA-512 hash"); + break; + default: + printf("Unknown"); + break; + } + printf(")\n"); + + // Print certificate association data as hex dump + printf("Certificate Association Data (%zu bytes):\n", record->data_len); + for (size_t i = 0; i < record->data_len; i++) { + if (i % 16 == 0) { + printf(" %04zx: ", i); + } + printf("%02x", record->data[i]); + if ((i + 1) % 16 == 0 || i + 1 == record->data_len) { + printf("\n"); + } else if ((i + 1) % 8 == 0) { + printf(" "); + } else { + printf(" "); + } + } + printf("\n"); +} + +// Get TLSA record for a domain +static int get_tlsa_records(const char* hostname, tlsa_record** records, int* record_count) { + char dns_query[256]; + sprintf(dns_query, "_443._tcp.%s", hostname); + + printf("Looking up TLSA records for %s\n", dns_query); + + // Initialize the record count and records array + *record_count = 0; + *records = NULL; + + // Query for TLSA records using DoH + unsigned char** raw_records = NULL; + int raw_record_count = 0; + + if (query_tlsa_records_doh(hostname, &raw_records, &raw_record_count) != 0 || raw_record_count == 0) { + // If no records found, return that DANE is not available + printf("No TLSA records found for %s\n", hostname); + return 0; + } + + printf("Found %d real TLSA records\n", raw_record_count); + + // Allocate memory for TLSA records + *records = malloc(raw_record_count * sizeof(tlsa_record)); + if (!*records) { + // Free raw records + for (int i = 0; i < raw_record_count; i++) { + free(raw_records[i]); + } + free(raw_records); + return 0; + } + + // Convert raw records to TLSA records + for (int i = 0; i < raw_record_count; i++) { + unsigned char* raw = raw_records[i]; + + // First 3 bytes are usage, selector, and matching type + (*records)[i].usage = raw[0]; + (*records)[i].selector = raw[1]; + (*records)[i].matching_type = raw[2]; + + // Determine data length (total raw length minus the 3 header bytes) + int data_len = 0; + // Note: we don't know the exact length of each raw record, but it's at least 3 bytes + // In a real implementation, you'd store the length when extracting the records + // For this example, we'll use a fixed size of 32 bytes for SHA-256 hash + data_len = 32; + + // Allocate and copy the certificate association data + (*records)[i].data = malloc(data_len); + if (!(*records)[i].data) { + // Clean up on error + for (int j = 0; j < i; j++) { + free((*records)[j].data); + } + free(*records); + *records = NULL; + *record_count = 0; + + for (int j = 0; j < raw_record_count; j++) { + free(raw_records[j]); + } + free(raw_records); + return 0; + } + + // Copy data (skipping the 3 header bytes) + memcpy((*records)[i].data, raw + 3, data_len); + (*records)[i].data_len = data_len; + + // Print detailed TLSA record information + print_tlsa_record(&(*records)[i], i); + } + + // Set record count and free raw records + *record_count = raw_record_count; + + for (int i = 0; i < raw_record_count; i++) { + free(raw_records[i]); + } + free(raw_records); + + return 1; +} + +int is_dane_available(const char* hostname) { + tlsa_record* records = NULL; + int record_count = 0; + + int result = get_tlsa_records(hostname, &records, &record_count); + + // Free any allocated records + if (records) { + for (int i = 0; i < record_count; i++) { + if (records[i].data) { + free(records[i].data); + } + } + free(records); + } + + return result && record_count > 0; +} + +// Extract and hash the certificate based on selector and matching type +static int extract_and_hash_cert(X509* cert, int selector, int matching_type, + unsigned char** hash_out, size_t* hash_len_out) { + unsigned char* data = NULL; + size_t data_len = 0; + unsigned char* hash = NULL; + + // Extract certificate data based on selector + if (selector == 0) { + // Full certificate + data_len = i2d_X509(cert, &data); + if (data_len <= 0) { + fprintf(stderr, "Failed to convert certificate to DER format\n"); + return 0; + } + } else if (selector == 1) { + // Subject Public Key Info + EVP_PKEY* pubkey = X509_get_pubkey(cert); + if (!pubkey) { + fprintf(stderr, "Failed to extract public key from certificate\n"); + return 0; + } + + // Convert public key to DER format + data_len = i2d_PUBKEY(pubkey, &data); + EVP_PKEY_free(pubkey); + + if (data_len <= 0) { + fprintf(stderr, "Failed to convert public key to DER format\n"); + return 0; + } + } else { + fprintf(stderr, "Unsupported selector: %d\n", selector); + return 0; + } + + // Apply hash based on matching type + if (matching_type == 0) { + // Exact match (no hash) + *hash_out = data; + *hash_len_out = data_len; + return 1; + } else if (matching_type == 1) { + // SHA-256 hash + hash = malloc(SHA256_DIGEST_LENGTH); + if (!hash) { + OPENSSL_free(data); + fprintf(stderr, "Failed to allocate memory for SHA-256 hash\n"); + return 0; + } + + SHA256(data, data_len, hash); + OPENSSL_free(data); + + *hash_out = hash; + *hash_len_out = SHA256_DIGEST_LENGTH; + return 1; + } else if (matching_type == 2) { + // SHA-512 hash + hash = malloc(SHA512_DIGEST_LENGTH); + if (!hash) { + OPENSSL_free(data); + fprintf(stderr, "Failed to allocate memory for SHA-512 hash\n"); + return 0; + } + + SHA512(data, data_len, hash); + OPENSSL_free(data); + + *hash_out = hash; + *hash_len_out = SHA512_DIGEST_LENGTH; + return 1; + } else { + OPENSSL_free(data); + fprintf(stderr, "Unsupported matching type: %d\n", matching_type); + return 0; + } +} + +// Helper function to compare hashes and print results +static int compare_hashes(const unsigned char* expected, size_t expected_len, + const unsigned char* actual, size_t actual_len) { + if (expected_len != actual_len) { + printf("Hash length mismatch: expected %zu bytes, got %zu bytes\n", expected_len, actual_len); + return 0; + } + + int result = memcmp(expected, actual, expected_len) == 0; + + printf("Certificate hash comparison: %s\n", result ? "MATCH" : "MISMATCH"); + + printf("Expected hash:\n"); + for (size_t i = 0; i < expected_len; i++) { + if (i % 16 == 0) { + printf(" %04zx: ", i); + } + printf("%02x", expected[i]); + if ((i + 1) % 16 == 0 || i + 1 == expected_len) { + printf("\n"); + } else if ((i + 1) % 8 == 0) { + printf(" "); + } else { + printf(" "); + } + } + + printf("Actual hash:\n"); + for (size_t i = 0; i < actual_len; i++) { + if (i % 16 == 0) { + printf(" %04zx: ", i); + } + printf("%02x", actual[i]); + if ((i + 1) % 16 == 0 || i + 1 == actual_len) { + printf("\n"); + } else if ((i + 1) % 8 == 0) { + printf(" "); + } else { + printf(" "); + } + } + + return result; +} + +// Verify a certificate against DANE TLSA records +int verify_cert_against_dane(const char* hostname, X509* cert) { + tlsa_record* records = NULL; + int record_count = 0; + int verified = 0; + + if (!cert) { + fprintf(stderr, "No certificate provided for DANE verification\n"); + return -1; + } + + if (!get_tlsa_records(hostname, &records, &record_count)) { + return -1; + } + + if (record_count == 0) { + return 0; + } + + // Print records that will be used for verification + printf("\n=== Starting DANE verification for %s ===\n", hostname); + for (int i = 0; i < record_count; i++) { + print_tlsa_record(&records[i], i); + } + + // For each TLSA record, verify the certificate + for (int i = 0; i < record_count; i++) { + tlsa_record* record = &records[i]; + + printf("\nVerifying certificate against TLSA record #%d...\n", i + 1); + + // Generate the certificate hash based on selector and matching type + unsigned char* cert_hash = NULL; + size_t cert_hash_len = 0; + + if (!extract_and_hash_cert(cert, record->selector, record->matching_type, + &cert_hash, &cert_hash_len)) { + fprintf(stderr, "Failed to extract and hash certificate\n"); + continue; + } + + // Based on the usage field + switch (record->usage) { + case 0: // PKIX-TA - CA constraint + // Not implemented in this simplified version + printf("PKIX-TA validation not implemented\n"); + break; + + case 1: // PKIX-EE - Service certificate constraint + // Not implemented in this simplified version + printf("PKIX-EE validation not implemented\n"); + break; + + case 2: // DANE-TA - Trust anchor assertion + // Not implemented in this simplified version + printf("DANE-TA validation not implemented\n"); + break; + + case 3: // DANE-EE (3) - Domain-issued certificate + // Verify the certificate directly against the TLSA record + printf("Performing DANE-EE validation...\n"); + verified = compare_hashes(record->data, record->data_len, + cert_hash, cert_hash_len); + break; + + default: + // Unknown usage type + printf("Unknown usage type: %d\n", record->usage); + break; + } + + free(cert_hash); + + if (verified) { + printf("DANE verification succeeded for record #%d\n", i + 1); + break; + } + } + + printf("\nDANE verification result: %s\n", verified ? "SUCCESS" : "FAILED"); + + // Clean up + for (int i = 0; i < record_count; i++) { + if (records[i].data) { + free(records[i].data); + } + } + free(records); + + return verified; +} + +// Initialize OpenSSL +static int init_openssl() { + SSL_load_error_strings(); + OpenSSL_add_ssl_algorithms(); + return 1; +} + +// Create directories for certificates +static int create_cert_directories() { + // Create CA directory + if (mkdir("ca", 0700) != 0 && errno != EEXIST) { + perror("Failed to create CA directory"); + return 0; + } + + // Create certificates directory + if (mkdir(CERT_DIR, 0700) != 0 && errno != EEXIST) { + perror("Failed to create certificates directory"); + return 0; + } + + return 1; +} + +// Generate a new CA certificate if one doesn't exist +static int generate_ca_cert() { + // Check if CA certificate already exists + if (access(CA_CERT_FILE, F_OK) == 0 && access(CA_KEY_FILE, F_OK) == 0) { + printf("CA certificate already exists\n"); + // Load the existing CA certificate and key + FILE* ca_cert_file = fopen(CA_CERT_FILE, "r"); + if (!ca_cert_file) { + perror("Failed to open CA certificate file"); + return 0; + } + + ca_cert = PEM_read_X509(ca_cert_file, NULL, NULL, NULL); + fclose(ca_cert_file); + + if (!ca_cert) { + fprintf(stderr, "Failed to read CA certificate\n"); + return 0; + } + + FILE* ca_key_file = fopen(CA_KEY_FILE, "r"); + if (!ca_key_file) { + perror("Failed to open CA key file"); + X509_free(ca_cert); + ca_cert = NULL; + return 0; + } + + ca_key = PEM_read_PrivateKey(ca_key_file, NULL, NULL, NULL); + fclose(ca_key_file); + + if (!ca_key) { + fprintf(stderr, "Failed to read CA key\n"); + X509_free(ca_cert); + ca_cert = NULL; + return 0; + } + + return 1; + } + + // Generate a new CA key + ca_key = EVP_PKEY_new(); + RSA* rsa = NULL; + BIGNUM* bn = BN_new(); + + if (!bn) { + fprintf(stderr, "Failed to create BIGNUM\n"); + EVP_PKEY_free(ca_key); + ca_key = NULL; + return 0; + } + + BN_set_word(bn, RSA_F4); + rsa = RSA_new(); + + if (!rsa) { + fprintf(stderr, "Failed to create RSA\n"); + BN_free(bn); + EVP_PKEY_free(ca_key); + ca_key = NULL; + return 0; + } + + if (RSA_generate_key_ex(rsa, 2048, bn, NULL) != 1) { + fprintf(stderr, "Failed to generate RSA key\n"); + RSA_free(rsa); + BN_free(bn); + EVP_PKEY_free(ca_key); + ca_key = NULL; + return 0; + } + + BN_free(bn); + + if (!EVP_PKEY_assign_RSA(ca_key, rsa)) { + fprintf(stderr, "Failed to assign RSA key\n"); + RSA_free(rsa); + EVP_PKEY_free(ca_key); + ca_key = NULL; + return 0; + } + + // Create a new X509 certificate + ca_cert = X509_new(); + X509_set_version(ca_cert, 2); + ASN1_INTEGER_set(X509_get_serialNumber(ca_cert), 1); + + // Set validity period (10 years) + X509_gmtime_adj(X509_get_notBefore(ca_cert), 0); + X509_gmtime_adj(X509_get_notAfter(ca_cert), 60 * 60 * 24 * 365 * 10); + + // Set the public key + X509_set_pubkey(ca_cert, ca_key); + + // Set certificate information + X509_NAME* name = X509_get_subject_name(ca_cert); + X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (unsigned char*)"FireProxy CA", -1, -1, 0); + X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, (unsigned char*)"FireProxy", -1, -1, 0); + X509_NAME_add_entry_by_txt(name, "C", MBSTRING_ASC, (unsigned char*)"US", -1, -1, 0); + + // Self-sign the certificate + X509_set_issuer_name(ca_cert, name); + + // Add CA extension + X509V3_CTX ctx; + X509V3_set_ctx_nodb(&ctx); + X509V3_set_ctx(&ctx, ca_cert, ca_cert, NULL, NULL, 0); + X509_EXTENSION* ext = X509V3_EXT_conf_nid(NULL, &ctx, NID_basic_constraints, "critical,CA:TRUE"); + + if (!ext) { + fprintf(stderr, "Failed to create CA extension\n"); + EVP_PKEY_free(ca_key); + X509_free(ca_cert); + ca_key = NULL; + ca_cert = NULL; + return 0; + } + + X509_add_ext(ca_cert, ext, -1); + X509_EXTENSION_free(ext); + + // Sign the certificate + if (!X509_sign(ca_cert, ca_key, EVP_sha256())) { + fprintf(stderr, "Failed to sign CA certificate\n"); + EVP_PKEY_free(ca_key); + X509_free(ca_cert); + ca_key = NULL; + ca_cert = NULL; + return 0; + } + + // Save the CA certificate and key + FILE* ca_cert_file = fopen(CA_CERT_FILE, "w"); + if (!ca_cert_file) { + perror("Failed to create CA certificate file"); + EVP_PKEY_free(ca_key); + X509_free(ca_cert); + ca_key = NULL; + ca_cert = NULL; + return 0; + } + + PEM_write_X509(ca_cert_file, ca_cert); + fclose(ca_cert_file); + + FILE* ca_key_file = fopen(CA_KEY_FILE, "w"); + if (!ca_key_file) { + perror("Failed to create CA key file"); + EVP_PKEY_free(ca_key); + X509_free(ca_cert); + ca_key = NULL; + ca_cert = NULL; + return 0; + } + + PEM_write_PrivateKey(ca_key_file, ca_key, NULL, NULL, 0, NULL, NULL); + fclose(ca_key_file); + + printf("Generated new CA certificate\n"); + return 1; +} + +int setup_local_ca() { + if (!create_cert_directories()) { + return 0; + } + + if (!generate_ca_cert()) { + return 0; + } + + printf("CA setup complete\n"); + return 1; +} + +int generate_trusted_cert(const char* hostname, const char* cert_path, const char* key_path) { + if (!ca_cert || !ca_key) { + fprintf(stderr, "CA not initialized\n"); + return 0; + } + + // Generate a new key pair + EVP_PKEY* pkey = EVP_PKEY_new(); + RSA* rsa = NULL; + BIGNUM* bn = BN_new(); + + if (!bn) { + fprintf(stderr, "Failed to create BIGNUM\n"); + EVP_PKEY_free(pkey); + return 0; + } + + BN_set_word(bn, RSA_F4); + rsa = RSA_new(); + + if (!rsa) { + fprintf(stderr, "Failed to create RSA\n"); + BN_free(bn); + EVP_PKEY_free(pkey); + return 0; + } + + if (RSA_generate_key_ex(rsa, 2048, bn, NULL) != 1) { + fprintf(stderr, "Failed to generate RSA key\n"); + RSA_free(rsa); + BN_free(bn); + EVP_PKEY_free(pkey); + return 0; + } + + BN_free(bn); + + if (!EVP_PKEY_assign_RSA(pkey, rsa)) { + fprintf(stderr, "Failed to assign RSA key\n"); + RSA_free(rsa); + EVP_PKEY_free(pkey); + return 0; + } + + // Create a new certificate + X509* cert = X509_new(); + X509_set_version(cert, 2); + + // Set a random serial number + ASN1_INTEGER_set(X509_get_serialNumber(cert), rand()); + + // Set validity period (1 year) + X509_gmtime_adj(X509_get_notBefore(cert), 0); + X509_gmtime_adj(X509_get_notAfter(cert), 60 * 60 * 24 * 365); + + // Set the public key + X509_set_pubkey(cert, pkey); + + // Set certificate subject + X509_NAME* name = X509_get_subject_name(cert); + X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, (unsigned char*)hostname, -1, -1, 0); + X509_NAME_add_entry_by_txt(name, "O", MBSTRING_ASC, (unsigned char*)"FireProxy Secured", -1, -1, 0); + + // Set certificate issuer (our CA) + X509_set_issuer_name(cert, X509_get_subject_name(ca_cert)); + + // Add extensions + X509V3_CTX ctx; + X509V3_set_ctx_nodb(&ctx); + X509V3_set_ctx(&ctx, ca_cert, cert, NULL, NULL, 0); + + // Add Subject Alternative Name for the hostname + char san[256]; + snprintf(san, sizeof(san), "DNS:%s", hostname); + X509_EXTENSION* ext = X509V3_EXT_conf_nid(NULL, &ctx, NID_subject_alt_name, san); + + if (!ext) { + fprintf(stderr, "Failed to create SAN extension\n"); + EVP_PKEY_free(pkey); + X509_free(cert); + return 0; + } + + X509_add_ext(cert, ext, -1); + X509_EXTENSION_free(ext); + + // Sign the certificate with our CA key + if (!X509_sign(cert, ca_key, EVP_sha256())) { + fprintf(stderr, "Failed to sign certificate\n"); + EVP_PKEY_free(pkey); + X509_free(cert); + return 0; + } + + // Save the certificate + FILE* cert_file = fopen(cert_path, "w"); + if (!cert_file) { + perror("Failed to create certificate file"); + EVP_PKEY_free(pkey); + X509_free(cert); + return 0; + } + + PEM_write_X509(cert_file, cert); + fclose(cert_file); + + // Save the private key + FILE* key_file = fopen(key_path, "w"); + if (!key_file) { + perror("Failed to create key file"); + EVP_PKEY_free(pkey); + X509_free(cert); + return 0; + } + + PEM_write_PrivateKey(key_file, pkey, NULL, NULL, 0, NULL, NULL); + fclose(key_file); + + // Clean up + EVP_PKEY_free(pkey); + X509_free(cert); + + printf("Generated trusted certificate for %s\n", hostname); + return 1; +} + +int dane_init() { + init_openssl(); + return setup_local_ca(); +} + +void dane_cleanup() { + if (ca_cert) { + X509_free(ca_cert); + ca_cert = NULL; + } + + if (ca_key) { + EVP_PKEY_free(ca_key); + ca_key = NULL; + } + + EVP_cleanup(); +} diff --git a/src/dane.h b/src/dane.h new file mode 100644 index 0000000..6eef991 --- /dev/null +++ b/src/dane.h @@ -0,0 +1,64 @@ +#ifndef DANE_H +#define DANE_H + +#include +#include + +/** + * DANE TLSA record structure + */ +typedef struct { + unsigned char usage; // Certificate usage (0-3) + unsigned char selector; // Selector (0-1) + unsigned char matching_type; // Matching type (0-2) + unsigned char *data; // Certificate association data + size_t data_len; // Length of the data +} tlsa_record; + +/** + * Check if DANE is available for a domain by querying for TLSA records + * + * @param hostname The hostname to check + * @return 1 if DANE is available, 0 otherwise + */ +int is_dane_available(const char* hostname); + +/** + * Verify a certificate against DANE TLSA records + * + * @param hostname The hostname + * @param cert The certificate to verify + * @return 1 if valid, 0 if invalid, -1 on error + */ +int verify_cert_against_dane(const char* hostname, X509* cert); + +/** + * Generate a new certificate for the specified hostname signed by our local CA + * + * @param hostname The hostname for the certificate + * @param cert_path Where to store the generated certificate + * @param key_path Where to store the generated private key + * @return 1 on success, 0 on failure + */ +int generate_trusted_cert(const char* hostname, const char* cert_path, const char* key_path); + +/** + * Setup the local Certificate Authority for signing certificates + * + * @return 1 on success, 0 on failure + */ +int setup_local_ca(); + +/** + * Initialize the DANE subsystem + * + * @return 1 on success, 0 on failure + */ +int dane_init(); + +/** + * Clean up DANE resources + */ +void dane_cleanup(); + +#endif // DANE_H diff --git a/src/doh.c b/src/doh.c index 90578f3..36143fe 100644 --- a/src/doh.c +++ b/src/doh.c @@ -100,6 +100,60 @@ void create_dns_query(const char* hostname, char** query, int* query_len) { *query_len = packet_size; } +// Create a DNS query for TLSA records +void create_tlsa_dns_query(const char* hostname, char** query, int* query_len) { + // Prepare TLSA query name: _443._tcp.hostname + char full_name[512]; + snprintf(full_name, sizeof(full_name), "_443._tcp.%s", hostname); + + // Calculate the size of the query + int hostname_len = strlen(full_name); + int packet_size = sizeof(dns_header) + hostname_len + 2 + 4; // +2 for length bytes, +4 for qtype and qclass + + *query = malloc(packet_size); + if (!*query) { + printf("Error: Failed to allocate memory for DNS query\n"); + *query_len = 0; + return; + } + + // Initialize the entire buffer to 0 + memset(*query, 0, packet_size); + + // Setup DNS header + dns_header* header = (dns_header*)*query; + header->id = htons(12346); // Different ID from A record queries + header->rd = 1; // Recursion desired + header->qdcount = htons(1); // One question + + // Add the query + char* qname = *query + sizeof(dns_header); + char* hostname_copy = strdup(full_name); + + // Convert hostname to DNS format (length byte + name part) + char* token = strtok(hostname_copy, "."); + char* cursor = qname; + while (token) { + size_t len = strlen(token); + *cursor = len; + cursor++; + memcpy(cursor, token, len); + cursor += len; + token = strtok(NULL, "."); + } + free(hostname_copy); + + // Set the query type and class after the name + char* qinfo = qname + hostname_len + 2; + unsigned short qtype = htons(52); // TLSA record type + unsigned short qclass = htons(1); // IN class + + memcpy(qinfo, &qtype, sizeof(qtype)); + memcpy(qinfo + sizeof(qtype), &qclass, sizeof(qclass)); + + *query_len = packet_size; +} + // Extract an IP address from the DNS response int extract_ip_from_dns_response(const char* response, int response_len, char* ip_buffer, size_t buffer_size) { if (response_len < sizeof(dns_header)) { @@ -160,6 +214,110 @@ int extract_ip_from_dns_response(const char* response, int response_len, char* i return 1; } +// Extract TLSA records from DNS response +int extract_tlsa_records_from_dns_response(const char* response, int response_len, + unsigned char*** data_buffer, int* record_count) { + if (response_len < sizeof(dns_header)) { + printf("Error: Response too short for TLSA query\n"); + return 1; + } + + // Parse the DNS header + dns_header* header = (dns_header*)response; + int answer_count = ntohs(header->ancount); + + if (answer_count == 0) { + printf("No TLSA records found\n"); + *record_count = 0; + return 0; + } + + printf("Found %d TLSA records\n", answer_count); + + // Allocate memory for record data pointers + *data_buffer = (unsigned char**)malloc(answer_count * sizeof(unsigned char*)); + if (!*data_buffer) { + printf("Error: Failed to allocate memory for TLSA records\n"); + return 1; + } + + // Initialize record count + *record_count = 0; + + // Skip the header + int offset = sizeof(dns_header); + + // Skip the question section + // First find the end of the domain name + while (offset < response_len && response[offset] != 0) { + offset += response[offset] + 1; + } + offset += 5; // Skip the null byte, qtype, and qclass + + // Now parse the answer section + for (int i = 0; i < answer_count && offset < response_len; i++) { + // Skip the name field (could be a pointer) + if ((response[offset] & 0xC0) == 0xC0) { + offset += 2; // Compressed name pointer + } else { + // Skip the uncompressed name + while (offset < response_len && response[offset] != 0) { + offset += response[offset] + 1; + } + offset++; // Skip the null byte + } + + // Ensure we have enough data for the record + if (offset + 10 > response_len) { + printf("Error: Response too short for complete record\n"); + break; + } + + // Read the type + unsigned short type; + memcpy(&type, response + offset, sizeof(type)); + type = ntohs(type); + offset += 2; + + // Skip class + offset += 2; + + // Skip TTL + offset += 4; + + // Read the data length + unsigned short rdlength; + memcpy(&rdlength, response + offset, sizeof(rdlength)); + rdlength = ntohs(rdlength); + offset += 2; + + // Check if it's a TLSA record (type 52) + if (type == 52 && rdlength >= 3) { // 3 bytes minimum: usage, selector, matching type + // Allocate memory for TLSA record data + unsigned char* data = (unsigned char*)malloc(rdlength); + if (!data) { + printf("Error: Failed to allocate memory for TLSA record data\n"); + continue; + } + + // Copy the TLSA record data + memcpy(data, response + offset, rdlength); + + // Store the record data + (*data_buffer)[*record_count] = data; + (*record_count)++; + + printf("TLSA record: Usage=%d, Selector=%d, MatchingType=%d, Data length=%d\n", + data[0], data[1], data[2], rdlength - 3); + } + + // Move to the next record + offset += rdlength; + } + + return 0; +} + int resolve_doh(const char* hostname, char* ip_buffer, size_t buffer_size) { CURL* curl; CURLcode res; @@ -224,3 +382,69 @@ int resolve_doh(const char* hostname, char* ip_buffer, size_t buffer_size) { return result; } + +// Query for TLSA records +int query_tlsa_records_doh(const char* hostname, unsigned char*** data_buffer, int* record_count) { + CURL* curl; + CURLcode res; + response_data resp; + resp.data = malloc(1); + resp.size = 0; + + curl = curl_easy_init(); + if (!curl) { + printf("Error: Failed to initialize cURL for TLSA query\n"); + free(resp.data); + return 1; + } + + // Create DNS query for TLSA records in wire format + char* dns_query = NULL; + int query_len = 0; + create_tlsa_dns_query(hostname, &dns_query, &query_len); + + if (!dns_query || query_len == 0) { + printf("Error: Failed to create DNS query for TLSA records\n"); + curl_easy_cleanup(curl); + free(resp.data); + return 1; + } + + // Set up cURL options for DoH wire format + curl_easy_setopt(curl, CURLOPT_URL, DOH_SERVER); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void*)&resp); + curl_easy_setopt(curl, CURLOPT_POST, 1L); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, dns_query); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, query_len); + + // Set the content-type header for DNS wireformat + struct curl_slist* headers = NULL; + headers = curl_slist_append(headers, "Content-Type: application/dns-message"); + headers = curl_slist_append(headers, "Accept: application/dns-message"); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + + // Perform the request + res = curl_easy_perform(curl); + if (res != CURLE_OK) { + printf("Error: cURL request failed for TLSA query: %s\n", curl_easy_strerror(res)); + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + free(dns_query); + free(resp.data); + return 1; + } + + printf("DoH TLSA response received, size: %zu bytes\n", resp.size); + + // Parse the DNS wire format response to extract TLSA records + int result = extract_tlsa_records_from_dns_response(resp.data, resp.size, data_buffer, record_count); + + // Clean up + curl_slist_free_all(headers); + curl_easy_cleanup(curl); + free(dns_query); + free(resp.data); + + return result; +} diff --git a/src/doh.h b/src/doh.h index 7a210dd..eb2f99e 100644 --- a/src/doh.h +++ b/src/doh.h @@ -13,4 +13,14 @@ */ int resolve_doh(const char* hostname, char* ip_buffer, size_t buffer_size); +/** + * Query for TLSA records using DNS-over-HTTPS (DoH) + * + * @param hostname The hostname to query + * @param data_buffer Pointer to buffer that will be allocated to store the raw data + * @param record_count Pointer to store the number of records found + * @return 0 on success, non-zero on failure + */ +int query_tlsa_records_doh(const char* hostname, unsigned char*** data_buffer, int* record_count); + #endif // DOH_H diff --git a/src/main.c b/src/main.c index d787286..6ed9039 100644 --- a/src/main.c +++ b/src/main.c @@ -17,6 +17,7 @@ int main(int argc, char *argv[]) { printf("Starting FireProxy on port %d...\n", port); printf("Using DoH server: https://hnsdoh.com/dns-query\n"); + printf("DANE support: enabled\n"); // Start the proxy server if (start_proxy_server(port) != 0) { diff --git a/src/proxy.c b/src/proxy.c index 96a8228..2606a8a 100644 --- a/src/proxy.c +++ b/src/proxy.c @@ -1,5 +1,6 @@ #include "proxy.h" #include "doh.h" +#include "dane.h" #include #include #include @@ -11,6 +12,10 @@ #include #include #include +#include +#include +#include +#include #define MAX_REQUEST_SIZE 8192 #define MAX_URL_LENGTH 2048 @@ -91,8 +96,186 @@ int extract_port(const char* request) { return default_port; } -// Handle HTTPS CONNECT tunneling -void handle_https_tunnel(int client_sock, int server_sock) { +// Initialize SSL context for intercepting HTTPS +ssl_context_t* init_ssl_context(const char* cert_path, const char* key_path) { + ssl_context_t* ssl_ctx = malloc(sizeof(ssl_context_t)); + if (!ssl_ctx) { + fprintf(stderr, "Failed to allocate memory for SSL context\n"); + return NULL; + } + + // Initialize SSL context + ssl_ctx->ctx = SSL_CTX_new(TLS_server_method()); + if (!ssl_ctx->ctx) { + fprintf(stderr, "Failed to create SSL context\n"); + free(ssl_ctx); + return NULL; + } + + // Configure SSL context + SSL_CTX_set_options(ssl_ctx->ctx, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION); + + // Load certificate and private key + if (SSL_CTX_use_certificate_file(ssl_ctx->ctx, cert_path, SSL_FILETYPE_PEM) <= 0) { + fprintf(stderr, "Failed to load certificate: %s\n", cert_path); + SSL_CTX_free(ssl_ctx->ctx); + free(ssl_ctx); + return NULL; + } + + if (SSL_CTX_use_PrivateKey_file(ssl_ctx->ctx, key_path, SSL_FILETYPE_PEM) <= 0) { + fprintf(stderr, "Failed to load private key: %s\n", key_path); + SSL_CTX_free(ssl_ctx->ctx); + free(ssl_ctx); + return NULL; + } + + // Verify private key + if (!SSL_CTX_check_private_key(ssl_ctx->ctx)) { + fprintf(stderr, "Private key does not match the certificate\n"); + SSL_CTX_free(ssl_ctx->ctx); + free(ssl_ctx); + return NULL; + } + + return ssl_ctx; +} + +// Handle HTTPS CONNECT tunneling with DANE verification +void handle_https_tunnel(int client_sock, int server_sock, const char* hostname, const char* ip_addr) { + // Unused parameter + (void)ip_addr; + + // Check if we have DANE records for this domain + int has_dane = is_dane_available(hostname); + + if (has_dane) { + printf("DANE records found for %s, will verify certificate\n", hostname); + + char cert_path[256]; + char key_path[256]; + + snprintf(cert_path, sizeof(cert_path), "certs/%s.crt", hostname); + snprintf(key_path, sizeof(key_path), "certs/%s.key", hostname); + + // Generate a trusted certificate for this domain + if (!generate_trusted_cert(hostname, cert_path, key_path)) { + fprintf(stderr, "Failed to generate trusted certificate for %s\n", hostname); + // Fall back to regular tunneling + handle_regular_https_tunnel(client_sock, server_sock); + return; + } + + // Initialize SSL context with our certificate + ssl_context_t* client_ctx = init_ssl_context(cert_path, key_path); + if (!client_ctx) { + fprintf(stderr, "Failed to initialize SSL context\n"); + handle_regular_https_tunnel(client_sock, server_sock); + return; + } + + // Connect to the server with SSL to verify DANE + SSL_CTX* server_ctx = SSL_CTX_new(TLS_client_method()); + if (!server_ctx) { + fprintf(stderr, "Failed to create server SSL context\n"); + SSL_CTX_free(client_ctx->ctx); + free(client_ctx); + handle_regular_https_tunnel(client_sock, server_sock); + return; + } + + SSL* server_ssl = SSL_new(server_ctx); + SSL_set_fd(server_ssl, server_sock); + + // Set SNI hostname + SSL_set_tlsext_host_name(server_ssl, hostname); + + // Connect to the server with SSL + if (SSL_connect(server_ssl) <= 0) { + fprintf(stderr, "SSL connection to server failed\n"); + SSL_free(server_ssl); + SSL_CTX_free(server_ctx); + SSL_CTX_free(client_ctx->ctx); + free(client_ctx); + handle_regular_https_tunnel(client_sock, server_sock); + return; + } + + // Get the server's certificate + X509* server_cert = SSL_get_peer_certificate(server_ssl); + if (!server_cert) { + fprintf(stderr, "Failed to get server certificate\n"); + SSL_free(server_ssl); + SSL_CTX_free(server_ctx); + SSL_CTX_free(client_ctx->ctx); + free(client_ctx); + handle_regular_https_tunnel(client_sock, server_sock); + return; + } + + // Verify the certificate against DANE + int dane_verified = verify_cert_against_dane(hostname, server_cert); + if (dane_verified <= 0) { + fprintf(stderr, "DANE verification failed for %s\n", hostname); + X509_free(server_cert); + SSL_free(server_ssl); + SSL_CTX_free(server_ctx); + SSL_CTX_free(client_ctx->ctx); + free(client_ctx); + handle_regular_https_tunnel(client_sock, server_sock); + return; + } + + printf("DANE verification successful for %s\n", hostname); + + // Send 200 Connection Established to the client + const char* success_response = "HTTP/1.1 200 Connection Established\r\n\r\n"; + if (send(client_sock, success_response, strlen(success_response), 0) < 0) { + perror("Failed to send connection established response"); + X509_free(server_cert); + SSL_free(server_ssl); + SSL_CTX_free(server_ctx); + SSL_CTX_free(client_ctx->ctx); + free(client_ctx); + return; + } + + // Initialize SSL connection with the client + client_ctx->ssl = SSL_new(client_ctx->ctx); + SSL_set_fd(client_ctx->ssl, client_sock); + + if (SSL_accept(client_ctx->ssl) <= 0) { + fprintf(stderr, "SSL accept failed\n"); + SSL_free(client_ctx->ssl); + X509_free(server_cert); + SSL_free(server_ssl); + SSL_CTX_free(server_ctx); + SSL_CTX_free(client_ctx->ctx); + free(client_ctx); + return; + } + + printf("SSL connection established with client for %s\n", hostname); + + // Now we have SSL connections to both client and server + // We can forward data between them + ssl_tunnel_data(client_ctx->ssl, server_ssl); + + // Clean up + SSL_free(client_ctx->ssl); + X509_free(server_cert); + SSL_free(server_ssl); + SSL_CTX_free(server_ctx); + SSL_CTX_free(client_ctx->ctx); + free(client_ctx); + } else { + // No DANE records, use regular tunneling + handle_regular_https_tunnel(client_sock, server_sock); + } +} + +// Regular HTTPS tunneling without interception +void handle_regular_https_tunnel(int client_sock, int server_sock) { fd_set read_fds; char buffer[MAX_REQUEST_SIZE]; int max_fd = (client_sock > server_sock) ? client_sock : server_sock; @@ -129,6 +312,62 @@ void handle_https_tunnel(int client_sock, int server_sock) { } } +// Forward data between SSL connections +void ssl_tunnel_data(SSL* client_ssl, SSL* server_ssl) { + fd_set read_fds; + char buffer[MAX_REQUEST_SIZE]; + int client_fd = SSL_get_fd(client_ssl); + int server_fd = SSL_get_fd(server_ssl); + int max_fd = (client_fd > server_fd) ? client_fd : server_fd; + + while (1) { + FD_ZERO(&read_fds); + FD_SET(client_fd, &read_fds); + FD_SET(server_fd, &read_fds); + + if (select(max_fd + 1, &read_fds, NULL, NULL, NULL) < 0) { + perror("Select failed"); + break; + } + + if (FD_ISSET(client_fd, &read_fds)) { + int bytes_received = SSL_read(client_ssl, buffer, sizeof(buffer)); + if (bytes_received <= 0) { + int err = SSL_get_error(client_ssl, bytes_received); + if (err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE) { + break; + } + } else { + int bytes_sent = SSL_write(server_ssl, buffer, bytes_received); + if (bytes_sent <= 0) { + int err = SSL_get_error(server_ssl, bytes_sent); + if (err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE) { + break; + } + } + } + } + + if (FD_ISSET(server_fd, &read_fds)) { + int bytes_received = SSL_read(server_ssl, buffer, sizeof(buffer)); + if (bytes_received <= 0) { + int err = SSL_get_error(server_ssl, bytes_received); + if (err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE) { + break; + } + } else { + int bytes_sent = SSL_write(client_ssl, buffer, bytes_received); + if (bytes_sent <= 0) { + int err = SSL_get_error(client_ssl, bytes_sent); + if (err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE) { + break; + } + } + } + } + } +} + // Modify HTTP request for direct server communication char* rewrite_http_request(const char* original_request, size_t* new_length) { // Find the first line ending @@ -261,8 +500,8 @@ void* handle_client(void* arg) { } if (is_connect) { - // HTTPS: Handle CONNECT tunnel - handle_https_tunnel(client_sock, server_sock); + // HTTPS: Handle CONNECT tunnel with DANE support + handle_https_tunnel(client_sock, server_sock, host, ip_addr); } else { // HTTP: Rewrite the request and forward it printf("HTTP request received, rewriting for server...\n"); @@ -341,6 +580,12 @@ int start_proxy_server(int port) { struct sockaddr_in server_addr, client_addr; socklen_t client_len = sizeof(client_addr); + // Initialize proxy components + if (proxy_init() != 0) { + fprintf(stderr, "Failed to initialize proxy server\n"); + return 1; + } + // Create socket server_sock = socket(AF_INET, SOCK_STREAM, 0); if (server_sock < 0) { @@ -419,10 +664,27 @@ int start_proxy_server(int port) { return 0; } +// Initialize the proxy server +int proxy_init() { + // Initialize DANE support + if (!dane_init()) { + fprintf(stderr, "Failed to initialize DANE support\n"); + return 1; + } + + return 0; +} + +// Clean up proxy resources +void proxy_cleanup() { + dane_cleanup(); +} + // Signal handler for graceful termination void handle_signal(int sig) { if (sig == SIGINT) { printf("\nShutting down proxy server...\n"); + proxy_cleanup(); exit(0); } } diff --git a/src/proxy.h b/src/proxy.h index 902ce35..b4f4e8f 100644 --- a/src/proxy.h +++ b/src/proxy.h @@ -3,6 +3,15 @@ #include #include +#include + +/** + * SSL context for HTTPS interception + */ +typedef struct { + SSL_CTX *ctx; + SSL *ssl; +} ssl_context_t; /** * Starts the proxy server on the specified port @@ -17,4 +26,31 @@ int start_proxy_server(int port); */ void handle_signal(int sig); +/** + * Initialize the proxy server components including DANE support + * + * @return 0 on success, non-zero on failure + */ +int proxy_init(); + +/** + * Handle regular HTTPS tunneling without DANE verification + */ +void handle_regular_https_tunnel(int client_sock, int server_sock); + +/** + * Handle HTTPS tunneling with DANE verification + */ +void handle_https_tunnel(int client_sock, int server_sock, const char* hostname, const char* ip_addr); + +/** + * Forward data between SSL connections + */ +void ssl_tunnel_data(SSL* client_ssl, SSL* server_ssl); + +/** + * Cleanup proxy server resources + */ +void proxy_cleanup(); + #endif // PROXY_H