net/tcp: Verify inbound TCP-AO signed segments
authorDmitry Safonov <dima@arista.com>
Mon, 23 Oct 2023 19:22:04 +0000 (20:22 +0100)
committerDavid S. Miller <davem@davemloft.net>
Fri, 27 Oct 2023 09:35:45 +0000 (10:35 +0100)
Now there is a common function to verify signature on TCP segments:
tcp_inbound_hash(). It has checks for all possible cross-interactions
with MD5 signs as well as with unsigned segments.

The rules from RFC5925 are:
(1) Any TCP segment can have at max only one signature.
(2) TCP connections can't switch between using TCP-MD5 and TCP-AO.
(3) TCP-AO connections can't stop using AO, as well as unsigned
    connections can't suddenly start using AO.

Co-developed-by: Francesco Ruggeri <fruggeri@arista.com>
Signed-off-by: Francesco Ruggeri <fruggeri@arista.com>
Co-developed-by: Salam Noureddine <noureddine@arista.com>
Signed-off-by: Salam Noureddine <noureddine@arista.com>
Signed-off-by: Dmitry Safonov <dima@arista.com>
Acked-by: David Ahern <dsahern@kernel.org>
Signed-off-by: David S. Miller <davem@davemloft.net>
include/net/dropreason-core.h
include/net/tcp.h
include/net/tcp_ao.h
net/ipv4/tcp.c
net/ipv4/tcp_ao.c
net/ipv4/tcp_ipv4.c
net/ipv6/tcp_ao.c
net/ipv6/tcp_ipv6.c

index 3af4464a9c5be0123f22a1958c839c3c7921ffae..7637137ae33ee2bbe6204f9ccab0181fdbcc27e2 100644 (file)
        FN(TCP_MD5NOTFOUND)             \
        FN(TCP_MD5UNEXPECTED)           \
        FN(TCP_MD5FAILURE)              \
+       FN(TCP_AONOTFOUND)              \
+       FN(TCP_AOUNEXPECTED)            \
+       FN(TCP_AOKEYNOTFOUND)           \
+       FN(TCP_AOFAILURE)               \
        FN(SOCKET_BACKLOG)              \
        FN(TCP_FLAGS)                   \
        FN(TCP_ZEROWINDOW)              \
@@ -163,6 +167,19 @@ enum skb_drop_reason {
         * to LINUX_MIB_TCPMD5FAILURE
         */
        SKB_DROP_REASON_TCP_MD5FAILURE,
+       /**
+        * @SKB_DROP_REASON_TCP_AONOTFOUND: no TCP-AO hash and one was expected
+        */
+       SKB_DROP_REASON_TCP_AONOTFOUND,
+       /**
+        * @SKB_DROP_REASON_TCP_AOUNEXPECTED: TCP-AO hash is present and it
+        * was not expected.
+        */
+       SKB_DROP_REASON_TCP_AOUNEXPECTED,
+       /** @SKB_DROP_REASON_TCP_AOKEYNOTFOUND: TCP-AO key is unknown */
+       SKB_DROP_REASON_TCP_AOKEYNOTFOUND,
+       /** @SKB_DROP_REASON_TCP_AOFAILURE: TCP-AO hash is wrong */
+       SKB_DROP_REASON_TCP_AOFAILURE,
        /**
         * @SKB_DROP_REASON_SOCKET_BACKLOG: failed to add skb to socket backlog (
         * see LINUX_MIB_TCPBACKLOGDROP)
index c7f80cc94d718c33796fd1bea8464857e179d9c9..8e1f835bad227e5f95b85aa2dcddcc29b3b18ffb 100644 (file)
@@ -1809,7 +1809,7 @@ tcp_md5_do_lookup_any_l3index(const struct sock *sk,
 enum skb_drop_reason
 tcp_inbound_md5_hash(const struct sock *sk, const struct sk_buff *skb,
                     const void *saddr, const void *daddr,
-                    int family, int dif, int sdif);
+                    int family, int l3index, const __u8 *hash_location);
 
 
 #define tcp_twsk_md5_key(twsk) ((twsk)->tw_md5_key)
@@ -1831,7 +1831,7 @@ tcp_md5_do_lookup_any_l3index(const struct sock *sk,
 static inline enum skb_drop_reason
 tcp_inbound_md5_hash(const struct sock *sk, const struct sk_buff *skb,
                     const void *saddr, const void *daddr,
-                    int family, int dif, int sdif)
+                    int family, int l3index, const __u8 *hash_location)
 {
        return SKB_NOT_DROPPED_YET;
 }
@@ -2730,4 +2730,53 @@ static inline bool tcp_ao_required(struct sock *sk, const void *saddr,
        return false;
 }
 
+/* Called with rcu_read_lock() */
+static inline enum skb_drop_reason
+tcp_inbound_hash(struct sock *sk, const struct request_sock *req,
+                const struct sk_buff *skb,
+                const void *saddr, const void *daddr,
+                int family, int dif, int sdif)
+{
+       const struct tcphdr *th = tcp_hdr(skb);
+       const struct tcp_ao_hdr *aoh;
+       const __u8 *md5_location;
+       int l3index;
+
+       /* Invalid option or two times meet any of auth options */
+       if (tcp_parse_auth_options(th, &md5_location, &aoh))
+               return SKB_DROP_REASON_TCP_AUTH_HDR;
+
+       if (req) {
+               if (tcp_rsk_used_ao(req) != !!aoh)
+                       return SKB_DROP_REASON_TCP_AOFAILURE;
+       }
+
+       /* sdif set, means packet ingressed via a device
+        * in an L3 domain and dif is set to the l3mdev
+        */
+       l3index = sdif ? dif : 0;
+
+       /* Fast path: unsigned segments */
+       if (likely(!md5_location && !aoh)) {
+               /* Drop if there's TCP-MD5 or TCP-AO key with any rcvid/sndid
+                * for the remote peer. On TCP-AO established connection
+                * the last key is impossible to remove, so there's
+                * always at least one current_key.
+                */
+               if (tcp_ao_required(sk, saddr, family))
+                       return SKB_DROP_REASON_TCP_AONOTFOUND;
+               if (unlikely(tcp_md5_do_lookup(sk, l3index, saddr, family))) {
+                       NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPMD5NOTFOUND);
+                       return SKB_DROP_REASON_TCP_MD5NOTFOUND;
+               }
+               return SKB_NOT_DROPPED_YET;
+       }
+
+       if (aoh)
+               return tcp_inbound_ao_hash(sk, skb, family, req, aoh);
+
+       return tcp_inbound_md5_hash(sk, skb, saddr, daddr, family,
+                                   l3index, md5_location);
+}
+
 #endif /* _TCP_H */
