Added noise package to core:crypto

Special thanks to Yawning for review, guidance, and massive updates.
This commit is contained in:
Lord_Hellgrim
2026-04-14 22:44:47 +00:00
committed by Yawning Angel
parent 19f249b475
commit 2623a8fa9f
5 changed files with 2531 additions and 0 deletions

429
core/crypto/noise/api.odin Normal file
View File

@@ -0,0 +1,429 @@
package noise
import "base:runtime"
import "core:crypto/ecdh"
// MAX_PACKET_SIZE is the maximum Noise message size, including TAG_SIZE
// if relevant (`seal_message`, `open_message`).
MAX_PACKET_SIZE :: 65535
// PSK_SIZE is the size of an optional handshake pre-shared symmetric key.
PSK_SIZE :: 32
// TAG_SIZE is the size of the AEAD authentication tag.
TAG_SIZE :: 16
// MAX_STEP_MSG_SIZE is the maximum per-handshake step message size,
// excluding the optional payload.
//
// `e` is DH_LEN, `s` is either DH_LEN or DH_LEN + TAG_SIZE, and there
// is a maximum of one per each message.
MAX_STEP_MSG_SIZE :: (MAX_DH_SIZE*2)+TAG_SIZE
// Status is the status of Noise protocol operation.
Status :: enum {
Ok,
// States
Handshake_Pending,
Handshake_Complete,
Handshake_Split,
Handshake_Failed,
// Errors
Invalid_Protocol_String,
Invalid_Pre_Shared_Key,
Invalid_DH_Key,
No_Self_Identity,
No_Peer_Identity,
Unexpected_Peer_Identity,
Unexpected_Pre_Shared_Key,
DH_Failure,
Invalid_Handshake_Message,
Decryption_Failure,
IV_Exhausted,
Invalid_Cipher_State,
Invalid_Destination_Buffer,
Invalid_Payload_Message,
Max_Packet_Size,
Out_Of_Memory,
}
// Handshake_State is the per-handshake state.
Handshake_State :: struct {
s: ecdh.Private_Key,
e: ecdh.Private_Key,
rs: ecdh.Public_Key,
re: ecdh.Public_Key,
psk: [PSK_SIZE]byte,
symmetric_state: Symmetric_State,
message_pattern: ^Message_Pattern,
current_message: int,
status: Status,
initiator: bool,
pre_set_e: bool,
}
// Cipher_States are the keyed AEAD instances and associated state,
// derived from a successful handshake.
Cipher_States :: struct {
c1_i_to_r: Cipher_State,
c2_r_to_i: Cipher_State,
initiator: bool,
}
// handshake_init initializes a Handshake_State with the provided parameters.
// The relevant values are copied into the Handshake_State instance, and
// can be discarded/sanitized right after handshake_init returns (eg: psk).
//
// Note: While this implementation supports setting `e`, this is primarily
// intended for testing, or cases where the runtime cryptographic entropy
// source is unavailable. Use of this functionality is STRONGLY
// discouraged.
@(require_results)
handshake_init :: proc(
self: ^Handshake_State,
initiator: bool,
prologue: []byte,
s: ^ecdh.Private_Key, // Our static key
rs: ^ecdh.Public_Key, // Peer static key
protocol_name: string,
psk: []byte = nil,
_e: ^ecdh.Private_Key = nil, // Our ephemeral key (for testing/RNG-less systems)
) -> Status {
return handshakestate_Initialize(
self,
initiator,
prologue,
s,
_e,
rs,
nil,
protocol_name,
psk,
)
}
// handshake_initiator_step takes an input_message received from the responder
// if any and an optional payload to be sent to the responder, and performs
// one step of the Noise handshake process, returning the message to be sent
// to the responder if any, the payload received from the responder if any,
// and the status of the handshake.
//
// The output message MUST be sent to the responder even if the status code
// returned is .Handshake_Complete.
//
// If the dst parameter is provided, the message and payload will be written
// to dst, otherwise new buffers will be allocated.
@(require_results)
handshake_initiator_step :: proc(
self: ^Handshake_State,
input_message: []byte,
payload: []byte = nil,
dst: []byte = nil,
allocator := context.allocator,
) -> ([]byte, []byte, Status) {
output_message: []byte
payload_buffer: []byte
status: Status
dst := dst
if input_message == nil {
output_message, status = handshakestate_WriteMessage(self, payload, dst, allocator)
} else {
payload_buffer, status = handshakestate_ReadMessage(self, input_message, dst, allocator)
if status == .Handshake_Pending {
if dst != nil {
dst = dst[len(payload_buffer):]
}
output_message, status = handshakestate_WriteMessage(self, payload, dst, allocator)
}
}
return output_message, payload_buffer, status
}
// handshake_responder_step takes a input_message received from the initiator,
// and and an optional payload to be sent to the initiator, and performs
// one step of the Noise handshake process, returning the message to be sent
// to the initiator if any, the payload received from the initiator if any,
// and the status of the handshake.
//
// The output message MUST be sent to the initiator even if the status code
// returned is .Handshake_Complete.
//
// If the dst parameter is provided, the message and payload will be written
// to dst, otherwise new buffers will be allocated.
@(require_results)
handshake_responder_step :: proc(
self: ^Handshake_State,
input_message: []byte,
payload: []byte = nil,
dst: []byte = nil,
allocator := context.allocator,
) -> ([]byte, []byte, Status) {
output_message: []byte
if input_message == nil {
return nil, nil, .Invalid_Handshake_Message
}
dst := dst
payload_buffer, status := handshakestate_ReadMessage(self, input_message, dst, allocator)
if status == .Handshake_Pending {
if dst != nil {
dst = dst[len(payload_buffer):]
}
output_message, status = handshakestate_WriteMessage(self, payload, dst, allocator)
}
return output_message, payload_buffer, status
}
// handshake_split initializes a Cipher_States instance from a completed
// handshake. This can be called once and only once per Handshake_State
// instance.
@(require_results)
handshake_split :: proc(self: ^Handshake_State, cipher_states: ^Cipher_States) -> Status {
if self.status != .Handshake_Complete {
return self.status
}
symmetricstate_Split(&self.symmetric_state, cipher_states)
if self.message_pattern.is_one_way {
cipherstate_reset(&cipher_states.c2_r_to_i)
cipher_states.c2_r_to_i.is_invalid = true
}
cipher_states.initiator = self.initiator
self.status = .Handshake_Split
return .Ok
}
// handshake_peer_identity returns the peer's static DH key used by
// a completed handshake.
//
// This returns a pointer to the Handshake_State's copy of the peer's
// public key, that will get wiped by handshake_reset. If the key is
// needed after a call to handshake_reset, it must be copied.
@(require_results)
handshake_peer_identity :: proc(self: ^Handshake_State) -> (^ecdh.Public_Key, Status) {
#partial switch self.status {
case .Handshake_Complete, .Handshake_Split:
case:
return nil, self.status
}
if ecdh.curve(&self.rs) == .Invalid {
return nil, .No_Peer_Identity
}
return &self.rs, .Ok
}
// handshake_hash returns the handshake transcript hash of a completed
// handshake, for the purposes of channel binding. See 11.2 of the
// specification for details on usage.
//
// This returns a slice to an internal buffer that will get wiped by
// handshake_reset. If the hash is needed after a call to handshake_reset,
// the slice must be copied.
@(require_results)
handshake_hash :: proc(self: ^Handshake_State) -> ([]byte, Status) {
#partial switch self.status {
case .Handshake_Complete, .Handshake_Split:
case:
return nil, self.status
}
return symmetricstate_GetHandshakeHash(&self.symmetric_state), .Ok
}
// handshake_reset sanitizes the Handshake_State. It is both safe and
// recommended to call this as soon as practical (after any calls to
// handshake_peer_identity, handshake_hash, and handshake_split are
// complete).
handshake_reset :: proc(self: ^Handshake_State) {
handshakestate_reset(self)
}
// seal_message encrypts the provided data, authenticates the aad and
// ciphertext, and returns the resulting ciphertext. The ciphertext
// will ALWAYS be `len(plaintext) + TAG_SIZE` bytes in length.
//
// If the dst parameter is provided, the ciphertext will be written
// to dst, otherwise a new buffer will be allocated.
@(require_results)
seal_message :: proc(self: ^Cipher_States, aad, plaintext: []byte, dst: []byte = nil, allocator := context.allocator) -> ([]byte, Status) {
data_len := len(plaintext)
dst := dst
did_alloc: bool
switch {
case dst == nil:
err: runtime.Allocator_Error
dst, err = make([]byte, data_len + TAG_SIZE, allocator)
if err != nil {
return nil, .Out_Of_Memory
}
did_alloc = true
case:
if len(dst) != data_len + TAG_SIZE {
return nil, .Invalid_Destination_Buffer
}
}
status: Status
switch self.initiator {
case true:
dst, status = cipherstate_EncryptWithAd(&self.c1_i_to_r, aad, plaintext, dst)
case false:
dst, status = cipherstate_EncryptWithAd(&self.c2_r_to_i, aad, plaintext, dst)
}
if status != .Ok && did_alloc {
delete(dst, allocator)
dst = nil
}
return dst, status
}
// open_message authenticates the aad and ciphertext, decrypts the
// ciphertext and returns the resulting plaintext. The plaintext will
// ALWAYS be `len(ciphertext) - TAG_SIZE` bytes in length.
//
// If the dst parameter is provided, the plaintext will be written to
// dst, otherwise a new buffer will be allocated.
@(require_results)
open_message :: proc(self: ^Cipher_States, aad, ciphertext: []byte, dst: []byte = nil, allocator := context.allocator) -> ([]byte, Status) {
if len(ciphertext) < TAG_SIZE {
return nil, .Invalid_Payload_Message
}
data_len := len(ciphertext) - TAG_SIZE
dst := dst
did_alloc: bool
switch {
case dst == nil:
if data_len > 0 {
err: runtime.Allocator_Error
dst, err = make([]byte, data_len, allocator)
if err != nil {
return nil, .Out_Of_Memory
}
did_alloc = true
}
case:
if len(dst) != data_len {
return nil, .Invalid_Destination_Buffer
}
}
status: Status
switch self.initiator {
case true:
dst, status = cipherstate_DecryptWithAd(&self.c2_r_to_i, aad, ciphertext, dst)
case false:
dst, status = cipherstate_DecryptWithAd(&self.c1_i_to_r, aad, ciphertext, dst)
}
if status != .Ok && did_alloc {
delete(dst, allocator)
dst = nil
}
return dst, status
}
// cipherstates_rekey updates the selected AEAD key, using a one way function.
// See 11.3 of the specification for examples of usage.
//
// Note: If one side updates the seal_key, the other side must update
// the non-seal_key and vice versa.
@(require_results)
cipherstates_rekey :: proc(self: ^Cipher_States, seal_key: bool) -> Status {
cs := cipherstates_cs(self, seal_key)
if cs.is_invalid {
return .Invalid_Cipher_State
}
if !cipherstate_HasKey(cs) {
return .Handshake_Pending
}
cipherstate_Rekey(cs)
return .Ok
}
// cipherstates_set_n sets the interal counter used to generate the AEAD
// IV to an explicit value. This can be used to deal with out-of-order
// transport messages. See 11.4 of the specification.
//
// WARNING: Reusing n across different aad/messages with the same Cipher_States
// will result in catastrophic loss of security.
@(require_results)
cipherstates_set_n :: proc(self: ^Cipher_States, seal_key: bool, n: u64) -> Status {
cs := cipherstates_cs(self, seal_key)
if cs.is_invalid {
return .Invalid_Cipher_State
}
if !cipherstate_HasKey(cs) {
return .Handshake_Pending
}
cs.n = n
return .Ok
}
// cipherstates_n returns the interal counter used to generate the AEAD
// IV. This can be used to deal with out-of-order transport messages.
// See 11.4 of the specification.
//
// WARNING: Reusing n across different aad/messages with the same Cipher_States
// will result in catastrophic loss of security.
@(require_results)
cipherstates_n :: proc(self: ^Cipher_States, seal_key: bool, n: u64) -> (u64, Status) {
cs := cipherstates_cs(self, seal_key)
if cs.is_invalid {
return 0, .Invalid_Cipher_State
}
if !cipherstate_HasKey(cs) {
return 0, .Handshake_Pending
}
return cs.n, .Ok
}
// cipherstates_reset sanitizes the Cipher_States.
cipherstates_reset :: proc(self: ^Cipher_States) {
self.initiator = false
cipherstate_reset(&self.c1_i_to_r)
cipherstate_reset(&self.c2_r_to_i)
}
@(private = "file")
cipherstates_cs :: proc(self: ^Cipher_States, seal_key: bool) -> ^Cipher_State {
switch self.initiator {
case true:
switch seal_key {
case true:
return &self.c1_i_to_r
case false:
return &self.c2_r_to_i
}
case false:
switch seal_key {
case true:
return &self.c2_r_to_i
case false:
return &self.c1_i_to_r
}
}
unreachable()
}

