[noise] Kernel-land C implementation of latest noise specification
Jason A. Donenfeld
Jason at zx2c4.com
Tue Jul 7 14:11:01 PDT 2015
On Tue, Jul 7, 2015 at 10:00 PM, Trevor Perrin <trevp at trevp.net> wrote:
>
> This is just a different ciphersuite, where you define a BLAKE2b-based
> KDF and HASH?
Yes. HASH(h) = Blake2b_hash(h) and KDF(k, n, input) =
Blake2b_keyedhash(GETKEY(k, n), input).
> The timestamp is just included in the payload, I hope?
I guess. It's the last encrypted thing that's read from the initial
handshake packet. That function works as:
1. Read clear e
2. Compute dhes and call kdf
3. Read encrypted s
4. Compute dhss and call kdf
5. Read encrypted t
6. See if s is part of internal list of accepted peers
7. Make sure t is greater than peer's previous t
8. h = HASH(h || message)
So it's in the payload, in so far as you consider the payload to be
the last encrypted thing in the message. I'm not treating it that way,
finding it easier to instead just define this:
struct noise_message_handshake_initiation {
struct noise_message_header header;
u16 sender_index;
u8 unencryped_ephemeral[NOISE_PUBLIC_KEY_LEN];
u8 encrypted_static[noise_encrypted_len(NOISE_PUBLIC_KEY_LEN)];
u8 encrypted_timestamp[noise_encrypted_len(NOISE_TIMESTAMP_LEN)];
} __attribute__((packed));
The response to this handshake uses a similar struct:
struct noise_message_handshake_response {
struct noise_message_header header;
u16 sender_index;
u16 receiver_index;
u8 encrypted_ephemeral[noise_encrypted_len(NOISE_PUBLIC_KEY_LEN)];
} __attribute__((packed));
> So how do you handle the prologue? Did you remove it, or do you just
> assume its zero-length?
There's not exactly a prologue anymore. Every packet has a header.
This header is simply this:
struct noise_message_header {
u8 type;
} __attribute__((packed));
And the type is one of these:
enum noise_message_type {
NOISE_MESSAGE_INVALID = 0,
NOISE_MESSAGE_HANDSHAKE_INITIATION = 1,
NOISE_MESSAGE_HANDSHAKE_RESPONSE = 2,
NOISE_MESSAGE_DATA = 3,
NOISE_MESSAGE_TOTAL = 4
};
If it's 1 or 2, then we get the structs I put above. If it's 3, then
it's a data message:
struct noise_message_data {
struct noise_message_header header;
u16 key_idx;
u8 encrypted_data[];
} __attribute__((packed));
So we have the handshake, and then after that, the application is free
to put whatever it wants inside the data message.
> Did you consider encoding type inside the prologue? This way the type
> will be included in the authenticated data, as well as any other
> cleartext data you might need to negotiate in future.
Right so instead of having a length parameter, the prologue is always
1 byte long, and contains the type. Every time something is encrypted,
it sets authtext to "h || everything-in-the-message-up-to-that-point",
as you've described in the spec. So in noise_message_data, the header
type parameter as well as key_idx are authenticated by
encrypted_data's tag. In the handshake messages above, the order and
contents are authenticated by each subsequent AE tag.
One other difference from your description is how I treat the "session
state". I do it like this:
struct noise_peer {
struct noise_handshake handshake;
struct noise_symmetric_key sending_key;
struct noise_symmetric_key receiving_key;
struct noise_symmetric_key previous_receiving_key;
unsigned long rekey_after_time;
};
The handshake struct contains all the various DH keys:
struct noise_handshake {
bool is_valid;
u8 latest_timestamp[NOISE_TIMESTAMP_LEN];
u8 static_public[NOISE_PUBLIC_KEY_LEN];
u8 static_private[NOISE_PUBLIC_KEY_LEN];
u8 ephemeral_public[NOISE_PUBLIC_KEY_LEN];
u8 ephemeral_private[NOISE_PUBLIC_KEY_LEN];
u8 remote_static[NOISE_PUBLIC_KEY_LEN];
u8 remote_ephemeral[NOISE_PUBLIC_KEY_LEN];
struct noise_symmetric_key key;
};
And the symmetric_key struct contains everything necessary for
symmetric encryption and decryption:
struct noise_symmetric_key {
bool is_valid;
u8 key[NOISE_SYMMETRIC_KEY_LEN];
atomic64_t counter;
u8 hash[NOISE_HASH_LEN];
unsigned long birthdate;
u16 index;
};
At handshake initiation, I set peer.handshake.key.h to its initial
value, peer.handshake.key.nonce to zero, and
peer.handshake.static_public/private and peer.handshake.remote_static
to the right pre-known values. During the various stages of the
handshake, the other variables are filled in. Then, when the handshake
is complete, and begin_session is called, all ephemeral keys are
deleted from the handshake, and peer.handshake.key is copied to
peer.sending_key and peer.receiving_key after calling GETKEY and
resetting the nonce counter, and finally peer.handshake.key is zeroed.
(There is some other book keeping stuff in there. I keep track of a
previous receiving key so I can decrypt old messages within in a
certain time frame, to fix a possible UDP race. I also give all keys a
birthdate so I can clear them from memory after some time. And keys
correspond to indexes, which are like IPSec's SPI field; replacements
for this field would be welcomed - the MPHF thread on messaging@ has
started to address that.)
This amounts to what you described in the specification, but organizes
the fields a little differently (and more efficiently from an
implementation perspective). I hope this is okay!
Any feedback on the general code and implementation? I'm all ears and
would love some criticism.
More information about the Noise
mailing list