protocol

package
v2.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 20, 2026 License: MIT Imports: 7 Imported by: 0

README

Protocol

goe2ee protocol aims to give a similar security level to TLS 1.3, using Diffie–Hellman key exchange, with some flexibility regarding sharing secrets between multiple connections from the same host. The key sharing should save handshakes, improving the initial performance.

For example, the full handshake can be described as:

sequenceDiagram
activate client
client->>server: sends auto-generate ECDH public-key + random id
activate server
server->>server: calculate shared secret and stores with id
server-->>client: sends auto-generate ECDH public-key signed
deactivate server
client->>client: validate signature
client->>client: calculate shared secret
loop every message
  client->>server: id + encypted message with shared secret
  activate server
  server-->>client: encrypted message with shared secret
  deactivate server
end
deactivate client

While other clients from the same host could share the same secret and jump directly to the encrypted message phase:

sequenceDiagram
loop every message
  client->>server: id + encypted message with shared secret
  activate server
  server-->>client: encrypted message with shared secret
  deactivate server
end

The protocol was also designed to work with both TCP and UDP protocols. For some application scenarios where the data isn't big and a delivery guarantee is not a requirement, the use of UDP could bring great performance benefits.

Finally, the protocol allows the client to retrieve the server public key using many different strategies, throwing light on certification authority alternatives, such as the DNSSEC chain of trust.

Structure

All requests follow the pattern:

+------------------|-----------------|-------------------+
| version (4 bits) | action (4 bits) | message (n bytes) |
+------------------|-----------------|-------------------+

Where:

  • version defines the protocol version. This is designed to allow up to 16 versions of the protocol;
  • action is an enumerate that tells the server what this request is about (4 bits will allow up to 16 actions);
  • message changes according to the action, it's optional.

The following actions are accepted:

Action Description
0x1 Hello
0x2 Setup
0x3 Process
0x4 Fetch key

Having a version will allow evolving of the protocol as new approaches are identified. This fixed header allows skipping the handshake stages if the shared secret was already agreed upon previously because the client doesn't need to follow a specific order of actions with the server. On the other hand, the server needs to keep a state of the shared secrets associated with IDs even after the client connection is closed. The technical approach for handling this cache can be decided by the server implementation.

Once the request is processed correctly, all server responses will follow the pattern:

+-----------------+-------------------+-------------------+
| success (1 bit) | reserved (7 bits) | message (n bytes) |
+-----------------+-------------------+-------------------+

Where:

  • success is an enabled flag (1) to indicate success;
  • reserved these flags are reserved for future use;
  • message changes according to the request action, it's optional.

Otherwise, if the server rejected the request for any reason, the response will follow the pattern below. Any request can return a failed response.

+-----------------+-------------------+----------------------+------------------------------+-------------------------+
| success (1 bit) | reserved (7 bits) | error-code (4 bytes) | error-message-size (8 bytes) | error-message (n bytes) |
+-----------------+-------------------+----------------------+------------------------------+-------------------------+

Where:

  • success is a disabled flag (0) to indicate failure;
  • error-code identifies the type of error that happened;
  • error-message-size defines the error message size. The maximum message size is an unsigned 64-bit number;
  • error-message will contain details of the error, it's optional.

The following error codes are accepted:

Error Code Description
0x01 Malformed request
0x02 Server error
0x03 Unknown client
0x04 Unsupported version
0x05 Replay detected

The structure of each action can be found in the following sections.

Hello (action 0x1)

Used to check if the connection with the server is still valid. It's the most basic mechanism of the protocol.

Request:

+------|------+
| 0001 | 0001 |
+------+------+

Response:

+---+-------------------+
| 1 | reserved (7 bits) |
+---+-------------------+
Setup (action 0x2)

This is the main action of the protocol, where the client and server exchange public keys so a new shared secret can be generated. The use of the Diffie–Hellman key exchange guarantees that the shared secret will never be carried over the network, increasing security.

At this stage, the client also provides an identifier to be associated with the shared secret. This identifier will be used on the server side to correctly select the client-shared secret to decrypt the message. It MUST be unique per host.

Request:

+------+------+---------------+---------------------------+----------------------+
| 0001 | 0010 | id (16 bytes) | public-key-size (4 bytes) | public-key (n bytes) |
+------+------+---------------+---------------------------+----------------------+

Where:

  • id is a client-unique identifier for the secret following RFC 4122. A client could have multiple connections with the same server, each one with a different secret, so an identifier is required to uniquely identify each client instance. This should also cover scenarios where there're network boxes between the client and server, such as proxies.
  • public-key is encoded in PKIX, ASN.1 DER form. The encoded public key is a SubjectPublicKeyInfo structure (see RFC 5280, Section 4.1). This is the ECDH key generated by the client on-the-fly.