View File

@@ -0,0 +1,35 @@
/*
An implementation of the Noise Protocol Framework (Revision 34).
The `fallback` modifier and deferred/multi-PSK patterns are not supported
for the sake of simplicity.
See:
- [[ https://noiseprotocol.org/ ]]
*/
package noise
// In general, to complete a noise handshake you must:
//
// - If you are initiating the connection, call `handshake_initiator_step`
// passing `nil` as the `input_message` parameter.
//
// - Send the resulting `[]byte` to the responder (generally a server) via
// the method of your choice. This MUST be done even if the status code
// returned is `.Handshake_Complete`.
//
// - If the status code returned by `handshake_initiator_step` was
// `.Handshake_Complete`, the handshake completed successfully,
// and it is now possible to validate the peer identity, obtain the
// handshake transcript hash, and most usefully call `handshake_split`
// to populate the `Cipher_States` struct that will be used to
// encrypt/decrypt data.
//
// Otherwise, read the response from the responder and feed the response
// data as the `input_message` to the next `handshake_initiator_step`
// until it returns `.Handshake_Complete`.
//
// - If you are the responder, the method is much the same, except you
// must pass a valid `input_message` received from an initiator to the
// first call to `handshake_responder_step`. Repeat until the returned
// status is `.Handshake_Complete`.

