// RecvActThree processes the final act (act three) sent from the initiator to // the responder. After processing this act, the responder learns of the // initiators's static public key. Decryption of the static key serves to // authenticate the initiator to the responder. func (b *BrontideMachine) RecvActThree(actThree [ActThreeSize]byte) error { var ( err error s [33 + 16]byte p [16]byte ) copy(s[:], actThree[:33+16]) copy(p[:], actThree[33+16:]) // s remotePub, err := b.DecryptAndHash(s[:]) if err != nil { return err } b.remoteStatic, err = btcec.ParsePubKey(remotePub, btcec.S256()) if err != nil { return err } // se se := btcec.GenerateSharedSecret(b.localEphemeral, b.remoteStatic) b.mixKey(se) if _, err := b.DecryptAndHash(p[:]); err != nil { return err } // With the final ECDH operation complete, derive the session sending // and receiving keys. b.split() return nil }
// RecvActTwo processes the second packet (act two) sent from the responder to // the initiator. A succesful processing of this packet authenticates the // initiator to the responder. func (b *BrontideMachine) RecvActTwo(actTwo [ActTwoSize]byte) error { var ( err error e [33]byte p [16]byte ) copy(e[:], actTwo[:33]) copy(p[:], actTwo[33:]) // e b.remoteEphemeral, err = btcec.ParsePubKey(e[:], btcec.S256()) if err != nil { return err } b.mixHash(b.remoteEphemeral.SerializeCompressed()) // ee s := btcec.GenerateSharedSecret(b.localEphemeral, b.remoteEphemeral) b.mixKey(s) if _, err := b.DecryptAndHash(p[:]); err != nil { return err } return nil }
// GenActTwo generates the second packet (act two) to be sent from the // responder to the initiator. The packet for act two is identify to that of // act one, but then results in a different ECDH operation between the // initiator's and responder's ephemeral keys. // // <- e, ee func (b *BrontideMachine) GenActTwo() ([ActTwoSize]byte, error) { var ( err error actTwo [ActTwoSize]byte ) // e b.localEphemeral, err = btcec.NewPrivateKey(btcec.S256()) if err != nil { return actTwo, err } ephemeral := b.localEphemeral.PubKey().SerializeCompressed() b.mixHash(b.localEphemeral.PubKey().SerializeCompressed()) // ee s := btcec.GenerateSharedSecret(b.localEphemeral, b.remoteEphemeral) b.mixKey(s) authPayload := b.EncryptAndHash([]byte{}) copy(actTwo[:33], ephemeral) copy(actTwo[33:], authPayload) return actTwo, nil }
// RecvActOne processes the act one packet sent by the initiator. The responder // executes the mirroed actions to that of the initiator extending the // handshake digest and deriving a new shared secret based on a ECDH with the // initiator's ephemeral key and reponder's static key. func (b *BrontideMachine) RecvActOne(actOne [ActOneSize]byte) error { var ( err error e [33]byte p [16]byte ) copy(e[:], actOne[:33]) copy(p[:], actOne[33:]) // e b.remoteEphemeral, err = btcec.ParsePubKey(e[:], btcec.S256()) if err != nil { return err } b.mixHash(b.remoteEphemeral.SerializeCompressed()) // es s := btcec.GenerateSharedSecret(b.localStatic, b.remoteEphemeral) b.mixKey(s) // If the initiator doesn't know our static key, then this operation // will fail. if _, err := b.DecryptAndHash(p[:]); err != nil { return err } return nil }
// GenActThree creates the final (act three) packet of the handshake. Act three // is to be sent from the initiator to the responder. The purpose of act three // is to transmit the initiator's public key under strong forwad secrecy to the // responder. This act also includes the final ECDH operation which yields the // final session. // // -> s, se func (b *BrontideMachine) GenActThree() ([ActThreeSize]byte, error) { var actThree [ActThreeSize]byte ourPubkey := b.localStatic.PubKey().SerializeCompressed() ciphertext := b.EncryptAndHash(ourPubkey) s := btcec.GenerateSharedSecret(b.localStatic, b.remoteEphemeral) b.mixKey(s) authPayload := b.EncryptAndHash([]byte{}) copy(actThree[:49], ciphertext) copy(actThree[49:], authPayload) // With the final ECDH operation complete, derive the session sending // and receiving keys. b.split() return actThree, nil }
// NewMixHeader creates a new mix header which is capable of // obliviously routing a message through the mix-net path outline by // 'paymentPath'. This function returns the created mix header along // with a derived shared secret for each node in the path. func NewMixHeader(paymentPath []*btcec.PublicKey, sessionKey *btcec.PrivateKey, rawHopPayloads [][]byte, assocData []byte) (*MixHeader, [][sharedSecretSize]byte, error) { // Each hop performs ECDH with our ephemeral key pair to arrive at a // shared secret. Additionally, each hop randomizes the group element // for the next hop by multiplying it by the blinding factor. This way // we only need to transmit a single group element, and hops can't link // a session back to us if they have several nodes in the path. numHops := len(paymentPath) hopEphemeralPubKeys := make([]*btcec.PublicKey, numHops) hopSharedSecrets := make([][sha256.Size]byte, numHops) hopBlindingFactors := make([][sha256.Size]byte, numHops) // Compute the triplet for the first hop outside of the main loop. // Within the loop each new triplet will be computed recursively based // off of the blinding factor of the last hop. hopEphemeralPubKeys[0] = sessionKey.PubKey() hopSharedSecrets[0] = sha256.Sum256(btcec.GenerateSharedSecret(sessionKey, paymentPath[0])) hopBlindingFactors[0] = computeBlindingFactor(hopEphemeralPubKeys[0], hopSharedSecrets[0][:]) // Now recursively compute the ephemeral ECDH pub keys, the shared // secret, and blinding factor for each hop. for i := 1; i <= numHops-1; i++ { // a_{n} = a_{n-1} x c_{n-1} -> (Y_prev_pub_key x prevBlindingFactor) hopEphemeralPubKeys[i] = blindGroupElement(hopEphemeralPubKeys[i-1], hopBlindingFactors[i-1][:]) // s_{n} = sha256( y_{n} x c_{n-1} ) -> // (Y_their_pub_key x x_our_priv) x all prev blinding factors yToX := blindGroupElement(paymentPath[i], sessionKey.D.Bytes()) hopSharedSecrets[i] = sha256.Sum256(multiScalarMult(yToX, hopBlindingFactors[:i]).X.Bytes()) // TODO(roasbeef): prob don't need to store all blinding factors, only the prev... // b_{n} = sha256(a_{n} || s_{n}) hopBlindingFactors[i] = computeBlindingFactor(hopEphemeralPubKeys[i], hopSharedSecrets[i][:]) } // Generate the padding, called "filler strings" in the paper. filler := generateHeaderPadding("rho", numHops, 2*securityParameter, hopSharedSecrets) hopFiller := generateHeaderPadding("gamma", numHops, HopPayloadSize, hopSharedSecrets) // Allocate and initialize fields to zero-filled slices var mixHeader [routingInfoSize]byte var hopPayloads [NumMaxHops * HopPayloadSize]byte // Same goes for the HMAC var next_hmac [20]byte next_address := bytes.Repeat([]byte{0x00}, 20) // Now we compute the routing information for each hop, along with a // MAC of the routing info using the shared key for that hop. for i := numHops - 1; i >= 0; i-- { rhoKey := generateKey("rho", hopSharedSecrets[i]) gammaKey := generateKey("gamma", hopSharedSecrets[i]) muKey := generateKey("mu", hopSharedSecrets[i]) // Shift and obfuscate routing info streamBytes := generateCipherStream(rhoKey, numStreamBytes) rightShift(mixHeader[:], 2*securityParameter) copy(mixHeader[:], next_address[:]) copy(mixHeader[securityParameter:], next_hmac[:]) xor(mixHeader[:], mixHeader[:], streamBytes[:routingInfoSize]) // Shift and obfuscate per-hop payload rightShift(hopPayloads[:], HopPayloadSize) copy(hopPayloads[:], rawHopPayloads[i]) hopStreamBytes := generateCipherStream(gammaKey, uint(len(hopPayloads))) xor(hopPayloads[:], hopPayloads[:], hopStreamBytes) // We need to overwrite these so every node generates a correct padding if i == numHops-1 { copy(mixHeader[len(mixHeader)-len(filler):], filler) copy(hopPayloads[len(hopPayloads)-len(hopFiller):], hopFiller) } packet := append(append(mixHeader[:], hopPayloads[:]...), assocData...) next_hmac = calcMac(muKey, packet) next_address = btcutil.Hash160(paymentPath[i].SerializeCompressed()) } header := &MixHeader{ Version: 0x01, EphemeralKey: hopEphemeralPubKeys[0], RoutingInfo: mixHeader, HeaderMAC: next_hmac, HopPayload: hopPayloads, } return header, hopSharedSecrets, nil }
// ProcessOnionPacket processes an incoming onion packet which has been forward // to the target Sphinx router. If the encoded ephemeral key isn't on the // target Elliptic Curve, then the packet is rejected. Similarly, if the // derived shared secret has been seen before the packet is rejected. Finally // if the MAC doesn't check the packet is again rejected. // // In the case of a successful packet processing, and ProcessedPacket struct is // returned which houses the newly parsed packet, along with instructions on // what to do next. func (r *Router) ProcessOnionPacket(onionPkt *OnionPacket, assocData []byte) (*ProcessedPacket, error) { mixHeader := onionPkt.Header dhKey := mixHeader.EphemeralKey routeInfo := mixHeader.RoutingInfo headerMac := mixHeader.HeaderMAC var hopPayload [HopPayloadSize]byte // Ensure that the public key is on our curve. if !r.onionKey.Curve.IsOnCurve(dhKey.X, dhKey.Y) { return nil, fmt.Errorf("pubkey isn't on secp256k1 curve") } // Compute our shared secret. sharedSecret := sha256.Sum256(btcec.GenerateSharedSecret(r.onionKey, dhKey)) // In order to mitigate replay attacks, if we've seen this particular // shared secret before, cease processing and just drop this forwarding // message. r.RLock() if _, ok := r.seenSecrets[sharedSecret]; ok { r.RUnlock() return nil, ErrReplayedPacket } r.RUnlock() // Using the derived shared secret, ensure the integrity of the routing // information by checking the attached MAC without leaking timing // information. message := append(append(routeInfo[:], mixHeader.HopPayload[:]...), assocData...) calculatedMac := calcMac(generateKey("mu", sharedSecret), message) if !hmac.Equal(headerMac[:], calculatedMac[:]) { return nil, fmt.Errorf("MAC mismatch %x != %x, rejecting forwarding message", headerMac, calculatedMac) } // The MAC checks out, mark this current shared secret as // processed in order to mitigate future replay attacks. We // need to check to see if we already know the secret again // since a replay might have happened while we were checking // the MAC. r.Lock() if _, ok := r.seenSecrets[sharedSecret]; ok { r.RUnlock() return nil, ErrReplayedPacket } r.seenSecrets[sharedSecret] = struct{}{} r.Unlock() // Attach the padding zeroes in order to properly strip an encryption // layer off the routing info revealing the routing information for the // next hop. var hopInfo [numStreamBytes]byte streamBytes := generateCipherStream(generateKey("rho", sharedSecret), numStreamBytes) headerWithPadding := append(routeInfo[:], bytes.Repeat([]byte{0}, 2*securityParameter)...) xor(hopInfo[:], headerWithPadding, streamBytes) // Randomize the DH group element for the next hop using the // deterministic blinding factor. blindingFactor := computeBlindingFactor(dhKey, sharedSecret[:]) nextDHKey := blindGroupElement(dhKey, blindingFactor[:]) // Parse out the ID of the next node in the route. var nextHop [securityParameter]byte copy(nextHop[:], hopInfo[:securityParameter]) // MAC and MixHeader for the next hop. var nextMac [securityParameter]byte copy(nextMac[:], hopInfo[securityParameter:securityParameter*2]) var nextMixHeader [routingInfoSize]byte copy(nextMixHeader[:], hopInfo[securityParameter*2:]) hopPayloadsWithPadding := append(mixHeader.HopPayload[:], bytes.Repeat([]byte{0x00}, HopPayloadSize)...) hopStreamBytes := generateCipherStream(generateKey("gamma", sharedSecret), uint(len(hopPayloadsWithPadding))) xor(hopPayloadsWithPadding, hopPayloadsWithPadding, hopStreamBytes) copy(hopPayload[:], hopPayloadsWithPadding[:HopPayloadSize]) var nextHopPayloads [NumMaxHops * HopPayloadSize]byte copy(nextHopPayloads[:], hopPayloadsWithPadding[HopPayloadSize:]) nextFwdMsg := &OnionPacket{ Header: &MixHeader{ Version: onionPkt.Header.Version, EphemeralKey: nextDHKey, RoutingInfo: nextMixHeader, HeaderMAC: nextMac, HopPayload: nextHopPayloads, }, } var action ProcessCode = MoreHops if bytes.Compare(bytes.Repeat([]byte{0x00}, 20), nextMac[:]) == 0 { action = ExitNode } return &ProcessedPacket{ Action: action, NextHop: nextHop, Packet: nextFwdMsg, HopPayload: hopPayload, }, nil }