Skip to content
This repository has been archived by the owner on Feb 8, 2023. It is now read-only.

Shared Secret constructions for Private Networks #177

Open
jbenet opened this issue Oct 19, 2016 · 5 comments
Open

Shared Secret constructions for Private Networks #177

jbenet opened this issue Oct 19, 2016 · 5 comments

Comments

@jbenet
Copy link
Member

jbenet commented Oct 19, 2016

Caveats:

  • this is a very basic encryption to segregate libp2p nodes into separate networks, using a network wide shared secret
  • this is not meant to be highly secure, as the underlying connection will run its own online encryption handshake, with authenticated encryption, ephemeral keys, forward secrecy, and so on.
  • this wrapper is merely to prevent nodes from connecting to each other when they are in different private networks.
  • Thus the threat model is:
    • we do not need authentication (bit flipping is ok, as it will be detected in the underlying cipher)
    • we do not need to protect against replay (replay will be detected/discarded in underlying cipher)
    • we use a network-wide shared secret and re-key when revoking access to a single node
    • we must protect against secret extraction (do not exchange the secret)
    • we must protect against ciphertext collection to derive the key/shared secret (ie derive per-session keys, use random IVs/nonces)
  • WARNING: i have not studied these carefully. this is notes. these may be broken. need review.

Construction 1:

  • 0-RTT
  • basic shard secret encryption
  • individual session keys
  • not safe to replay (fine because internal stream will encrypt with an online key echange, and a session key)
  • not authenticated, can flip bits (fine because internal stream will encrypt with AEAD)
  • no MAC overhead
|| = concat

SS = <shared secret>
conn = <plaintext connection>

N = randomNonce(32) # 256 bits
LK = LocalPublicKey() # use only if remote has it available already. (wouldn't allow connecting to unknown nodes to identify them via secio)
SK = sha2-256(SS || LK || N)  # session key
SC = AESCTR(SK) or CHACHA(SK) # stream cipher

conn.Write(N)
for {
  data := getOutgoingData()
  SC.XORKeyStream(data, data)
  conn.Write(data)
}

Though really:

for {  # AES has a 64GB key limit
  N = randomNonce(32) # 256 bits
  SK = sha2-256(SS || LK || N) # session key

  conn.Write(N)
  SC = AESCTR(SK) or CHACHA(SK)
  SW = cipher.StreamWriter{S: SC, W: conn} # XORs using given cipher

  written := 0
  for written < 63GB { # AES must be re-keyed after 64GB.
    data = getOutgoingData()
    n, _ = SW.Write(data)
    written += n
  }
}

Construction 2:

  • 1-RTT
  • basic shared secret encryption
  • individual session keys
  • safe to replay
  • not authenticated, can flip bits (fine because internal stream will encrypt with AEAD)
  • no MAC overhead
SS = <shared secret>
conn = <plaintext connection>

N1, N2 := make([]byte, 32) # 256 bits
N1 = randomNonce(32)
conn.Write(N1)
conn.Read(N2)
N = sortAndConcat(N1, N2) # to disregard order.

LK = localPublicKey()
RK = remotePublicKey() # only available if we know it beforehand. this is pre-secio
PKs = sortAndConcat(LK, RK) # to disregard order.
SK = sha2-256(SS || PKs || N)        # session key
SC = AESCTR(SK) or CHACHA(SK) # stream cipher

for {
  data := getOutgoingData()
  SC.XORKeyStream(data, data)
  conn.Write(data)
}

Construction 3:

  • 1-RTT
  • AEAD (AES-GCM or ChaCha/Poly1305)
  • individual session keys
  • safe to replay
  • authenticated
  • another MAC overhead (oof!)
@Kubuxu
Copy link
Member

Kubuxu commented Oct 20, 2016

This is outdated. See: #177 (comment)


My suggestion:
It is constriction quite similar to cjdns's:

  • 0-RTT
  • intended operation mode of XSalsa20
  • replay not possible, but will kill the connection as the state of counters will diverge and all we will get is garbage, it will be up to higher level protocol to kill the connection in this case (if some party can replay it can also kill the connection using TCP RST).
  • not authenticated, can flip bits (fine because internal stream will encrypt with AEAD)
  • no MAC overhead
  • no key hashing or knowledge about key needed
  • 128bit random nonce per session
  • 64bit counter per session (inexhaustible, I have estimated 500k+ years with 1,000,000 packets per seconds) which refreshes on reconnection
SS = <shared secret>
N = make([]byte, 24) // 192 bits
copy(N[8:], randomNonce(16))
conn.Write(N[8:])
i = int64(0)


for {
  binary.BigEndian.PutUint64(N, i)
  data := getOutgoingData()
  salsa20.XORKeyStream(data, data, N, SS)
  conn.Write(data)
  i++
}

@whyrusleeping
Copy link
Member

why not use varints?

Also, what is the necessity for having the counter? If its an xor'ed keystream it has to be ordered anyways.

@Kubuxu
Copy link
Member

Kubuxu commented Oct 20, 2016

why not use varints?

Where you want to put varints, the base of the nonce is sent once per connection.
If you mean PutUint64, then I always need 8 bytes.

Also, what is the necessity for having the counter?

Salsa20 is stateless, it can't be resumed (it has internal counter but it isn't exposed), that is why one uses counter as part of the nonce.

@Kubuxu
Copy link
Member

Kubuxu commented Oct 23, 2016

The schema had to change a bit as I didn't took into account read re-fragmentation of TCP buffers and no maximum frame length on this level (nor I wanted to introduce one).

Right now it is:

SS = <shared secret>
N = randomNonce(24) // 192bits
conn.Write(N)
S20 = salsa20.NewStream(SS, N)

for {
  data := getOutgoingData()
  S20.XORKeyStream(data, data)
  conn.Write(data)
}

Same benefits apply, most importatnly, there is no need for rekeying as it is with AES. Salsa20 stream is 2^70 bytes long as for the 64bit internal counter, versus AES stream modes using 32bit counters.

The performance of salsa20 is double of AES even on modern architectures with dedicated AES instructions (tested on i7-6800k) and it keeps being double on 32bit ARM. The difference will be even higher one AArch64 becomes more popular.

@dominictarr
Copy link

@Kubuxu's last suggestion is fine, you don't need framing or counters, because you have integrity from the next layer. However, you could just encrypt the handshake like this, but not the rest of the session and you'd still preserve your desired properties without another layer of encryption, but then you basically have a integrated design, but this clearly simple to implement as a layer.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

5 participants