Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

transitapi: add http handler enc/dec #1199

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions coordinator/internal/authority/authority.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,27 @@ func (m *Authority) walkTransitions(transitionRef [history.HashSize]byte, consum
return nil
}

// getState syncs the current state and returns the loaded current state.
func (m *Authority) getState() (state *State, err error) {
if err = m.syncState(); err != nil {
return nil, fmt.Errorf("syncing state: %w", err)
}
state = m.state.Load()
if state == nil {
return nil, errors.New("coordinator is not initialized")
}
return state, nil
}

// GetSeedEngine returns the seedengine of the current state.
func (m *Authority) GetSeedEngine() (*seedengine.SeedEngine, error) {
state, err := m.getState()
if err != nil {
return nil, fmt.Errorf("getting state: %w", err)
}
return state.SeedEngine, nil
}
Comment on lines +180 to +187
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is unsafe because you don't have the manifest that's valid for this seed engine. GetState should be exported and used for Credentials() and the transit engine.


// State is a snapshot of the Coordinator's manifest history.
type State struct {
SeedEngine *seedengine.SeedEngine
Expand Down
13 changes: 2 additions & 11 deletions coordinator/internal/authority/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,8 @@ func (a *Authority) Credentials(reg *prometheus.Registry, issuer atls.Issuer) (*
})

return &Credentials{
issuer: issuer,
getState: func() (*State, error) {
if err := a.syncState(); err != nil {
return nil, fmt.Errorf("syncing state: %w", err)
}
state := a.state.Load()
if state == nil {
return nil, errors.New("coordinator is not initialized")
}
return state, nil
},
issuer: issuer,
getState: a.getState,
logger: a.logger,
attestationFailuresCounter: attestationFailuresCounter,
kdsGetter: kdsGetter,
Expand Down
63 changes: 63 additions & 0 deletions coordinator/internal/transitengine/crypto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2024 Edgeless Systems GmbH
// SPDX-License-Identifier: AGPL-3.0-only

package transitengine

import (
"crypto/aes"
"crypto/cipher"

"github.com/edgelesssys/contrast/internal/crypto"
)

const (
// aesGCMNonceSize specifies the default nonce size in bytes used in AES GCM.
aesGCMNonceSize = 12
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// aesGCMKeySize specifies the default key size in bytes AES GCM.
aesGCMKeySize = 16
)

// symOpts holds parameters related to the performed symmetric encryption, specifyable as http request parameters.
type symOpts struct {
// Convergent TODO(jmxzo): add parameter support
convergent bool
// Nonce
nonce []byte
// AdditionalData
additionalData []byte //nolint
}

// symmetricEncryptRaw returns the encrypted plaintext based on the symmetric options and encryption key handed in.
func symmetricEncryptRaw(encKey, plaintext []byte, _ symOpts) ([]byte, error) {
aesCipher, err := aes.NewCipher(encKey)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(aesCipher)
jmxnzo marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}
nonce, err := crypto.GenerateRandomBytes(12)
if err != nil {
return nil, err
}
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to pass additionalData here, right?

return append(nonce, ciphertext...), nil
}

// symmetricDecryptRaw returns the decrypted ciphertext based on the symmetric options and encryption keys handed in.
func symmetricDecryptRaw(decKey, ciphertext []byte, opts symOpts) ([]byte, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit weird that the encryption function outputs nonce:ciphertext but we get the nonce here through symOpts. I'd have expected either

  1. The nonce is a separate output of encrypt, and a separate input of decrypt.
  2. The nonce is prepended to the ciphertext by encrypt, and stripped from the ciphertext by decrypt.
  3. encrypt returns a struct containing ciphertext and nonce, decrypt accepts such struct. This would likely replace prefixb64Ciphertext.

No. 3 might actually be best considering that we want to serialize the output - could be a method on that struct.

aesCipher, err := aes.NewCipher(decKey)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(aesCipher)
if err != nil {
return nil, err
}
plaintext, err := gcm.Open(nil, opts.nonce, ciphertext, nil)
if err != nil {
return nil, err
}
return plaintext, nil
}
101 changes: 101 additions & 0 deletions coordinator/internal/transitengine/crypto_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright 2024 Edgeless Systems GmbH
// SPDX-License-Identifier: AGPL-3.0-only

package transitengine

import (
"bytes"
"encoding/json"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"testing"

"github.com/edgelesssys/contrast/coordinator/internal/seedengine"
"github.com/stretchr/testify/require"
)

func TestCryptoAPICyclic(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not test crypto.go, but transitengine.go. May I suggest moving this test to transitengine_test.go and add a cyclic test for symmetric*cryptRaw here?

data := []map[string]string{
{"Plaintext": "vpIhKQhFuGwLv5B/XLYr960uZQ==", "Ciphertext": "vault:v1:d01M+wZYaG9LFnuc18s8oh6PuVFw3+7DBX4LkXXQ6d64DvmQt6qwjj1MHmA88UE="},
{"Plaintext": "O/XShnapt5hNMCZnP+4BZjH84CcWAwhxOUnGwKH7ja1ZYsZdyZrGeLT4EZtA1vWey04bAsi+viGmpYO98YbkCvSn7HZvglLh2DMv3Ach9SP2qjWw0NBa2rrfToI1dsE=", "Ciphertext": "vault:v1:onRtviw8oJv5cY4EngPYySbYlAYgEqWXk5WbD2X+jpRDR5d87Y2qE0Otc4KUKja8LYVb1zB1P40yWTgNb7srG4D7kuxlRFFbtkYaCAC3bLbz+QuumLFQd9RbsN7avbLtEmT8nay/1qvvp2e/MDv5oPoDIT0vzHorVI40"},
{"Plaintext": "lT3rQGMlxq680DdSKfIYYcfyCfMnP9ikxaO5b0mGRKRl4qNL3W9xkSW3QmaMwozCRfNMZhhDCbYokn6KEiGotlVInKt66QjBgXR2Nk9hIcez0LYt8W5pxD0lwTxC", "Ciphertext": "vault:v1:taQ++Amvoj0G1V+OQb0PjldLh5BRXmAhwlO38LRjajVIuHTEa69kfytU3mMaFEpG5JNVg3Cq6vSWH58n1NEmM6WDV79q/hxzaji68joq9uQeoyH3To9iBoRHE2T7jbxUvYLeBwgzFvM/YNk5EaFpNkfIwwxzNZ5gmQ=="},
{"Plaintext": "WC0sC1KtNw76hGVQTpeFNtPg94tJc64dE3rf0mhENsBMLhWmYinA99YbIGx0gSQEOkOsR1sPgSnEmxTvycQdNA==", "Ciphertext": "vault:v1:vjeAbnpFxh+atxQsk6OeumH6irWPAuRbim2UQ8ggNGQFpB4wnYxjUiydGgYixZ6x1Ad+STfbjwxLvij5ZpmyMxFsmrYZIQKCYE48mVUNI+VJa87zuMabQCMClOs="},
{"Plaintext": "iZo0IezTmQ6Ms1GJUbbY4nrsRydO31b6xuqlJwi+R9xLt1K9uaI8ZiuInXc92qpulYNaAWAiBmNNghKM0dpAPdSXwc93+YCT1Zm2i1cuW7H6Uz5tL7E=", "Ciphertext": "vault:v1:qqjtmjUKfzoAb3VeUgWjbRDdYu3K/cdmi7sEVcKPRiOdbY5OyuQrNQHtYZ/mje8hcPyHnmgDJEDOpjLhQgUG6yoYamqksut//lv7DDbKYbzroro5BRiCqQjhfqnhmna79maV5okq8zI5YZBoSSn36ivu"},
{"Plaintext": "FLiOXWBy8oVETtiNzfw0rbMgCfa3DVSfKL4GhR86EcluUe78nLiDxt0HtP5vTwaaz7mvLXu2nOtsdlz8kYY1YrZCLLNlYzjm/vYe++CcH/x+fDKepJem5le0BCsdog==", "Ciphertext": "vault:v1:0qp8vsJ+JFf5m5HekxQeUw0+gj/NdoDcmy7ExSw0G7PB1RBQ80T+TUMjSvmmgu02eQ2oCKEkfFMolfNt1zq+sZkLQuTLpbW8p+Vd8ALPGdyyD20MIb2ez6dm9nMM4jiXL2FkfARuHcHoY3/LCBQVLE+kkJLAdwze/Z4="},
{"Plaintext": "Xqv9SzcpNK99JF1I7xRAJ0FOkzka", "Ciphertext": "vault:v1:N431rZS6bcuGDDJ8Jh93yvih4oc5wHGtz02M2vFQ5IioIlZFqfv29nDImWNaZUW7Zw=="},
{"Plaintext": "6HQ035OxE30=", "Ciphertext": "vault:v1:ZUuU9vMiCa3CE7PvmYlJYhHQOJm2/Rk6xuUA7DSJu4ExttPl"},
{"Plaintext": "yqUBQzznRjbXMxhQkwo5q2Az3/6nvgRQ86uffx8ZqT7rufplfhJz+xDfvi4EOw==", "Ciphertext": "vault:v1:fpjOfUK8hZEk6DmcH715zdyTMAqyW9ymAMxQFgoMR2Q5639NYCA3rbrR4OKfGwCqO7UrBg3LeFksYBPhiO4pbX6dHXGYIfjDdgo="},
}
t.Run("encrypt-decrypt handler", func(t *testing.T) {
mux := NewTransitEngineAPI(&fakeSeedEngineAuthority{}, slog.Default())
for _, entry := range data {
t.Run("cyclic handler function testing", func(t *testing.T) {
var ciphertext, receivedPlaintext string
t.Run("encryption request handling", func(t *testing.T) {
require := require.New(t)
jsonBody, err := createReqBodyJSON("plaintext", entry["Plaintext"])
require.NoError(err)
req := httptest.NewRequest(http.MethodPut, "/v1/transit/encrypt/autounseal", bytes.NewReader(jsonBody))
req.Header.Set("Content-Type", "application/json")
require.NoError(err)
encRespBody := receiveResponseBody(t, mux, req)
require.NoError(err)
data, _ := encRespBody["data"].(map[string]any)
ciphertext, _ = data["ciphertext"].(string)
})

t.Run("decryption request handling", func(t *testing.T) {
require := require.New(t)
decryptReqBody, err := createReqBodyJSON("ciphertext", ciphertext)
require.NoError(err)
decryptReq := httptest.NewRequest(http.MethodPut, "/v1/transit/decrypt/autounseal", bytes.NewReader(decryptReqBody))
decryptReq.Header.Set("Content-Type", "application/json")
decRespBody := receiveResponseBody(t, mux, decryptReq)
data, _ := decRespBody["data"].(map[string]any)
receivedPlaintext, _ = data["plaintext"].(string)
require.Equal(entry["Plaintext"], receivedPlaintext, "Unexpected received plaintext after cycling handler functions")
})
})
}
})
}

func receiveResponseBody(t *testing.T, mux *http.ServeMux, req *http.Request) map[string]any {
require := require.New(t)
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
res := rec.Result()
defer res.Body.Close()
require.Equal(http.StatusOK, res.StatusCode)
respBody, err := io.ReadAll(res.Body)
require.NoError(err)
var respData map[string]any
err = json.Unmarshal(respBody, &respData)
require.NoError(err)
return respData
}

func createReqBodyJSON(bodyParameter, value string) ([]byte, error) {
body := map[string]string{
bodyParameter: value,
}
jsonBody, err := json.Marshal(body)
if err != nil {
return nil, err
}
return jsonBody, nil
}

type fakeSeedEngineAuthority struct{}

func (f *fakeSeedEngineAuthority) GetSeedEngine() (seedEngine *seedengine.SeedEngine, err error) {
salt := make([]byte, 32) // 32-byte salt initialized with zeros
secretSeed := make([]byte, 32) // 32-byte secret seed initialized with zeros
seedEngine, err = seedengine.New(secretSeed, salt)
if err != nil {
return nil, err
}
return seedEngine, nil
}
Loading
Loading