Response:

+---+-------------------+---------------------------+----------------------+--------------------+--------------------------+---------------------+
| 1 | reserved (7 bits) | public-key-size (4 bytes) | public-key (n bytes) | hash-type (1 byte) | signature-size (8 bytes) | signature (n bytes) |
+---+-------------------+---------------------------+----------------------+--------------------+--------------------------+---------------------+
  • public-key is encoded in PKIX, ASN.1 DER form. The encoded public key is a SubjectPublicKeyInfo structure (see RFC 5280, Section 4.1). This is the ECDH key generated by the server on-the-fly.
  • hash-type is the hash type used when generating the signature.
  • signature is the message signed with the server's global-key. The client should retrieve the server's public key somehow before the handshake.

The following hash types are accepted (SHA-1 is intentionally not supported as it is cryptographically broken):

Type Description
0x2 SHA-256
0x3 SHA-384
0x4 SHA-512

The signature covers the full handshake transcript rather than only the server's public key. This binds the signature to both parties and to the session id, so a valid signature cannot be transplanted onto a different key exchange. The transcript is built as:

+---------------------------+----------------------+---------------------------+----------------------+---------------+
| client-key-size (4 bytes) | client-key (n bytes) | server-key-size (4 bytes) | server-key (n bytes) | id (16 bytes) |
+---------------------------+----------------------+---------------------------+----------------------+---------------+

The signature algorithm follows the steps below.

  1. The server builds the transcript above from the client's public key (received in the setup request), its own freshly generated public key, and the id;
  2. Using the server's private global-key, it hashes and signs the transcript;
  3. The server attaches the hash type and the signature to the response;
  4. The client rebuilds the same transcript and validates the signature using the server's public global-key.
Process (action 0x3)

After all the handshake phase is done, is time to exchange encrypted messages using the shared secret. The client encrypts the message with the shared secret and sends the related identifier with the encrypted content. The response also contains the message encrypted with the same shared secret from the request.

Each message carries a counter. The AEAD nonce is derived deterministically from a direction byte (client→server or server→client) and this counter, so it is never transmitted and never reused under the same secret — a requirement for the security of AES-GCM. The counter is a per-secret value that must strictly increase for every message the client sends; when the shared secret is reused across several connections (see the client pool) the counter is shared and incremented atomically across all of them. The server keeps a sliding replay window per secret and rejects any counter it has already seen or that falls before the window (error code 0x05). The server echoes the request counter in its response and derives the reply nonce from it with the opposite direction.

Request:

+------+------+---------------------+-------------------------+---------------+--------------------+----------------------------------+-----------------------------+
| 0001 | 0011 | expectReply (1 bit) | reserved-flags (7 bits) | id (16 bytes) | counter (8 bytes)  | encrypted-message-size (8 bytes) | encrypted-message (n bytes) |
+------+------+---------------------+-------------------------+---------------+--------------------+----------------------------------+-----------------------------+

Where:

  • expectedReply allows the client to inform the server that it doesn't expect a response. A typical fire-and-forget request.
  • reserved-flags are flags reserved for future use.
  • id identifies the client when sending encrypted messages. The identifier is generated by the client during the setup process and can be shared for requests from the same host.
  • counter is the per-secret, strictly increasing message counter used to derive the nonce and to reject replays.
  • encrypted-message is the payload encrypted with the shared secret associated with the provided identifier.

Response:

+---+-------------------+--------------------+----------------------------------+-----------------------------+
| 1 | reserved (7 bits) | counter (8 bytes)  | encrypted-message-size (8 bytes) | encrypted-message (n bytes) |
+---+-------------------+--------------------+----------------------------------+-----------------------------+

Where:

  • counter is the counter from the request being answered, used to derive the response nonce.
Fetch key (action 0x4)

This is an alternative approach for the client to retrieve the server's public global-key. It's not recommended as it can be vulnerable to man-in-the-middle attacks.

Request:

+------+------+
| 0001 | 0100 |
+------+------+

Response:

+---+-------------------+------------------------+---------------------------+----------------------+
| 1 | reserved (7 bits) | key-algorithm (1 byte) | public-key-size (4 bytes) | public-key (n bytes) |
+---+-------------------+------------------------+---------------------------+----------------------+

Where:

  • key-algorithm stores the format for parsing the public key.
  • public-key is the public global-key of the server.

