#pragma once #include "ProtobufModule.h" #include "SinglePortModule.h" enum KeyVerificationState { KEY_VERIFICATION_IDLE, KEY_VERIFICATION_SENDER_HAS_INITIATED, KEY_VERIFICATION_SENDER_AWAITING_NUMBER, KEY_VERIFICATION_SENDER_AWAITING_USER, KEY_VERIFICATION_RECEIVER_AWAITING_USER, KEY_VERIFICATION_RECEIVER_AWAITING_HASH1, }; // KeyVerification Module overview // This module allows for two useful functions. First, it implements a 2sv process that can manually verify a trustworthy // connection with another node. It specifically verifies that the other node holds the correct private key for its public key, so // it is resistant to MitM attacks. Second, it can be used to bootstrap trust in a new node by carrying the public key in the // initial unencrypted message (in the hash1 field of the KeyVerification protobuf). This allows a user to manually verify a new // node even if they don't have that node in the local nodeDB at all. // The handshake process is as follows (NodeA = initiator, NodeB = responder): // 1. NodeA sends a KeyVerification message containing a random nonce and its own public key (in the // hash1 field) to NodeB. Implemented in sendInitialRequest(). It is PKI-encrypted if NodeA already // holds NodeB's key, otherwise channel-encrypted (the bootstrap case). // // 2. NodeB replies (allocReply()) with its own public key (hash1 field) and hash2. NodeB generates a // random 6-digit security number and stashes NodeA's public key (as a pending key if not already in // the nodeDB). It computes hash1 = SHA256(securityNumber, nonce, NodeA_num, NodeB_num, PK_A, PK_B), // then hash2 = SHA256(nonce, hash1). The reply is PKI-encrypted only if NodeB already held NodeA's // key; in the bootstrap case it is channel-encrypted so NodeA can read NodeB's key from hash1. // // 3. NodeA receives the reply (handleReceivedProtobuf()), checks the nonce, stashes NodeB's public key, // and prompts the user to enter the security number. The security number is never sent over the mesh // and must be communicated over a secondary channel. processSecurityNumber() recomputes hash1 from // the entered number and verifies SHA256(nonce, hash1) matches the received hash2. NodeA then sends // its hash1 back to NodeB in a PKI-encrypted KeyVerification message (the follow-on PKI packet) and // shows the KeyVerificationFinalPrompt menu, displaying 8 characters derived from hash1. // // 4. NodeB receives NodeA's hash1 (handleReceivedProtobuf(); required to be PKI-encrypted), checks it // matches the hash1 NodeB generated, and shows the same 8-character code for final confirmation. // // The final on-screen code comparison is the actual manual verification: the user confirms the codes // match on both devices, proving the two nodes agree on the same public keys (no MitM substitution). // PKI-encrypting the follow-on packet additionally proves each node holds the private key for the // agreed public key. class KeyVerificationModule : public ProtobufModule //, private concurrency::OSThread // { // CallbackObserver nodeStatusObserver = // CallbackObserver(this, &KeyVerificationModule::handleStatusUpdate); public: KeyVerificationModule(); /* : concurrency::OSThread("KeyVerification"), ProtobufModule("KeyVerification", meshtastic_PortNum_KEY_VERIFICATION_APP, &meshtastic_KeyVerification_msg) { nodeStatusObserver.observe(&nodeStatus->onNewStatus); setIntervalFromNow(setStartDelay()); // Wait until NodeInfo is sent }*/ virtual bool wantUIFrame() { return false; }; bool sendInitialRequest(NodeNum remoteNode); void generateVerificationCode(char *); // fills char with the user readable verification code uint32_t getCurrentRemoteNode() { return currentRemoteNode; } void commitVerifiedRemoteNode(); // Commit a pending key to NodeDB and mark the node manually verified protected: /* Called to handle a particular incoming message @return true if you've guaranteed you've handled this message and no other handlers should be considered for it */ virtual bool handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *p); // virtual meshtastic_MeshPacket *allocReply() override; // rather than add to the craziness that is the admin module, just handle those requests here. virtual AdminMessageHandleResult handleAdminMessageForModule(const meshtastic_MeshPacket &mp, meshtastic_AdminMessage *request, meshtastic_AdminMessage *response) override; /* * Send our Telemetry into the mesh */ bool sendMetrics(); virtual meshtastic_MeshPacket *allocReply() override; private: uint64_t currentNonce = 0; uint32_t currentNonceTimestamp = 0; // millis() the session opened, never refreshed, so a peer cannot hold the slot open indefinitely. uint32_t sessionStartedMs = 0; // millis() a remote-initiated session last ended. Spacing sessions bounds the slot DoS and the // banner spam; stamped at the end rather than the start so the cooldown is a real gap. uint32_t lastRemoteSessionMs = 0; bool sessionFromRemote = false; NodeNum currentRemoteNode = 0; uint32_t currentSecurityNumber = 0; KeyVerificationState currentState = KEY_VERIFICATION_IDLE; uint8_t hash1[32] = {0}; // uint8_t hash2[32] = {0}; // char message[40] = {0}; void processSecurityNumber(uint32_t); void updateState(bool resetTimer = true); // check the timeouts and maybe reset the state to idle void resetToIdle(); // Zero out module state }; extern KeyVerificationModule *keyVerificationModule;