[SEC] Add `keying` module
The keying modules tries to solve two problems, the lack of key
separation and the lack of AEAD being used for encryption. The currently
used `secrets` doesn't provide this and is hard to adjust to provide
this functionality.
For encryption, the additional data is now a parameter that can be used,
as the underlying primitive is an AEAD constructions. This allows for
context binding to happen and can be seen as defense-in-depth; it
ensures that if a value X is encrypted for context Y (e.g. ID=3,
Column="private_key") it will only decrypt if that context Y is also
given in the Decrypt function. This makes confused deputy attack harder
to exploit.[^1]
For key separation, HKDF is used to derives subkeys from some IKM, which
is the value of the `[service].SECRET_KEY` config setting. The context
for subkeys are hardcoded, any variable should be shuffled into the the
additional data parameter when encrypting.
[^1]: This is still possible, because the used AEAD construction is not
key-comitting. For Forgejo's current use-case this risk is negligible,
because the subkeys aren't known to a malicious user (which is required
for such attack), unless they also have access to the IKM (at which
point you can assume the whole system is compromised). See
https://scottarc.blog/2022/10/17/lucid-multi-key-deputies-require-commitment/
2024-08-20 23:13:04 +02:00
|
|
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
|
|
|
package keying_test
|
|
|
|
|
|
|
|
import (
|
2024-08-04 20:46:05 +02:00
|
|
|
"math"
|
[SEC] Add `keying` module
The keying modules tries to solve two problems, the lack of key
separation and the lack of AEAD being used for encryption. The currently
used `secrets` doesn't provide this and is hard to adjust to provide
this functionality.
For encryption, the additional data is now a parameter that can be used,
as the underlying primitive is an AEAD constructions. This allows for
context binding to happen and can be seen as defense-in-depth; it
ensures that if a value X is encrypted for context Y (e.g. ID=3,
Column="private_key") it will only decrypt if that context Y is also
given in the Decrypt function. This makes confused deputy attack harder
to exploit.[^1]
For key separation, HKDF is used to derives subkeys from some IKM, which
is the value of the `[service].SECRET_KEY` config setting. The context
for subkeys are hardcoded, any variable should be shuffled into the the
additional data parameter when encrypting.
[^1]: This is still possible, because the used AEAD construction is not
key-comitting. For Forgejo's current use-case this risk is negligible,
because the subkeys aren't known to a malicious user (which is required
for such attack), unless they also have access to the IKM (at which
point you can assume the whole system is compromised). See
https://scottarc.blog/2022/10/17/lucid-multi-key-deputies-require-commitment/
2024-08-20 23:13:04 +02:00
|
|
|
"testing"
|
|
|
|
|
|
|
|
"code.gitea.io/gitea/modules/keying"
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"golang.org/x/crypto/chacha20poly1305"
|
|
|
|
)
|
|
|
|
|
|
|
|
func TestKeying(t *testing.T) {
|
|
|
|
t.Run("Not initalized", func(t *testing.T) {
|
|
|
|
assert.Panics(t, func() {
|
|
|
|
keying.DeriveKey(keying.Context("TESTING"))
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("Initialization", func(t *testing.T) {
|
|
|
|
keying.Init([]byte{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07})
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("Context seperation", func(t *testing.T) {
|
|
|
|
key1 := keying.DeriveKey(keying.Context("TESTING"))
|
|
|
|
key2 := keying.DeriveKey(keying.Context("TESTING2"))
|
|
|
|
|
|
|
|
ciphertext := key1.Encrypt([]byte("This is for context TESTING"), nil)
|
|
|
|
|
|
|
|
plaintext, err := key2.Decrypt(ciphertext, nil)
|
|
|
|
require.Error(t, err)
|
|
|
|
assert.Empty(t, plaintext)
|
|
|
|
|
|
|
|
plaintext, err = key1.Decrypt(ciphertext, nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.EqualValues(t, "This is for context TESTING", plaintext)
|
|
|
|
})
|
|
|
|
|
|
|
|
context := keying.Context("TESTING PURPOSES")
|
|
|
|
plainText := []byte("Forgejo is run by [Redacted]")
|
|
|
|
var cipherText []byte
|
|
|
|
t.Run("Encrypt", func(t *testing.T) {
|
|
|
|
key := keying.DeriveKey(context)
|
|
|
|
|
|
|
|
cipherText = key.Encrypt(plainText, []byte{0x05, 0x06})
|
|
|
|
cipherText2 := key.Encrypt(plainText, []byte{0x05, 0x06})
|
|
|
|
|
|
|
|
// Ensure ciphertexts don't have an determistic output.
|
|
|
|
assert.NotEqualValues(t, cipherText, cipherText2)
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("Decrypt", func(t *testing.T) {
|
|
|
|
key := keying.DeriveKey(context)
|
|
|
|
|
|
|
|
t.Run("Succesful", func(t *testing.T) {
|
|
|
|
convertedPlainText, err := key.Decrypt(cipherText, []byte{0x05, 0x06})
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.EqualValues(t, plainText, convertedPlainText)
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("Not enougn additional data", func(t *testing.T) {
|
|
|
|
plainText, err := key.Decrypt(cipherText, []byte{0x05})
|
|
|
|
require.Error(t, err)
|
|
|
|
assert.Empty(t, plainText)
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("Too much additional data", func(t *testing.T) {
|
|
|
|
plainText, err := key.Decrypt(cipherText, []byte{0x05, 0x06, 0x07})
|
|
|
|
require.Error(t, err)
|
|
|
|
assert.Empty(t, plainText)
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("Incorrect nonce", func(t *testing.T) {
|
|
|
|
// Flip the first byte of the nonce.
|
|
|
|
cipherText[0] = ^cipherText[0]
|
|
|
|
|
|
|
|
plainText, err := key.Decrypt(cipherText, []byte{0x05, 0x06})
|
|
|
|
require.Error(t, err)
|
|
|
|
assert.Empty(t, plainText)
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("Incorrect ciphertext", func(t *testing.T) {
|
|
|
|
assert.Panics(t, func() {
|
|
|
|
key.Decrypt(nil, nil)
|
|
|
|
})
|
|
|
|
|
|
|
|
assert.Panics(t, func() {
|
|
|
|
cipherText := make([]byte, chacha20poly1305.NonceSizeX)
|
|
|
|
key.Decrypt(cipherText, nil)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
2024-08-04 20:46:05 +02:00
|
|
|
|
|
|
|
func TestKeyingColumnAndID(t *testing.T) {
|
|
|
|
assert.EqualValues(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, keying.ColumnAndID("table", math.MinInt64))
|
|
|
|
assert.EqualValues(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, keying.ColumnAndID("table", -1))
|
|
|
|
assert.EqualValues(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, keying.ColumnAndID("table", 0))
|
|
|
|
assert.EqualValues(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, keying.ColumnAndID("table", 1))
|
|
|
|
assert.EqualValues(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x3a, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, keying.ColumnAndID("table", math.MaxInt64))
|
|
|
|
|
|
|
|
assert.EqualValues(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, keying.ColumnAndID("table2", math.MinInt64))
|
|
|
|
assert.EqualValues(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, keying.ColumnAndID("table2", -1))
|
|
|
|
assert.EqualValues(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, keying.ColumnAndID("table2", 0))
|
|
|
|
assert.EqualValues(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, keying.ColumnAndID("table2", 1))
|
|
|
|
assert.EqualValues(t, []byte{0x74, 0x61, 0x62, 0x6c, 0x65, 0x32, 0x3a, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, keying.ColumnAndID("table2", math.MaxInt64))
|
|
|
|
}
|