这是indexloc提供的服务,不要输入任何密码
Skip to content

BYOK signature uses TURBO_REMOTE_CACHE_SIGNATURE_KEY env variable by default #963

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

Merged
merged 4 commits into from
Mar 29, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion cli/internal/cache/cache_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ func newHTTPCache(config *config.Config, recorder analytics.Recorder) *httpCache
// TODO(Gaspar): this should use RemoteCacheOptions.TeamId once we start
// enforcing team restrictions for repositories.
teamId: config.TeamId,
options: &config.TurboConfigJSON.RemoteCacheOptions.SignatureOptions,
enabled: config.TurboConfigJSON.RemoteCacheOptions.Signature,
},
}
}
14 changes: 3 additions & 11 deletions cli/internal/cache/cache_signature_authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,21 @@ import (
"hash"
"io"
"os"

"github.com/vercel/turborepo/cli/internal/fs"
)

type ArtifactSignatureAuthentication struct {
teamId string
options *fs.SignatureOptions
enabled bool
}

func (asa *ArtifactSignatureAuthentication) isEnabled() bool {
return asa.options.Enabled
return asa.enabled
}

// If the secret key is not found or the secret key length is 0, an error is returned
// Preference is given to the enviornment specifed secret key.
func (asa *ArtifactSignatureAuthentication) secretKey() ([]byte, error) {
secret := ""
switch {
case len(asa.options.KeyEnv) > 0:
secret = os.Getenv(asa.options.KeyEnv)
case len(asa.options.Key) > 0:
secret = asa.options.Key
}
secret := os.Getenv("TURBO_REMOTE_CACHE_SIGNATURE_KEY")
if len(secret) == 0 {
return nil, errors.New("signature secret key not found. You must specify a secret key or keyEnv name in your turbo.json config")
}
Expand Down
123 changes: 52 additions & 71 deletions cli/internal/cache/cache_signature_authentication_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/vercel/turborepo/cli/internal/fs"
)

func Test_SecretKey(t *testing.T) {
func Test_SecretKeySuccess(t *testing.T) {
teamId := "team_someid"
secret := "my-secret"
secretKeyEnvName := "TURBO_TEST_SIGNING_KEY"
secretKeyEnvName := "TURBO_REMOTE_CACHE_SIGNATURE_KEY"
secretKeyEnvValue := "my-secret-key-env"
t.Setenv(secretKeyEnvName, secretKeyEnvValue)

Expand All @@ -27,59 +25,52 @@ func Test_SecretKey(t *testing.T) {
{
name: "Accepts secret key",
asa: &ArtifactSignatureAuthentication{
teamId: teamId,
options: &fs.SignatureOptions{
Enabled: true,
Key: secret,
},
},
expectedSecretKey: secret,
expectedSecretKeyError: false,
},
{
name: "Accepts secret keyEnv",
asa: &ArtifactSignatureAuthentication{
teamId: teamId,
options: &fs.SignatureOptions{
Enabled: true,
KeyEnv: secretKeyEnvName,
},
},
expectedSecretKey: secretKeyEnvValue,
expectedSecretKeyError: false,
},
{
name: "Prefers secret keyEnv",
asa: &ArtifactSignatureAuthentication{
teamId: teamId,
options: &fs.SignatureOptions{
Enabled: true,
Key: secret,
KeyEnv: secretKeyEnvName,
},
teamId: teamId,
enabled: true,
},
expectedSecretKey: secretKeyEnvValue,
expectedSecretKeyError: false,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
secretKey, err := tc.asa.secretKey()
if tc.expectedSecretKeyError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tc.expectedSecretKey, string(secretKey))
}
})
}
}

