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

Support HMAC signing on artifact uploads #892

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 30 commits into from
Mar 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2a9d83b
Introduce RemoteCacheOptions field to turbo.json
gaspar09 Mar 16, 2022
242db9d
add pipeline to turbo json test
gaspar09 Mar 16, 2022
6807c04
Remove unused 'TurboConfigJSON.Outputs'. nit order and naming updates
gaspar09 Mar 16, 2022
9ebc4e6
Merge branch 'main' into gaspar/remote-cache-options
gaspar09 Mar 18, 2022
ab87060
update schema and add keyEnv option
gaspar09 Mar 18, 2022
a3c5fed
Merge branch 'main' into gaspar/remote-cache-options
gaspar09 Mar 19, 2022
1256d2f
Merge branch 'main' into gaspar/remote-cache-options
gaspar09 Mar 22, 2022
7386176
Explicitly read the artifact into memory before uploading to the remo…
gaspar09 Mar 21, 2022
7fe9a38
Merge branch 'gaspar/refactor-turboJSON-config' into gaspar/remote-ca…
gaspar09 Mar 23, 2022
be43d63
Merge branch 'main' into gaspar/remote-cache-options
gaspar09 Mar 23, 2022
bb2b646
Implement artifact HMAC signing as ArtifactSignatureAuthentication
gaspar09 Mar 23, 2022
b580834
nit: typo
gaspar09 Mar 23, 2022
b71f8e4
Implement Signature Stream Validator on artifact downloads.
gaspar09 Mar 23, 2022
6310e1f
nit: naming
gaspar09 Mar 23, 2022
57e02fe
typo fix. Add additional debug option
gaspar09 Mar 24, 2022
90219c2
Merge branch 'main' into gaspar/remote-cache-options
gaspar09 Mar 24, 2022
85f5647
Merge branch 'main' into gaspar/remote-cache-options
Mar 24, 2022
f3cdad9
fix client_test
gaspar09 Mar 24, 2022
abf109c
Merge branch 'main' into gaspar/remote-cache-options
jaredpalmer Mar 24, 2022
252698c
Merge branch 'main' into gaspar/remote-cache-options
jaredpalmer Mar 24, 2022
db3b169
Prefer KeyEnv over Key in turbo.json. Add more tests
gaspar09 Mar 24, 2022
a5d311c
Download response body before untar so that the signature can be veri…
gaspar09 Mar 24, 2022
1ae8635
add todo
gaspar09 Mar 24, 2022
3a29cec
Merge branch 'main' into gaspar/remote-cache-options
Mar 24, 2022
e289570
remove signing enabled on turborepo public config
gaspar09 Mar 24, 2022
ca87e7d
Merge branch 'main' into gaspar/remote-cache-options
jaredpalmer Mar 25, 2022
3787a51
feedback turbo.json naming remoteCacheOptions->remoteCache
gaspar09 Mar 25, 2022
ed6fcfc
update schema
gaspar09 Mar 25, 2022
7223fda
update schema
gaspar09 Mar 25, 2022
ab7fb3f
Merge branch 'main' into gaspar/remote-cache-options
gaspar09 Mar 25, 2022
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
20 changes: 19 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"version": "0.2.0",
"configurations": [
{
"name": "Basic Turbo Build",
"name": "Build Basic",
"type": "go",
"request": "launch",
"mode": "debug",
Expand All @@ -30,6 +30,24 @@
"program": "${workspaceRoot}/cli/cmd/turbo",
"cwd": "${workspaceRoot}/examples/basic",
"args": ["--version"]
},
{
"name": "Build All",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceRoot}/cli/cmd/turbo",
"cwd": "${workspaceRoot}",
"args": ["run", "build"]
},
{
"name": "Build All (Force)",
"type": "go",
"request": "launch",
"mode": "debug",
"program": "${workspaceRoot}/cli/cmd/turbo",
"cwd": "${workspaceRoot}",
"args": ["run", "build", "--force"]
}
]
}
54 changes: 50 additions & 4 deletions cli/internal/cache/cache_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package cache

import (
"archive/tar"
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"io/ioutil"
Expand All @@ -24,6 +26,7 @@ type httpCache struct {
config *config.Config
requestLimiter limiter
recorder analytics.Recorder
signerVerifier *ArtifactSignatureAuthentication
}

type limiter chan struct{}
Expand All @@ -49,7 +52,22 @@ func (cache *httpCache) Put(target, hash string, duration int, files []string) e

r, w := io.Pipe()
go cache.write(w, hash, files)
return cache.config.ApiClient.PutArtifact(hash, duration, r)

// Read the entire aritfact tar into memory so we can easily compute the signature.
// Note: retryablehttp.NewRequest reads the files into memory anyways so there's no
// additional overhead by doing the ioutil.ReadAll here instead.
artifactBody, err := ioutil.ReadAll(r)
if err != nil {
return fmt.Errorf("failed to store files in HTTP cache: %w", err)
}
tag := ""
if cache.signerVerifier.isEnabled() {
tag, err = cache.signerVerifier.generateTag(hash, artifactBody)
if err != nil {
return fmt.Errorf("failed to store files in HTTP cache: %w", err)
}
}
return cache.config.ApiClient.PutArtifact(hash, artifactBody, duration, tag)
}