View File

@@ -0,0 +1,686 @@
package noise
import "core:slice"
@(private)
Pre_Token :: enum {
res_s,
ini_s,
}
@(private)
Token :: enum {
e,
s,
ee,
es,
se,
ss,
psk,
}
@(private)
Message_Pattern :: struct {
pre_messages: []Pre_Token,
messages: [][]Token,
is_psk: bool,
is_one_way: bool,
}
// Handshake_Pattern is the list of currently supported Noise Handshake
// Patterns.
Handshake_Pattern :: enum {
Invalid,
// One way patterns
N,
K,
X,
// Fundamental patterns
XX,
NK,
NN,
KN,
KK,
NX,
KX,
XN,
IN,
XK,
IK,
IX,
// Recommended PSK patterns
Npsk0,
Kpsk0,
Xpsk1,
NNpsk0,
NNpsk2,
NKpsk0,
NKpsk2,
NXpsk2,
XNpsk3,
XKpsk3,
XXpsk3,
KNpsk0,
KNpsk2,
KKpsk0,
KKpsk2,
KXpsk2,
INpsk1,
INpsk2,
IKpsk1,
IKpsk2,
IXpsk2,
}
@(require_results)
pattern_requires_initiator_s :: proc(pattern: Handshake_Pattern) -> (pre: bool, hs: bool) {
p := HANDSHAKE_PATTERNS[pattern]
if slice.contains(p.pre_messages, Pre_Token.ini_s) {
pre = true
}
for msg, i in p.messages {
if i & 1 != 0 {
continue
}
if slice.contains(msg, Token.s) {
hs = true
break
}
}
return pre, hs
}
@(require_results)
pattern_requires_responder_s :: proc(pattern: Handshake_Pattern) -> (pre: bool, hs: bool) {
p := HANDSHAKE_PATTERNS[pattern]
if slice.contains(p.pre_messages, Pre_Token.res_s) {
pre = true
}
for msg, i in p.messages {
if i & 1 == 0 {
continue
}
if slice.contains(msg, Token.s) {
hs = true
break
}
}
return pre, hs
}
@(require_results)
pattern_is_psk :: proc(pattern: Handshake_Pattern) -> bool {
return HANDSHAKE_PATTERNS[pattern].is_psk
}
@(require_results)
pattern_is_one_way :: proc(pattern: Handshake_Pattern) -> bool {
return HANDSHAKE_PATTERNS[pattern].is_one_way
}
@(require_results)
pattern_num_messages :: proc(pattern: Handshake_Pattern) -> int {
return len(HANDSHAKE_PATTERNS[pattern].messages)
}
@(private)
HANDSHAKE_PATTERNS := [Handshake_Pattern]^Message_Pattern {
.Invalid = nil,
.N = &PATTERN_N,
.K = &PATTERN_K,
.X = &PATTERN_X,
.XX = &PATTERN_XX,
.NK = &PATTERN_NK,
.NN = &PATTERN_NN,
.KN = &PATTERN_KN,
.KK = &PATTERN_KK,
.NX = &PATTERN_NX,
.KX = &PATTERN_KX,
.XN = &PATTERN_XN,
.IN = &PATTERN_IN,
.XK = &PATTERN_XK,
.IK = &PATTERN_IK,
.IX = &PATTERN_IX,
.Npsk0 = &PATTERN_Npsk0,
.Kpsk0 = &PATTERN_Kpsk0,
.Xpsk1 = &PATTERN_Xpsk1,
.NNpsk0 = &PATTERN_NNpsk0,
.NNpsk2 = &PATTERN_NNpsk2,
.NKpsk0 = &PATTERN_NKpsk0,
.NKpsk2 = &PATTERN_NKpsk2,
.NXpsk2 = &PATTERN_NXpsk2,
.XNpsk3 = &PATTERN_XNpsk3,
.XKpsk3 = &PATTERN_XKpsk3,
.XXpsk3 = &PATTERN_XXpsk3,
.KNpsk0 = &PATTERN_KNpsk0,
.KNpsk2 = &PATTERN_KNpsk2,
.KKpsk0 = &PATTERN_KKpsk0,
.KKpsk2 = &PATTERN_KKpsk2,
.KXpsk2 = &PATTERN_KXpsk2,
.INpsk1 = &PATTERN_INpsk1,
.INpsk2 = &PATTERN_INpsk2,
.IKpsk1 = &PATTERN_IKpsk1,
.IKpsk2 = &PATTERN_IKpsk2,
.IXpsk2 = &PATTERN_IXpsk2,
}
// ------------- ONE WAY PATTERNS ---------------------------------------------------------
// N:
// <- s
// ...
// -> e, es
@(private,rodata)
PATTERN_N : Message_Pattern = {
pre_messages = {.res_s},
messages = {
{.e, .es},
},
is_one_way = true,
}
// K:
// -> s
// <- s
// ...
// -> e, es, ss
@(private,rodata)
PATTERN_K : Message_Pattern = {
pre_messages = {.ini_s, .res_s},
messages = {
{.e, .es, .ss},
},
is_one_way = true,
}
// X:
// <- s
// ...
// -> e, es, s, ss
@(private,rodata)
PATTERN_X : Message_Pattern = {
pre_messages = {.res_s},
messages = {
{.e, .es, .s, .ss},
},
is_one_way = true,
}
// ----------------------------------------------------------------------------------------
// ------------- FUNDAMENTAL PATTERNS -----------------------------------------------------
// XX:
// -> e
// <- e, ee, s, es
// -> s, se
@(private,rodata)
PATTERN_XX : Message_Pattern = {
pre_messages = nil,
messages = {
{.e},
{.e, .ee, .s, .es},
{.s, .se},
},
}
// NK:
// <- s
// ...
// -> e, es
// <- e, ee
@(private,rodata)
PATTERN_NK : Message_Pattern = {
pre_messages = {.res_s},
messages = {
{.e, .es},
{.e, .ee},
},
}
// NN:
// -> e
// <- e, ee
@(private,rodata)
PATTERN_NN : Message_Pattern = {
pre_messages = nil,
messages = {
{.e},
{.e, .ee},
},
}
// KN:
// -> s
// ...
// -> e
// <- e, ee, se
@(private,rodata)
PATTERN_KN : Message_Pattern = {
pre_messages = {.ini_s},
messages = {
{.e,},
{.e, .ee, .se},
},
}
// KK:
// -> s
// <- s
// ...
// -> e, es, ss
// <- e, ee, se
@(private,rodata)
PATTERN_KK : Message_Pattern = {
pre_messages = {.ini_s, .res_s},
messages = {
{.e, .es, .ss},
{.e, .ee, .se},
},
}
// NX:
// -> e
// <- e, ee, s, es
@(private,rodata)
PATTERN_NX : Message_Pattern = {
pre_messages = nil,
messages = {
{.e},
{.e, .ee, .s, .es},
},
}
// KX:
// -> s
// ...
// -> e
// <- e, ee, se, s, es
@(private,rodata)
PATTERN_KX : Message_Pattern = {
pre_messages = {.ini_s},
messages = {
{.e},
{.e, .ee, .se, .s, .es},
},
}
// XN:
// -> e
// <- e, ee
// -> s, se
@(private,rodata)
PATTERN_XN : Message_Pattern = {
pre_messages = nil,
messages = {
{.e},
{.e, .ee},
{.s, .se},
},
}
// IN:
// -> e, s
// <- e, ee, se
@(private,rodata)
PATTERN_IN : Message_Pattern = {
pre_messages = nil,
messages = {
{.e, .s},
{.e, .ee, .se},
},
}
// XK:
// <- s
// ...
// -> e, es
// <- e, ee
// -> s, se
@(private,rodata)
PATTERN_XK : Message_Pattern = {
pre_messages = {.res_s},
messages = {
{.e, .es},
{.e, .ee},
{.s, .se},
},
}
// IK:
// <- s
// ...
// -> e, es, s, ss
// <- e, ee, se
@(private,rodata)
PATTERN_IK : Message_Pattern = {
pre_messages = {.res_s},
messages = {
{.e, .es, .s, .ss},
{.e, .ee, .se},
},
}
// IX:
// -> e, s
// <- e, ee, se, s, es
@(private,rodata)
PATTERN_IX : Message_Pattern = {
pre_messages = nil,
messages = {
{.e, .s},
{.e, .ee, .se, .s, .es},
},
}
// ----------------------------------------------------------------------------------------
// ------------- PSK PATTERNS -------------------------------------------------------------
// Npsk0:
// <- s
// ...
// -> psk, e, es
@(private,rodata)
PATTERN_Npsk0 : Message_Pattern = {
pre_messages = {.res_s},
messages = {
{.psk, .e, .es},
},
is_psk = true,
is_one_way = true,
}
// K:
// -> s
// <- s
// ...
// -> psk, e, es, ss
@(private,rodata)
PATTERN_Kpsk0 : Message_Pattern = {
pre_messages = {.ini_s, .res_s},
messages = {
{.psk, .e, .es, .ss},
},
is_psk = true,
is_one_way = true,
}
// X:
// <- s
// ...
// -> e, es, s, ss, psk
@(private,rodata)
PATTERN_Xpsk1 : Message_Pattern = {
pre_messages = {.res_s},
messages = {
{.e, .es, .s, .ss, .psk},
},
is_psk = true,
is_one_way = true,
}
// NNpsk0:
// -> psk, e
// <- e, ee
@(private,rodata)
PATTERN_NNpsk0 : Message_Pattern = {
pre_messages = nil,
messages = {
{.psk, .e},
{.e, .ee},
},
is_psk = true,
}
// NNpsk2:
// -> e
// <- e, ee, psk
@(private,rodata)
PATTERN_NNpsk2 : Message_Pattern = {
pre_messages = nil,
messages = {
{.e},
{.e, .ee, .psk},
},
is_psk = true,
}
// NKpsk0:
// <- s
// ...
// -> psk, e, es
// <- e, ee
@(private,rodata)
PATTERN_NKpsk0 : Message_Pattern = {
pre_messages = {.res_s},
messages = {
{.psk, .e, .es},
{.e, .ee},
},
is_psk = true,
}
// NKpsk2:
// <- s
// ...
// -> e, es
// <- e, ee, psk
@(private,rodata)
PATTERN_NKpsk2 : Message_Pattern = {
pre_messages = {.res_s},
messages = {
{.e, .es},
{.e, .ee, .psk},
},
is_psk = true,
}
// NXpsk2:
// -> e
// <- e, ee, s, es, psk
@(private,rodata)
PATTERN_NXpsk2 : Message_Pattern = {
pre_messages = nil,
messages = {
{.e},
{.e, .ee, .s, .es, .psk},
},
is_psk = true,
}
// XNpsk3:
// -> e
// <- e, ee
// -> s, se, psk
@(private,rodata)
PATTERN_XNpsk3 : Message_Pattern = {
pre_messages = nil,
messages = {
{.e},
{.e, .ee},
{.s, .se, .psk},
},
is_psk = true,
}
// XKpsk3:
// <- s
// ...
// -> e, es
// <- e, ee
// -> s, se, psk
@(private,rodata)
PATTERN_XKpsk3 : Message_Pattern = {
pre_messages = {.res_s},
messages = {
{.e, .es},
{.e, .ee},
{.s, .se, .psk},
},
is_psk = true,
}
// XXpsk3:
// -> e
// <- e, ee, s, es
// -> s, se, psk
@(private,rodata)
PATTERN_XXpsk3 : Message_Pattern = {
pre_messages = nil,
messages = {
{.e},
{.e, .ee, .s, .es},
{.s, .se, .psk},
},
is_psk = true,
}
// KNpsk0:
// -> s
// ...
// -> psk, e
// <- e, ee, se
@(private,rodata)
PATTERN_KNpsk0 : Message_Pattern = {
pre_messages = {.ini_s},
messages = {
{.psk, .e},
{.e, .ee, .se},
},
is_psk = true,
}
// KNpsk2:
// -> s
// ...
// -> e
// <- e, ee, se, psk
@(private,rodata)
PATTERN_KNpsk2 : Message_Pattern = {
pre_messages = {.ini_s},
messages = {
{.e},
{.e, .ee, .se, .psk},
},
is_psk = true,
}
// KKpsk0:
// -> s
// <- s
// ...
// -> psk, e, es, ss
// <- e, ee, se
@(private,rodata)
PATTERN_KKpsk0 : Message_Pattern = {
pre_messages = {.ini_s, .res_s},
messages = {
{.psk, .e, .es, .ss},
{.e, .ee, .se},
},
is_psk = true,
}
// KKpsk2:
// -> s
// <- s
// ...
// -> e, es, ss
// <- e, ee, se, psk
@(private,rodata)
PATTERN_KKpsk2 : Message_Pattern = {
pre_messages = {.ini_s, .res_s},
messages = {
{.e, .es, .ss},
{.e, .ee, .se, .psk},
},
is_psk = true,
}
// KXpsk2:
// -> s
// ...
// -> e
// <- e, ee, se, s, es, psk
@(private,rodata)
PATTERN_KXpsk2 : Message_Pattern = {
pre_messages = {.ini_s},
messages = {
{.e},
{.e, .ee, .se, .s, .es, .psk},
},
is_psk = true,
}
// INpsk1:
// -> e, s, psk
// <- e, ee, se
@(private,rodata)
PATTERN_INpsk1 : Message_Pattern = {
pre_messages = nil,
messages = {
{.e, .s, .psk},
{.e, .ee, .se},
},
is_psk = true,
}
// INpsk2:
// -> e, s
// <- e, ee, se, psk
@(private,rodata)
PATTERN_INpsk2 : Message_Pattern = {
pre_messages = nil,
messages = {
{.e, .s},
{.e, .ee, .se, .psk},
},
is_psk = true,
}
// IKpsk1:
// <- s
// ...
// -> e, es, s, ss, psk
// <- e, ee, se
@(private,rodata)
PATTERN_IKpsk1 : Message_Pattern = {
pre_messages = {.res_s},
messages = {
{.e, .es, .s, .ss, .psk},
{.e, .ee, .se},
},
is_psk = true,
}
// IKpsk2:
// <- s
// ...
// -> e, es, s, ss
// <- e, ee, se, psk
@(private,rodata)
PATTERN_IKpsk2 : Message_Pattern = {
pre_messages = {.res_s},
messages = {
{.e, .es, .s, .ss},
{.e, .ee, .se, .psk},
},
is_psk = true,
}
// IXpsk2:
// -> e, s
// <- e, ee, se, s, es, psk
@(private,rodata)
PATTERN_IXpsk2 : Message_Pattern = {
pre_messages = nil,
messages = {
{.e, .s},
{.e, .ee, .se, .s, .es, .psk},
},
is_psk = true,
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,305 @@
package noise
import "core:bytes"
import "core:crypto"
import "core:crypto/aead"
import "core:crypto/ecdh"
import "core:crypto/hash"
import "core:fmt"
import "core:log"
import "core:math/rand"
import "core:testing"
@(private = "file")
DH_CURVES :: []ecdh.Curve {
.X25519,
.X448,
}
@(private = "file")
CIPHERS :: []aead.Algorithm{
.AES_GCM_256,
.CHACHA20POLY1305,
}
@(private = "file")
HASHES :: []hash.Algorithm{
.SHA256,
.SHA512,
.BLAKE2S,
.BLAKE2B,
}
@(test)
test_supported_protocols :: proc(t: ^testing.T) {
if !crypto.HAS_RAND_BYTES {
log.info("rand_bytes not supported - skipping")
return
}
protocol: Test_Protocol
for pattern in Handshake_Pattern {
if pattern == .Invalid {
continue
}
protocol.handshake_pattern = pattern
for dh in DH_CURVES {
protocol.dh = dh
for cipher in CIPHERS {
protocol.cipher = cipher
for hash in HASHES {
protocol.hash = hash
if !testing.expectf(
t,
test_noise_one_protocol(t, &protocol, context.temp_allocator),
"Failed protocol: %v", protocol,
) {
testing.fail(t)
break
}
}
}
}
}
}
@(private = "file")
test_noise_one_protocol :: proc(t: ^testing.T, protocol: ^Test_Protocol, allocator := context.allocator) -> bool {
protocol_name := test_protocol_string(protocol, allocator)
defer delete(protocol_name, allocator)
log.debugf("crypto/noise: %s", protocol_name)
is_one_way := pattern_is_one_way(protocol.handshake_pattern)
initiator_s, responder_s: ecdh.Private_Key
ini_s, res_s: ^ecdh.Private_Key
ini_s_pub, res_s_pub: ^ecdh.Public_Key
pre, hs := pattern_requires_initiator_s(protocol.handshake_pattern)
if pre || hs {
if !testing.expect(t, ecdh.private_key_generate(&initiator_s, protocol.dh), "failed to generate initiator s") {
return false
}
ini_s = &initiator_s
if pre {
ini_s_pub = &initiator_s._pub_key
}
}
pre, hs = pattern_requires_responder_s(protocol.handshake_pattern)
if pre || hs {
if !testing.expect(t, ecdh.private_key_generate(&responder_s, protocol.dh), "failed to generate responder s") {
return false
}
res_s = &responder_s
if pre {
res_s_pub = &responder_s._pub_key
}
}
psk_buf: [32]byte = ---
psk: []byte
if pattern_is_psk(protocol.handshake_pattern) {
crypto.rand_bytes(psk_buf[:])
psk = psk_buf[:]
}
ini_hs, res_hs: Handshake_State
status := handshake_init(&ini_hs, true, nil, ini_s, res_s_pub, protocol_name, psk)
if !testing.expectf(t, status == .Ok, "failed to initialize initiator Handshake_State: %v", status) {
return false
}
status = handshake_init(&res_hs, false, nil, res_s, ini_s_pub, protocol_name, psk)
if !testing.expectf(t, status == .Ok, "failed to initialize responder Handshake_State: %v", status) {
return false
}
ini_status, res_status: Status
ini_msg, res_msg: []byte
ini_payload, res_payload: []byte
hs_msg_buf: [MAX_STEP_MSG_SIZE]byte
for i := 0; ; i += 1{
if ini_status == .Handshake_Complete && res_status == .Handshake_Complete {
break
}
// Test the allocation path
res_msg, res_payload, ini_status = handshake_initiator_step(&ini_hs, ini_msg, allocator = allocator)
ini_msg = nil
if ini_status == .Handshake_Complete && res_status == .Handshake_Complete {
break
}
if !testing.expectf(t, len(res_payload) == 0, "step %d: unexpected responder payload: %x", i, res_payload) {
return false
}
if !testing.expectf(t, ini_status == .Handshake_Pending || ini_status == .Handshake_Complete, "step %d: initiator step failed: %v", i, ini_status) {
return false
}
// Test the non-allocation path
ini_msg, ini_payload, res_status = handshake_responder_step(&res_hs, res_msg, nil, hs_msg_buf[:])
delete(res_msg, allocator)
res_msg = nil
if !testing.expectf(t, len(ini_payload) == 0, "step %d: unexpected initiator payload: %x", i, ini_payload) {
return false
}
if !testing.expectf(t, res_status == .Handshake_Pending || res_status == .Handshake_Complete, "step %d: responder step failed: %v", i, res_status) {
return false
}
}
delete(res_msg, allocator)
hs_pub: ^ecdh.Public_Key
if ini_s != nil {
hs_pub, status = handshake_peer_identity(&res_hs)
if !testing.expect(t, status == .Ok) {
return false
}
if !testing.expectf(t, ecdh.public_key_equal(&ini_s._pub_key, hs_pub), "responder has incorrect initiator identity") {
return false
}
}
if res_s != nil {
hs_pub, status = handshake_peer_identity(&ini_hs)
if !testing.expect(t, status == .Ok) {
return false
}
if !testing.expectf(t, ecdh.public_key_equal(&res_s._pub_key, hs_pub), "initiator has incorrect responder identity") {
return false
}
}
h1, h2: []byte
h1, status = handshake_hash(&ini_hs)
if !testing.expect(t, status == .Ok) {
return false
}
h2, status = handshake_hash(&res_hs)
if !testing.expect(t, status == .Ok) {
return false
}
if !testing.expectf(t, bytes.equal(h1, h2), "handshake hash mismatch: %x != %x", h1, h2) {
return false
}
ini_cs, res_cs: Cipher_States
if !testing.expectf(t, .Ok == handshake_split(&ini_hs, &ini_cs), "failed to split initiator: %v") {
return false
}
if !testing.expectf(t, .Ok == handshake_split(&res_hs, &res_cs), "failed to split responder: %v") {
return false
}
handshake_reset(&ini_hs)
handshake_reset(&res_hs)
if !testing.expect(t, test_messages(t, &ini_cs, &res_cs, is_one_way, allocator), "message tests failed") {
return false
}
cipherstates_reset(&ini_cs)
cipherstates_reset(&res_cs)
return true
}
@(private = "file")
test_messages :: proc(t: ^testing.T, ini_cs, res_cs: ^Cipher_States, is_one_way: bool, allocator := context.allocator) -> bool {
ad_buf: [256]byte = ---
payload_buf: [MAX_PACKET_SIZE-TAG_SIZE]byte = ---
for i in 0..<10 {
ad := ad_buf[:rand.int_max(len(ad_buf))]
payload := payload_buf[:rand.int_max(len(payload_buf))]
_ = rand.read(payload)
_ = rand.read(ad)
// Initiator -> Responder (allocate buffers)
tx_msg, status := seal_message(ini_cs, ad, payload, allocator = allocator)
defer delete(tx_msg, allocator)
if !testing.expectf(t, status == .Ok, "i->r %d: seal failed: %v", i, status) {
return false
}
rx_dst: []byte
rx_dst, status = open_message(res_cs, ad, tx_msg, allocator = allocator)
defer delete(rx_dst, allocator)
if !testing.expectf(t, status == .Ok, "i->r %d: open failed: %v", i, status) {
return false
}
if !testing.expectf(t, bytes.equal(rx_dst, payload), "i->r %d: payload mismatch") {
return false
}
if i == 5 {
status = cipherstates_rekey(ini_cs, true)
if !testing.expectf(t, status == .Ok, "i %d: rekey failed: %v", i, status) {
return false
}
status = cipherstates_rekey(res_cs, false)
if !testing.expectf(t, status == .Ok, "r %d: rekey failed: %v", i, status) {
return false
}
}
if is_one_way {
continue
}
// Responder -> Initiator (reuse allocated buffers)
tx_msg, status = seal_message(res_cs, ad, payload, tx_msg)
if !testing.expectf(t, status == .Ok, "r->i %d: seal failed: %v", i, status) {
return false
}
_, status = open_message(ini_cs, ad, tx_msg, rx_dst)
if !testing.expectf(t, status == .Ok, "r->i %d: open failed: %v", i, status) {
return false
}
if !testing.expectf(t, bytes.equal(rx_dst, payload), "r-i %d: payload mismatch") {
return false
}
}
return true
}
@(private = "file")
Test_Protocol :: struct {
handshake_pattern: Handshake_Pattern,
dh: ecdh.Curve,
cipher: aead.Algorithm,
hash: hash.Algorithm,
}
@(private = "file")
test_protocol_string :: proc(protocol: ^Test_Protocol, allocator := context.allocator) -> string {
dh: string
#partial switch protocol.dh {
case .X25519: dh = "25519"
case .X448: dh = "448"
case: panic("crypto/noise: unsupported DH")
}
cipher: string
#partial switch protocol.cipher {
case .AES_GCM_256: cipher = "AESGCM"
case .CHACHA20POLY1305: cipher = "ChaChaPoly"
case: panic("crypto/noise: unsupported cipher")
}
hash: string
#partial switch protocol.hash {
case .SHA256: hash = "SHA256"
case .SHA512: hash = "SHA512"
case .BLAKE2S: hash = "Blake2s"
case .BLAKE2B: hash = "Blake2b"
case: panic("crypto/noise: unsupported hash")
}
return fmt.aprintf("Noise_%v_%v_%v_%v", protocol.handshake_pattern, dh, cipher, hash, allocator = allocator)
}