The following algorithms are accepted:

Algorithm Description
0x1 RSA
0x2 Ecdsa
0x3 Ed25519

Documentation

Overview

Package protocol provides the protocol used by the server and the client.

Index

Constants

View Source
const (
	// MaxKeySize is the maximum size in bytes accepted for a marshalled public
	// key field.
	MaxKeySize = 1 << 16 // 64 KiB

	// MaxSignatureSize is the maximum size in bytes accepted for a signature
	// field.
	MaxSignatureSize = 1 << 16 // 64 KiB

	// MaxMessageSize is the maximum size in bytes accepted for an encrypted
	// message payload.
	MaxMessageSize = 1 << 30 // 1 GiB
)

Maximum sizes accepted when parsing length-prefixed fields received from the network. Without these bounds a peer could advertise an arbitrarily large size and force the parser to allocate that much memory (or panic), which is a trivial denial-of-service vector. The limits are generous relative to any legitimate value while still preventing pathological allocations.

View Source
const (
	DirectionClientToServer byte = 0x00
	DirectionServerToClient byte = 0x01
)

Direction bytes distinguish the two halves of a conversation so that the client and the server never derive the same AEAD nonce from the same counter under the shared secret.

Variables

This section is empty.

Functions

func Nonce

func Nonce(direction byte, counter uint64, size int) []byte

Nonce builds a deterministic AEAD nonce of the given size from a direction byte and a message counter. Using a direction byte plus a per-secret monotonic counter guarantees the nonce is never reused under the same key, which is required for the security of AES-GCM. The size must be at least 9 bytes (one direction byte plus an 8-byte counter); GCM uses 12.

func SetupTranscript

func SetupTranscript(clientPublicKey, serverPublicKey *ecdh.PublicKey, id [16]byte) ([]byte, error)

