| // SPDX-License-Identifier: GPL-2.0-or-later | |
| /* Application-specific bits for 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" | |
| /* | |
| * Decode a default-style YFS ticket in a response and turn it into an | |
| * rxrpc-type key. | |
| * | |
| * struct rxgk_key { | |
| * afs_uint32 enctype; | |
| * opaque key<>; | |
| * }; | |
| * | |
| * struct RXGK_AuthName { | |
| * afs_int32 kind; | |
| * opaque data<AUTHDATAMAX>; | |
| * opaque display<AUTHPRINTABLEMAX>; | |
| * }; | |
| * | |
| * struct RXGK_Token { | |
| * rxgk_key K0; | |
| * RXGK_Level level; | |
| * rxgkTime starttime; | |
| * afs_int32 lifetime; | |
| * afs_int32 bytelife; | |
| * rxgkTime expirationtime; | |
| * struct RXGK_AuthName identities<>; | |
| * }; | |
| */ | |
| int rxgk_yfs_decode_ticket(struct rxrpc_connection *conn, struct sk_buff *skb, | |
| void *buffer, unsigned int ticket_len, | |
| struct key **_key) | |
| { | |
| struct rxrpc_key_token *token; | |
| const struct cred *cred = current_cred(); // TODO - use socket creds | |
| struct key *key; | |
| size_t pre_ticket_len, payload_len; | |
| unsigned int klen, enctype; | |
| void *payload, *ticket; | |
| __be32 *t, *p, *q, *tmp; | |
| int ret; | |
| _enter(""); | |
| if (ticket_len < 10 * sizeof(__be32)) | |
| return rxrpc_abort_conn(conn, skb, RXGK_INCONSISTENCY, -EPROTO, | |
| rxgk_abort_resp_short_yfs_tkt); | |
| /* Get the session key length */ | |
| tmp = buffer; | |
| enctype = ntohl(tmp[0]); | |
| klen = ntohl(tmp[1]); | |
| if (klen > ticket_len - 10 * sizeof(__be32)) | |
| return rxrpc_abort_conn(conn, skb, RXGK_INCONSISTENCY, -EPROTO, | |
| rxgk_abort_resp_short_yfs_key); | |
| pre_ticket_len = ((5 + 14) * sizeof(__be32) + | |
| xdr_round_up(klen) + | |
| sizeof(__be32)); | |
| payload_len = pre_ticket_len + xdr_round_up(ticket_len); | |
| payload = kzalloc(payload_len, GFP_NOFS); | |
| if (!payload) | |
| return -ENOMEM; | |
| /* We need to fill out the XDR form for a key payload that we can pass | |
| * to add_key(). Start by copying in the ticket so that we can parse | |
| * it. | |
| */ | |
| ticket = payload + pre_ticket_len; | |
| memcpy(ticket, buffer, ticket_len); | |
| /* Fill out the form header. */ | |
| p = payload; | |
| p[0] = htonl(0); /* Flags */ | |
| p[1] = htonl(1); /* len(cellname) */ | |
| p[2] = htonl(0x20000000); /* Cellname " " */ | |
| p[3] = htonl(1); /* #tokens */ | |
| p[4] = htonl(15 * sizeof(__be32) + xdr_round_up(klen) + | |
| xdr_round_up(ticket_len)); /* Token len */ | |
| /* Now fill in the body. Most of this we can just scrape directly from | |
| * the ticket. | |
| */ | |
| t = ticket + sizeof(__be32) * 2 + xdr_round_up(klen); | |
| q = payload + 5 * sizeof(__be32); | |
| q[0] = htonl(RXRPC_SECURITY_YFS_RXGK); | |
| q[1] = t[1]; /* begintime - msw */ | |
| q[2] = t[2]; /* - lsw */ | |
| q[3] = t[5]; /* endtime - msw */ | |
| q[4] = t[6]; /* - lsw */ | |
| q[5] = 0; /* level - msw */ | |
| q[6] = t[0]; /* - lsw */ | |
| q[7] = 0; /* lifetime - msw */ | |
| q[8] = t[3]; /* - lsw */ | |
| q[9] = 0; /* bytelife - msw */ | |
| q[10] = t[4]; /* - lsw */ | |
| q[11] = 0; /* enctype - msw */ | |
| q[12] = htonl(enctype); /* - lsw */ | |
| q[13] = htonl(klen); /* Key length */ | |
| q += 14; | |
| memcpy(q, ticket + sizeof(__be32) * 2, klen); | |
| q += xdr_round_up(klen) / 4; | |
| q[0] = htonl(ticket_len); | |
| q++; | |
| if (WARN_ON((unsigned long)q != (unsigned long)ticket)) { | |
| ret = -EIO; | |
| goto error; | |
| } | |
| /* Ticket appended above. */ | |
| q += xdr_round_up(ticket_len) / 4; | |
| if (WARN_ON((unsigned long)q - (unsigned long)payload != payload_len)) { | |
| ret = -EIO; | |
| goto error; | |
| } | |
| /* Now turn that into a key. */ | |
| key = key_alloc(&key_type_rxrpc, "x", | |
| GLOBAL_ROOT_UID, GLOBAL_ROOT_GID, cred, // TODO: Use socket owner | |
| KEY_USR_VIEW, | |
| KEY_ALLOC_NOT_IN_QUOTA, NULL); | |
| if (IS_ERR(key)) { | |
| _leave(" = -ENOMEM [alloc %ld]", PTR_ERR(key)); | |
| ret = PTR_ERR(key); | |
| goto error; | |
| } | |
| _debug("key %d", key_serial(key)); | |
| ret = key_instantiate_and_link(key, payload, payload_len, NULL, NULL); | |
| if (ret < 0) | |
| goto error_key; | |
| token = key->payload.data[0]; | |
| token->no_leak_key = true; | |
| *_key = key; | |
| key = NULL; | |
| ret = 0; | |
| goto error; | |
| error_key: | |
| key_put(key); | |
| error: | |
| kfree_sensitive(payload); | |
| _leave(" = %d", ret); | |
| return ret; | |
| } | |
| /* | |
| * Extract the token and set up a session key from the details. | |
| * | |
| * struct RXGK_TokenContainer { | |
| * afs_int32 kvno; | |
| * afs_int32 enctype; | |
| * opaque encrypted_token<>; | |
| * }; | |
| * | |
| * [tools.ietf.org/html/draft-wilkinson-afs3-rxgk-afs-08 sec 6.1] | |
| */ | |
| int rxgk_extract_token(struct rxrpc_connection *conn, struct sk_buff *skb, | |
| void *token, unsigned int token_len, | |
| struct key **_key) | |
| { | |
| const struct krb5_enctype *krb5; | |
| const struct krb5_buffer *server_secret; | |
| struct crypto_aead *token_enc = NULL; | |
| struct key *server_key; | |
| unsigned int ticket_len; | |
| void *ticket; | |
| u32 kvno, enctype; | |
| int ret, ec = 0; | |
| struct { | |
| __be32 kvno; | |
| __be32 enctype; | |
| __be32 token_len; | |
| } *container; | |
| if (token_len < sizeof(*container)) | |
| goto short_packet; | |
| /* Decode the RXGK_TokenContainer object. This tells us which server | |
| * key we should be using. We can then fetch the key, get the secret | |
| * and set up the crypto to extract the token. | |
| */ | |
| container = token; | |
| token += sizeof(*container); | |
| kvno = ntohl(container->kvno); | |
| enctype = ntohl(container->enctype); | |
| ticket_len = ntohl(container->token_len); | |
| if (ticket_len > xdr_round_down(token_len - sizeof(*container))) | |
| goto short_packet; | |
| _debug("KVNO %u", kvno); | |
| _debug("ENC %u", enctype); | |
| _debug("TLEN %u", ticket_len); | |
| server_key = rxrpc_look_up_server_security(conn, skb, kvno, enctype); | |
| if (IS_ERR(server_key)) | |
| goto cant_get_server_key; | |
| down_read(&server_key->sem); | |
| server_secret = (const void *)&server_key->payload.data[2]; | |
| ret = rxgk_set_up_token_cipher(server_secret, &token_enc, enctype, &krb5, GFP_NOFS); | |
| up_read(&server_key->sem); | |
| key_put(server_key); | |
| if (ret < 0) | |
| goto cant_get_token; | |
| /* We can now decrypt and parse the token/ticket. This allows us to | |
| * gain access to K0, from which we can derive the transport key and | |
| * thence decode the authenticator. | |
| */ | |
| ticket = token; | |
| ret = rxgk_decrypt(krb5, token_enc, &ticket, &ticket_len, &ec); | |
| crypto_free_aead(token_enc); | |
| token_enc = NULL; | |
| if (ret < 0) { | |
| if (ret != -ENOMEM) | |
| return rxrpc_abort_conn(conn, skb, ec, ret, | |
| rxgk_abort_resp_tok_dec); | |
| return ret; | |
| } | |
| ret = conn->security->default_decode_ticket(conn, skb, ticket, | |
| ticket_len, _key); | |
| if (ret < 0) | |
| goto cant_get_token; | |
| _leave(" = 0"); | |
| return ret; | |
| cant_get_server_key: | |
| ret = PTR_ERR(server_key); | |
| switch (ret) { | |
| case -ENOMEM: | |
| goto temporary_error; | |
| case -ENOKEY: | |
| case -EKEYREJECTED: | |
| case -EKEYEXPIRED: | |
| case -EKEYREVOKED: | |
| case -EPERM: | |
| return rxrpc_abort_conn(conn, skb, RXGK_BADKEYNO, -EKEYREJECTED, | |
| rxgk_abort_resp_tok_nokey); | |
| default: | |
| return rxrpc_abort_conn(conn, skb, RXGK_NOTAUTH, -EKEYREJECTED, | |
| rxgk_abort_resp_tok_keyerr); | |
| } | |
| cant_get_token: | |
| switch (ret) { | |
| case -ENOMEM: | |
| goto temporary_error; | |
| case -EINVAL: | |
| return rxrpc_abort_conn(conn, skb, RXGK_NOTAUTH, -EKEYREJECTED, | |
| rxgk_abort_resp_tok_internal_error); | |
| case -ENOPKG: | |
| return rxrpc_abort_conn(conn, skb, KRB5_PROG_KEYTYPE_NOSUPP, | |
| -EKEYREJECTED, rxgk_abort_resp_tok_nopkg); | |
| } | |
| 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. | |
| */ | |
| return ret; | |
| short_packet: | |
| return rxrpc_abort_conn(conn, skb, RXGK_PACKETSHORT, -EPROTO, | |
| rxgk_abort_resp_tok_short); | |
| } |