How to: Generate Nonces

For the security of the AEAD algorithms in NSec, it is critical that nonces are constructed in a way that the same nonce is never used more than once to encrypt data with the same key.

Security protocols generally need nonces generated in a specific way, which is why NSec doesn’t provide a general, easy-to-use solution. This “how to” shows how to implement two exemplary solutions with NSec:

See RFC 5116 for recommendations and more information on generating nonces.

TLS 1.2-style Nonces

RFC 5288 recommends the following way to generate nonces for use with AES-GCM in Transport Layer Security (TLS). This style is the default in TLS version 1.2.

TLS provides a secure channel between two a client and a server. Two keys are used per channel: one for sending messages from the client to the server, and one for sending messages from the server to the client. These are set up by the TLS handshake, which also outputs two Initialization Vectors (IV), one for each direction.

The 12-byte nonce required by AES-GCM is formed as follows:

  1. The 64-bit record sequence number is serialized as an 8-byte, big-endian value.

  2. The serialized sequence number is appended to the 4-byte IV.

The following C# example shows how to implement this with NSec:

public class Rfc5288
{
    private readonly AeadAlgorithm _algorithm;

    private readonly Key _sendKey;
    private Nonce _sendNonce;

    private readonly Key _receiveKey;
    private Nonce _receiveNonce;

    public Rfc5288(
        Role role,
        AeadAlgorithm algorithm,
        Key clientWriteKey,
        ReadOnlySpan<byte> clientWriteIV,
        Key serverWriteKey,
        ReadOnlySpan<byte> serverWriteIV)
    {
        Debug.Assert(algorithm.NonceSize == 12);
        Debug.Assert(clientWriteIV.Length == 4);
        Debug.Assert(serverWriteIV.Length == 4);

        _algorithm = algorithm;

        switch (role)
        {
        // if this is the server side, use serverWriteKey and
        // serverWriteIV for sending, and clientWriteKey and
        // clientWriteIV for receiving
        case Role.Server:
            _sendKey = serverWriteKey;
            _sendNonce = new Nonce(fixedField: serverWriteIV,
                                   counterFieldSize: 8);

            _receiveKey = clientWriteKey;
            _receiveNonce = new Nonce(fixedField: clientWriteIV,
                                      counterFieldSize: 8);
            break;

        // if this is the client side, use clientWriteKey and
        // clientWriteIV for sending, and serverWriteKey and
        // serverWriteIV for receiving
        case Role.Client:
            _sendKey = clientWriteKey;
            _sendNonce = new Nonce(fixedField: clientWriteIV,
                                   counterFieldSize: 8);

            _receiveKey = serverWriteKey;
            _receiveNonce = new Nonce(fixedField: serverWriteIV,
                                      counterFieldSize: 8);
            break;
        }
    }

    public void EncryptBeforeSend(
        ReadOnlySpan<byte> associatedData,
        ReadOnlySpan<byte> plaintext,
        Span<byte> ciphertext)
    {
        // encrypt the plaintext with the send nonce
        _algorithm.Encrypt(
            _sendKey,
            _sendNonce,
            associatedData,
            plaintext,
            ciphertext);

        // increment the counter field of the send nonce
        if (!Nonce.TryIncrement(ref _sendNonce))
        {
            // abort the connection when the counter field of the
            // send nonce reaches the maximum value
            _sendKey.Dispose();
            _receiveKey.Dispose();
        }
    }

    public bool DecryptAfterReceive(
        ReadOnlySpan<byte> associatedData,
        ReadOnlySpan<byte> ciphertext,
        Span<byte> plaintext)
    {
        // decrypt the ciphertext with the receive nonce
        if (!_algorithm.Decrypt(
            _receiveKey,
            _receiveNonce,
            associatedData,
            ciphertext,
            plaintext))
        {
            // abort the connection if decryption fails
            _sendKey.Dispose();
            _receiveKey.Dispose();
            return false;
        }

        // increment the counter field of the receive nonce
        if (!Nonce.TryIncrement(ref _receiveNonce))
        {
            // abort the connection when the counter field of the
            // receive nonce reaches the maximum value
            _sendKey.Dispose();
            _receiveKey.Dispose();
        }

        return true;
    }
}

TLS 1.3-style Nonces

RFC 7905 recommends the following way to generate nonces for use with ChaCha20-Poly1305 in Transport Layer Security (TLS). This style is the default in TLS version 1.3.