SetupTranscript builds the byte string that the server signs and the client verifies during the setup handshake. Binding both public keys and the session id (rather than only the server's ephemeral key) prevents an attacker from transplanting a valid server signature onto a different key exchange.

func WithProcessRequestExpectReply

func WithProcessRequestExpectReply(expectReply bool) func(*ProcessRequestOptions)

WithProcessRequestExpectReply indicates if the client is expecting a reply from the server. This is useful to avoid the client to wait for a reply when the server doesn't need to send one. By default is true.

Types

type Action

type Action uint8

Action is the type of action to be performed by the client/server.

const (
	ActionHello    Action = 0x1
	ActionSetup    Action = 0x2
	ActionProcess  Action = 0x3
	ActionFetchKey Action = 0x4
)

List of possible actions.

func (Action) String

func (a Action) String() string

String returns the string representation of the action.

type ErrorCode

type ErrorCode uint32

ErrorCode is the error code sent by the server.

const (
	ErrorCodeMalformedRequest   ErrorCode = 0x01
	ErrorCodeServerError        ErrorCode = 0x02
	ErrorCodeUnknownClient      ErrorCode = 0x03
	ErrorCodeUnsupportedVersion ErrorCode = 0x04
	ErrorCodeReplayDetected     ErrorCode = 0x05
)

List of possible error codes.

type ErrorResponse

type ErrorResponse struct {
	ResponseCommon
	// contains filtered or unexported fields
}

ErrorResponse is the response sent by the server to the client when an error occurs.

func NewErrorResponse

func NewErrorResponse(errorCode ErrorCode, errorMessage string) ErrorResponse

NewErrorResponse creates a new ErrorResponse.

func ParseErrorResponse

func ParseErrorResponse(r io.Reader, responseCommon ResponseCommon) (ErrorResponse, error)

ParseErrorResponse parses an ErrorResponse received from the server.

func (ErrorResponse) Bytes

func (e ErrorResponse) Bytes() []byte

Bytes returns the byte representation of the response.

func (ErrorResponse) ErrorCode

func (e ErrorResponse) ErrorCode() ErrorCode

ErrorCode returns the error code if the action failed.

func (ErrorResponse) ErrorMessage

func (e ErrorResponse) ErrorMessage() string

ErrorMessage returns the error message if the action failed.

type FetchKeyRequest

type FetchKeyRequest struct {
	RequestCommon
}

FetchKeyRequest is the request sent by the client to the server to retrieve the public key.

func NewFetchKeyRequest

func NewFetchKeyRequest() FetchKeyRequest

NewFetchKeyRequest creates a new FetchKeyRequest.

type FetchKeyResponse

type FetchKeyResponse struct {
	ResponseCommon
	// contains filtered or unexported fields
}

FetchKeyResponse is the response sent by the server to the client containing the public key.

func NewFetchKeyResponse

func NewFetchKeyResponse(keyAlgorithm KeyAlgorithm, publicKey []byte) FetchKeyResponse

NewFetchKeyResponse creates a new NewFetchKeyResponse.

func ParseFetchKeyResponse

func ParseFetchKeyResponse(r io.Reader, responseCommon ResponseCommon) (FetchKeyResponse, error)

ParseFetchKeyResponse parses a FetchKeyRespons received from a server.

func (FetchKeyResponse) Bytes

func (g FetchKeyResponse) Bytes() []byte

Bytes returns the byte representation of the FetchKeyResponse.

func (FetchKeyResponse) KeyAlgorithm

func (g FetchKeyResponse) KeyAlgorithm() KeyAlgorithm

KeyAlgorithm returns the algorithm used to generate the key-pair.

func (FetchKeyResponse) PublicKey

func (g FetchKeyResponse) PublicKey() []byte

PublicKey returns the public key.

type HashType

type HashType uint8

HashType is the hash type used for verifying the signature.

const (
	HashTypeSHA256 HashType = 0x2
	HashTypeSHA384 HashType = 0x3
	HashTypeSHA512 HashType = 0x4
)

Supported hash types. SHA-1 is intentionally not supported: it is cryptographically broken and must not be used for signatures.

func NewHashType

func NewHashType(h crypto.Hash) HashType

NewHashType creates a new HashType from the crypto.Hash.

func (HashType) CryptoHash

func (h HashType) CryptoHash() crypto.Hash

CryptoHash returns the crypto.Hash corresponding to the hash type.

func (HashType) String

func (h HashType) String() string

String returns the string representation of the hash type.

type HelloRequest

type HelloRequest struct {
	RequestCommon
}

HelloRequest is the request sent by the client to the server to keep the connection alive.

func NewHelloRequest

func NewHelloRequest() HelloRequest

NewHelloRequest creates a new HelloRequest.

type HelloResponse

type HelloResponse struct {
	ResponseCommon
}

HelloResponse is the response sent by the server to the client acknowledging that everything is fine.

func NewHelloResponse

func NewHelloResponse() HelloResponse

NewHelloResponse creates a new NewHelloResponse.

type KeyAlgorithm

type KeyAlgorithm uint8

KeyAlgorithm is the algorithm used to generate the key-pair.

const (
	KeyAlgorithmRSA     KeyAlgorithm = 0x1
	KeyAlgorithmECDSA   KeyAlgorithm = 0x2
	KeyAlgorithmED25519 KeyAlgorithm = 0x3
)

Supported key algorithms.

func (KeyAlgorithm) String

func (k KeyAlgorithm) String() string

String returns the string representation of the key algorithm.

type ProcessRequest

type ProcessRequest struct {
	RequestCommon
	// contains filtered or unexported fields
}

ProcessRequest is the request sent by the client to the server with a generic encrypted message.

func NewProcessRequest

func NewProcessRequest(
	id [16]byte,
	counter uint64,
	message []byte,
	optFuncs ...func(*ProcessRequestOptions),
) ProcessRequest

NewProcessRequest creates a new ProcessRequest. The counter must be unique and increasing for every message encrypted under the same shared secret; it is used to build the AEAD nonce and to reject replays.

func ParseProcessRequest

func ParseProcessRequest(requestCommon RequestCommon, r io.Reader) (ProcessRequest, error)

ParseProcessRequest parses a ProcessRequest received from a client. The request common part must have already been read. This could return an io.EOF error if the response is not complete.

func (ProcessRequest) Bytes

func (g ProcessRequest) Bytes() []byte

Bytes returns the byte representation of the ProcessRequest.

func (ProcessRequest) Counter

func (g ProcessRequest) Counter() uint64

Counter returns the message counter used to derive the AEAD nonce and to reject replays.

func (ProcessRequest) ExpectReply

func (g ProcessRequest) ExpectReply() bool

ExpectReply returns true if the client is expecting a reply from the server.

func (ProcessRequest) ID

func (g ProcessRequest) ID() [16]byte

ID returns the id of the shared secret.

func (ProcessRequest) Message

func (g ProcessRequest) Message() []byte

Message returns the encrypted message.

type ProcessRequestOptions

type ProcessRequestOptions struct {
	// contains filtered or unexported fields
}

ProcessRequestOptions contains the options for a ProcessRequest.

type ProcessResponse

type ProcessResponse struct {
	ResponseCommon
	// contains filtered or unexported fields
}

ProcessResponse is the response sent by the server to the client after generating a shared message.

func NewProcessResponse

func NewProcessResponse(counter uint64, message []byte) ProcessResponse

NewProcessResponse creates a new NewProcessResponse. The counter is echoed from the request it answers and is used to build the AEAD nonce.

func ParseProcessResponse

func ParseProcessResponse(r io.Reader, responseCommon ResponseCommon) (ProcessResponse, error)

ParseProcessResponse parses a ProcessRespons received from a server. This could return an io.EOF error if the response is not complete.

func (ProcessResponse) Bytes

func (g ProcessResponse) Bytes() []byte

Bytes returns the byte representation of the ProcessResponse.

func (ProcessResponse) Counter

func (g ProcessResponse) Counter() uint64

Counter returns the message counter used to derive the AEAD nonce.

func (ProcessResponse) Message

func (g ProcessResponse) Message() []byte

Message returns the encrypted message.

type RequestCommon

type RequestCommon struct {
	// contains filtered or unexported fields
}

RequestCommon is the common fields for all requests.

func ParseRequestCommon

func ParseRequestCommon(r io.Reader) (RequestCommon, error)

ParseRequestCommon parses the common fields of a request. It could return io.EOF if the client closes the connection.

func (RequestCommon) Action

func (r RequestCommon) Action() Action

Action returns the action to be performed.

func (RequestCommon) Bytes

func (r RequestCommon) Bytes() []byte

Bytes returns the byte representation of the request.

func (RequestCommon) Version

func (r RequestCommon) Version() Version

Version returns the protocol version.

type ResponseCommon

type ResponseCommon struct {
	// contains filtered or unexported fields
}

ResponseCommon is the common fields for all responses.

func ParseResponseCommon

func ParseResponseCommon(r io.Reader) (ResponseCommon, error)

ParseResponseCommon parses the common fields of a response. It could return io.EOF if the server closes the connection.

func (ResponseCommon) Bytes

func (r ResponseCommon) Bytes() []byte

Bytes returns the byte representation of the response.

func (ResponseCommon) Success

func (r ResponseCommon) Success() bool

Success returns true if the action was successful.

type SetupRequest

type SetupRequest struct {
	RequestCommon
	// contains filtered or unexported fields
}

SetupRequest is the request sent by the client to the server to register a shared secret.

func NewSetupRequest

func NewSetupRequest(id [16]byte, publicKey *ecdh.PublicKey) SetupRequest

NewSetupRequest creates a new SetupRequest.

func ParseSetupRequest

func ParseSetupRequest(requestCommon RequestCommon, r io.Reader) (SetupRequest, error)

ParseSetupRequest parses a SetupRequest received from a client. The request common part must have already been read.

func (SetupRequest) Bytes

func (s SetupRequest) Bytes() []byte

Bytes returns the byte representation of the SetupRequest.

func (SetupRequest) ID

func (s SetupRequest) ID() [16]byte

ID returns the id of the shared secret.

func (SetupRequest) PublicKey

func (s SetupRequest) PublicKey() *ecdh.PublicKey

PublicKey returns the public key for the Diffie-Hellman exchange.

type SetupResponse

type SetupResponse struct {
	ResponseCommon
	// contains filtered or unexported fields
}

SetupResponse is the response sent by the server to the client after generating a shared secret.

func NewSetupResponse

func NewSetupResponse(publicKey *ecdh.PublicKey) SetupResponse

NewSetupResponse creates a new SetupResponse.

func ParseSetupResponse

func ParseSetupResponse(responseCommon ResponseCommon, r io.Reader) (SetupResponse, error)

ParseSetupResponse parses a SetupResponse received from the server. The response common part must have already been read.

func (SetupResponse) Bytes

func (s SetupResponse) Bytes() []byte

Bytes returns the byte representation of the SetupResponse.

func (SetupResponse) HashType

func (s SetupResponse) HashType() HashType

HashType returns the hash type used to sign the response.

func (SetupResponse) PublicKey

func (s SetupResponse) PublicKey() *ecdh.PublicKey

PublicKey returns the public key for the Diffie-Hellman exchange.

func (*SetupResponse) SetSignature

func (s *SetupResponse) SetSignature(hashType HashType, signature []byte)

SetSignature sets the signature of the response.

func (SetupResponse) Signature

func (s SetupResponse) Signature() []byte

Signature prooves that the message was sent by the expected server, allowing to validate the response using the server's public key.

type Version

type Version uint8

Version is the protocol version.

const (
	Version1 Version = 1
)

List of supported versions.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL