From 151c9004bfcf3d713dce80c2c9e1bc23cbe43202 Mon Sep 17 00:00:00 2001 From: Pete Moore Date: Wed, 21 Feb 2018 10:24:19 +0100 Subject: [PATCH] Bug 1358545 - secure chain of trust key at runtime, and refuse to run a chain-of-trust task if the signing key is readable by task user --- artifacts_test.go | 159 ++++++++++++------------------- chain_of_trust.go | 61 +++++++++--- chain_of_trust_all-unix-style.go | 55 +++++++++++ chain_of_trust_windows.go | 48 ++++++++++ helper_test.go | 79 ++++++++++++++- main.go | 38 ++++++-- plat_windows.go | 29 ++++-- plat_windows_test.go | 42 +++++++- process/process_windows.go | 10 +- 9 files changed, 382 insertions(+), 139 deletions(-) create mode 100644 chain_of_trust_all-unix-style.go create mode 100644 chain_of_trust_windows.go diff --git a/artifacts_test.go b/artifacts_test.go index 55e7b251..474377c2 100644 --- a/artifacts_test.go +++ b/artifacts_test.go @@ -7,7 +7,6 @@ import ( "os" "path/filepath" "reflect" - "strings" "testing" "time" @@ -490,6 +489,13 @@ func TestProtectedArtifactsReplaced(t *testing.T) { taskID := scheduleAndExecute(t, td, payload) + // Chain of trust is not allowed when running as current user + // since signing key cannot be secured + if config.RunTasksAsCurrentUser { + expectChainOfTrustKeyNotSecureMessage(t, taskID) + return + } + ensureResolution(t, taskID, "completed", "completed") artifacts, err := myQueue.ListArtifacts(taskID, "0", "", "") @@ -813,146 +819,103 @@ func TestUpload(t *testing.T) { taskID := scheduleAndExecute(t, td, payload) + // Chain of trust is not allowed when running as current user + // since signing key cannot be secured + if config.RunTasksAsCurrentUser { + expectChainOfTrustKeyNotSecureMessage(t, taskID) + return + } + // some required substrings - not all, just a selection - expectedArtifacts := map[string]struct { - extracts []string - contentType string - contentEncoding string - expires tcclient.Time - }{ + expectedArtifacts := ExpectedArtifacts{ "public/logs/live_backing.log": { - extracts: []string{ + Extracts: []string{ "hello world!", "goodbye world!", `"instance-type": "p3.enormous"`, }, - contentType: "text/plain; charset=utf-8", - contentEncoding: "gzip", - expires: td.Expires, + ContentType: "text/plain; charset=utf-8", + ContentEncoding: "gzip", + Expires: td.Expires, }, "public/logs/live.log": { - extracts: []string{ + Extracts: []string{ "hello world!", "goodbye world!", "=== Task Finished ===", "Exit Code: 0", }, - contentType: "text/plain; charset=utf-8", - contentEncoding: "gzip", - expires: td.Expires, + ContentType: "text/plain; charset=utf-8", + ContentEncoding: "gzip", + Expires: td.Expires, }, "public/logs/certified.log": { - extracts: []string{ + Extracts: []string{ "hello world!", "goodbye world!", "=== Task Finished ===", "Exit Code: 0", }, - contentType: "text/plain; charset=utf-8", - contentEncoding: "gzip", - expires: td.Expires, + ContentType: "text/plain; charset=utf-8", + ContentEncoding: "gzip", + Expires: td.Expires, }, "public/chainOfTrust.json.asc": { // e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ./%%%/v/X // 8308d593eb56527137532595a60255a3fcfbe4b6b068e29b22d99742bad80f6f ./_/X.txt // a0ed21ab50992121f08da55365da0336062205fd6e7953dbff781a7de0d625b7 ./b/c/d.jpg - extracts: []string{ + Extracts: []string{ "8308d593eb56527137532595a60255a3fcfbe4b6b068e29b22d99742bad80f6f", }, - contentType: "text/plain; charset=utf-8", - contentEncoding: "gzip", - expires: td.Expires, + ContentType: "text/plain; charset=utf-8", + ContentEncoding: "gzip", + Expires: td.Expires, }, "public/build/X.txt": { - extracts: []string{ + Extracts: []string{ "test artifact", }, - contentType: "text/plain; charset=utf-8", - contentEncoding: "gzip", - expires: payload.Artifacts[0].Expires, + ContentType: "text/plain; charset=utf-8", + ContentEncoding: "gzip", + Expires: payload.Artifacts[0].Expires, }, "SampleArtifacts/b/c/d.jpg": { - extracts: []string{}, - contentType: "image/jpeg", - contentEncoding: "", // jpg files are blacklisted against gzip compression - expires: payload.Artifacts[0].Expires, + Extracts: []string{}, + ContentType: "image/jpeg", + ContentEncoding: "", // jpg files are blacklisted against gzip compression + Expires: payload.Artifacts[0].Expires, }, } - artifacts, err := myQueue.ListArtifacts(taskID, "0", "", "") + expectedArtifacts.Validate(t, taskID, 0) - if err != nil { - t.Fatalf("Error listing artifacts: %v", err) + b, _, _, _ := getArtifactContent(t, taskID, "public/chainOfTrust.json.asc") + if len(b) == 0 { + t.Fatalf("Could not retrieve content of public/chainOfTrust.json.asc") } - actualArtifacts := make(map[string]struct { - ContentType string `json:"contentType"` - Expires tcclient.Time `json:"expires"` - Name string `json:"name"` - StorageType string `json:"storageType"` - }, len(artifacts.Artifacts)) - - for _, actualArtifact := range artifacts.Artifacts { - actualArtifacts[actualArtifact.Name] = actualArtifact + // check openpgp signature is valid + pubKey, err := os.Open(filepath.Join("testdata", "public-openpgp-key")) + if err != nil { + t.Fatalf("Error opening public key file") } - - for artifact := range expectedArtifacts { - if a, ok := actualArtifacts[artifact]; ok { - if a.ContentType != expectedArtifacts[artifact].contentType { - t.Errorf("Artifact %s should have mime type '%v' but has '%s'", artifact, expectedArtifacts[artifact].contentType, a.ContentType) - } - if a.Expires.String() != expectedArtifacts[artifact].expires.String() { - t.Errorf("Artifact %s should have expiry '%s' but has '%s'", artifact, expires, a.Expires) - } - } else { - t.Errorf("Artifact '%s' not created", artifact) - } + defer pubKey.Close() + entityList, err := openpgp.ReadArmoredKeyRing(pubKey) + if err != nil { + t.Fatalf("Error decoding public key file") } - - // now check content was uploaded to Amazon, and is correct - + block, _ := clearsign.Decode(b) // signer of public/chainOfTrust.json.asc - signer := &openpgp.Entity{} - cotCert := &ChainOfTrustData{} - - for artifact, content := range expectedArtifacts { - b, rawResp, resp, url := getArtifactContent(t, taskID, artifact) - for _, requiredSubstring := range content.extracts { - if strings.Index(string(b), requiredSubstring) < 0 { - t.Errorf("Artifact '%s': Could not find substring %q in '%s'", artifact, requiredSubstring, string(b)) - } - } - if actualContentEncoding := rawResp.Header.Get("Content-Encoding"); actualContentEncoding != content.contentEncoding { - t.Fatalf("Expected Content-Encoding %q but got Content-Encoding %q for artifact %q from url %v", content.contentEncoding, actualContentEncoding, artifact, url) - } - if actualContentType := resp.Header.Get("Content-Type"); actualContentType != content.contentType { - t.Fatalf("Content-Type in Signed URL %v response (%v) does not match Content-Type of artifact (%v)", url, actualContentType, content.contentType) - } - // check openpgp signature is valid - if artifact == "public/chainOfTrust.json.asc" { - pubKey, err := os.Open(filepath.Join("testdata", "public-openpgp-key")) - if err != nil { - t.Fatalf("Error opening public key file") - } - defer pubKey.Close() - entityList, err := openpgp.ReadArmoredKeyRing(pubKey) - if err != nil { - t.Fatalf("Error decoding public key file") - } - block, _ := clearsign.Decode(b) - signer, err = openpgp.CheckDetachedSignature(entityList, bytes.NewBuffer(block.Bytes), block.ArmoredSignature.Body) - if err != nil { - t.Fatalf("Not able to validate openpgp signature of public/chainOfTrust.json.asc") - } - err = json.Unmarshal(block.Plaintext, cotCert) - if err != nil { - t.Fatalf("Could not interpret public/chainOfTrust.json as json") - } - } + signer, err := openpgp.CheckDetachedSignature(entityList, bytes.NewBuffer(block.Bytes), block.ArmoredSignature.Body) + if err != nil { + t.Fatalf("Not able to validate openpgp signature of public/chainOfTrust.json.asc") } - if signer == nil { - t.Fatalf("Signer of public/chainOfTrust.json.asc could not be established (is nil)") + var cotCert ChainOfTrustData + err = json.Unmarshal(block.Plaintext, &cotCert) + if err != nil { + t.Fatalf("Could not interpret public/chainOfTrust.json as json") } + if signer.Identities["Generic-Worker "] == nil { t.Fatalf("Did not get correct signer identity in public/chainOfTrust.json.asc - %#v", signer.Identities) } @@ -960,7 +923,7 @@ func TestUpload(t *testing.T) { // This trickery is to convert a TaskDefinitionResponse into a // TaskDefinitionRequest in order that we can compare. We cannot cast, so // need to transform to json as an intermediary step. - b, err := json.Marshal(cotCert.Task) + b, err = json.Marshal(cotCert.Task) if err != nil { t.Fatalf("Cannot marshal task into json - %#v\n%v", cotCert.Task, err) } diff --git a/chain_of_trust.go b/chain_of_trust.go index d6129064..2a79728b 100644 --- a/chain_of_trust.go +++ b/chain_of_trust.go @@ -11,11 +11,16 @@ import ( "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/clearsign" + "golang.org/x/crypto/openpgp/packet" "github.com/taskcluster/taskcluster-base-go/scopes" "github.com/taskcluster/taskcluster-client-go/queue" ) +const ( + ChainOfTrustKeyNotSecureMessage = "Was expecting attempt to read private chain of trust key as task user to fail - however, it did not!" +) + var ( certifiedLogPath = filepath.Join("generic-worker", "certified.log") certifiedLogName = "public/logs/certified.log" @@ -24,6 +29,7 @@ var ( ) type ChainOfTrustFeature struct { + PrivateKey *packet.PrivateKey } type ArtifactHash struct { @@ -50,7 +56,8 @@ type ChainOfTrustData struct { } type ChainOfTrustTaskFeature struct { - task *TaskRun + task *TaskRun + privKey *packet.PrivateKey } func (feature *ChainOfTrustFeature) Name() string { @@ -61,8 +68,32 @@ func (feature *ChainOfTrustFeature) PersistState() error { return nil } -func (feature *ChainOfTrustFeature) Initialise() error { - return nil +func (feature *ChainOfTrustFeature) Initialise() (err error) { + feature.PrivateKey, err = readPrivateKey() + if err != nil { + return + } + + // platform-specific mechanism to lock down file permissions + // of private signing key + err = secureSigningKey() + return +} + +func readPrivateKey() (privateKey *packet.PrivateKey, err error) { + var privKeyFile *os.File + privKeyFile, err = os.Open(config.SigningKeyLocation) + if err != nil { + return + } + defer privKeyFile.Close() + var entityList openpgp.EntityList + entityList, err = openpgp.ReadArmoredKeyRing(privKeyFile) + if err != nil { + return + } + privateKey = entityList[0].PrivateKey + return } func (feature *ChainOfTrustFeature) IsEnabled(task *TaskRun) bool { @@ -71,7 +102,8 @@ func (feature *ChainOfTrustFeature) IsEnabled(task *TaskRun) bool { func (feature *ChainOfTrustFeature) NewTaskFeature(task *TaskRun) TaskFeature { return &ChainOfTrustTaskFeature{ - task: task, + task: task, + privKey: feature.PrivateKey, } } @@ -88,6 +120,15 @@ func (cot *ChainOfTrustTaskFeature) RequiredScopes() scopes.Required { } func (cot *ChainOfTrustTaskFeature) Start() *CommandExecutionError { + // Return an error if the task user can read the private key file. + // We shouldn't be able to read the private key, if we can let's raise + // MalformedPayloadError, as it could be a problem with the task definition + // (for example, enabling chainOfTrust on a worker type that has + // runTasksAsCurrentUser enabled). + err := cot.ensureTaskUserCantReadPrivateCotKey() + if err != nil { + return MalformedPayloadError(err) + } return nil } @@ -149,17 +190,7 @@ func (cot *ChainOfTrustTaskFeature) Stop() *CommandExecutionError { } defer out.Close() - privKeyFile, e := os.Open(config.SigningKeyLocation) - if e != nil { - panic(e) - } - defer privKeyFile.Close() - entityList, e := openpgp.ReadArmoredKeyRing(privKeyFile) - if e != nil { - panic(e) - } - privKey := entityList[0].PrivateKey - w, e := clearsign.Encode(out, privKey, nil) + w, e := clearsign.Encode(out, cot.privKey, nil) if e != nil { panic(e) } diff --git a/chain_of_trust_all-unix-style.go b/chain_of_trust_all-unix-style.go new file mode 100644 index 00000000..ee371da3 --- /dev/null +++ b/chain_of_trust_all-unix-style.go @@ -0,0 +1,55 @@ +// +build !windows + +package main + +import ( + "fmt" + "os" + "os/user" + "strconv" + + "github.com/taskcluster/generic-worker/process" +) + +func (cot *ChainOfTrustTaskFeature) ensureTaskUserCantReadPrivateCotKey() error { + c, err := process.NewCommand([]string{"/bin/cat", config.SigningKeyLocation}, cwd, cot.task.EnvVars()) + if err != nil { + panic(fmt.Errorf("SERIOUS BUG: Could not create command (not even trying to execute it yet) to cat private chain of trust key - %v", err)) + } + r := c.Execute() + if !r.Failed() { + return fmt.Errorf(ChainOfTrustKeyNotSecureMessage) + } + return nil +} + +// Take ownership of private signing key, and then give it 0600 file permissions +func secureSigningKey() (err error) { + var currentUser *user.User + currentUser, err = user.Current() + if err != nil { + return err + } + var uid, gid int + uid, err = strconv.Atoi(currentUser.Uid) + if err != nil { + return err + } + gid, err = strconv.Atoi(currentUser.Gid) + if err != nil { + return err + } + err = os.Chown( + config.SigningKeyLocation, + uid, + gid, + ) + if err != nil { + return err + } + err = os.Chmod( + config.SigningKeyLocation, + 0600, + ) + return +} diff --git a/chain_of_trust_windows.go b/chain_of_trust_windows.go new file mode 100644 index 00000000..402469ad --- /dev/null +++ b/chain_of_trust_windows.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "log" + "time" + + "golang.org/x/sys/windows" + + acl "github.com/hectane/go-acl" + "github.com/taskcluster/generic-worker/process" +) + +func (cot *ChainOfTrustTaskFeature) ensureTaskUserCantReadPrivateCotKey() error { + loginInfo, err := TaskUserLoginInfo() + if err != nil { + panic(fmt.Errorf("SERIOUS BUG: Could not get login info of task user to check it can't read chain of trust private key - %v", err)) + } + TenSecondDeadline := time.Now().Add(time.Second * 10) + commandLine := `cmd.exe /c type "` + config.SigningKeyLocation + `"` + c, err := process.NewCommand(loginInfo, nil, &commandLine, &cwd, nil, TenSecondDeadline) + if err != nil { + panic(fmt.Errorf("SERIOUS BUG: Could not create command (not even trying to execute it yet) to cat private chain of trust key - %v", err)) + } + r := c.Execute() + if !r.Failed() { + log.Print(r.String()) + return fmt.Errorf(ChainOfTrustKeyNotSecureMessage) + } + return nil +} + +// Ensure only administrators have access permissions for the chain of trust +// private signing key file, and grant them full control. +func secureSigningKey() (err error) { + err = acl.Apply( + + // Private signing key file + config.SigningKeyLocation, + // delete existing permissions (ACLs) + true, + // don't inherit permissions (ACLs) + false, + // grant Administrators group full control + acl.GrantName(windows.GENERIC_ALL, "Administrators"), + ) + return +} diff --git a/helper_test.go b/helper_test.go index 2eea151c..80ed2e09 100644 --- a/helper_test.go +++ b/helper_test.go @@ -13,6 +13,8 @@ import ( "os" "path/filepath" "runtime" + "strconv" + "strings" "testing" "time" @@ -40,10 +42,6 @@ var ( func setup(t *testing.T, testName string) { // some basic setup... - cwd, err := os.Getwd() - if err != nil { - t.Fatalf("Test failed during setup phase!") - } testdataDir = filepath.Join(cwd, "testdata") // configure the worker @@ -254,6 +252,62 @@ func checkSHA256(t *testing.T, sha256Hex string, file string) { } } +type ArtifactTraits struct { + Extracts []string + ContentType string + ContentEncoding string + Expires tcclient.Time +} + +type ExpectedArtifacts map[string]ArtifactTraits + +func (expectedArtifacts ExpectedArtifacts) Validate(t *testing.T, taskID string, run int) { + + artifacts, err := myQueue.ListArtifacts(taskID, strconv.Itoa(run), "", "") + + if err != nil { + t.Fatalf("Error listing artifacts: %v", err) + } + + actualArtifacts := make(map[string]struct { + ContentType string `json:"contentType"` + Expires tcclient.Time `json:"expires"` + Name string `json:"name"` + StorageType string `json:"storageType"` + }, len(artifacts.Artifacts)) + + for _, actualArtifact := range artifacts.Artifacts { + actualArtifacts[actualArtifact.Name] = actualArtifact + } + + for artifact, expected := range expectedArtifacts { + if actual, ok := actualArtifacts[artifact]; ok { + if actual.ContentType != expected.ContentType { + t.Errorf("Artifact %s should have mime type '%v' but has '%s'", artifact, expected.ContentType, actual.ContentType) + } + if !time.Time(expected.Expires).IsZero() { + if actual.Expires.String() != expected.Expires.String() { + t.Errorf("Artifact %s should have expiry '%s' but has '%s'", artifact, expected.Expires, actual.Expires) + } + } + } else { + t.Errorf("Artifact '%s' not created", artifact) + } + b, rawResp, resp, url := getArtifactContent(t, taskID, artifact) + for _, requiredSubstring := range expected.Extracts { + if strings.Index(string(b), requiredSubstring) < 0 { + t.Errorf("Artifact '%s': Could not find substring %q in '%s'", artifact, requiredSubstring, string(b)) + } + } + if actualContentEncoding := rawResp.Header.Get("Content-Encoding"); actualContentEncoding != expected.ContentEncoding { + t.Fatalf("Expected Content-Encoding %q but got Content-Encoding %q for artifact %q from url %v", expected.ContentEncoding, actualContentEncoding, artifact, url) + } + if actualContentType := resp.Header.Get("Content-Type"); actualContentType != expected.ContentType { + t.Fatalf("Content-Type in Signed URL %v response (%v) does not match Content-Type of artifact (%v)", url, actualContentType, expected.ContentType) + } + } +} + func getArtifactContent(t *testing.T, taskID string, artifact string) ([]byte, *http.Response, *http.Response, *url.URL) { url, err := myQueue.GetLatestArtifact_SignedURL(taskID, artifact, 10*time.Minute) if err != nil { @@ -288,3 +342,20 @@ func ensureResolution(t *testing.T, taskID, state, reason string) { t.Fatalf("Expected task %v to resolve as '%v/%v' but resolved as '%v/%v'", taskID, state, reason, status.Status.State, status.Status.Runs[0].ReasonResolved) } } + +func expectChainOfTrustKeyNotSecureMessage(t *testing.T, taskID string) { + ensureResolution(t, taskID, "exception", "malformed-payload") + + expectedArtifacts := ExpectedArtifacts{ + "public/logs/live_backing.log": { + Extracts: []string{ + ChainOfTrustKeyNotSecureMessage, + }, + ContentType: "text/plain; charset=utf-8", + ContentEncoding: "gzip", + }, + } + + expectedArtifacts.Validate(t, taskID, 0) + return +} diff --git a/main.go b/main.go index d45c418b..f206fdc4 100644 --- a/main.go +++ b/main.go @@ -32,6 +32,8 @@ import ( ) var ( + // Current working directory of process + cwd string // Whether we are running under the aws provisioner configureForAws bool // General platform independent user settings, such as home directory, username... @@ -303,9 +305,12 @@ func initialiseFeatures() (err error) { Features = []Feature{ &LiveLogFeature{}, &OSGroupsFeature{}, - &ChainOfTrustFeature{}, &MountsFeature{}, &SupersedeFeature{}, + // keep chain of trust as low down as possible, as it checks permissions + // of signing key file, and a feature could change them, so we want these + // checks as late as possible + &ChainOfTrustFeature{}, } Features = append(Features, platformFeatures()...) for _, feature := range Features { @@ -529,6 +534,12 @@ func RunWorker() (exitCode ExitCode) { } }() + var err error + cwd, err = os.Getwd() + if err != nil { + panic(err) + } + log.Printf("Detected %s platform", runtime.GOOS) // number of tasks resolved since worker first ran // stored in a json file, since we may reboot between tasks etc @@ -540,7 +551,7 @@ func RunWorker() (exitCode ExitCode) { panic(err) } }(&tasksResolved) - err := taskCleanup() + err = taskCleanup() // any errors are fatal if err != nil { log.Printf("OH NO!!!\n\n%#v", err) @@ -1104,11 +1115,18 @@ func (task *TaskRun) Run() (err *executionErrors) { } } - taskFeatures := []TaskFeature{} + // tracks which Feature created which TaskFeature + type TaskFeatureOrigin struct { + t TaskFeature + f Feature + } + + taskFeatures := []TaskFeatureOrigin{} // create task features for _, feature := range Features { if feature.IsEnabled(task) { + log.Printf("Creating task feature %v...", feature.Name()) taskFeature := feature.NewTaskFeature(task) requiredScopes := taskFeature.RequiredScopes() scopesSatisfied, scopeValidationErr := scopes.Given(task.Definition.Scopes).Satisfies(requiredScopes, auth.NewNoAuth()) @@ -1130,7 +1148,13 @@ func (task *TaskRun) Run() (err *executionErrors) { task.featureArtifacts[a] = feature.Name() } } - taskFeatures = append(taskFeatures, taskFeature) + taskFeatures = append( + taskFeatures, + TaskFeatureOrigin{ + t: taskFeature, + f: feature, + }, + ) } } if err.Occurred() { @@ -1139,13 +1163,15 @@ func (task *TaskRun) Run() (err *executionErrors) { // start task features for _, taskFeature := range taskFeatures { - err.add(taskFeature.Start()) + + log.Printf("Starting task feature %v...", taskFeature.f.Name()) + err.add(taskFeature.t.Start()) if err.Occurred() { return } defer func(taskFeature TaskFeature) { err.add(taskFeature.Stop()) - }(taskFeature) + }(taskFeature.t) } defer func() { diff --git a/plat_windows.go b/plat_windows.go index 9b33f098..0360b033 100644 --- a/plat_windows.go +++ b/plat_windows.go @@ -127,7 +127,7 @@ func prepareTaskUser(userName string) (reboot bool) { } if script := config.RunAfterUserCreation; script != "" { var noDeadline time.Time - command, err := process.NewCommand(loginInfo, script, &taskContext.TaskDir, nil, noDeadline) + command, err := process.NewCommand(loginInfo, nil, &script, &taskContext.TaskDir, nil, noDeadline) if err != nil { panic(err) } @@ -213,16 +213,12 @@ func (task *TaskRun) generateCommand(index int) error { commandName := fmt.Sprintf("command_%06d", index) wrapper := filepath.Join(taskContext.TaskDir, commandName+"_wrapper.bat") log.Printf("Creating wrapper script: %v", wrapper) - loginInfo := &subprocess.LoginInfo{} - if !config.RunTasksAsCurrentUser { - hToken, err := win32.InteractiveUserToken(time.Minute) - if err != nil { - task.Error("Cannot get handle of interactive user") - return err - } - loginInfo.HUser = hToken + loginInfo, err := TaskUserLoginInfo() + if err != nil { + task.Errorf("Cannot get handle of interactive user: %v", err) + return err } - command, err := process.NewCommand(loginInfo, wrapper, &taskContext.TaskDir, nil, task.maxRunTimeDeadline) + command, err := process.NewCommand(loginInfo, nil, &wrapper, &taskContext.TaskDir, nil, task.maxRunTimeDeadline) if err != nil { return err } @@ -231,6 +227,19 @@ func (task *TaskRun) generateCommand(index int) error { return nil } +func TaskUserLoginInfo() (loginInfo *subprocess.LoginInfo, err error) { + loginInfo = &subprocess.LoginInfo{} + if !config.RunTasksAsCurrentUser { + var hToken syscall.Handle + hToken, err = win32.InteractiveUserToken(time.Minute) + if err != nil { + return + } + loginInfo.HUser = hToken + } + return +} + func (task *TaskRun) prepareCommand(index int) *CommandExecutionError { // In order that capturing of log files works, create a custom .bat file // for the task which redirects output to a log file... diff --git a/plat_windows_test.go b/plat_windows_test.go index 62dbf4c4..399386ee 100644 --- a/plat_windows_test.go +++ b/plat_windows_test.go @@ -1,6 +1,9 @@ package main -import "testing" +import ( + "path/filepath" + "testing" +) // Test APPDATA / LOCALAPPDATA folder are not shared between tasks func TestAppDataNotShared(t *testing.T) { @@ -95,3 +98,40 @@ func TestNoCreateFileMappingError(t *testing.T) { ensureResolution(t, taskID, "completed", "completed") } + +func TestChainOfTrustWithAdministratorPrivs(t *testing.T) { + setup(t, "TestChainOfTrustWithAdministratorPrivs") + defer teardown(t) + payload := GenericWorkerPayload{ + Command: []string{ + `type "` + filepath.Join(cwd, config.SigningKeyLocation) + `"`, + }, + MaxRunTime: 5, + OSGroups: []string{"Administrators"}, + Features: struct { + ChainOfTrust bool `json:"chainOfTrust,omitempty"` + }{ + ChainOfTrust: true, + }, + } + td := testTask(t) + td.Scopes = []string{ + "generic-worker:os-group:Administrators", + } + taskID := scheduleAndExecute(t, td, payload) + + if config.RunTasksAsCurrentUser { + // When running as current user, chain of trust key is not private so + // generic-worker should detect that it isn't secured from task user + // and cause malformed-payload exception. + expectChainOfTrustKeyNotSecureMessage(t, taskID) + return + + } + + // When bug 1439588 lands, if runAsAdministrator feature is enabled, the + // task resolution should be "exception" / "malformed-payload". However, + // without process elevation, Administrator rights are not available, so + // the task should resolve as "failed" / "failed". + ensureResolution(t, taskID, "failed", "failed") +} diff --git a/process/process_windows.go b/process/process_windows.go index 51957ca1..45108005 100644 --- a/process/process_windows.go +++ b/process/process_windows.go @@ -143,7 +143,7 @@ func (c *Command) Execute() (r *Result) { } } -func NewCommand(loginInfo *subprocess.LoginInfo, commandLine string, workingDirectory *string, env *[]string, deadline time.Time) (*Command, error) { +func NewCommand(loginInfo *subprocess.LoginInfo, applicationName *string, commandLine *string, workingDirectory *string, env *[]string, deadline time.Time) (*Command, error) { if deadline.IsZero() { log.Print("No deadline!") } else { @@ -153,9 +153,9 @@ func NewCommand(loginInfo *subprocess.LoginInfo, commandLine string, workingDire Subprocess: &subprocess.Subprocess{ TimeQuantum: time.Second / 4, Cmd: &subprocess.CommandLine{ - ApplicationName: nil, - CommandLine: &commandLine, - Parameters: nil, + ApplicationName: applicationName, + CommandLine: commandLine, + Parameters: nil, // unused on windows }, CurrentDirectory: workingDirectory, TimeLimit: 0, @@ -179,7 +179,7 @@ func NewCommand(loginInfo *subprocess.LoginInfo, commandLine string, workingDire }, Deadline: deadline, } - log.Printf("Created command: %v", commandLine) + log.Printf("Created command: %v to run from folder %v", *commandLine, *workingDirectory) return command, nil }