diff --git a/README.md b/README.md index c60ac28b45..2ed654d5b8 100644 --- a/README.md +++ b/README.md @@ -47,11 +47,12 @@ while Vita is running, without affecting unrelated routes. - Runs on commodity hardware - Implements IPsec for IPv4, specifically - *IP Encapsulating Security Payload* (ESP) in tunnel mode + *IP Encapsulating Security Payload* (ESP) in tunnel mode (audit needed) - Uses optimized AES-GCM 128-bit encryption based on a reference implementation by *Intel* for their AVX2 (generation-4) processors - Suitable for 1-Gigabit, 10-Gigabit (and beyond?) Ethernet -- Automated key exchange and rotation (work in progress!) +- Automated key exchange and rotation, with perfect forward secrecy (PFS) + (audit needed) - Dynamic reconfiguration (update routes while running) - Strong observability: access relevant statistics of a running Vita node diff --git a/src/Makefile.vita-test b/src/Makefile.vita-test index e15d329b27..7ea65e8e8f 100644 --- a/src/Makefile.vita-test +++ b/src/Makefile.vita-test @@ -1,4 +1,4 @@ -SRCPATTERN = '\([^/\#\.]*\|\(core/\|arch/\|jit/\|syscall/\|lib/lua/\|lib/protocol/\|lib/checksum\|lib/ipsec/\|lib/yang/\|lib/xsd_regexp\|lib/maxpc\|lib/ctable\|lib/binary_search\|lib/multi_copy\|lib/hash/\|lib/cltable\|lib/lpm/\|lib/interlink\|lib/hardware/\|lib/macaddress\|lib/numa\|apps/basic/\|apps/test/\|apps/packet_filter/\|pf\|apps/pcap/\|lib/pcap/\|apps/interlink/\|apps/intel_mp/\|lib/sodium\|program/snsh\|program/vita\)[^\#]*\)' +SRCPATTERN = '\([^/\#\.]*\|\(core/\|arch/\|jit/\|syscall/\|lib/lua/\|lib/protocol/\|lib/checksum\|lib/ipsec/\|lib/yang/\|lib/xsd_regexp\|lib/maxpc\|lib/ctable\|lib/binary_search\|lib/multi_copy\|lib/hash/\|lib/cltable\|lib/lpm/\|lib/interlink\|lib/hardware/\|lib/macaddress\|lib/numa\|apps/basic/\|apps/test/\|apps/packet_filter/\|pf\|apps/pcap/\|lib/pcap/\|apps/interlink/\|apps/intel_mp/\|program/snsh\|program/vita\)[^\#]*\)' all: SRCPATTERN=$(SRCPATTERN) $(MAKE) diff --git a/src/lib/ipsec/esp.lua b/src/lib/ipsec/esp.lua index ed17a57a44..e48659c561 100644 --- a/src/lib/ipsec/esp.lua +++ b/src/lib/ipsec/esp.lua @@ -188,15 +188,13 @@ function decrypt:new (conf) end function decrypt:decrypt_payload (ptr, length) - if not self.esp:new_from_mem(ptr, length) - or self.esp:spi() ~= self.spi - then return nil end - + -- NB: bounds check is performed by caller + local esp = self.esp:new_from_mem(ptr, esp:sizeof()) local iv_start = ptr + ESP_SIZE local ctext_start = ptr + self.CTEXT_OFFSET local ctext_length = length - self.PLAIN_OVERHEAD - local seq_low = self.esp:seq_no() + local seq_low = esp:seq_no() local seq_high = tonumber( C.check_seq_no(seq_low, self.seq.no, self.window, self.window_size) ) diff --git a/src/lib/sodium.h b/src/lib/sodium.h deleted file mode 100644 index 105d724a15..0000000000 --- a/src/lib/sodium.h +++ /dev/null @@ -1,30 +0,0 @@ -// sodium/core.h -int sodium_init(void); - -// sodium/randombytes.h: -void randombytes_buf(void * const buf, const size_t size); - -// sodium/crypto_aead_xchacha20poly1305.h -enum { - crypto_aead_xchacha20poly1305_ietf_NPUBBYTES = 24U, - crypto_aead_xchacha20poly1305_ietf_KEYBYTES = 32U, - crypto_aead_xchacha20poly1305_ietf_ABYTES = 16U -}; -int crypto_aead_xchacha20poly1305_ietf_encrypt(unsigned char *c, - unsigned long long *clen_p, - const unsigned char *m, - unsigned long long mlen, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *nsec, - const unsigned char *npub, - const unsigned char *k); -int crypto_aead_xchacha20poly1305_ietf_decrypt(unsigned char *m, - unsigned long long *mlen_p, - unsigned char *nsec, - const unsigned char *c, - unsigned long long clen, - const unsigned char *ad, - unsigned long long adlen, - const unsigned char *npub, - const unsigned char *k); diff --git a/src/program/vita/README.config b/src/program/vita/README.config index 23502f0eff..3c9585b7e6 100644 --- a/src/program/vita/README.config +++ b/src/program/vita/README.config @@ -23,6 +23,7 @@ CONFIGURATION SYNTAX net_cidr4 ; gw_ip4 ; preshared_key ; + spi ; NOTES @@ -40,15 +41,19 @@ NOTES configure an Ethernet address. Each route is given a human readable identifier that can be used for - documentation purposes. This identifier has no other bearing. The destination - IPv4 subnet and gateway are specified with an IPv4 prefix in CIDR notation, - and an IPv4 address respectively. Finally, each route is assigned a unique, - preshared 256-bit key, encoded as a hexadecimal string (two digits for each - octet, most significant digit first). + documentation purposes. This identifier has no other bearing. The route’s + destination IPv4 subnet and gateway are specified with an IPv4 prefix in CIDR + notation, and an IPv4 address respectively. For authentication, each route is + assigned a unique, pre-shared 256-bit key, encoded as a hexadecimal string + (two digits for each octet, most significant digit first). Finally, a unique + Security Parameter Index (SPI), which must be a positive integer equal or + greater than 256, is specified for tagging and associating encrypted traffic + for a given route. Like the pre-shared key, the SPI must be the same for both + ends of a route. - While the default configuration should be generally applicable, negotiation - timeout and the lifetime of security associations can be specified in - seconds. + While the default configuration should be generally applicable, the + negotiation timeout and lifetime of Security Associations (SA) can be + specified in seconds. EXAMPLE @@ -66,6 +71,7 @@ EXAMPLE net_cidr4 192.168.20.0/24; gw_ip4 203.0.113.2; preshared_key 91440DA06376A668AC4959A840A125D75FB544E8AA25A08B813E49F0A4B2E270; + spi 1001; } route { @@ -73,6 +79,7 @@ EXAMPLE net_cidr4 192.168.30.0/24; gw_ip4 203.0.113.3; preshared_key CF0BDD7A058BE55C12B7F2AA30D23FF01BDF8BE6571F2019ED7F7EBD3DA97B47; + spi 1002; } sa_ttl 86400; diff --git a/src/program/vita/README.exchange b/src/program/vita/README.exchange new file mode 100644 index 0000000000..22300394d2 --- /dev/null +++ b/src/program/vita/README.exchange @@ -0,0 +1,89 @@ +VITA: SIMPLE KEY EXCHANGE (vita-ske, version 1g) + + A simple key negotiation protocol based on pre-shared symmetric keys that + provides authentication, perfect forward secrecy, and is immune to replay + attacks. + +Primitives (from libsodium 1.0.15): + + • HMAC: crypto_auth_hmacsha512256 (key is 256‑bits, output is 256‑bits) + • DH: crypto_scalarmult_curve25519 (keys are 256‑bits, output is 256‑bits) + • HASH: crypto_generichash_blake2b (output is choosen to be 160‑bits) + +Notational Conventions: + + → m + Denotes that we receive the message m. + + ← m + Denotes that we send the message m. + + a ‖ b + Denotes a concatenated with b. + +Description: + + Let k be a pre-shared symmetric key. Let spi be a “Security Parameter Index” + (SPI). Let n1 and n2 be random 256‑bit nonces, where n1 is chosen by us. + + ← n1 + → n2 + + Let (p1, s1) and (p2, s2) be random, ephemeral, asymmetric key pairs, where + (p1, s1) is choosen by us. Let h1 = HMAC(k, spi ‖ n1 ‖ n2 ‖ p1). + + ← p1 ‖ h1 + → p2 ‖ h2 + + Ensure that h2 = HMAC(k, spi ‖ n2 ‖ n1 ‖ p2). Let q = DH(s1, p2), and ensure + that p2 is a valid argument. Let rx = HASH(q ‖ p1 ‖ p2) and + tx = HASH(q ‖ p2 ‖ p1) be key material. Assign (spi, rx) to the incoming + “Security Association” (SA), and (spi, tx) to the outgoing SA. + + The description above illustrates the perspective of an active party adhering + to the protocol, i.e. the exchange is initiated by us. An opposite, passive + party adhering to the protocol, i.e. one that is merely responding to a key + exchange initiated by an active party, must ensure that the tuple + (spi, p2, h2) was received and authenticated before computing and sending its + response tuple (spi, p1, h1). For a passive party the order of messages during + the exchange is reversed: + + → n2 + ← n1 + → p2 ‖ h2 (ensure h2 is verified before we reply) + ← p1 ‖ h1 + + Note that there might be no passive party in an exchange if both parties have + initiated the exchange before receiving an initial nonce message. + +Security Proof: + + Assuming an attacker can not guess k, and n1 has enough entropy so that the + probability that the tuple (n1, p2) has occurred before is negligible, they + are unable to produce the value h2 = HMAC(k, spi ‖ n2 ‖ n1 ‖ p2), and thus are + unable to complete a key exchange. + + Assuming an attacker can not guess s1 or s2, they are unable to produce + q = DH(s1, p2) or q = DH(s2, p1), and subsequently derive rx or tx, and thus + perfect forward secrecy is provided. + + A party passively adhering to the protocol will not produce a tuple + (spi, p1, h1) unless it has previously authenticated its counterpart tuple + (spi, p2, h2), and thus can not be used as an oracle. + +Notes: + + • Future versions of this protocol may introduce the use of a key derivation + function (KDF), such as libsodium’s crypto_kdf_blake2b_derive_from_key, to + derive additional key material from rx or tx. + +References: + + • The spiped protocol: + https://github.com/Tarsnap/spiped/blob/d1e62a8/DESIGN.md#spiped-design + • Additional discussion of the spiped protocol: + https://github.com/Tarsnap/spiped/issues/151 + • The Sodium crypto library (libsodium): + https://download.libsodium.org/doc/ + • Security Architecture for the Internet Protocol: + https://tools.ietf.org/html/rfc4301 diff --git a/src/program/vita/conftest.snabb b/src/program/vita/conftest.snabb index abe1abf150..511a3ac5ca 100755 --- a/src/program/vita/conftest.snabb +++ b/src/program/vita/conftest.snabb @@ -47,7 +47,8 @@ local conf1 = { loopback = { net_cidr4 = "192.168.10.0/24", gw_ip4 = "203.0.113.1", - preshared_key = string.rep("00", 32) + preshared_key = string.rep("00", 32), + spi = 1001 } } } @@ -70,7 +71,8 @@ local conf2 = { loopback = { net_cidr4 = "192.168.10.0/24", gw_ip4 = "203.0.113.1", - preshared_key = string.rep("FF", 32) + preshared_key = string.rep("FF", 32), + spi = 1001 } } } @@ -89,7 +91,8 @@ local conf3 = { loopback = { net_cidr4 = "192.168.10.0/24", gw_ip4 = "203.0.113.2", - preshared_key = string.rep("FF", 32) + preshared_key = string.rep("FF", 32), + spi = 1001 } } } @@ -106,16 +109,58 @@ local conf4 = { public_nexthop_ip4 = "203.0.113.2", route = { loopback = { - net_cidr4 = "192.168.10.0/24", + net_cidr4 = "192.168.10.0/16", gw_ip4 = "203.0.113.2", - preshared_key = string.rep("FF", 32) + preshared_key = string.rep("FF", 32), + spi = 1001 } } } -print("Change route public_ip4...") +print("Change public_ip4 and route net_cidr4...") commit_conf(conf4) S.sleep(3) +local conf5 = { + private_interface = { macaddr = "52:54:00:00:00:00" }, + public_interface = { macaddr = "52:54:00:00:00:FF" }, + private_ip4 = "192.168.10.1", + public_ip4 = "203.0.113.2", + private_nexthop_ip4 = "192.168.10.1", + public_nexthop_ip4 = "203.0.113.2", + route = { + loopback2 = { + net_cidr4 = "192.168.10.0/16", + gw_ip4 = "203.0.113.2", + preshared_key = string.rep("FF", 32), + spi = 1001 + } + } +} +print("Change route id...") +commit_conf(conf5) +S.sleep(3) + +local conf6 = { + private_interface = { macaddr = "52:54:00:00:00:00" }, + public_interface = { macaddr = "52:54:00:00:00:FF" }, + private_ip4 = "192.168.10.1", + public_ip4 = "203.0.113.2", + private_nexthop_ip4 = "192.168.10.1", + public_nexthop_ip4 = "203.0.113.2", + negotiation_ttl = 1, + route = { + loopback2 = { + net_cidr4 = "192.168.10.0/16", + gw_ip4 = "203.0.113.2", + preshared_key = string.rep("FF", 32), + spi = 1001 + } + } +} +print("Change negotiation_ttl...") +commit_conf(conf6) +S.sleep(3) + print("Remove route...") commit_conf(conf0) S.sleep(3) diff --git a/src/program/vita/exchange.lua b/src/program/vita/exchange.lua index 99839d4620..df561ad64f 100644 --- a/src/program/vita/exchange.lua +++ b/src/program/vita/exchange.lua @@ -2,16 +2,142 @@ module(...,package.seeall) +-- This module handles KEY NEGOTIATION with peers and SA CONFIGURATION, which +-- includes dynamically reacting to changes to the routes defined in Vita’s +-- root configuration. For each route defined in the gateway’s configuration a +-- pair of SAs (inbound and outbound) is negotiated and maintained. On change, +-- the set of SAs is written to configuration files picked up by the esp_worker +-- and dsp_worker processes. +-- +-- (neg. proto.) +-- || +-- || +-- --> KeyManager *--> esp_worker +-- | +-- \--> dsp_worker + +-- +-- All things considered, this is the hairy part of Vita, as it covers touchy +-- things such as key generation and expiration, and ultimately presents Vita’s +-- main exploitation surface. On the upside, this module’s data plane doesn’t +-- need worry as much about soft real-time requirements as others, as its +-- generally low-throughput. It can (and should) primarily focus on safety, +-- and can afford more costly dynamic high-level language features to do so. +-- At least to the extent where to doesn’t enable low-traffic DoS, that is. +-- +-- In order to insulate failure, this module is composed of three subsystems: +-- +-- 1. The KeyManager app handles the data plane traffic (key negotiation +-- requests and responses) and configuration plane changes (react to +-- configuration changes and generate configurations for negotiated SAs). +-- +-- It tries its best to avoid clobbering valid SA configurations too. I.e. +-- SAs whose routes are not changed in a configuration transition are +-- unaffected by the ensuing re-configuration, allowing for seamless +-- addition of new routes and network address renumbering. +-- +-- Whenever SAs are invalidated, i.e. because the route’s pre-shared key or +-- SPI is changed, or because a route is removed entirely, or because the +-- lifetime of a SA pair has expired (sa_ttl), it is destroyed, and +-- eventually re-negotiated if applicable. +-- +-- Note that the KeyManager app will attempts to re-negotiate SAs long +-- before they expire (specifically, once half of sa_ttl has passed), in +-- order to avoid loss of tunnel connectivity during re-negotiation. +-- +-- Negotiation requests are fed from the input port to the individual +-- Protocol finite-state machine (described below in 2.) of a route, and +-- associated to routes via the Transport wrapper (described below in 3.). +-- Replies and outgoing requests (also obtained by mediating with the +-- Protocol fsm) are sent via the output port. +-- +-- Any meaningful events regarding SA negotiation and expiry are logged and +-- registered in the following counters: +-- +-- rxerrors count of all erroneous incoming requests +-- (includes all others counters) +-- +-- route_errors count of requests that couldn’t be associated +-- to any configured route +-- +-- protocol_errors count of requests that violated the protocol +-- (order of messages and message format) +-- +-- authentication_errors count of requests that were detected to be +-- unauthentic (had an erroneous MAC, this +-- includes packets corrupted during transit) +-- +-- public_key_errors count of public keys that were rejected +-- because they were considered unsafe +-- +-- negotiations_initiated count of negotiations initiated by us +-- +-- negotiations_expired count of negotiations expired +-- (negotiation_ttl) +-- +-- nonces_negotiated count of nonce pairs that were exchanged +-- (elevated count can indicate DoS attempts) +-- +-- keypairs_negotiated count of ephemeral key pairs that were +-- exchanged +-- +-- keypairs_expired count of ephemeral key pairs that have +-- expired (sa_ttl) +-- +-- 2. The Protocol subsysem implements vita-ske1 (the cryptographic key +-- exchange protocol defined in README.exchange) as a finite-state machine +-- with a timeout (negotiation_ttl) in a way that should be mostly DoS +-- resistant, i.e. it can’t be put into a waiting state by inbound +-- requests. +-- +-- For a state transition diagram see: fsm-protocol.svg +-- +-- Alternatively it has been considered to implement the protocol on top of +-- a connection based transport protocol (like TCP), i.e. allow multiple +-- concurrent negotiations for each individual route. Such a protocol +-- implementation wasn’t immediately available, and implementing one seemed +-- daunting, and that’s why now each route has just its one own Protocol +-- fsm. +-- +-- The Protocol fsm requires its user (the KeyManager app) to “know” about +-- the state transitions of the exchange protocol, but it is written in a +-- way that intends to make fatal misuse impossible, given that one sticks +-- to its public API methods. I.e. it is driven by calling the methods +-- +-- initiate_exchange +-- receive_nonce +-- exchange_key +-- receive_key +-- derive_ephemeral_keys +-- reset_if_expired +-- +-- which uphold invariants that should ensure any resulting key material is +-- trustworthy, signal any error conditions to the caller, and maintain +-- general consistency of the protocol so that it doesn’t get stuck. +-- Hopefully, the worst consequence of misusing the Protocol fsm is failure +-- to negotiate a key pair. +-- +-- 3. The Transport header is a super-light transport header that encodes the +-- target SPI and message type of the protocol requests it precedes. It is +-- used by the KeyManager app to parse requests and associate them to the +-- correct route by SPI. It uses the IP protocol type 99 for “any private +-- encryption scheme”. +-- +-- It exists explicitly separate from the KeyManager app and Protocol fsm, +-- to clarify that it is interchangable, and logically unrelated to either +-- components. + local S = require("syscall") local ffi = require("ffi") local shm = require("core.shm") local counter = require("core.counter") +local header = require("lib.protocol.header") local lib = require("core.lib") local ipv4 = require("lib.protocol.ipv4") local yang = require("lib.yang.yang") local schemata = require("program.vita.schemata") -local logger = lib.logger_new({ rate = 32, module = 'KeyManager' }) -require("lib.sodium_h") +local audit = lib.logger_new({rate=32, module='KeyManager'}) +require("program.vita.sodium_h") local C = ffi.C PROTOCOL = 99 -- “Any private encryption scheme” @@ -24,22 +150,32 @@ KeyManager = { esp_keyfile = {required=true}, dsp_keyfile = {required=true}, negotiation_ttl = {default=10}, - sa_ttl = {default=(7 * 24 * 60 * 60)} + sa_ttl = {default=(24 * 60 * 60)} }, shm = { rxerrors = {counter}, route_errors = {counter}, + protocol_errors = {counter}, authentication_errors = {counter}, + public_key_errors = {counter}, + negotiations_initiated = {counter}, negotiations_expired = {counter}, - keypairs_exchanged = {counter}, + nonces_negotiated = {counter}, + keypairs_negotiated = {counter}, keypairs_expired = {counter} } } -local status = { expired = 0, negotiating = 1, ready = 2 } +local status = { expired = 0, rekey = 1, ready = 2 } function KeyManager:new (conf) - local o = { routes = {}, ip = ipv4:new({}) } + local o = { + routes = {}, + ip = ipv4:new({}), + transport = Transport.header:new({}), + nonce_message = Protocol.nonce_message:new({}), + key_message = Protocol.key_message:new({}) + } local self = setmetatable(o, { __index = KeyManager }) self:reconfig(conf) assert(C.sodium_init() >= 0, "Failed to initialize libsodium.") @@ -52,49 +188,49 @@ function KeyManager:reconfig (conf) if route.id == id then return route end end end - local function route_equal (x, y) - return x.id == y.id - and x.gw_ip4 == y.gw_ip4 - and lib.equal(x.preshared_key, y.preshared_key) + local function route_match (route, preshared_key, spi) + return lib.equal(route.preshared_key, preshared_key) + and route.spi == spi end local function free_route (route) if route.status ~= status.expired then - timer.deactivate(route.timeout) - logger:log("Expiring keys for "..route.gw_ip4.." (reconfig)") + audit:log("Expiring keys for '"..route.id.."' (reconfig)") self:expire_route(route) end end - -- NB: if node_ip4 changes, all ephemeral keys are invalidated - local new_node_ip4n = ipv4:pton(conf.node_ip4) - -- compute new set of routes local new_routes = {} for id, route in pairs(conf.routes) do + local new_key = lib.hexundump(route.preshared_key, + Protocol.preshared_key_bytes) local old_route = find_route(id) - local new_route = { - id = id, - gw_ip4 = route.gw_ip4, - gw_ip4n = ipv4:pton(route.gw_ip4), -- for fast compare - preshared_key = lib.hexundump( - route.preshared_key, - C.crypto_aead_xchacha20poly1305_ietf_KEYBYTES - ), - status = status.expired, - tx_sa = nil, rx_sa = nil, - timeout = nil - } - if old_route - and route_equal(new_route, old_route) - and lib.equal(self.node_ip4n, new_node_ip4n) - then + if old_route and route_match(old_route, new_key, route.spi) then -- keep old route table.insert(new_routes, old_route) + -- if negotation_ttl has changed, swap out old protocol fsm for a new + -- one with the adjusted timeout, effectively resetting the fsm + if conf.negotiation_ttl ~= self.negotiation_ttl then + audit:log("Protocol reset for "..id.." (reconfig)") + old_route.protocol = Protocol:new(old_route.spi, + old_route.preshared_key, + conf.negotiation_ttl) + end else + -- insert new new route + local new_route = { + id = id, + gw_ip4n = ipv4:pton(route.gw_ip4), + preshared_key = new_key, + spi = route.spi, + status = status.expired, + rx_sa = nil, tx_sa = nil, + sa_timeout = nil, rekey_timeout = nil, + protocol = Protocol:new(route.spi, new_key, conf.negotiation_ttl) + } + table.insert(new_routes, new_route) -- clean up after the old route if necessary if old_route then free_route(old_route) end - -- insert new route - table.insert(new_routes, new_route) end end @@ -104,7 +240,7 @@ function KeyManager:reconfig (conf) end -- switch to new configuration - self.node_ip4n = new_node_ip4n + self.node_ip4n = ipv4:pton(conf.node_ip4) self.routes = new_routes self.esp_keyfile = shm.root.."/"..shm.resolve(conf.esp_keyfile) self.dsp_keyfile = shm.root.."/"..shm.resolve(conf.dsp_keyfile) @@ -113,126 +249,140 @@ function KeyManager:reconfig (conf) end function KeyManager:push () + -- handle negotiation protocol requests local input = self.input.input while not link.empty(input) do local request = link.receive(input) self:handle_negotiation(request) packet.free(request) end + + -- process protocol timeouts and initiate (re-)negotiation for SAs for _, route in ipairs(self.routes) do - if route.status == status.expired then + if route.protocol:reset_if_expired() == Protocol.code.expired then + counter.add(self.shm.negotiations_expired) + audit:log("Negotiation expired for '"..route.id.."' (negotiation_ttl") + end + if route.status < status.ready then self:negotiate(route) + elseif route.rekey_timeout() then + route.status = status.rekey + elseif route.sa_timeout() then + counter.add(self.shm.keypairs_expired) + audit:log("Keys expired for '"..route.id.."' (sa_ttl)") + self:expire_route(route) end end end -local function randombytes (n) - local bytes = ffi.new("uint8_t[?]", n) - C.randombytes_buf(bytes, n) - return bytes -end - function KeyManager:negotiate (route) - logger:log("Sending key exchange request to "..route.gw_ip4) - - route.status = status.negotiating - self:set_negotiation_timeout(route) - - route.tx_sa = { - mode = "aes-gcm-128-12", - spi = math.max(1, ffi.cast("uint32_t *", randombytes(2))[0]), - key = lib.hexdump(ffi.string(randombytes(16), 16)), - salt = lib.hexdump(ffi.string(randombytes(4), 4)) - } - - link.transmit(self.output.output, self:request(route)) + local ecode, nonce_message = + route.protocol:initiate_exchange(self.nonce_message) + if not ecode then + counter.add(self.shm.negotiations_initiated) + audit:log("Initiating negotiation for '"..route.id.."'") + link.transmit(self.output.output, self:request(route, nonce_message)) + end end function KeyManager:handle_negotiation (request) - local route, sa = self:parse_request(request) - if not route then + local route, message = self:parse_request(request) + + if not (self:handle_nonce_request(route, message) + or self:handle_key_request(route, message)) then counter.add(self.shm.rxerrors) - logger:log("Ignoring malformed key exchange request") - return + audit:log("Rejected invalid negotiation request") end +end - logger:log("Received key exchange request from "..route.gw_ip4) +function KeyManager:handle_nonce_request (route, message) + if not route or message ~= self.nonce_message then return end - route.rx_sa = sa + local ecode, response = route.protocol:receive_nonce(message) + if ecode == Protocol.code.protocol then + counter.add(self.shm.protocol_errors) + return false + else assert(not ecode) end - if route.status == status.negotiating then - counter.add(self.shm.keypairs_exchanged) - logger:log("Completed key exchange with "..route.gw_ip4) - route.status = status.ready - timer.deactivate(route.timeout) - self:set_sa_timeout(route) - self:commit_ephemeral_keys() + counter.add(self.shm.nonces_negotiated) + audit:log("Negotiated nonces for '"..route.id.."'") + + if response then + link.transmit(self.output.output, self:request(route, response)) else - self:negotiate(route) + audit:log("Offering keys for '"..route.id.."'") + local _, key_message = route.protocol:exchange_key(self.key_message) + link.transmit(self.output.output, self:request(route, key_message)) end + + return true end -function KeyManager:set_negotiation_timeout (route) - route.timeout = timer.new( - "negotiation_ttl", - function () - counter.add(self.shm.negotiations_expired) - logger:log("Expiring keys for "..route.gw_ip4.." (negotiation_ttl)") - self:expire_route(route) - end, - self.negotiation_ttl * 1e9 - ) - timer.activate(route.timeout) +function KeyManager:handle_key_request (route, message) + if not route or message ~= self.key_message then return end + + local ecode, response = route.protocol:receive_key(message) + if ecode == Protocol.code.protocol then + counter.add(self.shm.protocol_errors) + return false + elseif ecode == Protocol.code.authentication then + counter.add(self.shm.authentication_errors) + return false + else assert(not ecode) end + + local ecode, rx, tx = route.protocol:derive_ephemeral_keys() + if ecode == Protocol.code.parameter then + counter.add(self.shm.public_key_errors) + return false + else assert(not ecode) end + + counter.add(self.shm.keypairs_negotiated) + audit:log("Completed key exchange for '"..route.id.."'") + + if response then + link.transmit(self.output.output, self:request(route, response)) + end + + self:configure_route(route, rx, tx) + + return true end -function KeyManager:set_sa_timeout (route) - route.timeout = timer.new( - "sa_ttl", - function () - counter.add(self.shm.keypairs_expired) - logger:log("Expiring keys for "..route.gw_ip4.." (sa_ttl)") - self:expire_route(route) - end, - self.sa_ttl * 1e9 - ) - timer.activate(route.timeout) +function KeyManager:configure_route (route, rx, tx) + route.status = status.ready + route.rx_sa = { + mode = "aes-gcm-128-12", + spi = route.spi, + key = lib.hexdump(rx.key), + salt = lib.hexdump(rx.salt) + } + route.tx_sa = { + mode = "aes-gcm-128-12", + spi = route.spi, + key = lib.hexdump(tx.key), + salt = lib.hexdump(tx.salt) + } + route.sa_timeout = lib.timeout(self.sa_ttl) + route.rekey_timeout = lib.timeout(self.sa_ttl/2) + self:commit_ephemeral_keys() end function KeyManager:expire_route (route) route.status = status.expired route.tx_sa = nil route.rx_sa = nil - route.timeout = nil + route.sa_timeout = nil + route.rekey_timeout = nil self:commit_ephemeral_keys() end -local request_t = ffi.typeof([[struct { - uint32_t spi; - uint8_t key[16]; - uint8_t salt[4]; -} __attribute__((packed))]]) - -local request_t_ptr_t = ffi.typeof("$*", request_t) -local request_t_length = ffi.sizeof(request_t) - -local request_trailer_t = ffi.typeof([[struct { - uint8_t icv[]]..C.crypto_aead_xchacha20poly1305_ietf_ABYTES..[[]; - uint8_t nonce[]]..C.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES..[[]; -} __attribute__((packed))]]) - -local request_trailer_t_ptr_t = ffi.typeof("$*", request_trailer_t) -local request_trailer_t_length = ffi.sizeof(request_trailer_t) - -local request_length = - ipv4:sizeof() + request_t_length + request_trailer_t_length - -local request_aad_length = 8 -- IPv4 source and destination addresses - -function KeyManager:request (route) +function KeyManager:request (route, message) local request = packet.allocate() self.ip:new({ - total_length = request_length, + total_length = ipv4:sizeof() + + Transport.header:sizeof() + + message:sizeof(), ttl = 64, protocol = PROTOCOL, src = self.node_ip4n, @@ -240,43 +390,30 @@ function KeyManager:request (route) }) packet.append(request, self.ip:header(), ipv4:sizeof()) - packet.resize(request, request_length) - - local body = ffi.cast(request_t_ptr_t, request.data + ipv4:sizeof()) - body.spi = lib.htonl(route.tx_sa.spi) - ffi.copy(body.key, lib.hexundump(route.tx_sa.key, 16), 16) - ffi.copy(body.salt, lib.hexundump(route.tx_sa.salt, 4), 4) - - local trailer = ffi.cast(request_trailer_t_ptr_t, - request.data + ipv4:sizeof() + request_t_length) - C.randombytes_buf(trailer.nonce, - C.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES) - - local ciphertext = ffi.cast("uint8_t *", body) + self.transport:new({ + spi = route.spi, + message_type = (message == self.nonce_message + and Transport.message_type.nonce) + or (message == self.key_message + and Transport.message_type.key) + }) + packet.append(request, self.transport:header(), Transport.header:sizeof()) - C.crypto_aead_xchacha20poly1305_ietf_encrypt( - -- encrypt in-place, no clen_p - ciphertext, nil, ciphertext, request_t_length, - -- use src and dst IP as additional authentication data - request.data, request_aad_length, - -- no secret nonce (nsec), use nonce from trailer and route’s key - nil, trailer.nonce, route.preshared_key - ) + packet.append(request, message:header(), message:sizeof()) return request end function KeyManager:parse_request (request) - if request.length ~= request_length then return end - - self.ip:new_from_mem(request.data, ipv4:sizeof()) - if self.ip:protocol() ~= PROTOCOL or not self.ip:dst_eq(self.node_ip4n) then + local transport = self.transport:new_from_mem(request.data, request.length) + if not transport then + counter.add(self.shm.protocol_errors) return end local route = nil for _, r in ipairs(self.routes) do - if self.ip:src_eq(r.gw_ip4n) then + if transport:spi() == r.spi then route = r break end @@ -286,54 +423,310 @@ function KeyManager:parse_request (request) return end - local body = ffi.cast(request_t_ptr_t, request.data + ipv4:sizeof()) - local trailer = ffi.cast(request_trailer_t_ptr_t, - request.data + ipv4:sizeof() + request_t_length) - local ciphertext = ffi.cast("uint8_t *", body) - - if 0 ~= C.crypto_aead_xchacha20poly1305_ietf_decrypt( - -- decrypt in-place, no secret nonce (nsec), no mlen_p - ciphertext, nil, nil, ciphertext, - -- cyphertext length - request_t_length + C.crypto_aead_xchacha20poly1305_ietf_ABYTES, - -- authenticate src and dst addresses - request.data, request_aad_length, - -- use nonce from trailer and route’s key - trailer.nonce, route.preshared_key - ) then - counter.add(self.shm.authentication_errors) + local data = request.data + Transport.header:sizeof() + local length = request.length - Transport.header:sizeof() + local message = (transport:message_type() == Transport.message_type.nonce + and self.nonce_message:new_from_mem(data, length)) + or (transport:message_type() == Transport.message_type.key + and self.key_message:new_from_mem(data, length)) + if not message then + counter.add(self.shm.protocol_errors) return end - local sa = { - mode = "aes-gcm-128-12", - spi = lib.ntohl(body.spi), - key = lib.hexdump(ffi.string(body.key, 16)), - salt = lib.hexdump(ffi.string(body.salt, 4)) - } - - return route, sa + return route, message end local function store_ephemeral_keys (path, keys) local f = assert(io.open(path, "w"), "Unable to open file: "..path) - yang.print_data_for_schema(schemata['ephemeral-keys'], {route=keys}, f) + yang.print_data_for_schema(schemata['ephemeral-keys'], {sa=keys}, f) f:close() end --- ephemeral_keys := { { gw_ip4=(IPv4), [ sa=(SA) ] }, ... } +-- ephemeral_keys := { =(SA), ... } + function KeyManager:commit_ephemeral_keys () local esp_keys, dsp_keys = {}, {} for _, route in ipairs(self.routes) do - esp_keys[route.id] = { - gw_ip4 = route.gw_ip4, - sa = (route.status == status.ready) and route.tx_sa or nil - } - dsp_keys[route.id] = { - gw_ip4 = route.gw_ip4, - sa = (route.status == status.ready) and route.rx_sa or nil - } + if route.status == status.ready then + esp_keys[route.id] = route.tx_sa + dsp_keys[route.id] = route.rx_sa + end end store_ephemeral_keys(self.esp_keyfile, esp_keys) store_ephemeral_keys(self.dsp_keyfile, dsp_keys) end + +-- Vita: simple key exchange (vita-ske, version 1g). See README.exchange + +Protocol = { + status = { idle = 0, wait_nonce = 1, wait_key = 2, complete = 3 }, + code = { protocol = 0, authentication = 1, parameter = 2, expired = 3}, + preshared_key_bytes = C.crypto_auth_hmacsha512256_KEYBYTES, + public_key_bytes = C.crypto_scalarmult_curve25519_BYTES, + secret_key_bytes = C.crypto_scalarmult_curve25519_SCALARBYTES, + auth_code_bytes = C.crypto_auth_hmacsha512256_BYTES, + nonce_bytes = 32, + spi_t = ffi.typeof("union { uint32_t u32; uint8_t bytes[4]; }"), + buffer_t = ffi.typeof("uint8_t[?]"), + key_t = ffi.typeof[[ + union { + uint8_t bytes[20]; + struct { + uint8_t key[16]; + uint8_t salt[4]; + } __attribute__((packed)) slot; + } + ]], + nonce_message = subClass(header), + key_message = subClass(header) +} +Protocol.nonce_message:init({ + [1] = ffi.typeof([[ + struct { + uint8_t nonce[]]..Protocol.nonce_bytes..[[]; + } __attribute__((packed)) + ]]) +}) +Protocol.key_message:init({ + [1] = ffi.typeof([[ + struct { + uint8_t public_key[]]..Protocol.public_key_bytes..[[]; + uint8_t auth_code[]]..Protocol.auth_code_bytes..[[]; + } __attribute__((packed)) + ]]) +}) + +-- Public API + +function Protocol.nonce_message:new (config) + local o = Protocol.nonce_message:superClass().new(self) + o:nonce(config.nonce) + return o +end + +function Protocol.nonce_message:nonce (nonce) + local h = self:header() + if nonce ~= nil then + ffi.copy(h.nonce, nonce, ffi.sizeof(h.nonce)) + end + return h.nonce +end + +function Protocol.key_message:new (config) + local o = Protocol.key_message:superClass().new(self) + o:public_key(config.public_key) + o:auth_code(config.auth_code) + return o +end + +function Protocol.key_message:public_key (public_key) + local h = self:header() + if public_key ~= nil then + ffi.copy(h.public_key, public_key, ffi.sizeof(h.public_key)) + end + return h.public_key +end + +function Protocol.key_message:auth_code (auth_code) + local h = self:header() + if auth_code ~= nil then + ffi.copy(h.auth_code, auth_code, ffi.sizeof(h.auth_code)) + end + return h.auth_code +end + +function Protocol:new (spi, key, timeout) + local o = { + status = Protocol.status.idle, + timeout = timeout, + deadline = nil, + k = ffi.new(Protocol.buffer_t, Protocol.preshared_key_bytes), + spi = ffi.new(Protocol.spi_t), + n1 = ffi.new(Protocol.buffer_t, Protocol.nonce_bytes), + n2 = ffi.new(Protocol.buffer_t, Protocol.nonce_bytes), + s1 = ffi.new(Protocol.buffer_t, Protocol.secret_key_bytes), + p1 = ffi.new(Protocol.buffer_t, Protocol.public_key_bytes), + p2 = ffi.new(Protocol.buffer_t, Protocol.public_key_bytes), + h = ffi.new(Protocol.buffer_t, Protocol.auth_code_bytes), + q = ffi.new(Protocol.buffer_t, Protocol.secret_key_bytes), + e = ffi.new(Protocol.key_t), + hmac_state = ffi.new("struct crypto_auth_hmacsha512256_state"), + hash_state = ffi.new("struct crypto_generichash_blake2b_state") + } + ffi.copy(o.k, key, ffi.sizeof(o.k)) + o.spi.u32 = lib.htonl(spi) + return setmetatable(o, {__index=Protocol}) +end + +function Protocol:initiate_exchange (nonce_message) + if self.status == Protocol.status.idle then + self.status = Protocol.status.wait_nonce + self:set_deadline() + return nil, self:send_nonce(nonce_message) + else return Protocol.code.protocol end +end + +function Protocol:receive_nonce (nonce_message) + if self.status == Protocol.status.idle then + self:intern_nonce(nonce_message) + return nil, self:send_nonce(nonce_message) + elseif self.status == Protocol.status.wait_nonce then + self:intern_nonce(nonce_message) + self.status = Protocol.status.wait_key + self:set_deadline() + return nil + else return Protocol.code.protocol end +end + +function Protocol:exchange_key (key_message) + if self.status == Protocol.status.wait_key then + return nil, self:send_key(key_message) + else return Protocol.code.protocol end +end + +function Protocol:receive_key (key_message) + if self.status == Protocol.status.idle + or self.status == Protocol.status.wait_key then + if self:intern_key(key_message) then + local response = self.status == Protocol.status.idle + and self:send_key(key_message) + self.status = Protocol.status.complete + return nil, response + else return Protocol.code.authentication end + else return Protocol.code.protocol end +end + +function Protocol:derive_ephemeral_keys () + if self.status == Protocol.status.complete then + self:reset() + if self:derive_shared_secret() then + local rx = self:derive_key_material(self.p1, self.p2) + local tx = self:derive_key_material(self.p2, self.p1) + return nil, rx, tx + else return Protocol.code.paramter end + else return Protocol.code.protocol end +end + +function Protocol:reset_if_expired () + if self.deadline and self.deadline() then + self:reset() + return Protocol.code.expired + end +end + +-- Internal methods + +function Protocol:send_nonce (nonce_message) + C.randombytes_buf(self.n1, ffi.sizeof(self.n1)) + return nonce_message:new({nonce=self.n1}) +end + +function Protocol:intern_nonce (nonce_message) + ffi.copy(self.n2, nonce_message:nonce(), ffi.sizeof(self.n2)) +end + +function Protocol:send_key (key_message) + local spi, k, n1, n2, s1, p1 = + self.spi, self.k, self.n1, self.n2, self.s1, self.p1 + local state, h1 = self.hmac_state, self.h + C.randombytes_buf(s1, ffi.sizeof(s1)) + C.crypto_scalarmult_curve25519_base(p1, s1) + C.crypto_auth_hmacsha512256_init(state, k, ffi.sizeof(k)) + C.crypto_auth_hmacsha512256_update(state, spi.bytes, ffi.sizeof(spi)) + C.crypto_auth_hmacsha512256_update(state, n1, ffi.sizeof(n1)) + C.crypto_auth_hmacsha512256_update(state, n2, ffi.sizeof(n2)) + C.crypto_auth_hmacsha512256_update(state, p1, ffi.sizeof(p1)) + C.crypto_auth_hmacsha512256_final(state, h1) + return key_message:new({public_key=p1, auth_code=h1}) +end + +function Protocol:intern_key (m) + local spi, k, n1, n2, p2 = self.spi, self.k, self.n1, self.n2, self.p2 + local state, h2 = self.hmac_state, self.h + C.crypto_auth_hmacsha512256_init(state, k, ffi.sizeof(k)) + C.crypto_auth_hmacsha512256_update(state, spi.bytes, ffi.sizeof(spi)) + C.crypto_auth_hmacsha512256_update(state, n2, ffi.sizeof(n2)) + C.crypto_auth_hmacsha512256_update(state, n1, ffi.sizeof(n1)) + C.crypto_auth_hmacsha512256_update(state, m:public_key(), ffi.sizeof(p2)) + C.crypto_auth_hmacsha512256_final(state, h2) + if C.sodium_memcmp(h2, m:auth_code(), ffi.sizeof(h2)) == 0 then + ffi.copy(p2, m:public_key(), ffi.sizeof(p2)) + return true + end +end + +function Protocol:derive_shared_secret () + return C.crypto_scalarmult_curve25519(self.q, self.s1, self.p2) == 0 +end + +function Protocol:derive_key_material (salt_a, salt_b) + local q, e, state = self.q, self.e, self.hash_state + C.crypto_generichash_blake2b_init(state, nil, 0, ffi.sizeof(e)) + C.crypto_generichash_blake2b_update(state, q, ffi.sizeof(q)) + C.crypto_generichash_blake2b_update(state, salt_a, ffi.sizeof(salt_a)) + C.crypto_generichash_blake2b_update(state, salt_b, ffi.sizeof(salt_b)) + C.crypto_generichash_blake2b_final(state, e.bytes, ffi.sizeof(e.bytes)) + return { key = ffi.string(e.slot.key, ffi.sizeof(e.slot.key)), + salt = ffi.string(e.slot.salt, ffi.sizeof(e.slot.salt)) } +end + +function Protocol:reset () + self.deadline = nil + self.status = Protocol.status.idle +end + +function Protocol:set_deadline () + self.deadline = lib.timeout(self.timeout) +end + +-- Assertions about the world (-: + +assert(Protocol.preshared_key_bytes == 32) +assert(Protocol.public_key_bytes == 32) +assert(Protocol.auth_code_bytes == 32) + +-- Transport wrapper for vita-ske that encompasses an SPI to map requests to +-- routes, and a message type to facilitate parsing. +-- +-- NB: might have to replace this with a UDP based header to get key exchange +-- requests through protocol filters. + +Transport = { + message_type = { nonce = 1, key = 2 }, + header = subClass(header) +} +Transport.header:init({ + [1] = ffi.typeof[[ + struct { + uint32_t spi; + uint8_t message_type; + uint8_t reserved[3]; + } __attribute__((packed)) + ]] +}) + +-- Public API + +function Transport.header:new (config) + local o = Transport.header:superClass().new(self) + o:spi(config.spi) + o:message_type(config.message_type) + return o +end + +function Transport.header:spi (spi) + local h = self:header() + if spi ~= nil then + h.spi = lib.htonl(spi) + end + return lib.ntohl(h.spi) +end + +function Transport.header:message_type (message_type) + local h = self:header() + if message_type ~= nil then + h.message_type = message_type + end + return h.message_type +end diff --git a/src/program/vita/fsm-protocol.svg b/src/program/vita/fsm-protocol.svg new file mode 100644 index 0000000000..6ec7fb1bc1 --- /dev/null +++ b/src/program/vita/fsm-protocol.svg @@ -0,0 +1,516 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/program/vita/nexthop.lua b/src/program/vita/nexthop.lua index 4b8e188648..e31d6f2292 100644 --- a/src/program/vita/nexthop.lua +++ b/src/program/vita/nexthop.lua @@ -22,6 +22,7 @@ NextHop4 = { nexthop_ip4 = {required=true} }, shm = { + protocol_errors = {counter}, arp_requests = {counter}, arp_replies = {counter}, arp_errors = {counter}, @@ -98,7 +99,15 @@ function NextHop4:push () -- Forward packets to next hop for _, input in ipairs(self.forward) do while not link.empty(input) do - link.transmit(output, self:encapsulate(link.receive(input), 0x0800)) + local p = link.receive(input) + local ip4 = self.ip4:new_from_mem(p.data, p.length) + if ip4 and ip4:ttl() > 0 then + ip4:ttl(ip4:ttl() - 1) + ip4:checksum() + link.transmit(output, self:encapsulate(p, 0x0800)) + else + counter.add(self.shm.protocol_errors) + end end end diff --git a/src/program/vita/route.lua b/src/program/vita/route.lua index fa16ae1abd..358f38f155 100644 --- a/src/program/vita/route.lua +++ b/src/program/vita/route.lua @@ -6,11 +6,13 @@ local counter = require("core.counter") local ethernet = require("lib.protocol.ethernet") local ipv4 = require("lib.protocol.ipv4") local arp = require("lib.protocol.arp") +local esp_header = require("lib.protocol.esp") local esp = require("lib.ipsec.esp") local exchange = require("program.vita.exchange") local lpm = require("lib.lpm.lpm4_248").LPM4_248 local ctable = require("lib.ctable") local ffi = require("ffi") +local packet_buffer -- route := { net_cidr4=(CIDR4), gw_ip4=(IPv4), preshared_key=(KEY) } @@ -79,9 +81,7 @@ function PrivateRouter:push () for i = 0, fwd4_cursor - 1 do local p = fwd4_packets[i] local ip4 = self.ip4:new_from_mem(p.data, ipv4:sizeof()) - if ip4 and ip4:checksum_ok() and ip4:ttl() > 1 then - ip4:ttl(ip4:ttl() - 1) - ip4:checksum() + if ip4 and ip4:checksum_ok() then fwd4_packets[new_cursor] = p new_cursor = new_cursor + 1 else @@ -138,6 +138,7 @@ function PublicRouter:new (conf) routes = {}, eth = ethernet:new({}), ip4 = ipv4:new({}), + esp = esp_header:new({}), ip4_packets = packet_buffer(), fwd4_packets = packet_buffer(), protocol_packets = packet_buffer(), @@ -145,7 +146,7 @@ function PublicRouter:new (conf) } for _, route in pairs(conf.routes) do o.routes[#o.routes+1] = { - gw_ip4 = assert(route.gw_ip4, "Missing gw_ip4"), + spi = assert(route.spi, "Missing SPI"), link = nil } end @@ -153,17 +154,16 @@ function PublicRouter:new (conf) end function PublicRouter:link () - local ipv4_addr_t = ffi.typeof("uint8_t[4]") local index_t = ffi.typeof("uint32_t") self.routing_table4 = ctable.new{ - key_type = ipv4_addr_t, + key_type = index_t, value_type = index_t } for index, route in ipairs(self.routes) do assert(ffi.cast(index_t, index) == index, "index overflow") - route.link = self.output[config.link_name(route.gw_ip4)] + route.link = self.output[tostring(route.spi)] if route.link then - self.routing_table4:add(ipv4:pton(route.gw_ip4), index) + self.routing_table4:add(route.spi, index) end end end @@ -194,11 +194,13 @@ function PublicRouter:push () for i = 0, ip4_cursor - 1 do local p = ip4_packets[i] local ip4 = self.ip4:new_from_mem(p.data, p.length) + and self.ip4:checksum_ok() + and self.ip4 if ip4 and ip4:protocol() == esp.PROTOCOL then - fwd4_packets[fwd4_cursor] = p + fwd4_packets[fwd4_cursor] = packet.shiftleft(p, ipv4:sizeof()) fwd4_cursor = fwd4_cursor + 1 elseif ip4 and ip4:protocol() == exchange.PROTOCOL then - protocol_packets[protocol_cursor] = p + protocol_packets[protocol_cursor] = packet.shiftleft(p, ipv4:sizeof()) protocol_cursor = protocol_cursor + 1 else packet.free(p) @@ -207,23 +209,6 @@ function PublicRouter:push () end end - local new_cursor = 0 - for i = 0, fwd4_cursor - 1 do - local p = fwd4_packets[i] - local ip4 = self.ip4:new_from_mem(p.data, ipv4:sizeof()) - if ip4:checksum_ok() and ip4:ttl() > 1 then - ip4:ttl(ip4:ttl() - 1) - ip4:checksum() - fwd4_packets[new_cursor] = p - new_cursor = new_cursor + 1 - else - packet.free(p) - counter.add(self.shm.rxerrors) - counter.add(self.shm.protocol_errors) - end - end - fwd4_cursor = new_cursor - for i = 0, fwd4_cursor - 1 do self:forward4(fwd4_packets[i]) end @@ -237,15 +222,15 @@ function PublicRouter:push () end end -function PublicRouter:find_route4 (src) - return self.routes[self.routing_table4:lookup_ptr(src).value].link +function PublicRouter:find_route4 (spi) + return self.routes[self.routing_table4:lookup_ptr(spi).value].link end function PublicRouter:forward4 (p) - self.ip4:new_from_mem(p.data, p.length) - local route = self:find_route4(self.ip4:src()) + local route = self.esp:new_from_mem(p.data, p.length) + and self:find_route4(self.esp:spi()) if route then - link.transmit(route, packet.shiftleft(p, ipv4:sizeof())) + link.transmit(route, p) else packet.free(p) counter.add(self.shm.rxerrors) diff --git a/src/program/vita/schemata.lua b/src/program/vita/schemata.lua index e037021d08..cafbec2d08 100644 --- a/src/program/vita/schemata.lua +++ b/src/program/vita/schemata.lua @@ -7,8 +7,8 @@ module(...,package.seeall) local yang = require("lib.yang.yang") return { - ['esp-gateway'] = - yang.load_schema_by_name('vita-esp-gateway', nil, "program.vita"), ['ephemeral-keys'] = - yang.load_schema_by_name('vita-ephemeral-keys', nil, "program.vita") + yang.load_schema_by_name('vita-ephemeral-keys', nil, "program.vita"), + ['esp-gateway'] = + yang.load_schema_by_name('vita-esp-gateway', nil, "program.vita") } diff --git a/src/program/vita/sodium.h b/src/program/vita/sodium.h new file mode 100644 index 0000000000..82f28b8b07 --- /dev/null +++ b/src/program/vita/sodium.h @@ -0,0 +1,71 @@ +/* Use of this source code is governed by the GNU AGPL license; see COPYING. */ + +/* Bindings for libsodium @ 1.0.15 */ + +// core.h +int sodium_init(void); + +// randombytes.h: +void randombytes_buf(void * const buf, const size_t size); + +// utils.h +int sodium_memcmp(const void * const b1_, const void * const b2_, size_t len); + +// crypto_hash_sha512.h +struct crypto_hash_sha512_state { + uint64_t state[8]; + uint64_t count[2]; + uint8_t buf[128]; +}; + +// crypto_auth_hmacsha512256.h +enum { + crypto_auth_hmacsha512256_BYTES = 32U, + crypto_auth_hmacsha512256_KEYBYTES = 32U +}; +struct crypto_auth_hmacsha512256_state { + struct crypto_hash_sha512_state ictx; + struct crypto_hash_sha512_state octx; +}; +int crypto_auth_hmacsha512256_init(struct crypto_auth_hmacsha512256_state *state, + const unsigned char *key, + size_t keylen); +int crypto_auth_hmacsha512256_update(struct crypto_auth_hmacsha512256_state *state, + const unsigned char *in, + unsigned long long inlen); +int crypto_auth_hmacsha512256_final(struct crypto_auth_hmacsha512256_state *state, + unsigned char *out); + +// crypto_scalarmult_curve25519.h +enum { + crypto_scalarmult_curve25519_BYTES = 32U, + crypto_scalarmult_curve25519_SCALARBYTES = 32U +}; +int crypto_scalarmult_curve25519_base(unsigned char *q, + const unsigned char *n); +int crypto_scalarmult_curve25519(unsigned char *q, + const unsigned char *n, + const unsigned char *p); + +// crypto_generichash_blake2b.h +enum { + crypto_generichash_blake2b_BYTES = 32U, + crypto_generichash_blake2b_KEYBYTES = 32U +}; +struct crypto_generichash_blake2b_state { + uint64_t h[8]; + uint64_t t[2]; + uint64_t f[2]; + uint8_t buf[2 * 128]; + size_t buflen; + uint8_t last_node; +} __attribute__ ((aligned(64))); +int crypto_generichash_blake2b_init(struct crypto_generichash_blake2b_state *state, + const unsigned char *key, + const size_t keylen, const size_t outlen); +int crypto_generichash_blake2b_update(struct crypto_generichash_blake2b_state *state, + const unsigned char *in, + unsigned long long inlen); +int crypto_generichash_blake2b_final(struct crypto_generichash_blake2b_state *state, + unsigned char *out, + const size_t outlen); diff --git a/src/program/vita/test.snabb b/src/program/vita/test.snabb index 1735575ad2..2c6fc8b377 100755 --- a/src/program/vita/test.snabb +++ b/src/program/vita/test.snabb @@ -53,7 +53,8 @@ local conf = { loopback = { net_cidr4 = "192.168.10.0/24", gw_ip4 = "203.0.113.1", - preshared_key = string.rep("00", 32) + preshared_key = string.rep("00", 32), + spi = 1000 } }, negotiation_ttl = 1 diff --git a/src/program/vita/vita-ephemeral-keys.yang b/src/program/vita/vita-ephemeral-keys.yang index 03874d27e5..6ba5254bc5 100644 --- a/src/program/vita/vita-ephemeral-keys.yang +++ b/src/program/vita/vita-ephemeral-keys.yang @@ -2,21 +2,16 @@ module vita-ephemeral-keys { namespace vita:ephemeral-keys; prefix ephemeral-keys; - import ietf-inet-types { prefix inet; } - + typedef spi { type uint32 { range "256..max"; } } typedef key16 { type string { patttern '([0-9a-fA-F]{2}\s*){16}'; } } typedef key4 { type string { patttern '([0-9a-fA-F]{2}\s*){4}'; } } - list route { - key id; unique "gw_ip4"; + list sa { + key id; unique "spi key"; leaf id { type string; mandatory true; } - leaf gw_ip4 { type inet:ipv4-address-no-zone; mandatory true; } - container sa { - presence "Present if a SA was negotiated."; - leaf mode { type string; mandatory true; } - leaf spi { type uint32 { range "1..max"; } mandatory true; } - leaf key { type key16; mandatory true; } - leaf salt { type key4; mandatory true; } - } + leaf mode { type string; mandatory true; } + leaf spi { type spi; mandatory true; } + leaf key { type key16; mandatory true; } + leaf salt { type key4; mandatory true; } } } diff --git a/src/program/vita/vita-esp-gateway.yang b/src/program/vita/vita-esp-gateway.yang index 586bbee364..697850b9e8 100644 --- a/src/program/vita/vita-esp-gateway.yang +++ b/src/program/vita/vita-esp-gateway.yang @@ -4,6 +4,7 @@ module vita-esp-gateway { import ietf-yang-types { prefix yang; } import ietf-inet-types { prefix inet; } + import vita-ephemeral-keys { prefix ephemeral-keys; } typedef pci-address { type string { @@ -31,11 +32,12 @@ module vita-esp-gateway { leaf public_nexthop_ip4 { type inet:ipv4-address-no-zone; mandatory true; } list route { - key id; unique "gw_ip4 net_cidr4"; + key id; unique "net_cidr4 preshared_key spi"; leaf id { type string; mandatory true; } leaf net_cidr4 { type inet:ipv4-prefix; mandatory true; } leaf gw_ip4 { type inet:ipv4-address-no-zone; mandatory true; } leaf preshared_key { type key32; mandatory true; } + leaf spi { type ephemeral-keys:spi; mandatory true; } } leaf negotiation_ttl { type time-to-live; } diff --git a/src/program/vita/vita.lua b/src/program/vita/vita.lua index 7e0fac255b..f1fc5f2e65 100644 --- a/src/program/vita/vita.lua +++ b/src/program/vita/vita.lua @@ -113,13 +113,13 @@ function configure_private_router (conf, append) for _, route in pairs(conf.route) do local private_in = "PrivateRouter."..config.link_name(route.net_cidr4) - local ESP_in = "ESP_"..config.link_name(route.gw_ip4).."_in" + local ESP_in = "ESP_"..route.spi.."_in" config.app(c, ESP_in, Transmitter, {name="group/interlink/"..ESP_in, create=true}) config.link(c, private_in.." -> "..ESP_in..".input") local private_out = "PrivateNextHop."..config.link_name(route.net_cidr4) - local DSP_out = "DSP_"..config.link_name(route.gw_ip4).."_out" + local DSP_out = "DSP_"..route.spi.."_out" config.app(c, DSP_out, Receiver, {name="group/interlink/"..DSP_out, create=true}) config.link(c, DSP_out..".output -> "..private_out) @@ -159,14 +159,14 @@ function configure_public_router (conf, append) config.link(c, "KeyExchange.output -> PublicNextHop.protocol") for _, route in pairs(conf.route) do - local public_in = "PublicRouter."..config.link_name(route.gw_ip4) - local DSP_in = "DSP_"..config.link_name(route.gw_ip4).."_in" + local public_in = "PublicRouter."..route.spi + local DSP_in = "DSP_"..route.spi.."_in" config.app(c, DSP_in, Transmitter, {name="group/interlink/"..DSP_in, create=true}) config.link(c, public_in.." -> "..DSP_in..".input") local public_out = "PublicNextHop."..config.link_name(route.gw_ip4) - local ESP_out = "ESP_"..config.link_name(route.gw_ip4).."_out" + local ESP_out = "ESP_"..route.spi.."_out" local Tunnel = "Tunnel_"..config.link_name(route.gw_ip4) config.app(c, ESP_out, Receiver, {name="group/interlink/"..ESP_out, create=true}) @@ -259,24 +259,22 @@ function public_router_loopback_worker (confpath, cpu, memnode) end --- ephemeral_keys := { { gw_ip4=(IPv4), [ sa=(SA) ] }, ... } (see exchange) +-- ephemeral_keys := { =(SA), ... } (see exchange) function configure_esp (ephemeral_keys) local c = config.new() - for _, route in pairs(ephemeral_keys.route) do - -- Configure interlink receiver/transmitter for route - local ESP_in = "ESP_"..config.link_name(route.gw_ip4).."_in" - local ESP_out = "ESP_"..config.link_name(route.gw_ip4).."_out" + for _, sa in pairs(ephemeral_keys.sa) do + -- Configure interlink receiver/transmitter for inbound SA + local ESP_in = "ESP_"..sa.spi.."_in" + local ESP_out = "ESP_"..sa.spi.."_out" config.app(c, ESP_in, Receiver, {name="group/interlink/"..ESP_in}) config.app(c, ESP_out, Transmitter, {name="group/interlink/"..ESP_out}) - -- Configure SA if present - if route.sa then - local ESP = "ESP_"..route.sa.spi - config.app(c, ESP, tunnel.Encapsulate, route.sa) - config.link(c, ESP_in..".output -> "..ESP..".input4") - config.link(c, ESP..".output -> "..ESP_out..".input") - end + -- Configure inbound SA + local ESP = "ESP_"..sa.spi + config.app(c, ESP, tunnel.Encapsulate, sa) + config.link(c, ESP_in..".output -> "..ESP..".input4") + config.link(c, ESP..".output -> "..ESP_out..".input") end return c @@ -285,19 +283,17 @@ end function configure_dsp (ephemeral_keys) local c = config.new() - for _, route in pairs(ephemeral_keys.route) do - -- Configure interlink receiver/transmitter for route - local DSP_in = "DSP_"..config.link_name(route.gw_ip4).."_in" - local DSP_out = "DSP_"..config.link_name(route.gw_ip4).."_out" + for _, sa in pairs(ephemeral_keys.sa) do + -- Configure interlink receiver/transmitter for outbound SA + local DSP_in = "DSP_"..sa.spi.."_in" + local DSP_out = "DSP_"..sa.spi.."_out" config.app(c, DSP_in, Receiver, {name="group/interlink/"..DSP_in}) config.app(c, DSP_out, Transmitter, {name="group/interlink/"..DSP_out}) - -- Configure SA if present - if route.sa then - local DSP = "DSP_"..route.sa.spi - config.app(c, DSP, tunnel.Decapsulate, route.sa) - config.link(c, DSP_in..".output -> "..DSP..".input") - config.link(c, DSP..".output4 -> "..DSP_out..".input") - end + -- Configure outbound SA + local DSP = "DSP_"..sa.spi + config.app(c, DSP, tunnel.Decapsulate, sa) + config.link(c, DSP_in..".output -> "..DSP..".input") + config.link(c, DSP..".output4 -> "..DSP_out..".input") end return c