-
Notifications
You must be signed in to change notification settings - Fork 14
ENG-1632: Add org key custody backend (client-hosted HTTPS key service) #3814
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
Changes from 1 commit
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| package orgkey | ||
|
|
||
| import ( | ||
| "os" | ||
| "path/filepath" | ||
|
|
||
| "github.com/ActiveState/cli/internal/constants" | ||
| "github.com/ActiveState/cli/internal/errs" | ||
| "github.com/ActiveState/cli/internal/logging" | ||
| ) | ||
|
|
||
| // cacheFileName is the on-disk cache file (the validated contract JSON) under the config dir. | ||
| const cacheFileName = "private_ingredient_orgkey.json" | ||
|
|
||
| func (p *provider) diskCacheEnabled() bool { | ||
| return p.cfg.GetBool(constants.PrivateIngredientCacheKeyConfig) | ||
| } | ||
|
|
||
| func (p *provider) cachePath() string { | ||
| return filepath.Join(p.cfg.ConfigPath(), cacheFileName) | ||
| } | ||
|
|
||
| // readDiskCache returns the cached contract bytes if a usable cache file exists. | ||
| // A missing file is normal (first run); an unsafe or unreadable file is ignored | ||
| // with a warning so the run falls back to a fresh fetch. | ||
| func (p *provider) readDiskCache() ([]byte, bool) { | ||
| path := p.cachePath() | ||
| info, err := os.Stat(path) | ||
| if err != nil { | ||
| return nil, false | ||
| } | ||
| if err := checkCacheMode(info); err != nil { | ||
| logging.Warning("Ignoring on-disk org key cache: %v", errs.JoinMessage(err)) | ||
| return nil, false | ||
| } | ||
| b, err := os.ReadFile(path) | ||
| if err != nil { | ||
| logging.Warning("Could not read on-disk org key cache: %v", errs.JoinMessage(err)) | ||
| return nil, false | ||
| } | ||
| return b, true | ||
| } | ||
|
|
||
| // writeDiskCache persists the validated contract for reuse by later runs, | ||
| // owner-readable only. | ||
| func (p *provider) writeDiskCache(raw []byte) error { | ||
| if err := os.WriteFile(p.cachePath(), raw, 0600); err != nil { | ||
| return errs.Wrap(err, "unable to write org key cache") | ||
| } | ||
| return nil | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| //go:build !windows | ||
| // +build !windows | ||
|
|
||
| package orgkey | ||
|
|
||
| import ( | ||
| "os" | ||
|
|
||
| "github.com/ActiveState/cli/internal/errs" | ||
| ) | ||
|
|
||
| // checkCacheMode rejects a cache file that is readable or writable by anyone | ||
| // other than the owner (anything beyond u+rw). | ||
| func checkCacheMode(info os.FileInfo) error { | ||
| if info.Mode()&0177 != 0 { | ||
| return errs.New("cache file %q must be mode 0600", info.Name()) | ||
| } | ||
| return nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| //go:build windows | ||
| // +build windows | ||
|
|
||
| package orgkey | ||
|
|
||
| import "os" | ||
|
|
||
| // checkCacheMode is a no-op on Windows, where POSIX permission bits do not | ||
| // apply; the cache file is written to the owner's config directory. | ||
| func checkCacheMode(info os.FileInfo) error { | ||
| return nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package orgkey | ||
|
|
||
| import "github.com/ActiveState/cli/internal/constants" | ||
|
|
||
| // stringConfigReader reads string-valued config options. | ||
| type stringConfigReader interface { | ||
| GetString(key string) string | ||
| } | ||
|
|
||
| // SanitizeChildEnv removes private-ingredient key-service credentials from env | ||
| // so they are never propagated to child process environments. | ||
| func SanitizeChildEnv(cfg stringConfigReader, env map[string]string) { | ||
| if tokenEnv := cfg.GetString(constants.PrivateIngredientBearerTokenEnvConfig); tokenEnv != "" { | ||
| delete(env, tokenEnv) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| package orgkey | ||
|
|
||
| import ( | ||
| "crypto/ecdsa" | ||
| "crypto/elliptic" | ||
| "crypto/rand" | ||
| "crypto/x509" | ||
| "crypto/x509/pkix" | ||
| "encoding/base64" | ||
| "encoding/json" | ||
| "encoding/pem" | ||
| "math/big" | ||
| "net/http/httptest" | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/ActiveState/cli/internal/artifactcrypto" | ||
| "github.com/ActiveState/cli/internal/constants" | ||
| ) | ||
|
|
||
| // testKey is a fixed 32-byte AES-256 key used across tests. | ||
| func testKey() []byte { | ||
| k := make([]byte, artifactcrypto.KeySize) | ||
| for i := range k { | ||
| k[i] = byte(i + 1) | ||
| } | ||
| return k | ||
| } | ||
|
|
||
| // fakeConfig is an in-memory implementation of configurable. | ||
| type fakeConfig struct { | ||
| strings map[string]string | ||
| bools map[string]bool | ||
| dir string | ||
| } | ||
|
|
||
| func newFakeConfig(t *testing.T) *fakeConfig { | ||
| return &fakeConfig{ | ||
| strings: map[string]string{}, | ||
| bools: map[string]bool{}, | ||
| dir: t.TempDir(), | ||
| } | ||
| } | ||
|
|
||
| func (f *fakeConfig) GetString(key string) string { return f.strings[key] } | ||
| func (f *fakeConfig) GetBool(key string) bool { return f.bools[key] } | ||
| func (f *fakeConfig) ConfigPath() string { return f.dir } | ||
|
|
||
| // contractFields returns a valid contract as a field map, so tests can mutate | ||
| // individual fields before marshaling. | ||
| func contractFields(key []byte, org, keyID string) map[string]string { | ||
| return map[string]string{ | ||
| "schema": contractSchema, | ||
| "org": org, | ||
| "key_id": keyID, | ||
| "algorithm": contractAlgorithm, | ||
| "encoding": contractEncoding, | ||
| "key": "b64:" + base64.StdEncoding.EncodeToString(key), | ||
| "fingerprint": artifactcrypto.Fingerprint(key), | ||
| } | ||
| } | ||
|
|
||
| func mustJSON(t *testing.T, v interface{}) []byte { | ||
| t.Helper() | ||
| b, err := json.Marshal(v) | ||
| if err != nil { | ||
| t.Fatalf("marshal: %v", err) | ||
| } | ||
| return b | ||
| } | ||
|
|
||
| // writeServerCA writes the test server's certificate to a temp PEM file and | ||
| // returns its path, for use as the configured key-service CA. | ||
| func writeServerCA(t *testing.T, srv *httptest.Server) string { | ||
| t.Helper() | ||
| path := filepath.Join(t.TempDir(), "ca.pem") | ||
| pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: srv.Certificate().Raw}) | ||
| if err := os.WriteFile(path, pemBytes, 0600); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| return path | ||
| } | ||
|
|
||
| // genClientCert generates a self-signed client certificate and key, writes them | ||
| // to temp files, and returns their paths (for the mTLS path). | ||
| func genClientCert(t *testing.T) (certPath, keyPath string) { | ||
| t.Helper() | ||
| priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| tmpl := x509.Certificate{ | ||
| SerialNumber: big.NewInt(1), | ||
| Subject: pkix.Name{CommonName: "test-client"}, | ||
| NotBefore: time.Now().Add(-time.Hour), | ||
| NotAfter: time.Now().Add(time.Hour), | ||
| KeyUsage: x509.KeyUsageDigitalSignature, | ||
| ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, | ||
| } | ||
| der, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &priv.PublicKey, priv) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| keyDER, err := x509.MarshalECPrivateKey(priv) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
|
|
||
| dir := t.TempDir() | ||
| certPath = filepath.Join(dir, "client.crt") | ||
| keyPath = filepath.Join(dir, "client.key") | ||
| certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) | ||
| keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}) | ||
| if err := os.WriteFile(certPath, certPEM, 0600); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if err := os.WriteFile(keyPath, keyPEM, 0600); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| return certPath, keyPath | ||
| } | ||
|
|
||
| // configForServer returns a fakeConfig pointed at srv with its CA trusted. | ||
| func configForServer(t *testing.T, srv *httptest.Server) *fakeConfig { | ||
| cfg := newFakeConfig(t) | ||
| cfg.strings[constants.PrivateIngredientKeyServiceURLConfig] = srv.URL | ||
| cfg.strings[constants.PrivateIngredientKeyServiceCAConfig] = writeServerCA(t, srv) | ||
| return cfg | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.