func Test_SecretKeyErrors(t *testing.T) {
teamId := "team_someid"

// Env secret key TURBO_REMOTE_CACHE_SIGNATURE_KEY is not set

cases := []struct {
name string
asa *ArtifactSignatureAuthentication
expectedSecretKey string
expectedSecretKeyError bool
}{
{
name: "Secret key not defined errors",
asa: &ArtifactSignatureAuthentication{
teamId: teamId,
options: &fs.SignatureOptions{
Enabled: true,
},
teamId: teamId,
enabled: true,
},
expectedSecretKey: "",
expectedSecretKeyError: true,
},
{
name: "Secret key is empty errors",
asa: &ArtifactSignatureAuthentication{
teamId: teamId,
options: &fs.SignatureOptions{
Enabled: true,
Key: "",
},
teamId: teamId,
enabled: true,
},
expectedSecretKey: "",
expectedSecretKeyError: true,
Expand All @@ -103,7 +94,9 @@ func Test_GenerateTagAndValidate(t *testing.T) {
teamId := "team_someid"
hash := "the-artifact-hash"
artifactBody := []byte("the artifact body as bytes")
secret := "my-secret"
secretKeyEnvName := "TURBO_REMOTE_CACHE_SIGNATURE_KEY"
secretKeyEnvValue := "my-secret-key-env"
t.Setenv(secretKeyEnvName, secretKeyEnvValue)

cases := []struct {
name string
Expand All @@ -114,49 +107,37 @@ func Test_GenerateTagAndValidate(t *testing.T) {
{
name: "Uses hash to generate tag",
asa: &ArtifactSignatureAuthentication{
teamId: teamId,
options: &fs.SignatureOptions{
Enabled: true,
Key: secret,
},
teamId: teamId,
enabled: true,
},
expectedTagMatches: testUtilGetHMACTag(hash, teamId, artifactBody, secret),
expectedTagDoesNotMatch: testUtilGetHMACTag("wrong-hash", teamId, artifactBody, secret),
expectedTagMatches: testUtilGetHMACTag(hash, teamId, artifactBody, secretKeyEnvValue),
expectedTagDoesNotMatch: testUtilGetHMACTag("wrong-hash", teamId, artifactBody, secretKeyEnvValue),
},
{
name: "Uses teamId to generate tag",
asa: &ArtifactSignatureAuthentication{
teamId: teamId,
options: &fs.SignatureOptions{
Enabled: true,
Key: secret,
},
teamId: teamId,
enabled: true,
},
expectedTagMatches: testUtilGetHMACTag(hash, teamId, artifactBody, secret),
expectedTagDoesNotMatch: testUtilGetHMACTag(hash, "wrong-teamId", artifactBody, secret),
expectedTagMatches: testUtilGetHMACTag(hash, teamId, artifactBody, secretKeyEnvValue),
expectedTagDoesNotMatch: testUtilGetHMACTag(hash, "wrong-teamId", artifactBody, secretKeyEnvValue),
},
{
name: "Uses artifactBody to generate tag",
asa: &ArtifactSignatureAuthentication{
teamId: teamId,
options: &fs.SignatureOptions{
Enabled: true,
Key: secret,
},
teamId: teamId,
enabled: true,
},
expectedTagMatches: testUtilGetHMACTag(hash, teamId, artifactBody, secret),
expectedTagDoesNotMatch: testUtilGetHMACTag(hash, teamId, []byte("wrong-artifact-body"), secret),
expectedTagMatches: testUtilGetHMACTag(hash, teamId, artifactBody, secretKeyEnvValue),
expectedTagDoesNotMatch: testUtilGetHMACTag(hash, teamId, []byte("wrong-artifact-body"), secretKeyEnvValue),
},
{
name: "Uses secret to generate tag",
asa: &ArtifactSignatureAuthentication{
teamId: teamId,
options: &fs.SignatureOptions{
Enabled: true,
Key: secret,
},
teamId: teamId,
enabled: true,
},
expectedTagMatches: testUtilGetHMACTag(hash, teamId, artifactBody, secret),
expectedTagMatches: testUtilGetHMACTag(hash, teamId, artifactBody, secretKeyEnvValue),
expectedTagDoesNotMatch: testUtilGetHMACTag(hash, teamId, artifactBody, "wrong-secret"),
},
}
Expand Down
9 changes: 2 additions & 7 deletions cli/internal/fs/package_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,9 @@ func ReadTurboConfigJSON(path string) (*TurboConfigJSON, error) {
return turboConfig, nil
}

type SignatureOptions struct {
Enabled bool `json:"enabled,omitempty"`
Key string `json:"key,omitempty"`
KeyEnv string `json:"keyEnv,omitempty"`
}
type RemoteCacheOptions struct {
TeamId string `json:"teamId,omitempty"`
SignatureOptions SignatureOptions `json:"signature,omitempty"`
TeamId string `json:"teamId,omitempty"`
Signature bool `json:"signature,omitempty"`
}

type PPipeline struct {
Expand Down
2 changes: 1 addition & 1 deletion cli/internal/fs/package_json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func Test_ParseTurboConfigJson(t *testing.T) {
dev := Pipeline{nil, &BoolFalse, nil, PPipeline{nil, &BoolFalse, nil}}
pipelineExpected := map[string]Pipeline{"build": build, "lint": lint, "dev": dev}

remoteCacheOptionsExpected := RemoteCacheOptions{"team_id", SignatureOptions{true, "key", ""}}
remoteCacheOptionsExpected := RemoteCacheOptions{"team_id", true}
assert.EqualValues(t, pipelineExpected, turboConfig.Pipeline)
assert.EqualValues(t, remoteCacheOptionsExpected, turboConfig.RemoteCacheOptions)
}
5 changes: 1 addition & 4 deletions cli/internal/fs/testdata/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@
},
"remoteCache": {
"teamId": "team_id",
"signature": {
"enabled": true,
"key": "key"
}
"signature": true
}
}
18 changes: 18 additions & 0 deletions docs/pages/docs/features/remote-caching.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,24 @@ If you are building and hosting your apps on Vercel, then Remote Caching will be

Please refer to the [Vercel documentation](https://vercel.com/docs/concepts/git/monorepos#turborepo?utm_source=turborepo.org&utm_medium=referral&utm_campaign=docs-link) for instructions.

### Artifact Integrity and Authenticity Verification

You can enable Turborepo to sign artifacts with a secret key before uploading them to the Remote Cache. Turborepo uses `HMAC-SHA256` signatures on artifacts using a secret key you provide.
Turborepo will verify the remote cache artifacts' integrity and authenticity when they're downloaded.
Any artifacts that fail to verify will be ignored and treated as a cache miss by Turborepo.

To enable this feature, set the `remoteCache` options on your turbo config to include `signature: true`. Then specify your secret key by declaring the `TURBO_REMOTE_CACHE_SIGNATURE_KEY` environment variable.

```jsonc
{
"$schema": "https://turborepo.org/schema.json",
"remoteCache": {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we might need a section defining the remoteCache similar to https://turborepo.org/docs/features/pipelines#defining-a-pipeline

Copy link
Contributor Author

@gaspar09 gaspar09 Mar 28, 2022

Choose a reason for hiding this comment

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

depends on how we want to introduce --remote-only and teamId enforcement from turbo.json

// Indicates if signature verifcation is enabled.
"signature": true
}
}
```

## Custom Remote Caches

You can self-host your own Remote Cache or use other remote caching service providers as long as they comply with Turborepo's Remote Caching Server API.
Expand Down
40 changes: 4 additions & 36 deletions docs/schema.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,45 +97,13 @@ export interface Pipeline {
}

export interface RemoteCache {
/**
* The teamId used in requests to the Remote Cache.
*/
teamId?: string;
/**
* Configuration options that control the integrity and authentication checks for
* artifacts uploaded to and downloaded from the remote cache.
*
* @default {}
*/
signature?: Signature;
}

export interface Signature {
/**
* Indicates if signature verification is enabled for requests to the remote cache. When
* `enabled` is `true`, Turborepo will sign every uploaded artifact using the `key`.
* Turborepo will reject any downloaded artifacts that have an invalid signature or are
* missing a signature.
* `true`, Turborepo will sign every uploaded artifact using the value of the environment
* variable `TURBO_REMOTE_CACHE_SIGNATURE_KEY`. Turborepo will reject any downloaded artifacts
* that have an invalid signature or are missing a signature.
*
* @default false
*/
enabled?: boolean;
/**
* The secret key to use for signing and verifying signatures on artifacts uploaded to
* the remote cache.
*
* If both `key` and `keyEnv` are present, then `key` will be used.
*
* @default ""
*/
key?: string;
/**
* The environment variable that contains the value of the secret key used for signing
* and verifying signatures on artifacts uploaded to the remote cache.
*
* If both `key` and `keyEnv` are present, then `key` will be used.
*
* @default ""
*/
keyEnv?: string;
signature?: boolean;
}