Skip to content

Commit ceb306f

Browse files
committed
feat: checksum pinning
1 parent 474eec1 commit ceb306f

17 files changed

+195
-15
lines changed

errors/errors.go

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const (
2626
CodeTaskfileNetworkTimeout
2727
CodeTaskfileInvalid
2828
CodeTaskfileCycle
29+
CodeTaskfileDoesNotMatchChecksum
2930
)
3031

3132
// Task related exit codes

errors/errors_taskfile.go

+21
Original file line numberDiff line numberDiff line change
@@ -187,3 +187,24 @@ func (err TaskfileCycleError) Error() string {
187187
func (err TaskfileCycleError) Code() int {
188188
return CodeTaskfileCycle
189189
}
190+
191+
// TaskfileDoesNotMatchChecksum is returned when a Taskfile's checksum does not
192+
// match the one pinned in the parent Taskfile.
193+
type TaskfileDoesNotMatchChecksum struct {
194+
URI string
195+
ExpectedChecksum string
196+
ActualChecksum string
197+
}
198+
199+
func (err *TaskfileDoesNotMatchChecksum) Error() string {
200+
return fmt.Sprintf(
201+
"task: The checksum of the Taskfile at %q does not match!\ngot: %q\nwant: %q",
202+
err.URI,
203+
err.ActualChecksum,
204+
err.ExpectedChecksum,
205+
)
206+
}
207+
208+
func (err *TaskfileDoesNotMatchChecksum) Code() int {
209+
return CodeTaskfileDoesNotMatchChecksum
210+
}

executor_test.go

+19
Original file line numberDiff line numberDiff line change
@@ -958,3 +958,22 @@ func TestFuzzyModel(t *testing.T) {
958958
WithTask("install"),
959959
)
960960
}
961+
962+
func TestIncludeChecksum(t *testing.T) {
963+
t.Parallel()
964+
965+
NewExecutorTest(t,
966+
WithName("correct"),
967+
WithExecutorOptions(
968+
task.WithDir("testdata/includes_checksum/correct"),
969+
),
970+
)
971+
972+
NewExecutorTest(t,
973+
WithName("incorrect"),
974+
WithExecutorOptions(
975+
task.WithDir("testdata/includes_checksum/incorrect"),
976+
),
977+
WithSetupError(),
978+
)
979+
}

taskfile/ast/include.go

+5
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type (
2424
AdvancedImport bool
2525
Vars *Vars
2626
Flatten bool
27+
Checksum string
2728
}
2829
// Includes is an ordered map of namespaces to includes.
2930
Includes struct {
@@ -165,6 +166,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
165166
Aliases []string
166167
Excludes []string
167168
Vars *Vars
169+
Checksum string
168170
}
169171
if err := node.Decode(&includedTaskfile); err != nil {
170172
return errors.NewTaskfileDecodeError(err, node)
@@ -178,6 +180,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
178180
include.AdvancedImport = true
179181
include.Vars = includedTaskfile.Vars
180182
include.Flatten = includedTaskfile.Flatten
183+
include.Checksum = includedTaskfile.Checksum
181184
return nil
182185
}
183186

@@ -200,5 +203,7 @@ func (include *Include) DeepCopy() *Include {
200203
AdvancedImport: include.AdvancedImport,
201204
Vars: include.Vars.DeepCopy(),
202205
Flatten: include.Flatten,
206+
Aliases: deepcopy.Slice(include.Aliases),
207+
Checksum: include.Checksum,
203208
}
204209
}

taskfile/node.go

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ type Node interface {
1717
Parent() Node
1818
Location() string
1919
Dir() string
20+
Checksum() string
21+
Verify(checksum string) bool
2022
ResolveEntrypoint(entrypoint string) (string, error)
2123
ResolveDir(dir string) (string, error)
2224
}

taskfile/node_base.go

+17-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ type (
77
// designed to be embedded in other node types so that this boilerplate code
88
// does not need to be repeated.
99
BaseNode struct {
10-
parent Node
11-
dir string
10+
parent Node
11+
dir string
12+
checksum string
1213
}
1314
)
1415

@@ -32,10 +33,24 @@ func WithParent(parent Node) NodeOption {
3233
}
3334
}
3435

36+
func WithChecksum(checksum string) NodeOption {
37+
return func(node *BaseNode) {
38+
node.checksum = checksum
39+
}
40+
}
41+
3542
func (node *BaseNode) Parent() Node {
3643
return node.parent
3744
}
3845

3946
func (node *BaseNode) Dir() string {
4047
return node.dir
4148
}
49+
50+
func (node *BaseNode) Checksum() string {
51+
return node.checksum
52+
}
53+
54+
func (node *BaseNode) Verify(checksum string) bool {
55+
return node.checksum == "" || node.checksum == checksum
56+
}

taskfile/reader.go

+42-11
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ func (r *Reader) include(ctx context.Context, node Node) error {
250250
AdvancedImport: include.AdvancedImport,
251251
Excludes: include.Excludes,
252252
Vars: include.Vars,
253+
Checksum: include.Checksum,
253254
}
254255
if err := cache.Err(); err != nil {
255256
return err
@@ -267,6 +268,7 @@ func (r *Reader) include(ctx context.Context, node Node) error {
267268

268269
includeNode, err := NewNode(entrypoint, include.Dir, r.insecure,
269270
WithParent(node),
271+
WithChecksum(include.Checksum),
270272
)
271273
if err != nil {
272274
if include.Optional {
@@ -362,7 +364,24 @@ func (r *Reader) readNodeContent(ctx context.Context, node Node) ([]byte, error)
362364
if node, isRemote := node.(RemoteNode); isRemote {
363365
return r.readRemoteNodeContent(ctx, node)
364366
}
365-
return node.Read()
367+
368+
// Read the Taskfile
369+
b, err := node.Read()
370+
if err != nil {
371+
return nil, err
372+
}
373+
374+
// If the given checksum doesn't match the sum pinned in the Taskfile
375+
checksum := checksum(b)
376+
if !node.Verify(checksum) {
377+
return nil, &errors.TaskfileDoesNotMatchChecksum{
378+
URI: node.Location(),
379+
ExpectedChecksum: node.Checksum(),
380+
ActualChecksum: checksum,
381+
}
382+
}
383+
384+
return b, nil
366385
}
367386

368387
func (r *Reader) readRemoteNodeContent(ctx context.Context, node RemoteNode) ([]byte, error) {
@@ -427,17 +446,29 @@ func (r *Reader) readRemoteNodeContent(ctx context.Context, node RemoteNode) ([]
427446
}
428447

429448
r.debugf("found remote file at %q\n", node.Location())
449+
450+
// If the given checksum doesn't match the sum pinned in the Taskfile
430451
checksum := checksum(downloadedBytes)
431-
prompt := cache.ChecksumPrompt(checksum)
432-
433-
// Prompt the user if required
434-
if prompt != "" {
435-
if err := func() error {
436-
r.promptMutex.Lock()
437-
defer r.promptMutex.Unlock()
438-
return r.promptf(prompt, node.Location())
439-
}(); err != nil {
440-
return nil, &errors.TaskfileNotTrustedError{URI: node.Location()}
452+
if !node.Verify(checksum) {
453+
return nil, &errors.TaskfileDoesNotMatchChecksum{
454+
URI: node.Location(),
455+
ExpectedChecksum: node.Checksum(),
456+
ActualChecksum: checksum,
457+
}
458+
}
459+
460+
// If there is no manual checksum pin, run the automatic checks
461+
if node.Checksum() == "" {
462+
// Prompt the user if required
463+
prompt := cache.ChecksumPrompt(checksum)
464+
if prompt != "" {
465+
if err := func() error {
466+
r.promptMutex.Lock()
467+
defer r.promptMutex.Unlock()
468+
return r.promptf(prompt, node.Location())
469+
}(); err != nil {
470+
return nil, &errors.TaskfileNotTrustedError{URI: node.Location()}
471+
}
441472
}
442473
}
443474

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
version: '3'
2+
3+
includes:
4+
included:
5+
taskfile: ../included.yml
6+
internal: true
7+
checksum: c97f39eb96fe3fa5fe2a610d244b8449897b06f0c93821484af02e0999781bf5
8+
9+
tasks:
10+
default:
11+
cmds:
12+
- task: included:default
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
task: [included:default] echo "Hello, World!"
2+
Hello, World!
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
version: '3'
2+
3+
includes:
4+
included:
5+
taskfile: https://taskfile.dev
6+
internal: true
7+
checksum: c153e97e0b3a998a7ed2e61064c6ddaddd0de0c525feefd6bba8569827d8efe9
8+
9+
tasks:
10+
default:
11+
cmds:
12+
- task: included:default
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
version: '3'
2+
3+
tasks:
4+
default:
5+
cmds:
6+
- echo "Hello, World!"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
version: '3'
2+
3+
includes:
4+
included:
5+
taskfile: ../included.yml
6+
internal: true
7+
checksum: foo
8+
9+
tasks:
10+
default:
11+
cmds:
12+
- task: included:default
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
task: The checksum of the Taskfile at "/home/pete/dev/github.com/go-task/task/testdata/includes_checksum/included.yml" does not match!
2+
got: "c97f39eb96fe3fa5fe2a610d244b8449897b06f0c93821484af02e0999781bf5"
3+
want: "foo"

testdata/includes_checksum/incorrect/testdata/TestIncludeChecksum-incorrect.golden

Whitespace-only changes.

website/docs/experiments/remote_taskfiles.mdx

+36-2
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,11 @@ includes:
182182

183183
## Security
184184

185+
### Automatic checksums
186+
185187
Running commands from sources that you do not control is always a potential
186-
security risk. For this reason, we have added some checks when using remote
187-
Taskfiles:
188+
security risk. For this reason, we have added some automatic checks when using
189+
remote Taskfiles:
188190

189191
1. When running a task from a remote Taskfile for the first time, Task will
190192
print a warning to the console asking you to check that you are sure that you
@@ -209,6 +211,38 @@ flag. Before enabling this flag, you should:
209211
containing a commit hash) to prevent Task from automatically accepting a
210212
prompt that says a remote Taskfile has changed.
211213

214+
### Manual checksum pinning
215+
216+
Alternatively, if you expect the contents of your remote files to be a constant
217+
value, you can pin the checksum of the included file instead:
218+
219+
```yaml
220+
version: '3'
221+
222+
includes:
223+
included:
224+
taskfile: https://taskfile.dev
225+
checksum: c153e97e0b3a998a7ed2e61064c6ddaddd0de0c525feefd6bba8569827d8efe9
226+
```
227+
228+
This will disable the automatic checksum prompts discussed above. However, if
229+
the checksums do not match, Task will exit immediately with an error. When
230+
setting this up for the first time, you may not know the correct value of the
231+
checksum. There are a couple of ways you can obtain this:
232+
233+
1. Add the include normally without the `checksum` key. The first time you run
234+
the included Taskfile, a `.task/remote` temporary directory is created. Find
235+
the correct set of files for your included Taskfile and open the file that
236+
ends with `.checksum`. You can copy the contents of this file and paste it
237+
into the `checksum` key of your include. This method is safest as it allows
238+
you to inspect the downloaded Taskfile before you pin it.
239+
2. Alternatively, add the include with a temporary random value in the
240+
`checksum` key. When you try to run the Taskfile, you will get an error that
241+
will report the incorrect expected checksum and the actual checksum. You can
242+
copy the actual checksum and replace your temporary random value.
243+
244+
### TLS
245+
212246
Task currently supports both `http` and `https` URLs. However, the `http`
213247
requests will not execute by default unless you run the task with the
214248
`--insecure` flag. This is to protect you from accidentally running a remote

website/docs/reference/schema.mdx

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ toc_max_heading_level: 5
3434
| `internal` | `bool` | `false` | Stops any task in the included Taskfile from being callable on the command line. These commands will also be omitted from the output when used with `--list`. |
3535
| `aliases` | `[]string` | | Alternative names for the namespace of the included Taskfile. |
3636
| `vars` | `map[string]Variable` | | A set of variables to apply to the included Taskfile. |
37+
| `checksum` | `string` | | The checksum of the file you expect to include. If the checksum does not match, the file will not be included. |
3738

3839
:::info
3940

website/static/schema.json

+4
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,10 @@
684684
"vars": {
685685
"description": "A set of variables to apply to the included Taskfile.",
686686
"$ref": "#/definitions/vars"
687+
},
688+
"checksum": {
689+
"description": "The checksum of the file you expect to include. If the checksum does not match, the file will not be included.",
690+
"type": "string"
687691
}
688692
}
689693
}

0 commit comments

Comments
 (0)