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 12 commits into
base: main
Choose a base branch
from

Conversation

jmxnzo
Copy link
Contributor

@jmxnzo jmxnzo commented Jan 31, 2025

This PR adds basic http handling functionality for the encrypt and decrypt endpoints of the transit engine API, allowing the auto-unsealing process for user-managed Vaults. Additionally a cyclic cryptographic unit test, testing the implemented handler functions for encrypt and decrypt was added.

ToDo's

  • add logic to extract name parameter of URL, specifying the workloadSecretID
  • authorize transit engine requests to name parameter with client cert policy hash
  • update seedengine logic
  • Implement Vault json error bodies

@jmxnzo jmxnzo added no changelog PRs not listed in the release notes do not merge This shouldn't be merged at this point labels Jan 31, 2025
@jmxnzo jmxnzo marked this pull request as ready for review January 31, 2025 16:28
@jmxnzo jmxnzo requested a review from burgerdev as a code owner January 31, 2025 16:28
@jmxnzo jmxnzo changed the title transitapi: add hhtp handler enc/dec transitapi: add http handler enc/dec Jan 31, 2025
@jmxnzo jmxnzo force-pushed the http-handler/transit-engine-api branch 4 times, most recently from 2b3b3ce to 9cd6b17 Compare February 6, 2025 15:11
Comment on lines +133 to +145
eg.Go(func() error {
mux := transitengine.NewTransitEngineAPI(meshAuth, logger)
logger.Info("Transit Engine API initialized")
port := 8200
fmt.Printf("Serving transit engine API on port %d\n", port)
if err := http.ListenAndServe(fmt.Sprintf(":%d", port), mux); err != nil {
logger.Error("Failed to start transit engine API", "err", err)
}
return nil
})

Copy link
Contributor

Choose a reason for hiding this comment

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

This should only be enabled after authorization.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Of course, it was intended for testing purposes only and i'll drop this when squashing everything!

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's leave the comment open until it's actually resolved.

@jmxnzo jmxnzo force-pushed the http-handler/transit-engine-api branch from 8c98f39 to 26f2591 Compare February 10, 2025 20:02
@jmxnzo jmxnzo force-pushed the http-handler/transit-engine-api branch from d12a895 to 031533a Compare February 11, 2025 13:48
Copy link
Contributor

@burgerdev burgerdev left a comment

Choose a reason for hiding this comment

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

Thanks for the thorough review, @3u13r!

http.Error(w, fmt.Sprint("URL version: %w", err), http.StatusBadRequest)
return
}
key, err := deriveEncryptionKey(authority, workloadSecretID+strVersion)
Copy link
Contributor

Choose a reason for hiding this comment

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

This potentially allows access to secrets for other workloads, because we can't tell the boundary between workloadSecretId and strVersion. E.g.:

workloadSecretId := `pod1`
strVersion := `2`
// vs
workloadSecretId := `pod`
strVersion := `12`

I think we should either derive twice (first for the name, then for the version), or define a separation character, ensure that the version does not contain that character and then derive for strVersion + sepChar + id (because the id does not have any character set limitations).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for pointing out and the thorough review! I changed this to your suggestion. StrVersion implicitly always was ensured to match ^v(\d+)$, therefore i added '_' as seperator and changed the order to mitigate the described attack possibilities.

Copy link
Contributor Author

@jmxnzo jmxnzo Feb 17, 2025

Choose a reason for hiding this comment

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

But as described in the other comments, i dropped the strVersion completely as it does not specify the key_version as expected and used the key_version value instead. In the decryptionRequest, the version is still matched against the regex ^v(\d+)$, because it is part of the ciphertext field. And for the encryptionRequest the key_version value is autoparsed during json unmarshalling to int.

Copy link
Contributor Author

@jmxnzo jmxnzo Feb 18, 2025

Choose a reason for hiding this comment

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

Can i mark this as resolved this with the updated separation logic? @burgerdev

