forked from vanadium-archive/go.ref
/
principal.go
223 lines (209 loc) · 8.35 KB
/
principal.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
// Copyright 2016 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package agentlib
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
"time"
"v.io/v23/security"
"v.io/v23/verror"
"v.io/x/lib/metadata"
"v.io/x/lib/vlog"
"v.io/x/ref"
vsecurity "v.io/x/ref/lib/security"
"v.io/x/ref/services/agent"
"v.io/x/ref/services/agent/internal/constants"
"v.io/x/ref/services/agent/internal/launcher"
"v.io/x/ref/services/agent/internal/lock"
"v.io/x/ref/services/agent/internal/version"
)
var (
errNotADirectory = verror.Register(pkgPath+".errNotADirectory", verror.NoRetry, "{1:}{2:} {3} is not a directory{:_}")
errFilepathAbs = verror.Register(pkgPath+".errAbsFailed", verror.NoRetry, "{1:}{2:} filepath.Abs failed for {3}")
errFindAgent = verror.Register(pkgPath+".errFindAgent", verror.NoRetry, "{1:}{2:} couldn't find a suitable agent binary ({3}) or load principal in the address space of the current process ({4})")
errLaunchAgent = verror.Register(pkgPath+".errLaunchAgent", verror.NoRetry, "{1:}{2:} couldn't launch agent ({3}) or load principal locally ({4})")
errLoadLocally = verror.Register(pkgPath+".errLoadLocally", verror.NoRetry, "{1:}{2:} couldn't load principal in the address space of the current process{:_}")
)
type localPrincipal struct {
security.Principal
close func() error
}
func (p *localPrincipal) Close() error {
return p.close()
}
func loadPrincipalLocally(credentials string) (agent.Principal, error) {
p, err := vsecurity.LoadPersistentPrincipal(credentials, nil)
if err != nil {
return nil, err
}
agentDir := constants.AgentDir(credentials)
credsLock := lock.NewDirLock(credentials)
agentLock := lock.NewDirLock(agentDir)
if err := credsLock.Lock(); err != nil {
return nil, err
}
defer credsLock.Unlock()
if err := os.MkdirAll(agentDir, 0700); err != nil {
return nil, err
}
if grabbedLock, err := agentLock.TryLock(); err != nil {
return nil, err
} else if !grabbedLock {
return nil, fmt.Errorf("principal already locked by another process. Examine the contents of the lock file in %v for details", agentDir)
}
return &localPrincipal{Principal: p, close: agentLock.Unlock}, nil
}
// TODO(caprita): The agent is expected to be up for the entire lifetime of a
// client; however, it should be possible for the client app to survive an agent
// death by having the app restart the agent and reconnecting. The main barrier
// to making the agent restartable by clients dynamically is the requirement to
// fetch the principal decryption passphrase over stdin. Look into using
// something like pinentry.
// LoadPrincipal loads a principal (private key, BlessingRoots, BlessingStore)
// from the provided directory using the security agent. If an agent serving
// the principal is not present, a new one is started as a separate daemon
// process. The new agent may use os.Stdin and os.Stdout in order to fetch a
// private key decryption passphrase. If an agent serving the principal is not
// found and a new one cannot be started, LoadPrincipal tries to load the
// principal in the current process' address space, which will be exclusive for
// this process; if that fails too (for example, because the principal is
// encrypted), an error is returned. The caller should call Close on the
// returned Principal once it's no longer used, in order to free up resources
// and allow the agent to terminate once it has no more clients.
func LoadPrincipal(credsDir string) (agent.Principal, error) {
if finfo, err := os.Stat(credsDir); err != nil || err == nil && !finfo.IsDir() {
return nil, verror.New(errNotADirectory, nil, credsDir)
}
// Use an absolute path to the credentials directory to avoid relying on
// a specific cwd setting when starting the agent.
if path, err := filepath.Abs(credsDir); err != nil {
return nil, verror.New(errFilepathAbs, nil, credsDir, err)
} else {
credsDir = path
}
sockPath := constants.SocketPath(credsDir)
// Since we don't hold a lock between LaunchAgent and
// NewAgentPrincipal, the agent could go away by the time we try to
// connect to it (e.g. it decides there are no clients and exits
// voluntarily); even if we held a lock, the agent could still crash in
// the meantime. Therefore, we retry a few times before giving up.
tries, maxTries := 0, 5
var (
agentBin string
agentVersion version.T
)
dontStartAgent := os.Getenv(ref.EnvCredentialsNoAgent) != ""
for {
// If the socket exists and we're able to connect over the
// socket to an existing agent, we're done. This works even if
// the client only has access to the socket but no write access
// to the credentials dir or agent dir required in order to
// launch an agent.
if p, err := NewAgentPrincipal(sockPath, 0); err == nil {
return p, nil
} else if tries == maxTries {
return nil, err
}
// If launching an agent is not in the cards, just try loading
// the principal locally.
if dontStartAgent {
p, err := loadPrincipalLocally(credsDir)
if err != nil {
return nil, verror.New(errLoadLocally, nil, err)
}
return p, nil
}
// Go ahead and try to launch an agent. Implicitly requires the
// current process to have write permissions to the credentials
// directory.
tries++
if agentBin == "" {
var err error
if agentBin, agentVersion, err = findAgent(); err != nil {
// Try loading the principal in memory without
// an external agent.
p, lerr := loadPrincipalLocally(credsDir)
if lerr != nil {
return nil, verror.New(errFindAgent, nil, err, lerr)
}
vlog.Infof("Couldn't find a suitable agent binary (%v); loaded principal in the current process' address space.", err)
return p, nil
}
}
if err := launcher.LaunchAgent(
credsDir,
agentBin,
false,
fmt.Sprintf("--%s=%s", constants.TimeoutFlag, time.Minute),
fmt.Sprintf("--%s=%v", constants.VersionFlag, agentVersion)); err != nil {
// Try loading the principal in memory without an
// external agent.
// NOTE(caprita): If the agent fails to start because of
// bad/missing decryption passphrase, retrying locally
// is futile. There's some finessing we can do.
p, lerr := loadPrincipalLocally(credsDir)
if lerr != nil {
return nil, verror.New(errLaunchAgent, nil, err, lerr)
}
vlog.Infof("Couldn't launch agent (%v); loaded principal in the current process' address space.", err)
return p, nil
}
}
}
const agentBinName = "v23agentd"
// findAgent tries to locate an agent binary that we can run.
func findAgent() (string, version.T, error) {
// TODO(caprita): Besides checking PATH, we can also look under
// JIRI_ROOT. Also, consider caching a copy of the agent binary under
// <creds dir>/agent?
var defaultVersion version.T
agentBin, err := exec.LookPath(agentBinName)
if err != nil {
return "", defaultVersion, fmt.Errorf("failed to find %v: %v", agentBinName, err)
}
// Verify that the binary we found contains a compatible agent version
// range in its metadata.
cmd := exec.Command(agentBin, "--metadata")
var b bytes.Buffer
cmd.Stdout = &b
cmd.Dir = os.TempDir()
if err = cmd.Start(); err != nil {
return "", defaultVersion, fmt.Errorf("failed to run %v: %v", cmd.Args, err)
}
waitErr := make(chan error, 1)
go func() {
waitErr <- cmd.Wait()
}()
timeout := 5 * time.Second
select {
case err := <-waitErr:
if err != nil {
return "", defaultVersion, fmt.Errorf("%v failed: %v", cmd.Args, err)
}
case <-time.After(timeout):
if err := cmd.Process.Kill(); err != nil {
vlog.Errorf("Failed to kill %v (PID:%d): %v", cmd.Args, cmd.Process.Pid, err)
}
if err := <-waitErr; err != nil {
vlog.Errorf("%v failed: %v", cmd.Args, err)
}
return "", defaultVersion, fmt.Errorf("%v failed to complete in %v", cmd.Args, timeout)
}
md, err := metadata.FromXML(b.Bytes())
if err != nil {
return "", defaultVersion, fmt.Errorf("%v output failed to parse as XML metadata: %v", cmd.Args, err)
}
versionRange, err := version.RangeFromString(nil, md.Lookup("v23agentd.VersionMin"), md.Lookup("v23agentd.VersionMax"))
if err != nil {
return "", defaultVersion, fmt.Errorf("%v output does not contain a valid version range: %v", cmd.Args, err)
}
versionToUse, err := version.Common(nil, versionRange, version.Supported)
if err != nil {
return "", defaultVersion, fmt.Errorf("%v version incompatible: %v", cmd.Args, err)
}
return agentBin, versionToUse, nil
}