index 1d69978e349a7e0797998eb700a84b53a67538d5..1c7c0a5d187711f8d5390f436d2e3d28cfd6f357 100644 (file)
@@ -111,6 +111,9 @@ struct tcp6_ao_context {
 };
 
 struct tcp_sigpool;
+#define TCP_AO_ESTABLISHED (TCPF_ESTABLISHED | TCPF_FIN_WAIT1 | TCPF_FIN_WAIT2 | \
+                           TCPF_CLOSE | TCPF_CLOSE_WAIT | \
+                           TCPF_LAST_ACK | TCPF_CLOSING)
 
 int tcp_ao_transmit_skb(struct sock *sk, struct sk_buff *skb,
                        struct tcp_ao_key *key, struct tcphdr *th,
@@ -130,6 +133,10 @@ int tcp_ao_calc_traffic_key(struct tcp_ao_key *mkt, u8 *key, void *ctx,
                            unsigned int len, struct tcp_sigpool *hp);
 void tcp_ao_destroy_sock(struct sock *sk, bool twsk);
 void tcp_ao_time_wait(struct tcp_timewait_sock *tcptw, struct tcp_sock *tp);
+enum skb_drop_reason tcp_inbound_ao_hash(struct sock *sk,
+                       const struct sk_buff *skb, unsigned short int family,
+                       const struct request_sock *req,
+                       const struct tcp_ao_hdr *aoh);
 struct tcp_ao_key *tcp_ao_do_lookup(const struct sock *sk,
                                    const union tcp_ao_addr *addr,
                                    int family, int sndid, int rcvid);
@@ -208,6 +215,13 @@ static inline void tcp_ao_syncookie(struct sock *sk, const struct sk_buff *skb,
 {
 }
 
+static inline enum skb_drop_reason tcp_inbound_ao_hash(struct sock *sk,
+               const struct sk_buff *skb, unsigned short int family,
+               const struct request_sock *req, const struct tcp_ao_hdr *aoh)
+{
+       return SKB_NOT_DROPPED_YET;
+}
+
 static inline struct tcp_ao_key *tcp_ao_do_lookup(const struct sock *sk,
                const union tcp_ao_addr *addr, int family, int sndid, int rcvid)
 {
index 369e2a41bc1b92329458f6476b2882e0530fee50..eb71212a09d8273028e26da0c5e6bfd83c8c2a0a 100644 (file)
@@ -4375,42 +4375,23 @@ EXPORT_SYMBOL(tcp_md5_hash_key);
 enum skb_drop_reason
 tcp_inbound_md5_hash(const struct sock *sk, const struct sk_buff *skb,
                     const void *saddr, const void *daddr,
-                    int family, int dif, int sdif)
+                    int family, int l3index, const __u8 *hash_location)
 {
-       /*
-        * This gets called for each TCP segment that arrives
-        * so we want to be efficient.
+       /* This gets called for each TCP segment that has TCP-MD5 option.
         * We have 3 drop cases:
         * o No MD5 hash and one expected.
         * o MD5 hash and we're not expecting one.
         * o MD5 hash and its wrong.
         */
-       const __u8 *hash_location = NULL;
-       struct tcp_md5sig_key *hash_expected;
        const struct tcphdr *th = tcp_hdr(skb);
        const struct tcp_sock *tp = tcp_sk(sk);
-       int genhash, l3index;
+       struct tcp_md5sig_key *key;
        u8 newhash[16];
+       int genhash;
 
-       /* sdif set, means packet ingressed via a device
-        * in an L3 domain and dif is set to the l3mdev
-        */
-       l3index = sdif ? dif : 0;
-
-       hash_expected = tcp_md5_do_lookup(sk, l3index, saddr, family);
-       if (tcp_parse_auth_options(th, &hash_location, NULL))
-               return SKB_DROP_REASON_TCP_AUTH_HDR;
-
-       /* We've parsed the options - do we have a hash? */
-       if (!hash_expected && !hash_location)
-               return SKB_NOT_DROPPED_YET;
+       key = tcp_md5_do_lookup(sk, l3index, saddr, family);
 
-       if (hash_expected && !hash_location) {
-               NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPMD5NOTFOUND);
-               return SKB_DROP_REASON_TCP_MD5NOTFOUND;
-       }
-
-       if (!hash_expected && hash_location) {
+       if (!key && hash_location) {
                NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPMD5UNEXPECTED);
                return SKB_DROP_REASON_TCP_MD5UNEXPECTED;
        }
@@ -4420,14 +4401,10 @@ tcp_inbound_md5_hash(const struct sock *sk, const struct sk_buff *skb,
         * IPv4-mapped case.
         */
        if (family == AF_INET)
-               genhash = tcp_v4_md5_hash_skb(newhash,
-                                             hash_expected,
-                                             NULL, skb);
+               genhash = tcp_v4_md5_hash_skb(newhash, key, NULL, skb);
        else
-               genhash = tp->af_specific->calc_md5_hash(newhash,
-                                                        hash_expected,
+               genhash = tp->af_specific->calc_md5_hash(newhash, key,
                                                         NULL, skb);
-
        if (genhash || memcmp(hash_location, newhash, 16) != 0) {
                NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPMD5FAILURE);
                if (family == AF_INET) {
index de3710758d55ee394e0f33a33d5e0bf79529ab32..6c5815713b7351c983aa0debb05935fa8748e2ae 100644 (file)
@@ -761,6 +761,148 @@ void tcp_ao_syncookie(struct sock *sk, const struct sk_buff *skb,
        treq->maclen = tcp_ao_maclen(key);
 }
 
+static enum skb_drop_reason
+tcp_ao_verify_hash(const struct sock *sk, const struct sk_buff *skb,
+                  unsigned short int family, struct tcp_ao_info *info,
+                  const struct tcp_ao_hdr *aoh, struct tcp_ao_key *key,
+                  u8 *traffic_key, u8 *phash, u32 sne)
+{
+       u8 maclen = aoh->length - sizeof(struct tcp_ao_hdr);
+       const struct tcphdr *th = tcp_hdr(skb);
+       void *hash_buf = NULL;
+
+       if (maclen != tcp_ao_maclen(key))
+               return SKB_DROP_REASON_TCP_AOFAILURE;
+
+       hash_buf = kmalloc(tcp_ao_digest_size(key), GFP_ATOMIC);
+       if (!hash_buf)
+               return SKB_DROP_REASON_NOT_SPECIFIED;
+
+       /* XXX: make it per-AF callback? */
+       tcp_ao_hash_skb(family, hash_buf, key, sk, skb, traffic_key,
+                       (phash - (u8 *)th), sne);
+       if (memcmp(phash, hash_buf, maclen)) {
+               kfree(hash_buf);
+               return SKB_DROP_REASON_TCP_AOFAILURE;
+       }
+       kfree(hash_buf);
+       return SKB_NOT_DROPPED_YET;
+}
+
+enum skb_drop_reason
+tcp_inbound_ao_hash(struct sock *sk, const struct sk_buff *skb,
+                   unsigned short int family, const struct request_sock *req,
+                   const struct tcp_ao_hdr *aoh)
+{
+       const struct tcphdr *th = tcp_hdr(skb);
+       u8 *phash = (u8 *)(aoh + 1); /* hash goes just after the header */
+       struct tcp_ao_info *info;
+       enum skb_drop_reason ret;
+       struct tcp_ao_key *key;
+       __be32 sisn, disn;
+       u8 *traffic_key;
+       u32 sne = 0;
+
+       info = rcu_dereference(tcp_sk(sk)->ao_info);
+       if (!info)
+               return SKB_DROP_REASON_TCP_AOUNEXPECTED;
+
+       if (unlikely(th->syn)) {
+               sisn = th->seq;
+               disn = 0;
+       }
+
+       /* Fast-path */
+       if (likely((1 << sk->sk_state) & TCP_AO_ESTABLISHED)) {
+               enum skb_drop_reason err;
+               struct tcp_ao_key *current_key;
+
+               /* Check if this socket's rnext_key matches the keyid in the
+                * packet. If not we lookup the key based on the keyid
+                * matching the rcvid in the mkt.
+                */
+               key = READ_ONCE(info->rnext_key);
+               if (key->rcvid != aoh->keyid) {
+                       key = tcp_ao_established_key(info, -1, aoh->keyid);
+                       if (!key)
+                               goto key_not_found;
+               }
+
+               /* Delayed retransmitted SYN */
+               if (unlikely(th->syn && !th->ack))
+                       goto verify_hash;
+
+               sne = 0;
+               /* Established socket, traffic key are cached */
+               traffic_key = rcv_other_key(key);
+               err = tcp_ao_verify_hash(sk, skb, family, info, aoh, key,
+                                        traffic_key, phash, sne);
+               if (err)
+                       return err;
+               current_key = READ_ONCE(info->current_key);
+               /* Key rotation: the peer asks us to use new key (RNext) */
+               if (unlikely(aoh->rnext_keyid != current_key->sndid)) {
+                       /* If the key is not found we do nothing. */
+                       key = tcp_ao_established_key(info, aoh->rnext_keyid, -1);
+                       if (key)
+                               /* pairs with tcp_ao_del_cmd */
+                               WRITE_ONCE(info->current_key, key);
+               }
+               return SKB_NOT_DROPPED_YET;
+       }
+
+       /* Lookup key based on peer address and keyid.
+        * current_key and rnext_key must not be used on tcp listen
+        * sockets as otherwise:
+        * - request sockets would race on those key pointers
+        * - tcp_ao_del_cmd() allows async key removal
+        */
+       key = tcp_ao_inbound_lookup(family, sk, skb, -1, aoh->keyid);
+       if (!key)
+               goto key_not_found;
+
+       if (th->syn && !th->ack)
+               goto verify_hash;
+
+       if ((1 << sk->sk_state) & (TCPF_LISTEN | TCPF_NEW_SYN_RECV)) {
+               /* Make the initial syn the likely case here */
+               if (unlikely(req)) {
+                       sne = 0;
+                       sisn = htonl(tcp_rsk(req)->rcv_isn);
+                       disn = htonl(tcp_rsk(req)->snt_isn);
+               } else if (unlikely(th->ack && !th->syn)) {
+                       /* Possible syncookie packet */
+                       sisn = htonl(ntohl(th->seq) - 1);
+                       disn = htonl(ntohl(th->ack_seq) - 1);
+                       sne = 0;
+               } else if (unlikely(!th->syn)) {
+                       /* no way to figure out initial sisn/disn - drop */
+                       return SKB_DROP_REASON_TCP_FLAGS;
+               }
+       } else if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
+               disn = info->lisn;
+               if (th->syn || th->rst)
+                       sisn = th->seq;
+               else
+                       sisn = info->risn;
+       } else {
+               WARN_ONCE(1, "TCP-AO: Unexpected sk_state %d", sk->sk_state);
+               return SKB_DROP_REASON_TCP_AOFAILURE;
+       }
+verify_hash:
+       traffic_key = kmalloc(tcp_ao_digest_size(key), GFP_ATOMIC);
+       if (!traffic_key)
+               return SKB_DROP_REASON_NOT_SPECIFIED;
+       tcp_ao_calc_key_skb(key, traffic_key, skb, sisn, disn, family);
+       ret = tcp_ao_verify_hash(sk, skb, family, info, aoh, key,
+                                traffic_key, phash, sne);
+       kfree(traffic_key);
+       return ret;
+
+key_not_found:
+       return SKB_DROP_REASON_TCP_AOKEYNOTFOUND;
+}
+
 static int tcp_ao_cache_traffic_keys(const struct sock *sk,
                                     struct tcp_ao_info *ao,
                                     struct tcp_ao_key *ao_key)
index bdf0224ae827536104c8bc5c9d0dbf00bec67c84..f39ccefa78dcd3ef7026ec7771e37f6dd09afbdd 100644 (file)
@@ -2204,9 +2204,9 @@ process:
                if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb))
                        drop_reason = SKB_DROP_REASON_XFRM_POLICY;
                else
-                       drop_reason = tcp_inbound_md5_hash(sk, skb,
-                                                  &iph->saddr, &iph->daddr,
-                                                  AF_INET, dif, sdif);
+                       drop_reason = tcp_inbound_hash(sk, req, skb,
+                                                      &iph->saddr, &iph->daddr,
+                                                      AF_INET, dif, sdif);
                if (unlikely(drop_reason)) {
                        sk_drops_add(sk, skb);
                        reqsk_put(req);
@@ -2283,8 +2283,8 @@ process:
                goto discard_and_relse;
        }
 
-       drop_reason = tcp_inbound_md5_hash(sk, skb, &iph->saddr,
-                                          &iph->daddr, AF_INET, dif, sdif);
+       drop_reason = tcp_inbound_hash(sk, NULL, skb, &iph->saddr, &iph->daddr,
+                                      AF_INET, dif, sdif);
        if (drop_reason)
                goto discard_and_relse;
 
index 99753e12c08c5bd98afdad7ebd3a7f97cfca7370..8b04611c9078ecf33e4c6c4b4dd9da5f9cb187e5 100644 (file)
@@ -53,11 +53,12 @@ int tcp_v6_ao_calc_key_skb(struct tcp_ao_key *mkt, u8 *key,
                           const struct sk_buff *skb,
                           __be32 sisn, __be32 disn)
 {
-       const struct ipv6hdr *iph = ipv6_hdr(skb);
-       const struct tcphdr *th = tcp_hdr(skb);
+       const struct ipv6hdr *iph = ipv6_hdr(skb);
+       const struct tcphdr *th = tcp_hdr(skb);
 
-       return tcp_v6_ao_calc_key(mkt, key, &iph->saddr, &iph->daddr,
-                                th->source, th->dest, sisn, disn);
+       return tcp_v6_ao_calc_key(mkt, key, &iph->saddr,
+                                 &iph->daddr, th->source,
+                                 th->dest, sisn, disn);
 }
 
 int tcp_v6_ao_calc_key_sk(struct tcp_ao_key *mkt, u8 *key,
index 8c5c96187a72812e4a7c62f121d2beef08946ed1..d2724383d263b2188ee33274810662e2227a7a75 100644 (file)
@@ -1785,9 +1785,9 @@ process:
                if (!xfrm6_policy_check(sk, XFRM_POLICY_IN, skb))
                        drop_reason = SKB_DROP_REASON_XFRM_POLICY;
                else
-                       drop_reason = tcp_inbound_md5_hash(sk, skb,
-                                                          &hdr->saddr, &hdr->daddr,
-                                                          AF_INET6, dif, sdif);
+                       drop_reason = tcp_inbound_hash(sk, req, skb,
+                                                      &hdr->saddr, &hdr->daddr,
+                                                      AF_INET6, dif, sdif);
                if (drop_reason) {
                        sk_drops_add(sk, skb);
                        reqsk_put(req);
@@ -1861,8 +1861,8 @@ process:
                goto discard_and_relse;
        }
 
-       drop_reason = tcp_inbound_md5_hash(sk, skb, &hdr->saddr, &hdr->daddr,
-                                          AF_INET6, dif, sdif);
+       drop_reason = tcp_inbound_hash(sk, NULL, skb, &hdr->saddr, &hdr->daddr,
+                                      AF_INET6, dif, sdif);
        if (drop_reason)
                goto discard_and_relse;
 
@@ -2089,6 +2089,7 @@ static const struct tcp_sock_af_ops tcp_sock_ipv6_mapped_specific = {
        .ao_lookup      =       tcp_v6_ao_lookup,
        .calc_ao_hash   =       tcp_v4_ao_hash_skb,
        .ao_parse       =       tcp_v6_parse_ao,
+       .ao_calc_key_sk =       tcp_v4_ao_calc_key_sk,
 #endif
 };
 #endif