Comment on lines +133 to +145
eg.Go(func() error {
mux := transitengine.NewTransitEngineAPI(meshAuth, logger)
logger.Info("Transit Engine API initialized")
port := 8200
fmt.Printf("Serving transit engine API on port %d\n", port)
if err := http.ListenAndServe(fmt.Sprintf(":%d", port), mux); err != nil {
logger.Error("Failed to start transit engine API", "err", err)
}
return nil
})

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's leave the comment open until it's actually resolved.

@jmxnzo jmxnzo force-pushed the http-handler/transit-engine-api branch 2 times, most recently from 8b6dbdb to 047b6bd Compare February 17, 2025 16:07
This commit drops the version parsing of the URL, as it was refering to the API endpoint
and not the key_version which was a wrong assumption.
It introduces the key_version field to the encryption request and
adds the necessary logic to the derivation, to ensure inputs are
synthesized and seperated by disallowed characters.
It reduces the (un)marshalling functions to only implement ciphertext container
marshalling and instead rely on the starndard go library.
@jmxnzo jmxnzo force-pushed the http-handler/transit-engine-api branch from 047b6bd to 5879177 Compare February 17, 2025 16:09
@jmxnzo jmxnzo force-pushed the http-handler/transit-engine-api branch from 55d25fa to f2c3a9f Compare February 18, 2025 07:00
Comment on lines +13 to +24
type (
// ciphertextContainer describes a base64-encoded ciphertext prepended with the nonce and specified key version.
ciphertextContainer struct {
nonce []byte
ciphertext []byte
keyVersion uint
}
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Needs gofmt.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, I was expecting gofmt to convert this to

type ciphertextContainer struct {
}

Please do that manually.

encodedNonce := b64.StdEncoding.EncodeToString(c.nonce)
encodedCiphertext := b64.StdEncoding.EncodeToString(c.ciphertext)
// Convert to "vault:vX:base64" format
versioned := fmt.Sprintf("vault:v%d:%s%s", c.keyVersion, encodedNonce, encodedCiphertext)
Copy link
Contributor

Choose a reason for hiding this comment

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

What happens if keyVersion==0, considering that Vault key versions start with 1?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Regarding https://developer.hashicorp.com/vault/api-docs/secret/transit#encrypt-data default of key_version is 0, i looked it up to be compatible.

Copy link
Contributor

Choose a reason for hiding this comment

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

Are you referring to key_version (int: 0)? I took that to mean

The default for key_version is 0, which means "use latest key".

which would be consistent with the example responses in the docs.

@jmxnzo jmxnzo force-pushed the http-handler/transit-engine-api branch from f2c3a9f to 9c72d62 Compare February 18, 2025 10:30
@jmxnzo jmxnzo force-pushed the http-handler/transit-engine-api branch from 9c72d62 to 029fcec Compare February 18, 2025 10:59
@jmxnzo jmxnzo requested a review from burgerdev February 18, 2025 11:16
Copy link
Contributor

@burgerdev burgerdev left a comment

Choose a reason for hiding this comment

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

We're closing in!

Comment on lines +13 to +24
type (
// ciphertextContainer describes a base64-encoded ciphertext prepended with the nonce and specified key version.
ciphertextContainer struct {
nonce []byte
ciphertext []byte
keyVersion uint
}
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, I was expecting gofmt to convert this to

type ciphertextContainer struct {
}

Please do that manually.

Comment on lines +155 to +156
// parseRequest parses the given HTTP request body into the struct, which should represent to the
// received request body.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// parseRequest parses the given HTTP request body into the struct, which should represent to the
// received request body.
// parseRequest parses the given HTTP request body into the struct

Comment on lines +130 to +133
data, exist := decRespBody["data"].(map[string]any)
require.True(exist)
receivedPlaintext, exist = data["plaintext"].(string)
require.True(exist)
Copy link
Contributor

Choose a reason for hiding this comment

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

These are not existence checks, but type cast checks. If the expected fields are not found, this will panic. You could avoid the type cast by assigning the expected type to begin with:

var decRespBody map[string]map[string]string

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
do not merge This shouldn't be merged at this point no changelog PRs not listed in the release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants