[noise] Stateful Hash Object Proposal

Trevor Perrin trevp at trevp.net
Fri Nov 16 02:12:28 PST 2018


My initial post on this was messy, here's a cleaned up proposal:

API
-------

StatefulHashObject {

    Absorb(bytes)
    Squeeze(len) -> bytes
    Ratchet()

    Clone() -> StatefulHashObject

    // Below functions are optional.
    // If supported, Encrypt() must have same properties as:
    //   key = Squeeze'(32);
    //   ciphertext = Authenticated-Encryption(key, plaintext)
    //   Absorb(ciphertext)
    //   return ciphertext
    //
    //   where Squeeze' is a domain-separated version of Squeeze()
    //   and ciphertext is 16 bytes longer than plaintext

    Encrypt(bytes) -> bytes
    Decrypt(bytes) -> bytes
}


Example SHOs
-------------

>From traditional hashes (SHA256, BLAKE2, etc):
  Absorb = incrementally hash
  Squeeze = output the hash value
  Ratchet = absorb zeros up to the next internal block boundary, so
that the internal buffer is cleared and the internal chaining variable
is updated

STROBE:
  Absorb = AD
  Squeeze = PRF(len)
  Ratchet = RATCHET
  Encrypt = ENC, PRF(16) # for tag
  Decrypt = DEC, PRF(16) # for tag


SymmetricState
---------------

The Noise SymmetricState now contains a SHO.  If the SHO doesn't
support Encrypt/Decrypt, the SymmetricState also contains a
CipherState with k and n variables (as before).

The functions are mostly the same, except GetSymmetricKey() and
GetAdditionalSymmetricKey() to calculate k and "Additional Symmetric
Keys" on demand:

MixKey(key):
  SHO.Absorb(key)

MixHash(data):
  SHO.Absorb(data)

MixKeyAndHash(data):
  SHO.Absorb(data)

GetHandshakeHash():
  clone = SHO.Clone()
  clone.Absorb("h")
  return clone.Squeeze(HASHLEN)

EncryptAndHash(plaintext):
  if has_cipherstate:
    // same as before
  else:
    return SHO.Encrypt(plaintext)

DecryptAndHash(ciphertext):
  if has_cipherstate:
    // same as before
  else:
    return SHO.Decrypt(ciphertext)

Split():
  clone_i = SHO.Clone()
  clone_i.Absorb("i")
  key_i = clone_i.Squeeze(32)

  clone_r = SHO.Clone()
  clone_r.Absorb("r")
  key_r = clone_r.Squeeze(32)

  return (key_i, key_r)

GetSymmetricKey():
  if has_cipherstate:
    return self.k
  else:
    clone = Clone()
    clone.Absorb("k")
    return clone.Squeeze(32)

GetAdditionalSymmetricKey(label):
  if has_cipherstate:
    // todo, see [1]
  else:
    // varint length is byte-reversed so it's parseable from the end:
    clone = Clone()
    clone.Absorb(label || reversed_len(label) || "a")
    return clone.Squeeze(32)


Processing rules
-----------------
Handshake patterns are compiled into SymmetricState operations mostly
the same as before, except:

 * A varint length field is absorbed before the handshake payload and
the prologue.  Any variable-length fields added in future will also
need a varint length absorbed before them.

 * In interactive patterns, Ratchet() is called after processing the
handshake payload in each handshake message except the last one.  This
ensures any internal buffer in the SHO can be "flushed" into the
internal chaining variable via a one-way function, so a compromise
can't recover the buffer's old contents (assuming enough entropy).


Naming
-------

The "SHO" variants of a hash function just add the "SHO/" prefix to the name:
SHO/SHA256
SHO/SHA512
SHO/BLAKE2s
SHO/BLAKE2b
SHO/SHAKE128
SHO/SHAKE256
etc

But other SHOs could be named differently, e.g. "STROBE/1.0.2".


Example
---------

The XX pattern derives 4 keys for handshake payload encryption, then
creates (via Split) two keys for traffic encryption.  The keys for
Noise_XX_25519_AESGCM_SHO/SHA256 would be calculated as follows:

XX:
  -> e
  <- e, ee, [1] s, es, [2] [payload]
  -> [3] s, se, [4] [payload]

  [5,6] = split()

transcript = "Noise_XX_25519_AESGCM_SHO/SHA256" || len(prologue) ||
prologue || e)
transcript += zeros // pad to block boundary, for Ratchet
transcript += e || ee
key1 = SHA256(transcript || "k")

transcript += s || es
key2 = SHA256(transcript || "k")

transcript += len(payload) || payload
transcript += zeros // pad to block boundary, for Ratchet
key3 = SHA256(transcript || "k")

transcript4 += s || se
key4 = SHA256(transcript || "k")

transcript += len(payload) || payload
key_i = SHA256(transcript || "i")
key_r = SHA256(transcript || "r")

Trevor

[1] https://moderncrypto.org/mail-archive/noise/2018/001853.html


More information about the Noise mailing list