/* * Copyright (C) 2009 Martin Willi * Hochschule fuer Technik Rapperswil * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation; either version 2 of the License, or (at your * option) any later version. See . * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. */ #include "simaka_message.h" #include "simaka_manager.h" #include #include typedef struct private_simaka_message_t private_simaka_message_t; typedef struct hdr_t hdr_t; typedef struct attr_hdr_t attr_hdr_t; typedef struct attr_t attr_t; /** * packed EAP-SIM/AKA header struct */ struct hdr_t { /** EAP code (REQUEST/RESPONSE) */ uint8_t code; /** unique message identifier */ uint8_t identifier; /** length of whole message */ uint16_t length; /** EAP type => EAP_SIM/EAP_AKA */ uint8_t type; /** SIM subtype */ uint8_t subtype; /** reserved bytes */ uint16_t reserved; } __attribute__((__packed__)); /** * packed EAP-SIM/AKA attribute header struct */ struct attr_hdr_t { /** attribute type */ uint8_t type; /** attibute length */ uint8_t length; } __attribute__((__packed__)); /** * SIM/AKA attribute, parsed */ struct attr_t { /** type of attribute */ simaka_attribute_t type; /** length of data */ size_t len; /** start of data, variable length */ char data[]; }; ENUM_BEGIN(simaka_subtype_names, AKA_CHALLENGE, AKA_IDENTITY, "AKA_CHALLENGE", "AKA_AUTHENTICATION_REJECT", "AKA_3", "AKA_SYNCHRONIZATION_FAILURE", "AKA_IDENTITY"); ENUM_NEXT(simaka_subtype_names, SIM_START, AKA_CLIENT_ERROR, AKA_IDENTITY, "SIM_START", "SIM_CHALLENGE", "SIM/AKA_NOTIFICATION", "SIM/AKA_REAUTHENTICATION", "SIM/AKA_CLIENT_ERROR"); ENUM_END(simaka_subtype_names, AKA_CLIENT_ERROR); ENUM_BEGIN(simaka_attribute_names, AT_RAND, AT_CLIENT_ERROR_CODE, "AT_RAND", "AT_AUTN", "AT_RES", "AT_AUTS", "AT_5", "AT_PADDING", "AT_NONCE_MT", "AT_8", "AT_9", "AT_PERMANENT_ID_REQ", "AT_MAC", "AT_NOTIFICATION", "AT_ANY_ID_REQ", "AT_IDENTITY", "AT_VERSION_LIST", "AT_SELECTED_VERSION", "AT_FULLAUTH_ID_REQ", "AT_18", "AT_COUNTER", "AT_COUNTER_TOO_SMALL", "AT_NONCE_S", "AT_CLIENT_ERROR_CODE"); ENUM_NEXT(simaka_attribute_names, AT_IV, AT_RESULT_IND, AT_CLIENT_ERROR_CODE, "AT_IV", "AT_ENCR_DATA", "AT_131", "AT_NEXT_PSEUDONYM", "AT_NEXT_REAUTH_ID", "AT_CHECKCODE", "AT_RESULT_IND"); ENUM_END(simaka_attribute_names, AT_RESULT_IND); ENUM_BEGIN(simaka_notification_names, SIM_GENERAL_FAILURE_AA, SIM_GENERAL_FAILURE_AA, "General failure after authentication"); ENUM_NEXT(simaka_notification_names, SIM_TEMP_DENIED, SIM_TEMP_DENIED, SIM_GENERAL_FAILURE_AA, "User has been temporarily denied access"); ENUM_NEXT(simaka_notification_names, SIM_NOT_SUBSCRIBED, SIM_NOT_SUBSCRIBED, SIM_TEMP_DENIED, "User has not subscribed to the requested service"); ENUM_NEXT(simaka_notification_names, SIM_GENERAL_FAILURE, SIM_GENERAL_FAILURE, SIM_NOT_SUBSCRIBED, "General failure"); ENUM_NEXT(simaka_notification_names, SIM_SUCCESS, SIM_SUCCESS, SIM_GENERAL_FAILURE, "User has been successfully authenticated"); ENUM_END(simaka_notification_names, SIM_SUCCESS); ENUM(simaka_client_error_names, SIM_UNABLE_TO_PROCESS, SIM_RANDS_NOT_FRESH, "unable to process packet", "unsupported version", "insufficient number of challenges", "RANDs are not fresh", ); /** * Check if an EAP-SIM/AKA attribute is skippable */ bool simaka_attribute_skippable(simaka_attribute_t attribute) { bool skippable = !((int)attribute >= 0 && attribute <= 127); DBG1(DBG_LIB, "%sskippable EAP-SIM/AKA attribute %N", skippable ? "ignoring " : "found non-", simaka_attribute_names, attribute); return skippable; } /** * Private data of an simaka_message_t object. */ struct private_simaka_message_t { /** * Public simaka_message_t interface. */ simaka_message_t public; /** * EAP message, starting with EAP header */ hdr_t *hdr; /** * List of parsed attributes, attr_t */ linked_list_t *attributes; /** * Currently parsing AT_ENCR_DATA wrapped attributes? */ bool encrypted; /** * crypto helper */ simaka_crypto_t *crypto; /** * Phase a NOTIFICATION is sent within */ bool p_bit; /** * MAC value, pointing into message */ chunk_t mac; /** * ENCR_DATA value, pointing into message */ chunk_t encr; /** * IV value, pointing into message */ chunk_t iv; }; METHOD(simaka_message_t, is_request, bool, private_simaka_message_t *this) { return this->hdr->code == EAP_REQUEST; } METHOD(simaka_message_t, get_identifier, uint8_t, private_simaka_message_t *this) { return this->hdr->identifier; } METHOD(simaka_message_t, get_subtype, simaka_subtype_t, private_simaka_message_t *this) { return this->hdr->subtype; } METHOD(simaka_message_t, get_type, eap_type_t, private_simaka_message_t *this) { return this->hdr->type; } CALLBACK(attr_enum_filter, bool, void *null, enumerator_t *orig, va_list args) { attr_t *attr; simaka_attribute_t *type; chunk_t *data; VA_ARGS_VGET(args, type, data); if (orig->enumerate(orig, &attr)) { *type = attr->type; *data = chunk_create(attr->data, attr->len); return TRUE; } return FALSE; } METHOD(simaka_message_t, create_attribute_enumerator, enumerator_t*, private_simaka_message_t *this) { return enumerator_create_filter( this->attributes->create_enumerator(this->attributes), attr_enum_filter, NULL, NULL); } METHOD(simaka_message_t, add_attribute, void, private_simaka_message_t *this, simaka_attribute_t type, chunk_t data) { attr_t *attr; attr = malloc(sizeof(attr_t) + data.len); attr->len = data.len; attr->type = type; memcpy(attr->data, data.ptr, data.len); this->attributes->insert_last(this->attributes, attr); } /** * Error handling for unencrypted attributes */ static bool not_encrypted(simaka_attribute_t type) { DBG1(DBG_LIB, "received unencrypted %N", simaka_attribute_names, type); return FALSE; } /** * Error handling for invalid length */ static bool invalid_length(simaka_attribute_t type) { DBG1(DBG_LIB, "invalid length of %N", simaka_attribute_names, type); return FALSE; } /** * Call SIM/AKA message hooks */ static void call_hook(private_simaka_message_t *this, bool inbound, bool decrypted) { simaka_manager_t *mgr; switch (this->hdr->type) { case EAP_SIM: mgr = lib->get(lib, "sim-manager"); break; case EAP_AKA: mgr = lib->get(lib, "aka-manager"); break; default: return; } mgr->message_hook(mgr, &this->public, inbound, decrypted); } /** * Parse attributes from a chunk of data */ static bool parse_attributes(private_simaka_message_t *this, chunk_t in) { while (in.len) { attr_hdr_t *hdr; chunk_t data; if (in.len < sizeof(attr_hdr_t)) { DBG1(DBG_LIB, "found short %N attribute header", eap_type_names, this->hdr->type); return FALSE; } hdr = (attr_hdr_t*)in.ptr; switch (hdr->type) { /* attributes without data */ case AT_COUNTER_TOO_SMALL: if (!this->encrypted) { return not_encrypted(hdr->type); } /* FALL */ case AT_ANY_ID_REQ: case AT_PERMANENT_ID_REQ: case AT_FULLAUTH_ID_REQ: { if (hdr->length != 1 || in.len < 4) { return invalid_length(hdr->type); } data = chunk_empty; in = chunk_skip(in, 4); break; } /* attributes with two bytes data */ case AT_COUNTER: if (!this->encrypted) { return not_encrypted(hdr->type); } /* FALL */ case AT_CLIENT_ERROR_CODE: case AT_SELECTED_VERSION: case AT_NOTIFICATION: { if (hdr->length != 1 || in.len < 4) { return invalid_length(hdr->type); } data = chunk_create(in.ptr + 2, 2); in = chunk_skip(in, 4); break; } /* attributes with an additional actual-length in bits or bytes */ case AT_NEXT_PSEUDONYM: case AT_NEXT_REAUTH_ID: if (!this->encrypted) { return not_encrypted(hdr->type); } /* FALL */ case AT_RES: case AT_IDENTITY: case AT_VERSION_LIST: { uint16_t len; if (hdr->length < 1 || in.len < 4) { return invalid_length(hdr->type); } memcpy(&len, in.ptr + 2, 2); len = ntohs(len); if (hdr->type == AT_RES) { /* AT_RES uses length encoding in bits */ len /= 8; } if (len > hdr->length * 4 || len > in.len) { return invalid_length(hdr->type); } data = chunk_create(in.ptr + 4, len); in = chunk_skip(in, hdr->length * 4); break; } /* attributes with two reserved bytes, 16 bytes length */ case AT_NONCE_S: if (!this->encrypted) { return not_encrypted(hdr->type); } /* FALL */ case AT_AUTN: case AT_NONCE_MT: case AT_IV: case AT_MAC: { if (hdr->length != 5 || in.len < 20) { return invalid_length(hdr->type); } data = chunk_create(in.ptr + 4, 16); in = chunk_skip(in, 20); break; } /* attributes with two reserved bytes, variable length */ case AT_ENCR_DATA: case AT_RAND: { if (hdr->length * 4 > in.len || in.len < 4) { return invalid_length(hdr->type); } data = chunk_create(in.ptr + 4, hdr->length * 4 - 4); in = chunk_skip(in, hdr->length * 4); break; } /* attributes with no reserved bytes, 14 bytes length */ case AT_AUTS: { if (hdr->length != 4 || in.len < 16) { return invalid_length(hdr->type); } data = chunk_create(in.ptr + 2, 14); in = chunk_skip(in, 16); break; } /* other attributes (with 4n + 2 length) */ case AT_PADDING: default: { if (hdr->length * 4 > in.len || in.len < 4) { return invalid_length(hdr->type); } data = chunk_create(in.ptr + 2, hdr->length * 4 - 2); in = chunk_skip(in, hdr->length * 4); break; } } /* handle special attributes */ switch (hdr->type) { case AT_MAC: this->mac = data; break; case AT_IV: this->iv = data; break; case AT_ENCR_DATA: this->encr = data; break; case AT_PADDING: break; case AT_NOTIFICATION: if (this->p_bit) { /* remember P bit for MAC verification */ this->p_bit = !!(data.ptr[0] & 0x40); } else if (!this->encrypted) { DBG1(DBG_LIB, "found P-bit 0 notify in unencrypted message"); return FALSE; } /* FALL */ default: add_attribute(this, hdr->type, data); break; } } call_hook(this, TRUE, this->encrypted); return TRUE; } /** * Decrypt a message and parse the decrypted attributes */ static bool decrypt(private_simaka_message_t *this) { bool success; crypter_t *crypter; chunk_t plain; crypter = this->crypto->get_crypter(this->crypto); if (!crypter || !this->iv.len || !this->encr.len || this->encrypted) { return TRUE; } if (this->encr.len % crypter->get_block_size(crypter)) { DBG1(DBG_LIB, "%N ENCR_DATA not a multiple of block size", eap_type_names, this->hdr->type); return FALSE; } if (!crypter->decrypt(crypter, this->encr, this->iv, &plain)) { return FALSE; } this->encrypted = TRUE; success = parse_attributes(this, plain); this->encrypted = FALSE; free(plain.ptr); return success; } METHOD(simaka_message_t, parse, bool, private_simaka_message_t *this) { chunk_t in; if (this->attributes->get_count(this->attributes)) { /* Already parsed. Try to decrypt and parse AT_ENCR_DATA. */ return decrypt(this); } in = chunk_create((char*)this->hdr, ntohs(this->hdr->length)); if (!parse_attributes(this, chunk_skip(in, sizeof(hdr_t)))) { return FALSE; } /* try to decrypt if we already have keys */ return decrypt(this); } METHOD(simaka_message_t, verify, bool, private_simaka_message_t *this, chunk_t sigdata) { chunk_t data, backup; signer_t *signer; signer = this->crypto->get_signer(this->crypto); switch (this->hdr->subtype) { case SIM_START: case SIM_CLIENT_ERROR: /* AKA_CLIENT_ERROR: */ case AKA_AUTHENTICATION_REJECT: case AKA_SYNCHRONIZATION_FAILURE: case AKA_IDENTITY: /* skip MAC if available */ return TRUE; case SIM_CHALLENGE: case AKA_CHALLENGE: case SIM_REAUTHENTICATION: /* AKA_REAUTHENTICATION: */ { if (!this->mac.ptr || !signer) { /* require MAC, but not found */ DBG1(DBG_LIB, "%N message requires a MAC, but none found", simaka_subtype_names, this->hdr->subtype); return FALSE; } break; } case SIM_NOTIFICATION: /* AKA_NOTIFICATION: */ { if (this->p_bit) { /* MAC not verified if in Phase 1 */ return TRUE; } if (!this->mac.ptr || !signer) { DBG1(DBG_LIB, "%N message has a phase 0 notify, but " "no MAC found", simaka_subtype_names, this->hdr->subtype); return FALSE; } break; } default: /* unknown message? */ DBG1(DBG_LIB, "signature rule for %N messages missing", simaka_subtype_names, this->hdr->subtype); return FALSE; } /* zero MAC for verification */ backup = chunk_clonea(this->mac); memset(this->mac.ptr, 0, this->mac.len); data = chunk_create((char*)this->hdr, ntohs(this->hdr->length)); if (sigdata.len) { data = chunk_cata("cc", data, sigdata); } if (!signer->verify_signature(signer, data, backup)) { DBG1(DBG_LIB, "%N MAC verification failed", eap_type_names, this->hdr->type); return FALSE; } return TRUE; } METHOD(simaka_message_t, generate, bool, private_simaka_message_t *this, chunk_t sigdata, chunk_t *gen) { /* buffers large enough for messages we generate */ char out_buf[1024], encr_buf[512]; enumerator_t *enumerator; chunk_t out, encr, data, *target, mac = chunk_empty; simaka_attribute_t type; attr_hdr_t *hdr; uint16_t len; signer_t *signer; call_hook(this, FALSE, TRUE); out = chunk_create(out_buf, sizeof(out_buf)); encr = chunk_create(encr_buf, sizeof(encr_buf)); /* copy header */ memcpy(out.ptr, this->hdr, sizeof(hdr_t)); out = chunk_skip(out, sizeof(hdr_t)); /* encode attributes */ enumerator = create_attribute_enumerator(this); while (enumerator->enumerate(enumerator, &type, &data)) { /* encrypt this attribute? */ switch (type) { case AT_NONCE_S: case AT_NEXT_PSEUDONYM: case AT_NEXT_REAUTH_ID: case AT_COUNTER: case AT_COUNTER_TOO_SMALL: target = &encr; break; case AT_NOTIFICATION: /* P bit not set, encrypt */ if (!(data.ptr[0] & 0x40)) { target = &encr; break; } /* FALL */ default: target = &out; break; } hdr = (attr_hdr_t*)target->ptr; hdr->type = type; /* encode type specific */ switch (type) { /* attributes without data */ case AT_COUNTER_TOO_SMALL: case AT_ANY_ID_REQ: case AT_PERMANENT_ID_REQ: case AT_FULLAUTH_ID_REQ: { hdr->length = 1; memset(target->ptr + 2, 0, 2); *target = chunk_skip(*target, 4); break; } /* attributes with two bytes data */ case AT_COUNTER: case AT_CLIENT_ERROR_CODE: case AT_SELECTED_VERSION: case AT_NOTIFICATION: { hdr->length = 1; memcpy(target->ptr + 2, data.ptr, 2); *target = chunk_skip(*target, 4); break; } /* attributes with an additional actual-length in bits or bytes */ case AT_NEXT_PSEUDONYM: case AT_NEXT_REAUTH_ID: case AT_IDENTITY: case AT_VERSION_LIST: case AT_RES: { uint16_t len, padding; len = htons(data.len); if (type == AT_RES) { /* AT_RES uses length encoding in bits */ len *= 8; } memcpy(target->ptr + 2, &len, sizeof(len)); memcpy(target->ptr + 4, data.ptr, data.len); hdr->length = data.len / 4 + 1; padding = (4 - (data.len % 4)) % 4; if (padding) { hdr->length++; memset(target->ptr + 4 + data.len, 0, padding); } *target = chunk_skip(*target, hdr->length * 4); break; } /* attributes with two reserved bytes, 16 bytes length */ case AT_NONCE_S: case AT_NONCE_MT: case AT_AUTN: { hdr->length = 5; memset(target->ptr + 2, 0, 2); memcpy(target->ptr + 4, data.ptr, data.len); *target = chunk_skip(*target, 20); break; } /* attributes with two reserved bytes, variable length */ case AT_RAND: { hdr->length = 1 + data.len / 4; memset(target->ptr + 2, 0, 2); memcpy(target->ptr + 4, data.ptr, data.len); *target = chunk_skip(*target, data.len + 4); break; } /* attributes with no reserved bytes, 14 bytes length */ case AT_AUTS: { hdr->length = 4; memcpy(target->ptr + 2, data.ptr, data.len); *target = chunk_skip(*target, 16); break; } default: { DBG1(DBG_LIB, "no rule to encode %N, skipped", simaka_attribute_names, type); break; } } } enumerator->destroy(enumerator); /* encrypt attributes, if any */ if (encr.len < sizeof(encr_buf)) { chunk_t iv; size_t bs, padding; crypter_t *crypter; rng_t *rng; crypter = this->crypto->get_crypter(this->crypto); bs = crypter->get_block_size(crypter); iv.len = crypter->get_iv_size(crypter); /* add AT_PADDING attribute */ padding = bs - ((sizeof(encr_buf) - encr.len) % bs); if (padding) { hdr = (attr_hdr_t*)encr.ptr; hdr->type = AT_PADDING; hdr->length = padding / 4; memset(encr.ptr + 2, 0, padding - 2); encr = chunk_skip(encr, padding); } encr = chunk_create(encr_buf, sizeof(encr_buf) - encr.len); /* add IV attribute */ hdr = (attr_hdr_t*)out.ptr; hdr->type = AT_IV; hdr->length = iv.len / 4 + 1; memset(out.ptr + 2, 0, 2); out = chunk_skip(out, 4); rng = this->crypto->get_rng(this->crypto); if (!rng->get_bytes(rng, iv.len, out.ptr)) { return FALSE; } iv = chunk_clonea(chunk_create(out.ptr, iv.len)); out = chunk_skip(out, iv.len); /* inline encryption */ if (!crypter->encrypt(crypter, encr, iv, NULL)) { return FALSE; } /* add ENCR_DATA attribute */ hdr = (attr_hdr_t*)out.ptr; hdr->type = AT_ENCR_DATA; hdr->length = encr.len / 4 + 1; memset(out.ptr + 2, 0, 2); memcpy(out.ptr + 4, encr.ptr, encr.len); out = chunk_skip(out, encr.len + 4); } /* include MAC ? */ signer = this->crypto->get_signer(this->crypto); switch (this->hdr->subtype) { case SIM_CHALLENGE: case AKA_CHALLENGE: case SIM_REAUTHENTICATION: /* AKA_REAUTHENTICATION: */ /* TODO: Notifications without P bit */ { size_t bs; bs = signer->get_block_size(signer); hdr = (attr_hdr_t*)out.ptr; hdr->type = AT_MAC; hdr->length = bs / 4 + 1; memset(out.ptr + 2, 0, 2 + bs); mac = chunk_create(out.ptr + 4, bs); out = chunk_skip(out, bs + 4); break; } default: break; } /* calculate message length */ out = chunk_create(out_buf, sizeof(out_buf) - out.len); len = htons(out.len); memcpy(out.ptr + 2, &len, sizeof(len)); /* generate MAC */ if (mac.len) { data = chunk_cata("cc", out, sigdata); if (!signer->get_signature(signer, data, mac.ptr)) { return FALSE; } } call_hook(this, FALSE, FALSE); *gen = chunk_clone(out); return TRUE; } METHOD(simaka_message_t, destroy, void, private_simaka_message_t *this) { this->attributes->destroy_function(this->attributes, free); free(this->hdr); free(this); } /** * Generic constructor. */ static simaka_message_t *simaka_message_create_data(chunk_t data, simaka_crypto_t *crypto) { private_simaka_message_t *this; hdr_t *hdr = (hdr_t*)data.ptr; if (data.len < sizeof(hdr_t) || hdr->length != htons(data.len)) { DBG1(DBG_LIB, "EAP-SIM/AKA header has invalid length"); return NULL; } if (hdr->code != EAP_REQUEST && hdr->code != EAP_RESPONSE) { DBG1(DBG_LIB, "invalid EAP code in EAP-SIM/AKA message", eap_type_names, hdr->type); return NULL; } if (hdr->type != EAP_SIM && hdr->type != EAP_AKA) { DBG1(DBG_LIB, "invalid EAP type in EAP-SIM/AKA message", eap_type_names, hdr->type); return NULL; } INIT(this, .public = { .is_request = _is_request, .get_identifier = _get_identifier, .get_type = _get_type, .get_subtype = _get_subtype, .create_attribute_enumerator = _create_attribute_enumerator, .add_attribute = _add_attribute, .parse = _parse, .verify = _verify, .generate = _generate, .destroy = _destroy, }, .attributes = linked_list_create(), .crypto = crypto, .p_bit = TRUE, .hdr = malloc(data.len), ); memcpy(this->hdr, hdr, data.len); return &this->public; } /** * See header. */ simaka_message_t *simaka_message_create_from_payload(chunk_t data, simaka_crypto_t *crypto) { return simaka_message_create_data(data, crypto); } /** * See header. */ simaka_message_t *simaka_message_create(bool request, uint8_t identifier, eap_type_t type, simaka_subtype_t subtype, simaka_crypto_t *crypto) { hdr_t hdr = { .code = request ? EAP_REQUEST : EAP_RESPONSE, .identifier = identifier, .length = htons(sizeof(hdr_t)), .type = type, .subtype = subtype, }; return simaka_message_create_data(chunk_create((char*)&hdr, sizeof(hdr)), crypto); }