| // SPDX-License-Identifier: GPL-2.0-or-later |
| /* GSSAPI-based RxRPC security |
| * |
| * Copyright (C) 2025 Red Hat, Inc. All Rights Reserved. |
| * Written by David Howells (dhowells@redhat.com) |
| */ |
| |
| #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt |
| |
| #include <linux/net.h> |
| #include <linux/skbuff.h> |
| #include <linux/slab.h> |
| #include <linux/key-type.h> |
| #include "ar-internal.h" |
| #include "rxgk_common.h" |
| |
| /* |
| * Parse the information from a server key |
| */ |
| static int rxgk_preparse_server_key(struct key_preparsed_payload *prep) |
| { |
| const struct krb5_enctype *krb5; |
| struct krb5_buffer *server_key = (void *)&prep->payload.data[2]; |
| unsigned int service, sec_class, kvno, enctype; |
| int n = 0; |
| |
| _enter("%zu", prep->datalen); |
| |
| if (sscanf(prep->orig_description, "%u:%u:%u:%u%n", |
| &service, &sec_class, &kvno, &enctype, &n) != 4) |
| return -EINVAL; |
| |
| if (prep->orig_description[n]) |
| return -EINVAL; |
| |
| krb5 = crypto_krb5_find_enctype(enctype); |
| if (!krb5) |
| return -ENOPKG; |
| |
| prep->payload.data[0] = (struct krb5_enctype *)krb5; |
| |
| if (prep->datalen != krb5->key_len) |
| return -EKEYREJECTED; |
| |
| server_key->len = prep->datalen; |
| server_key->data = kmemdup(prep->data, prep->datalen, GFP_KERNEL); |
| if (!server_key->data) |
| return -ENOMEM; |
| |
| _leave(" = 0"); |
| return 0; |
| } |
| |
| static void rxgk_free_server_key(union key_payload *payload) |
| { |
| struct krb5_buffer *server_key = (void *)&payload->data[2]; |
| |
| kfree_sensitive(server_key->data); |
| } |
| |
| static void rxgk_free_preparse_server_key(struct key_preparsed_payload *prep) |
| { |
| rxgk_free_server_key(&prep->payload); |
| } |
| |
| static void rxgk_destroy_server_key(struct key *key) |
| { |
| rxgk_free_server_key(&key->payload); |
| } |
| |
| static void rxgk_describe_server_key(const struct key *key, struct seq_file *m) |
| { |
| const struct krb5_enctype *krb5 = key->payload.data[0]; |
| |
| if (krb5) |
| seq_printf(m, ": %s", krb5->name); |
| } |
| |
| /* |
| * Handle rekeying the connection when we see our limits overrun or when the |
| * far side decided to rekey. |
| * |
| * Returns a ref on the context if successful or -ESTALE if the key is out of |
| * date. |
| */ |
| static struct rxgk_context *rxgk_rekey(struct rxrpc_connection *conn, |
| const u16 *specific_key_number) |
| { |
| struct rxgk_context *gk, *dead = NULL; |
| unsigned int key_number, current_key, mask = ARRAY_SIZE(conn->rxgk.keys) - 1; |
| bool crank = false; |
| |
| _enter("%d", specific_key_number ? *specific_key_number : -1); |
| |
| mutex_lock(&conn->security_lock); |
| |
| current_key = conn->rxgk.key_number; |
| if (!specific_key_number) { |
| key_number = current_key; |
| } else { |
| if (*specific_key_number == (u16)current_key) |
| key_number = current_key; |
| else if (*specific_key_number == (u16)(current_key - 1)) |
| key_number = current_key - 1; |
| else if (*specific_key_number == (u16)(current_key + 1)) |
| goto crank_window; |
| else |
| goto bad_key; |
| } |
| |
| gk = conn->rxgk.keys[key_number & mask]; |
| if (!gk) |
| goto generate_key; |
| if (!specific_key_number && |
| test_bit(RXGK_TK_NEEDS_REKEY, &gk->flags)) |
| goto crank_window; |
| |
| grab: |
| refcount_inc(&gk->usage); |
| mutex_unlock(&conn->security_lock); |
| rxgk_put(dead); |
| return gk; |
| |
| crank_window: |
| trace_rxrpc_rxgk_rekey(conn, current_key, |
| specific_key_number ? *specific_key_number : -1); |
| if (current_key == UINT_MAX) |
| goto bad_key; |
| if (current_key + 1 == UINT_MAX) |
| set_bit(RXRPC_CONN_DONT_REUSE, &conn->flags); |
| |
| key_number = current_key + 1; |
| if (WARN_ON(conn->rxgk.keys[key_number & mask])) |
| goto bad_key; |
| crank = true; |
| |
| generate_key: |
| gk = conn->rxgk.keys[current_key & mask]; |
| gk = rxgk_generate_transport_key(conn, gk->key, key_number, GFP_NOFS); |
| if (IS_ERR(gk)) { |
| mutex_unlock(&conn->security_lock); |
| return gk; |
| } |
| |
| write_lock(&conn->security_use_lock); |
| if (crank) { |
| current_key++; |
| conn->rxgk.key_number = current_key; |
| dead = conn->rxgk.keys[(current_key - 2) & mask]; |
| conn->rxgk.keys[(current_key - 2) & mask] = NULL; |
| } |
| conn->rxgk.keys[current_key & mask] = gk; |
| write_unlock(&conn->security_use_lock); |
| goto grab; |
| |
| bad_key: |
| mutex_unlock(&conn->security_lock); |
| return ERR_PTR(-ESTALE); |
| } |
| |
| /* |
| * Get the specified keying context. |
| * |
| * Returns a ref on the context if successful or -ESTALE if the key is out of |
| * date. |
| */ |
| static struct rxgk_context *rxgk_get_key(struct rxrpc_connection *conn, |
| const u16 *specific_key_number) |
| { |
| struct rxgk_context *gk; |
| unsigned int key_number, current_key, mask = ARRAY_SIZE(conn->rxgk.keys) - 1; |
| |
| _enter("{%u},%d", |
| conn->rxgk.key_number, specific_key_number ? *specific_key_number : -1); |
| |
| read_lock(&conn->security_use_lock); |
| |
| current_key = conn->rxgk.key_number; |
| if (!specific_key_number) { |
| key_number = current_key; |
| } else { |
| /* Only the bottom 16 bits of the key number are exposed in the |
| * header, so we try and keep the upper 16 bits in step. The |
| * whole 32 bits are used to generate the TK. |
| */ |
| if (*specific_key_number == (u16)current_key) |
| key_number = current_key; |
| else if (*specific_key_number == (u16)(current_key - 1)) |
| key_number = current_key - 1; |
| else if (*specific_key_number == (u16)(current_key + 1)) |
| goto rekey; |
| else |
| goto bad_key; |
| } |
| |
| gk = conn->rxgk.keys[key_number & mask]; |
| if (!gk) |
| goto slow_path; |
| if (!specific_key_number && |
| key_number < UINT_MAX) { |
| if (time_after(jiffies, gk->expiry) || |
| gk->bytes_remaining < 0) { |
| set_bit(RXGK_TK_NEEDS_REKEY, &gk->flags); |
| goto slow_path; |
| } |
| |
| if (test_bit(RXGK_TK_NEEDS_REKEY, &gk->flags)) |
| goto slow_path; |
| } |
| |
| refcount_inc(&gk->usage); |
| read_unlock(&conn->security_use_lock); |
| return gk; |
| |
| rekey: |
| _debug("rekey"); |
| if (current_key == UINT_MAX) |
| goto bad_key; |
| gk = conn->rxgk.keys[current_key & mask]; |
| if (gk) |
| set_bit(RXGK_TK_NEEDS_REKEY, &gk->flags); |
| slow_path: |
| read_unlock(&conn->security_use_lock); |
| return rxgk_rekey(conn, specific_key_number); |
| bad_key: |
| read_unlock(&conn->security_use_lock); |
| return ERR_PTR(-ESTALE); |
| } |
| |
| /* |
| * initialise connection security |
| */ |
| static int rxgk_init_connection_security(struct rxrpc_connection *conn, |
| struct rxrpc_key_token *token) |
| { |
| struct rxgk_context *gk; |
| int ret; |
| |
| _enter("{%d,%u},{%x}", |
| conn->debug_id, conn->rxgk.key_number, key_serial(conn->key)); |
| |
| conn->security_ix = token->security_index; |
| conn->security_level = token->rxgk->level; |
| |
| if (rxrpc_conn_is_client(conn)) { |
| conn->rxgk.start_time = ktime_get(); |
| do_div(conn->rxgk.start_time, 100); |
| } |
| |
| gk = rxgk_generate_transport_key(conn, token->rxgk, conn->rxgk.key_number, |
| GFP_NOFS); |
| if (IS_ERR(gk)) |
| return PTR_ERR(gk); |
| conn->rxgk.enctype = gk->krb5->etype; |
| conn->rxgk.keys[gk->key_number & 3] = gk; |
| |
| switch (conn->security_level) { |
| case RXRPC_SECURITY_PLAIN: |
| case RXRPC_SECURITY_AUTH: |
| case RXRPC_SECURITY_ENCRYPT: |
| break; |
| default: |
| ret = -EKEYREJECTED; |
| goto error; |
| } |
| |
| ret = 0; |
| error: |
| _leave(" = %d", ret); |
| return ret; |
| } |
| |
| /* |
| * Clean up the crypto on a call. |
| */ |
| static void rxgk_free_call_crypto(struct rxrpc_call *call) |
| { |
| } |
| |
| /* |
| * Work out how much data we can put in a packet. |
| */ |
| static struct rxrpc_txbuf *rxgk_alloc_txbuf(struct rxrpc_call *call, size_t remain, gfp_t gfp) |
| { |
| enum krb5_crypto_mode mode; |
| struct rxgk_context *gk; |
| struct rxrpc_txbuf *txb; |
| size_t shdr, alloc, limit, part, offset, gap; |
| |
| switch (call->conn->security_level) { |
| default: |
| alloc = umin(remain, RXRPC_JUMBO_DATALEN); |
| return rxrpc_alloc_data_txbuf(call, alloc, 1, gfp); |
| case RXRPC_SECURITY_AUTH: |
| shdr = 0; |
| mode = KRB5_CHECKSUM_MODE; |
| break; |
| case RXRPC_SECURITY_ENCRYPT: |
| shdr = sizeof(struct rxgk_header); |
| mode = KRB5_ENCRYPT_MODE; |
| break; |
| } |
| |
| gk = rxgk_get_key(call->conn, NULL); |
| if (IS_ERR(gk)) |
| return NULL; |
| |
| /* Work out the maximum amount of data that will fit. */ |
| alloc = RXRPC_JUMBO_DATALEN; |
| limit = crypto_krb5_how_much_data(gk->krb5, mode, &alloc, &offset); |
| |
| if (remain < limit - shdr) { |
| part = remain; |
| alloc = crypto_krb5_how_much_buffer(gk->krb5, mode, |
| shdr + part, &offset); |
| gap = 0; |
| } else { |
| part = limit - shdr; |
| gap = RXRPC_JUMBO_DATALEN - alloc; |
| alloc = RXRPC_JUMBO_DATALEN; |
| } |
| |
| rxgk_put(gk); |
| |
| txb = rxrpc_alloc_data_txbuf(call, alloc, 16, gfp); |
| if (!txb) |
| return NULL; |
| |
| txb->crypto_header = offset; |
| txb->sec_header = shdr; |
| txb->offset += offset + shdr; |
| txb->space = part; |
| |
| /* Clear excess space in the packet */ |
| if (gap) |
| memset(txb->data + alloc - gap, 0, gap); |
| return txb; |
| } |
| |
| /* |
| * Integrity mode (sign a packet - level 1 security) |
| */ |
| static int rxgk_secure_packet_integrity(const struct rxrpc_call *call, |
| struct rxgk_context *gk, |
| struct rxrpc_txbuf *txb) |
| { |
| struct rxgk_header *hdr; |
| struct scatterlist sg[1]; |
| struct krb5_buffer metadata; |
| int ret = -ENOMEM; |
| |
| _enter(""); |
| |
| hdr = kzalloc(sizeof(*hdr), GFP_NOFS); |
| if (!hdr) |
| goto error_gk; |
| |
| hdr->epoch = htonl(call->conn->proto.epoch); |
| hdr->cid = htonl(call->cid); |
| hdr->call_number = htonl(call->call_id); |
| hdr->seq = htonl(txb->seq); |
| hdr->sec_index = htonl(call->security_ix); |
| hdr->data_len = htonl(txb->len); |
| metadata.len = sizeof(*hdr); |
| metadata.data = hdr; |
| |
| sg_init_table(sg, 1); |
| sg_set_buf(&sg[0], txb->data, txb->alloc_size); |
| |
| ret = crypto_krb5_get_mic(gk->krb5, gk->tx_Kc, &metadata, |
| sg, 1, txb->alloc_size, |
| txb->crypto_header, txb->sec_header + txb->len); |
| if (ret >= 0) { |
| txb->pkt_len = ret; |
| if (txb->alloc_size == RXRPC_JUMBO_DATALEN) |
| txb->jumboable = true; |
| gk->bytes_remaining -= ret; |
| } |
| kfree(hdr); |
| error_gk: |
| rxgk_put(gk); |
| _leave(" = %d", ret); |
| return ret; |
| } |
| |
| /* |
| * wholly encrypt a packet (level 2 security) |
| */ |
| static int rxgk_secure_packet_encrypted(const struct rxrpc_call *call, |
| struct rxgk_context *gk, |
| struct rxrpc_txbuf *txb) |
| { |
| struct rxgk_header *hdr; |
| struct scatterlist sg[1]; |
| int ret; |
| |
| _enter("%x", txb->len); |
| |
| /* Insert the header into the buffer. */ |
| hdr = txb->data + txb->crypto_header; |
| hdr->epoch = htonl(call->conn->proto.epoch); |
| hdr->cid = htonl(call->cid); |
| hdr->call_number = htonl(call->call_id); |
| hdr->seq = htonl(txb->seq); |
| hdr->sec_index = htonl(call->security_ix); |
| hdr->data_len = htonl(txb->len); |
| |
| sg_init_table(sg, 1); |
| sg_set_buf(&sg[0], txb->data, txb->alloc_size); |
| |
| ret = crypto_krb5_encrypt(gk->krb5, gk->tx_enc, |
| sg, 1, txb->alloc_size, |
| txb->crypto_header, txb->sec_header + txb->len, |
| false); |
| if (ret >= 0) { |
| txb->pkt_len = ret; |
| if (txb->alloc_size == RXRPC_JUMBO_DATALEN) |
| txb->jumboable = true; |
| gk->bytes_remaining -= ret; |
| } |
| |
| rxgk_put(gk); |
| _leave(" = %d", ret); |
| return ret; |
| } |
| |
| /* |
| * checksum an RxRPC packet header |
| */ |
| static int rxgk_secure_packet(struct rxrpc_call *call, struct rxrpc_txbuf *txb) |
| { |
| struct rxgk_context *gk; |
| int ret; |
| |
| _enter("{%d{%x}},{#%u},%u,", |
| call->debug_id, key_serial(call->conn->key), txb->seq, txb->len); |
| |
| gk = rxgk_get_key(call->conn, NULL); |
| if (IS_ERR(gk)) |
| return PTR_ERR(gk) == -ESTALE ? -EKEYREJECTED : PTR_ERR(gk); |
| |
| ret = key_validate(call->conn->key); |
| if (ret < 0) { |
| rxgk_put(gk); |
| return ret; |
| } |
| |
| call->security_enctype = gk->krb5->etype; |
| txb->cksum = htons(gk->key_number); |
| |
| switch (call->conn->security_level) { |
| case RXRPC_SECURITY_PLAIN: |
| rxgk_put(gk); |
| txb->pkt_len = txb->len; |
| return 0; |
| case RXRPC_SECURITY_AUTH: |
| return rxgk_secure_packet_integrity(call, gk, txb); |
| case RXRPC_SECURITY_ENCRYPT: |
| return rxgk_secure_packet_encrypted(call, gk, txb); |
| default: |
| rxgk_put(gk); |
| return -EPERM; |
| } |
| } |
| |
| /* |
| * Integrity mode (check the signature on a packet - level 1 security) |
| */ |
| static int rxgk_verify_packet_integrity(struct rxrpc_call *call, |
| struct rxgk_context *gk, |
| struct sk_buff *skb) |
| { |
| struct rxrpc_skb_priv *sp = rxrpc_skb(skb); |
| struct rxgk_header *hdr; |
| struct krb5_buffer metadata; |
| unsigned int offset = sp->offset, len = sp->len; |
| size_t data_offset = 0, data_len = len; |
| u32 ac; |
| int ret = -ENOMEM; |
| |
| _enter(""); |
| |
| crypto_krb5_where_is_the_data(gk->krb5, KRB5_CHECKSUM_MODE, |
| &data_offset, &data_len); |
| |
| hdr = kzalloc(sizeof(*hdr), GFP_NOFS); |
| if (!hdr) |
| goto put_gk; |
| |
| hdr->epoch = htonl(call->conn->proto.epoch); |
| hdr->cid = htonl(call->cid); |
| hdr->call_number = htonl(call->call_id); |
| hdr->seq = htonl(sp->hdr.seq); |
| hdr->sec_index = htonl(call->security_ix); |
| hdr->data_len = htonl(data_len); |
| |
| metadata.len = sizeof(*hdr); |
| metadata.data = hdr; |
| ret = rxgk_verify_mic_skb(gk->krb5, gk->rx_Kc, &metadata, |
| skb, &offset, &len, &ac); |
| kfree(hdr); |
| if (ret == -EPROTO) { |
| rxrpc_abort_eproto(call, skb, ac, |
| rxgk_abort_1_verify_mic_eproto); |
| } else { |
| sp->offset = offset; |
| sp->len = len; |
| } |
| |
| put_gk: |
| rxgk_put(gk); |
| _leave(" = %d", ret); |
| return ret; |
| } |
| |
| /* |
| * Decrypt an encrypted packet (level 2 security). |
| */ |
| static int rxgk_verify_packet_encrypted(struct rxrpc_call *call, |
| struct rxgk_context *gk, |
| struct sk_buff *skb) |
| { |
| struct rxrpc_skb_priv *sp = rxrpc_skb(skb); |
| struct rxgk_header hdr; |
| unsigned int offset = sp->offset, len = sp->len; |
| int ret; |
| u32 ac; |
| |
| _enter(""); |
| |
| ret = rxgk_decrypt_skb(gk->krb5, gk->rx_enc, skb, &offset, &len, &ac); |
| if (ret == -EPROTO) |
| rxrpc_abort_eproto(call, skb, ac, rxgk_abort_2_decrypt_eproto); |
| if (ret < 0) |
| goto error; |
| |
| if (len < sizeof(hdr)) { |
| ret = rxrpc_abort_eproto(call, skb, RXGK_PACKETSHORT, |
| rxgk_abort_2_short_header); |
| goto error; |
| } |
| |
| /* Extract the header from the skb */ |
| ret = skb_copy_bits(skb, offset, &hdr, sizeof(hdr)); |
| if (ret < 0) { |
| ret = rxrpc_abort_eproto(call, skb, RXGK_PACKETSHORT, |
| rxgk_abort_2_short_encdata); |
| goto error; |
| } |
| offset += sizeof(hdr); |
| len -= sizeof(hdr); |
| |
| if (ntohl(hdr.epoch) != call->conn->proto.epoch || |
| ntohl(hdr.cid) != call->cid || |
| ntohl(hdr.call_number) != call->call_id || |
| ntohl(hdr.seq) != sp->hdr.seq || |
| ntohl(hdr.sec_index) != call->security_ix || |
| ntohl(hdr.data_len) > len) { |
| ret = rxrpc_abort_eproto(call, skb, RXGK_SEALEDINCON, |
| rxgk_abort_2_short_data); |
| goto error; |
| } |
| |
| sp->offset = offset; |
| sp->len = ntohl(hdr.data_len); |
| ret = 0; |
| error: |
| rxgk_put(gk); |
| _leave(" = %d", ret); |
| return ret; |
| } |
| |
| /* |
| * Verify the security on a received packet or subpacket (if part of a |
| * jumbo packet). |
| */ |
| static int rxgk_verify_packet(struct rxrpc_call *call, struct sk_buff *skb) |
| { |
| struct rxrpc_skb_priv *sp = rxrpc_skb(skb); |
| struct rxgk_context *gk; |
| u16 key_number = sp->hdr.cksum; |
| |
| _enter("{%d{%x}},{#%u}", |
| call->debug_id, key_serial(call->conn->key), sp->hdr.seq); |
| |
| gk = rxgk_get_key(call->conn, &key_number); |
| if (IS_ERR(gk)) { |
| switch (PTR_ERR(gk)) { |
| case -ESTALE: |
| return rxrpc_abort_eproto(call, skb, RXGK_BADKEYNO, |
| rxgk_abort_bad_key_number); |
| default: |
| return PTR_ERR(gk); |
| } |
| } |
| |
| call->security_enctype = gk->krb5->etype; |
| switch (call->conn->security_level) { |
| case RXRPC_SECURITY_PLAIN: |
| rxgk_put(gk); |
| return 0; |
| case RXRPC_SECURITY_AUTH: |
| return rxgk_verify_packet_integrity(call, gk, skb); |
| case RXRPC_SECURITY_ENCRYPT: |
| return rxgk_verify_packet_encrypted(call, gk, skb); |
| default: |
| rxgk_put(gk); |
| return -ENOANO; |
| } |
| } |
| |
| /* |
| * Allocate memory to hold a challenge or a response packet. We're not running |
| * in the io_thread, so we can't use ->tx_alloc. |
| */ |
| static struct page *rxgk_alloc_packet(size_t total_len) |
| { |
| gfp_t gfp = GFP_NOFS; |
| int order; |
| |
| order = get_order(total_len); |
| if (order > 0) |
| gfp |= __GFP_COMP; |
| return alloc_pages(gfp, order); |
| } |
| |
| /* |
| * Issue a challenge. |
| */ |
| static int rxgk_issue_challenge(struct rxrpc_connection *conn) |
| { |
| struct rxrpc_wire_header *whdr; |
| struct bio_vec bvec[1]; |
| struct msghdr msg; |
| struct page *page; |
| size_t len = sizeof(*whdr) + sizeof(conn->rxgk.nonce); |
| u32 serial; |
| int ret; |
| |
| _enter("{%d}", conn->debug_id); |
| |
| get_random_bytes(&conn->rxgk.nonce, sizeof(conn->rxgk.nonce)); |
| |
| /* We can't use conn->tx_alloc without a lock */ |
| page = rxgk_alloc_packet(sizeof(*whdr) + sizeof(conn->rxgk.nonce)); |
| if (!page) |
| return -ENOMEM; |
| |
| bvec_set_page(&bvec[0], page, len, 0); |
| iov_iter_bvec(&msg.msg_iter, WRITE, bvec, 1, len); |
| |
| msg.msg_name = &conn->peer->srx.transport; |
| msg.msg_namelen = conn->peer->srx.transport_len; |
| msg.msg_control = NULL; |
| msg.msg_controllen = 0; |
| msg.msg_flags = MSG_SPLICE_PAGES; |
| |
| whdr = page_address(page); |
| whdr->epoch = htonl(conn->proto.epoch); |
| whdr->cid = htonl(conn->proto.cid); |
| whdr->callNumber = 0; |
| whdr->seq = 0; |
| whdr->type = RXRPC_PACKET_TYPE_CHALLENGE; |
| whdr->flags = conn->out_clientflag; |
| whdr->userStatus = 0; |
| whdr->securityIndex = conn->security_ix; |
| whdr->_rsvd = 0; |
| whdr->serviceId = htons(conn->service_id); |
| |
| memcpy(whdr + 1, conn->rxgk.nonce, sizeof(conn->rxgk.nonce)); |
| |
| serial = rxrpc_get_next_serials(conn, 1); |
| whdr->serial = htonl(serial); |
| |
| trace_rxrpc_tx_challenge(conn, serial, 0, *(u32 *)&conn->rxgk.nonce); |
| |
| ret = do_udp_sendmsg(conn->local->socket, &msg, len); |
| if (ret > 0) |
| conn->peer->last_tx_at = ktime_get_seconds(); |
| __free_page(page); |
| |
| if (ret < 0) { |
| trace_rxrpc_tx_fail(conn->debug_id, serial, ret, |
| rxrpc_tx_point_rxgk_challenge); |
| return -EAGAIN; |
| } |
| |
| trace_rxrpc_tx_packet(conn->debug_id, whdr, |
| rxrpc_tx_point_rxgk_challenge); |
| _leave(" = 0"); |
| return 0; |
| } |
| |
| /* |
| * Validate a challenge packet. |
| */ |
| static bool rxgk_validate_challenge(struct rxrpc_connection *conn, |
| struct sk_buff *skb) |
| { |
| struct rxrpc_skb_priv *sp = rxrpc_skb(skb); |
| u8 nonce[20]; |
| |
| if (!conn->key) { |
| rxrpc_abort_conn(conn, skb, RX_PROTOCOL_ERROR, -EPROTO, |
| rxgk_abort_chall_no_key); |
| return false; |
| } |
| |
| if (key_validate(conn->key) < 0) { |
| rxrpc_abort_conn(conn, skb, RXGK_EXPIRED, -EPROTO, |
| rxgk_abort_chall_key_expired); |
| return false; |
| } |
| |
| if (skb_copy_bits(skb, sizeof(struct rxrpc_wire_header), |
| nonce, sizeof(nonce)) < 0) { |
| rxrpc_abort_conn(conn, skb, RXGK_PACKETSHORT, -EPROTO, |
| rxgk_abort_chall_short); |
| return false; |
| } |
| |
| trace_rxrpc_rx_challenge(conn, sp->hdr.serial, 0, *(u32 *)nonce, 0); |
| return true; |
| } |
| |
| /** |
| * rxgk_kernel_query_challenge - Query RxGK-specific challenge parameters |
| * @challenge: The challenge packet to query |
| * |
| * Return: The Kerberos 5 encoding type for the challenged connection. |
| */ |
| u32 rxgk_kernel_query_challenge(struct sk_buff *challenge) |
| { |
| struct rxrpc_skb_priv *sp = rxrpc_skb(challenge); |
| |
| return sp->chall.conn->rxgk.enctype; |
| } |
| EXPORT_SYMBOL(rxgk_kernel_query_challenge); |
| |
| /* |
| * Fill out the control message to pass to userspace to inform about the |
| * challenge. |
| */ |
| static int rxgk_challenge_to_recvmsg(struct rxrpc_connection *conn, |
| struct sk_buff *challenge, |
| struct msghdr *msg) |
| { |
| struct rxgk_challenge chall; |
| |
| chall.base.service_id = conn->service_id; |
| chall.base.security_index = conn->security_ix; |
| chall.enctype = conn->rxgk.enctype; |
| |
| return put_cmsg(msg, SOL_RXRPC, RXRPC_CHALLENGED, sizeof(chall), &chall); |
| } |
| |
| /* |
| * Insert the requisite amount of XDR padding for the length given. |
| */ |
| static int rxgk_pad_out(struct sk_buff *response, size_t len, size_t offset) |
| { |
| __be32 zero = 0; |
| size_t pad = xdr_round_up(len) - len; |
| int ret; |
| |
| if (!pad) |
| return 0; |
| |
| ret = skb_store_bits(response, offset, &zero, pad); |
| if (ret < 0) |
| return ret; |
| return pad; |
| } |
| |
| /* |
| * Insert the header into the response. |
| */ |
| static noinline ssize_t rxgk_insert_response_header(struct rxrpc_connection *conn, |
| struct rxgk_context *gk, |
| struct sk_buff *response, |
| size_t offset) |
| { |
| struct rxrpc_skb_priv *rsp = rxrpc_skb(response); |
| |
| struct { |
| struct rxrpc_wire_header whdr; |
| __be32 start_time_msw; |
| __be32 start_time_lsw; |
| __be32 ticket_len; |
| } h; |
| int ret; |
| |
| rsp->resp.kvno = gk->key_number; |
| rsp->resp.version = gk->krb5->etype; |
| |
| h.whdr.epoch = htonl(conn->proto.epoch); |
| h.whdr.cid = htonl(conn->proto.cid); |
| h.whdr.callNumber = 0; |
| h.whdr.serial = 0; |
| h.whdr.seq = 0; |
| h.whdr.type = RXRPC_PACKET_TYPE_RESPONSE; |
| h.whdr.flags = conn->out_clientflag; |
| h.whdr.userStatus = 0; |
| h.whdr.securityIndex = conn->security_ix; |
| h.whdr.cksum = htons(gk->key_number); |
| h.whdr.serviceId = htons(conn->service_id); |
| h.start_time_msw = htonl(upper_32_bits(conn->rxgk.start_time)); |
| h.start_time_lsw = htonl(lower_32_bits(conn->rxgk.start_time)); |
| h.ticket_len = htonl(gk->key->ticket.len); |
| |
| ret = skb_store_bits(response, offset, &h, sizeof(h)); |
| return ret < 0 ? ret : sizeof(h); |
| } |
| |
| /* |
| * Construct the authenticator to go in the response packet |
| * |
| * struct RXGK_Authenticator { |
| * opaque nonce[20]; |
| * opaque appdata<>; |
| * RXGK_Level level; |
| * unsigned int epoch; |
| * unsigned int cid; |
| * unsigned int call_numbers<>; |
| * }; |
| */ |
| static ssize_t rxgk_construct_authenticator(struct rxrpc_connection *conn, |
| struct sk_buff *challenge, |
| const struct krb5_buffer *appdata, |
| struct sk_buff *response, |
| size_t offset) |
| { |
| struct { |
| u8 nonce[20]; |
| __be32 appdata_len; |
| } a; |
| struct { |
| __be32 level; |
| __be32 epoch; |
| __be32 cid; |
| __be32 call_numbers_count; |
| __be32 call_numbers[4]; |
| } b; |
| int ret; |
| |
| ret = skb_copy_bits(challenge, sizeof(struct rxrpc_wire_header), |
| a.nonce, sizeof(a.nonce)); |
| if (ret < 0) |
| return -EPROTO; |
| |
| a.appdata_len = htonl(appdata->len); |
| |
| ret = skb_store_bits(response, offset, &a, sizeof(a)); |
| if (ret < 0) |
| return ret; |
| offset += sizeof(a); |
| |
| if (appdata->len) { |
| ret = skb_store_bits(response, offset, appdata->data, appdata->len); |
| if (ret < 0) |
| return ret; |
| offset += appdata->len; |
| |
| ret = rxgk_pad_out(response, appdata->len, offset); |
| if (ret < 0) |
| return ret; |
| offset += ret; |
| } |
| |
| b.level = htonl(conn->security_level); |
| b.epoch = htonl(conn->proto.epoch); |
| b.cid = htonl(conn->proto.cid); |
| b.call_numbers_count = htonl(4); |
| b.call_numbers[0] = htonl(conn->channels[0].call_counter); |
| b.call_numbers[1] = htonl(conn->channels[1].call_counter); |
| b.call_numbers[2] = htonl(conn->channels[2].call_counter); |
| b.call_numbers[3] = htonl(conn->channels[3].call_counter); |
| |
| ret = skb_store_bits(response, offset, &b, sizeof(b)); |
| if (ret < 0) |
| return ret; |
| return sizeof(a) + xdr_round_up(appdata->len) + sizeof(b); |
| } |
| |
| static ssize_t rxgk_encrypt_authenticator(struct rxrpc_connection *conn, |
| struct rxgk_context *gk, |
| struct sk_buff *response, |
| size_t offset, |
| size_t alloc_len, |
| size_t auth_offset, |
| size_t auth_len) |
| { |
| struct scatterlist sg[16]; |
| int nr_sg; |
| |
| sg_init_table(sg, ARRAY_SIZE(sg)); |
| nr_sg = skb_to_sgvec(response, sg, offset, alloc_len); |
| if (unlikely(nr_sg < 0)) |
| return nr_sg; |
| return crypto_krb5_encrypt(gk->krb5, gk->resp_enc, sg, nr_sg, alloc_len, |
| auth_offset, auth_len, false); |
| } |
| |
| /* |
| * Construct the response. |
| * |
| * struct RXGK_Response { |
| * rxgkTime start_time; |
| * RXGK_Data token; |
| * opaque authenticator<RXGK_MAXAUTHENTICATOR> |
| * }; |
| */ |
| static int rxgk_construct_response(struct rxrpc_connection *conn, |
| struct sk_buff *challenge, |
| struct krb5_buffer *appdata) |
| { |
| struct rxrpc_skb_priv *csp, *rsp; |
| struct rxgk_context *gk; |
| struct sk_buff *response; |
| size_t len, auth_len, authx_len, offset, auth_offset, authx_offset; |
| __be32 tmp; |
| int ret; |
| |
| gk = rxgk_get_key(conn, NULL); |
| if (IS_ERR(gk)) |
| return PTR_ERR(gk); |
| |
| auth_len = 20 + (4 + appdata->len) + 12 + (1 + 4) * 4; |
| authx_len = crypto_krb5_how_much_buffer(gk->krb5, KRB5_ENCRYPT_MODE, |
| auth_len, &auth_offset); |
| len = sizeof(struct rxrpc_wire_header) + |
| 8 + (4 + xdr_round_up(gk->key->ticket.len)) + (4 + authx_len); |
| |
| response = alloc_skb_with_frags(0, len, 0, &ret, GFP_NOFS); |
| if (!response) |
| goto error; |
| rxrpc_new_skb(response, rxrpc_skb_new_response_rxgk); |
| response->len = len; |
| response->data_len = len; |
| |
| ret = rxgk_insert_response_header(conn, gk, response, 0); |
| if (ret < 0) |
| goto error; |
| offset = ret; |
| |
| ret = skb_store_bits(response, offset, gk->key->ticket.data, gk->key->ticket.len); |
| if (ret < 0) |
| goto error; |
| offset += gk->key->ticket.len; |
| ret = rxgk_pad_out(response, gk->key->ticket.len, offset); |
| if (ret < 0) |
| goto error; |
| |
| authx_offset = offset + ret + 4; /* Leave a gap for the length. */ |
| |
| ret = rxgk_construct_authenticator(conn, challenge, appdata, response, |
| authx_offset + auth_offset); |
| if (ret < 0) |
| goto error; |
| auth_len = ret; |
| |
| ret = rxgk_encrypt_authenticator(conn, gk, response, |
| authx_offset, authx_len, |
| auth_offset, auth_len); |
| if (ret < 0) |
| goto error; |
| authx_len = ret; |
| |
| tmp = htonl(authx_len); |
| ret = skb_store_bits(response, authx_offset - 4, &tmp, 4); |
| if (ret < 0) |
| goto error; |
| |
| ret = rxgk_pad_out(response, authx_len, authx_offset + authx_len); |
| if (ret < 0) |
| goto error; |
| len = authx_offset + authx_len + ret; |
| |
| if (len != response->len) { |
| response->len = len; |
| response->data_len = len; |
| } |
| |
| csp = rxrpc_skb(challenge); |
| rsp = rxrpc_skb(response); |
| rsp->resp.len = len; |
| rsp->resp.challenge_serial = csp->hdr.serial; |
| rxrpc_post_response(conn, response); |
| response = NULL; |
| ret = 0; |
| |
| error: |
| rxrpc_free_skb(response, rxrpc_skb_put_response); |
| rxgk_put(gk); |
| _leave(" = %d", ret); |
| return ret; |
| } |
| |
| /* |
| * Respond to a challenge packet. |
| */ |
| static int rxgk_respond_to_challenge(struct rxrpc_connection *conn, |
| struct sk_buff *challenge, |
| struct krb5_buffer *appdata) |
| { |
| _enter("{%d,%x}", conn->debug_id, key_serial(conn->key)); |
| |
| if (key_validate(conn->key) < 0) |
| return rxrpc_abort_conn(conn, NULL, RXGK_EXPIRED, -EPROTO, |
| rxgk_abort_chall_key_expired); |
| |
| return rxgk_construct_response(conn, challenge, appdata); |
| } |
| |
| static int rxgk_respond_to_challenge_no_appdata(struct rxrpc_connection *conn, |
| struct sk_buff *challenge) |
| { |
| struct krb5_buffer appdata = {}; |
| |
| return rxgk_respond_to_challenge(conn, challenge, &appdata); |
| } |
| |
| /** |
| * rxgk_kernel_respond_to_challenge - Respond to a challenge with appdata |
| * @challenge: The challenge to respond to |
| * @appdata: The application data to include in the RESPONSE authenticator |
| * |
| * Allow a kernel application to respond to a CHALLENGE with application data |
| * to be included in the RxGK RESPONSE Authenticator. |
| * |
| * Return: %0 if successful and a negative error code otherwise. |
| */ |
| int rxgk_kernel_respond_to_challenge(struct sk_buff *challenge, |
| struct krb5_buffer *appdata) |
| { |
| struct rxrpc_skb_priv *csp = rxrpc_skb(challenge); |
| |
| return rxgk_respond_to_challenge(csp->chall.conn, challenge, appdata); |
| } |
| EXPORT_SYMBOL(rxgk_kernel_respond_to_challenge); |
| |
| /* |
| * Parse sendmsg() control message and respond to challenge. We need to see if |
| * there's an appdata to fish out. |
| */ |
| static int rxgk_sendmsg_respond_to_challenge(struct sk_buff *challenge, |
| struct msghdr *msg) |
| { |
| struct krb5_buffer appdata = {}; |
| struct cmsghdr *cmsg; |
| |
| for_each_cmsghdr(cmsg, msg) { |
| if (cmsg->cmsg_level != SOL_RXRPC || |
| cmsg->cmsg_type != RXRPC_RESP_RXGK_APPDATA) |
| continue; |
| if (appdata.data) |
| return -EINVAL; |
| appdata.data = CMSG_DATA(cmsg); |
| appdata.len = cmsg->cmsg_len - sizeof(struct cmsghdr); |
| } |
| |
| return rxgk_kernel_respond_to_challenge(challenge, &appdata); |
| } |
| |
| /* |
| * Verify the authenticator. |
| * |
| * struct RXGK_Authenticator { |
| * opaque nonce[20]; |
| * opaque appdata<>; |
| * RXGK_Level level; |
| * unsigned int epoch; |
| * unsigned int cid; |
| * unsigned int call_numbers<>; |
| * }; |
| */ |
| static int rxgk_do_verify_authenticator(struct rxrpc_connection *conn, |
| const struct krb5_enctype *krb5, |
| struct sk_buff *skb, |
| __be32 *p, __be32 *end) |
| { |
| u32 app_len, call_count, level, epoch, cid, i; |
| |
| _enter(""); |
| |
| if (memcmp(p, conn->rxgk.nonce, 20) != 0) |
| return rxrpc_abort_conn(conn, skb, RXGK_NOTAUTH, -EPROTO, |
| rxgk_abort_resp_bad_nonce); |
| p += 20 / sizeof(__be32); |
| |
| app_len = ntohl(*p++); |
| if (app_len > (end - p) * sizeof(__be32)) |
| return rxrpc_abort_conn(conn, skb, RXGK_NOTAUTH, -EPROTO, |
| rxgk_abort_resp_short_applen); |
| |
| p += xdr_round_up(app_len) / sizeof(__be32); |
| if (end - p < 4) |
| return rxrpc_abort_conn(conn, skb, RXGK_NOTAUTH, -EPROTO, |
| rxgk_abort_resp_short_applen); |
| |
| level = ntohl(*p++); |
| epoch = ntohl(*p++); |
| cid = ntohl(*p++); |
| call_count = ntohl(*p++); |
| |
| if (level != conn->security_level || |
| epoch != conn->proto.epoch || |
| cid != conn->proto.cid || |
| call_count > 4) |
| return rxrpc_abort_conn(conn, skb, RXGK_NOTAUTH, -EPROTO, |
| rxgk_abort_resp_bad_param); |
| |
| if (end - p < call_count) |
| return rxrpc_abort_conn(conn, skb, RXGK_NOTAUTH, -EPROTO, |
| rxgk_abort_resp_short_call_list); |
| |
| for (i = 0; i < call_count; i++) { |
| u32 call_id = ntohl(*p++); |
| |
| if (call_id > INT_MAX) |
| return rxrpc_abort_conn(conn, skb, RXGK_NOTAUTH, -EPROTO, |
| rxgk_abort_resp_bad_callid); |
| |
| if (call_id < conn->channels[i].call_counter) |
| return rxrpc_abort_conn(conn, skb, RXGK_NOTAUTH, -EPROTO, |
| rxgk_abort_resp_call_ctr); |
| |
| if (call_id > conn->channels[i].call_counter) { |
| if (conn->channels[i].call) |
| return rxrpc_abort_conn(conn, skb, RXGK_NOTAUTH, -EPROTO, |
| rxgk_abort_resp_call_state); |
| |
| conn->channels[i].call_counter = call_id; |
| } |
| } |
| |
| _leave(" = 0"); |
| return 0; |
| } |
| |
| /* |
| * Extract the authenticator and verify it. |
| */ |
| static int rxgk_verify_authenticator(struct rxrpc_connection *conn, |
| const struct krb5_enctype *krb5, |
| struct sk_buff *skb, |
| unsigned int auth_offset, unsigned int auth_len) |
| { |
| void *auth; |
| __be32 *p; |
| int ret; |
| |
| auth = kmalloc(auth_len, GFP_NOFS); |
| if (!auth) |
| return -ENOMEM; |
| |
| ret = skb_copy_bits(skb, auth_offset, auth, auth_len); |
| if (ret < 0) { |
| ret = rxrpc_abort_conn(conn, skb, RXGK_NOTAUTH, -EPROTO, |
| rxgk_abort_resp_short_auth); |
| goto error; |
| } |
| |
| p = auth; |
| ret = rxgk_do_verify_authenticator(conn, krb5, skb, p, p + auth_len); |
| error: |
| kfree(auth); |
| return ret; |
| } |
| |
| /* |
| * Verify a response. |
| * |
| * struct RXGK_Response { |
| * rxgkTime start_time; |
| * RXGK_Data token; |
| * opaque authenticator<RXGK_MAXAUTHENTICATOR> |
| * }; |
| */ |
| static int rxgk_verify_response(struct rxrpc_connection *conn, |
| struct sk_buff *skb) |
| { |
| const struct krb5_enctype *krb5; |
| struct rxrpc_key_token *token; |
| struct rxrpc_skb_priv *sp = rxrpc_skb(skb); |
| struct rxgk_response rhdr; |
| struct rxgk_context *gk; |
| struct key *key = NULL; |
| unsigned int offset = sizeof(struct rxrpc_wire_header); |
| unsigned int len = skb->len - sizeof(struct rxrpc_wire_header); |
| unsigned int token_offset, token_len; |
| unsigned int auth_offset, auth_len; |
| __be32 xauth_len; |
| int ret, ec; |
| |
| _enter("{%d}", conn->debug_id); |
| |
| /* Parse the RXGK_Response object */ |
| if (sizeof(rhdr) + sizeof(__be32) > len) |
| goto short_packet; |
| |
| if (skb_copy_bits(skb, offset, &rhdr, sizeof(rhdr)) < 0) |
| goto short_packet; |
| offset += sizeof(rhdr); |
| len -= sizeof(rhdr); |
| |
| token_offset = offset; |
| token_len = ntohl(rhdr.token_len); |
| if (xdr_round_up(token_len) + sizeof(__be32) > len) |
| goto short_packet; |
| |
| trace_rxrpc_rx_response(conn, sp->hdr.serial, 0, sp->hdr.cksum, token_len); |
| |
| offset += xdr_round_up(token_len); |
| len -= xdr_round_up(token_len); |
| |
| if (skb_copy_bits(skb, offset, &xauth_len, sizeof(xauth_len)) < 0) |
| goto short_packet; |
| offset += sizeof(xauth_len); |
| len -= sizeof(xauth_len); |
| |
| auth_offset = offset; |
| auth_len = ntohl(xauth_len); |
| if (auth_len < len) |
| goto short_packet; |
| if (auth_len & 3) |
| goto inconsistent; |
| if (auth_len < 20 + 9 * 4) |
| goto auth_too_short; |
| |
| /* We need to extract and decrypt the token and instantiate a session |
| * key for it. This bit, however, is application-specific. If |
| * possible, we use a default parser, but we might end up bumping this |
| * to the app to deal with - which might mean a round trip to |
| * userspace. |
| */ |
| ret = rxgk_extract_token(conn, skb, token_offset, token_len, &key); |
| if (ret < 0) |
| goto out; |
| |
| /* We now have a key instantiated from the decrypted ticket. We can |
| * pass this to the application so that they can parse the ticket |
| * content and we can use the session key it contains to derive the |
| * keys we need. |
| * |
| * Note that we have to switch enctype at this point as the enctype of |
| * the ticket doesn't necessarily match that of the transport. |
| */ |
| token = key->payload.data[0]; |
| conn->security_level = token->rxgk->level; |
| conn->rxgk.start_time = __be64_to_cpu(rhdr.start_time); |
| |
| gk = rxgk_generate_transport_key(conn, token->rxgk, sp->hdr.cksum, GFP_NOFS); |
| if (IS_ERR(gk)) { |
| ret = PTR_ERR(gk); |
| goto cant_get_token; |
| } |
| |
| krb5 = gk->krb5; |
| |
| trace_rxrpc_rx_response(conn, sp->hdr.serial, krb5->etype, sp->hdr.cksum, token_len); |
| |
| /* Decrypt, parse and verify the authenticator. */ |
| ret = rxgk_decrypt_skb(krb5, gk->resp_enc, skb, |
| &auth_offset, &auth_len, &ec); |
| if (ret < 0) { |
| rxrpc_abort_conn(conn, skb, RXGK_SEALEDINCON, ret, |
| rxgk_abort_resp_auth_dec); |
| goto out; |
| } |
| |
| ret = rxgk_verify_authenticator(conn, krb5, skb, auth_offset, auth_len); |
| if (ret < 0) |
| goto out; |
| |
| conn->key = key; |
| key = NULL; |
| ret = 0; |
| out: |
| key_put(key); |
| _leave(" = %d", ret); |
| return ret; |
| |
| inconsistent: |
| ret = rxrpc_abort_conn(conn, skb, RXGK_INCONSISTENCY, -EPROTO, |
| rxgk_abort_resp_xdr_align); |
| goto out; |
| auth_too_short: |
| ret = rxrpc_abort_conn(conn, skb, RXGK_PACKETSHORT, -EPROTO, |
| rxgk_abort_resp_short_auth); |
| goto out; |
| short_packet: |
| ret = rxrpc_abort_conn(conn, skb, RXGK_PACKETSHORT, -EPROTO, |
| rxgk_abort_resp_short_packet); |
| goto out; |
| |
| cant_get_token: |
| switch (ret) { |
| case -ENOMEM: |
| goto temporary_error; |
| case -EINVAL: |
| ret = rxrpc_abort_conn(conn, skb, RXGK_NOTAUTH, -EKEYREJECTED, |
| rxgk_abort_resp_internal_error); |
| goto out; |
| case -ENOPKG: |
| ret = rxrpc_abort_conn(conn, skb, KRB5_PROG_KEYTYPE_NOSUPP, |
| -EKEYREJECTED, rxgk_abort_resp_nopkg); |
| goto out; |
| } |
| |
| temporary_error: |
| /* Ignore the response packet if we got a temporary error such as |
| * ENOMEM. We just want to send the challenge again. Note that we |
| * also come out this way if the ticket decryption fails. |
| */ |
| goto out; |
| } |
| |
| /* |
| * clear the connection security |
| */ |
| static void rxgk_clear(struct rxrpc_connection *conn) |
| { |
| int i; |
| |
| for (i = 0; i < ARRAY_SIZE(conn->rxgk.keys); i++) |
| rxgk_put(conn->rxgk.keys[i]); |
| } |
| |
| /* |
| * Initialise the RxGK security service. |
| */ |
| static int rxgk_init(void) |
| { |
| return 0; |
| } |
| |
| /* |
| * Clean up the RxGK security service. |
| */ |
| static void rxgk_exit(void) |
| { |
| } |
| |
| /* |
| * RxRPC YFS GSSAPI-based security |
| */ |
| const struct rxrpc_security rxgk_yfs = { |
| .name = "yfs-rxgk", |
| .security_index = RXRPC_SECURITY_YFS_RXGK, |
| .no_key_abort = RXGK_NOTAUTH, |
| .init = rxgk_init, |
| .exit = rxgk_exit, |
| .preparse_server_key = rxgk_preparse_server_key, |
| .free_preparse_server_key = rxgk_free_preparse_server_key, |
| .destroy_server_key = rxgk_destroy_server_key, |
| .describe_server_key = rxgk_describe_server_key, |
| .init_connection_security = rxgk_init_connection_security, |
| .alloc_txbuf = rxgk_alloc_txbuf, |
| .secure_packet = rxgk_secure_packet, |
| .verify_packet = rxgk_verify_packet, |
| .free_call_crypto = rxgk_free_call_crypto, |
| .issue_challenge = rxgk_issue_challenge, |
| .validate_challenge = rxgk_validate_challenge, |
| .challenge_to_recvmsg = rxgk_challenge_to_recvmsg, |
| .sendmsg_respond_to_challenge = rxgk_sendmsg_respond_to_challenge, |
| .respond_to_challenge = rxgk_respond_to_challenge_no_appdata, |
| .verify_response = rxgk_verify_response, |
| .clear = rxgk_clear, |
| .default_decode_ticket = rxgk_yfs_decode_ticket, |
| }; |