// write writes a series of files into the given Writer.
Expand Down Expand Up @@ -134,8 +152,8 @@ func (cache *httpCache) logFetch(hit bool, hash string, duration int) {
cache.recorder.LogEvent(payload)
}

func (cache *httpCache) retrieve(key string) (bool, []string, int, error) {
resp, err := cache.config.ApiClient.FetchArtifact(key, nil)
func (cache *httpCache) retrieve(hash string) (bool, []string, int, error) {
resp, err := cache.config.ApiClient.FetchArtifact(hash, nil)
if err != nil {
return false, nil, 0, err
}
Expand All @@ -157,7 +175,29 @@ func (cache *httpCache) retrieve(key string) (bool, []string, int, error) {
b, _ := ioutil.ReadAll(resp.Body)
return false, files, duration, fmt.Errorf("%s", string(b))
}
gzr, err := gzip.NewReader(resp.Body)
artifactReader := resp.Body
if cache.signerVerifier.isEnabled() {
expectedTag := resp.Header.Get("x-artifact-tag")
if expectedTag == "" {
// If the verifier is enabled all incoming artifact downloads must have a signature
return false, nil, 0, errors.New("artifact verification failed: Downloaded artifact is missing required x-artifact-tag header")
}
b, _ := ioutil.ReadAll(artifactReader)
if err != nil {
return false, nil, 0, fmt.Errorf("artifact verifcation failed: %w", err)
}
isValid, err := cache.signerVerifier.validate(hash, b, expectedTag)
if err != nil {
return false, nil, 0, fmt.Errorf("artifact verifcation failed: %w", err)
}
if !isValid {
err = fmt.Errorf("artifact verification failed: artifact tag does not match expected tag %s", expectedTag)
return false, nil, 0, err
}
// The artifact has been verified and the body can be read and untarred
artifactReader = ioutil.NopCloser(bytes.NewReader(b))
}
gzr, err := gzip.NewReader(artifactReader)
if err != nil {
return false, files, duration, err
}
Expand Down Expand Up @@ -236,5 +276,11 @@ func newHTTPCache(config *config.Config, recorder analytics.Recorder) *httpCache
config: config,
requestLimiter: make(limiter, 20),
recorder: recorder,
signerVerifier: &ArtifactSignatureAuthentication{
// TODO(Gaspar): this should use RemoteCacheOptions.TeamId once we start
// enforcing team restrictions for repositories.
teamId: config.TeamId,
options: &config.TurboConfigJSON.RemoteCacheOptions.SignatureOptions,
},
}
}
110 changes: 110 additions & 0 deletions cli/internal/cache/cache_signature_authentication.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package cache

import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"hash"
"io"
"os"

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

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

func (asa *ArtifactSignatureAuthentication) isEnabled() bool {
return asa.options.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
}
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")
}
return []byte(secret), nil
}

func (asa *ArtifactSignatureAuthentication) generateTag(hash string, artifactBody []byte) (string, error) {
tag, err := asa.getTagGenerator(hash)
if err != nil {
return "", err
}
tag.Write(artifactBody)
return base64.StdEncoding.EncodeToString(tag.Sum(nil)), nil
}

func (asa *ArtifactSignatureAuthentication) getTagGenerator(hash string) (hash.Hash, error) {
teamId := asa.teamId
secret, err := asa.secretKey()
if err != nil {
return nil, err
}
artifactMetadata := &struct {
Hash string `json:"hash"`
TeamId string `json:"teamId"`
}{
Hash: hash,
TeamId: teamId,
}
metadata, err := json.Marshal(artifactMetadata)
if err != nil {
return nil, err
}

// TODO(Gaspar) Support additional signing algorithms here
h := hmac.New(sha256.New, secret)
h.Write(metadata)
return h, nil
}

func (asa *ArtifactSignatureAuthentication) validate(hash string, artifactBody []byte, expectedTag string) (bool, error) {
computedTag, err := asa.generateTag(hash, artifactBody)
if err != nil {
return false, fmt.Errorf("failed to verify artifact tag: %w", err)
}
return hmac.Equal([]byte(computedTag), []byte(expectedTag)), nil
}

func (asa *ArtifactSignatureAuthentication) streamValidator(hash string, incomingReader io.ReadCloser) (io.ReadCloser, *StreamValidator, error) {
tag, err := asa.getTagGenerator(hash)
if err != nil {
return nil, nil, err
}

tee := io.TeeReader(incomingReader, tag)
artifactReader := readCloser{tee, incomingReader}
return artifactReader, &StreamValidator{tag}, nil
}

type StreamValidator struct {
currentHash hash.Hash
}

func (sv *StreamValidator) Validate(expectedTag string) bool {
computedTag := base64.StdEncoding.EncodeToString(sv.currentHash.Sum(nil))
return hmac.Equal([]byte(computedTag), []byte(expectedTag))
}

func (sv *StreamValidator) CurrentValue() string {
return base64.StdEncoding.EncodeToString(sv.currentHash.Sum(nil))
}

type readCloser struct {
io.Reader
io.Closer
}
Loading