As noted above, TLS provides a secure channel between two a client and a server. Two keys are used per channel: one for sending messages from the client to the server, and one for sending messages from the server to the client. These are set up by the TLS handshake, which also outputs two Initialization Vectors (IV), one for each direction.

The 12-byte nonce required by ChaCha20-Poly1305 is formed as follows:

  1. The 64-bit record sequence number is serialized as an 8-byte, big-endian value and padded on the left with four 0x00 bytes.

  2. The padded sequence number is XORed with the 12-byte IV.

The following C# example shows how to implement this with NSec:

public class Rfc7905
{
    private readonly AeadAlgorithm _algorithm;

    private readonly Key _sendKey;
    private readonly Nonce _sendIV;
    private Nonce _sendSequenceNumber;

    private readonly Key _receiveKey;
    private readonly Nonce _receiveIV;
    private Nonce _receiveSequenceNumber;

    public Rfc7905(
        Role role,
        AeadAlgorithm algorithm,
        Key clientWriteKey,
        ReadOnlySpan<byte> clientWriteIV,
        Key serverWriteKey,
        ReadOnlySpan<byte> serverWriteIV)
    {
        Debug.Assert(algorithm.NonceSize == 12);
        Debug.Assert(clientWriteIV.Length == 12);
        Debug.Assert(serverWriteIV.Length == 12);

        _algorithm = algorithm;

        switch (role)
        {
        // if this is the server side, use serverWriteKey and
        // serverWriteIV for sending, and clientWriteKey and
        // clientWriteIV for receiving
        case Role.Server:
            _sendKey = serverWriteKey;
            _sendIV = new Nonce(fixedField: serverWriteIV,
                                counterFieldSize: 0);
            _sendSequenceNumber = new Nonce(fixedFieldSize: 4,
                                            counterFieldSize: 8);

            _receiveKey = clientWriteKey;
            _receiveIV = new Nonce(fixedField: clientWriteIV,
                                   counterFieldSize: 0);
            _receiveSequenceNumber = new Nonce(fixedFieldSize: 4,
                                               counterFieldSize: 8);
            break;

        // if this is the client side, use clientWriteKey and
        // clientWriteIV for sending, and serverWriteKey and
        // serverWriteIV for receiving
        case Role.Client:
            _sendKey = clientWriteKey;
            _sendIV = new Nonce(fixedField: clientWriteIV,
                                counterFieldSize: 0);
            _sendSequenceNumber = new Nonce(fixedFieldSize: 4,
                                            counterFieldSize: 8);

            _receiveKey = serverWriteKey;
            _receiveIV = new Nonce(fixedField: serverWriteIV,
                                   counterFieldSize: 0);
            _receiveSequenceNumber = new Nonce(fixedFieldSize: 4,
                                               counterFieldSize: 8);
            break;
        }
    }

    public void EncryptBeforeSend(
        ReadOnlySpan<byte> associatedData,
        ReadOnlySpan<byte> plaintext,
        Span<byte> ciphertext)
    {
        // encrypt the plaintext with the send sequence number XORed
        // with the send IV as the nonce
        _algorithm.Encrypt(
            _sendKey,
            _sendSequenceNumber ^ _sendIV,
            associatedData,
            plaintext,
            ciphertext);

        // increment the send sequence number
        if (!Nonce.TryIncrement(ref _sendSequenceNumber))
        {
            // abort the connection when the send sequence number
            // reaches the maximum value
            _sendKey.Dispose();
            _receiveKey.Dispose();
        }
    }

    public bool DecryptAfterReceive(
        ReadOnlySpan<byte> associatedData,
        ReadOnlySpan<byte> ciphertext,
        Span<byte> plaintext)
    {
        // decrypt the ciphertext with the receive sequence number
        // XORed with the receive IV as the nonce
        if (!_algorithm.Decrypt(
            _receiveKey,
            _receiveSequenceNumber ^ _receiveIV,
            associatedData,
            ciphertext,
            plaintext))
        {
            // abort the connection if decryption fails
            _sendKey.Dispose();
            _receiveKey.Dispose();
            return false;
        }

        // increment the receive sequence number
        if (!Nonce.TryIncrement(ref _receiveSequenceNumber))
        {
            // abort the connection when the receive sequence number
            // reaches the maximum value
            _sendKey.Dispose();
            _receiveKey.Dispose();
        }

        return true;
    }
}

References