hancock

Check-in [d7ecc454b7]
Login

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:cmd/hancock verify: produce concise human-readable summary
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: d7ecc454b7c98b99837fd9a485962fcb7bf3f0bb4ff89fa6de5b8ab7963b12af
User & Date: dnc 2019-06-09 10:59:16
Context
2019-06-09
11:40
manifest.Check() imposes limits on manifest data check-in: d3764da499 user: dnc tags: trunk
10:59
cmd/hancock verify: produce concise human-readable summary check-in: d7ecc454b7 user: dnc tags: trunk
2019-06-08
21:12
manifest now supports human-readable messages check-in: 657eb8d965 user: dnc tags: trunk
Changes
Hide Diffs Unified Diffs Ignore Whitespace Patch

Changes to cmd/hancock/verify.go.

40
41
42
43
44
45
46























































47
48
49
50
51
52
53
..
65
66
67
68
69
70
71

72
73
74
75
76
77
78
..
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
...
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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248












249
250
	"time"

	"github.com/pkg/errors"
	"golang.org/x/crypto/ssh"
	"src.d10.dev/command"
	"src.d10.dev/hancock/model"
)
























































func init() {
	command.RegisterOperation(command.Operation{
		Handler:     verifyMain,
		Name:        "verify",
		Syntax:      "verify -index <URL> [-r] <file or directory> [...]",
		Description: "Check the authenticity of a source file.",
................................................................................
	defaultIndex := cfg.Section("").Key("index").MustString("https://hancock.beyondcentral.com")
	defaultIPFS := cfg.Section("").Key("ipfs").MustString("http://localhost:8080")

	strictFlag := command.OperationFlagSet.Bool("strict", false, "strict mode requires source to be endorsed")
	command.OperationFlagSet.Var(&verifyIndexFlag, "index", fmt.Sprintf("URL of authenticity index (default %s)", defaultIndex))
	ipfsFlag := command.OperationFlagSet.String("ipfs", defaultIPFS, "URL of manifests")
	recurseFlag := command.OperationFlagSet.Bool("r", false, "verify all files in directory")

	err = command.OperationFlagSet.Parse(command.Args()[1:])
	if err != nil {
		return err
	}
	if len(verifyIndexFlag) == 0 && len(defaultIndex) > 0 {
		verifyIndexFlag = append(verifyIndexFlag, strings.Split(defaultIndex, ",")...) // comma separated list allowed in config file
	}
................................................................................
	}
	// TODO(dnc): validate an index is reachable

	if len(command.OperationFlagSet.Args()) == 0 {
		return errors.New("Expected <file or directory> parameter.")
	}

	// authorized keys, from config files
	type auth struct {
		name   string
		public ssh.PublicKey
	}

	authorities := make([]auth, 0)
	for _, key := range cfg.Section("authority").Keys() {
		public, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Value()))
		if err != nil {
			command.Check(errors.Wrapf(err, "failed to parse public key (%q %q)", key, key.Value()))
		}
		authorities = append(authorities, auth{key.Name(), public})
	}

	httpclient := http.Client{
		Timeout: time.Duration(10 * time.Second),
	}

	noRecurse := errors.New("no recurse") // prepare for filepath.Walk





	// TODO(dnc): goroutines for performance
	for _, arg := range command.OperationFlagSet.Args() {
		err = filepath.Walk(arg, func(filepath string, info os.FileInfo, err error) error {
			if err != nil {
				command.Error(err) // log error
				return nil         // continue walk
			}
			if info.IsDir() {
				if *recurseFlag == false {
					return noRecurse // end of walk
				} else {
					return nil // noop (we only process files)
				}
			}

			verified := false



			f, err := os.Open(filepath)
			if err != nil {
				command.Error(errors.Wrapf(err, "failed to open file (%q)", filepath))
				return nil // continue walk
			}
			defer f.Close()
			cid := model.NewSha256CID(nil)
			_, err = io.Copy(cid, f)
			if err != nil {
				command.Error(err)
................................................................................
								continue
							}

							// checks
							err = testimony.Verify()
							if err != nil {
								// BUG(dnc): may not be an error, if another valid testimony applies to the same file.
								command.Info(errors.Wrapf(err, "invalid testimony (%q)", filepath))
								continue
							}

							// path check is disabled because an identical file may appear in more than one place.  TODO(dnc) fix this, or enable strict mode
							//if manifest.Path != filepath {
							//	command.Infof("path mismatch (local: %q, testimony: %q)", filepath, manifest.Path)
							//	continue
							//}

							timestamp := time.Unix(manifest.Time, 0)

							if manifest.Quality&model.Valid == 0 {
								// Testimony is repudiation.
								command.Errorf("%q repudiated by %s (%q) as %s on %s", filepath, manifest.Role, authority.name, model.QualityString(manifest.Quality), timestamp.Format(time.RFC850)) // Errorf will cause non-zero exit status





								continue
							}

							if command.V(1) {
								// TODO(dnc) format time in friendly "days ago" syntax
								if filepath != manifest.Path {
									command.Infof("%q confirmed by %s (%q) as %q on %s", filepath, manifest.Role, authority.name, manifest.Path, timestamp.Format(time.RFC850))
								} else {
									command.Infof("%q confirmed by %s (%q) on %s", filepath, manifest.Role, authority.name, timestamp.Format(time.RFC850))
								}

							}
							verified = true // TODO(dnc): consider a count of weighted authorities, rather than simple verified or not
						default:
							command.V(2).Info(filepath, u.String(), resp.Status)
							continue
						} // end ipfs status
					default:
						command.V(2).Info(filepath, u.String(), resp.Status)
					} // end index status
				} // end index loop
			} // end authority loop

			if !verified {
				if *strictFlag {
					// command.Error sets exit status code (not zero)
					command.Errorf("%q not verified", filepath)
					command.Exit() // exit early when strict mode (should this be only when verbose is off?)
				} else if command.V(2) {
					command.Infof("%q not verified", filepath)
				}
			}
			return nil // continue file walk
		})
		if err == noRecurse {
			return fmt.Errorf("Expected file name. Use -r to recurse directories (%q).", arg)
		} else if err != nil {
			command.Error(err) // log error, continue arg loop
		}
	}












	return nil
}







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







 







>







 







<
<
<
<
<
<
|





|








>
>
>
>


|













>
>

|

|







 







|




|
|



<
<


<
>
>
>
>
>



|
<
<
<
<
<
|
>
|
<

|



|







|


|










>
>
>
>
>
>
>
>
>
>
>
>


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
...
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
...
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
...
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260


261
262

263
264
265
266
267
268
269
270
271





272
273
274

275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
	"time"

	"github.com/pkg/errors"
	"golang.org/x/crypto/ssh"
	"src.d10.dev/command"
	"src.d10.dev/hancock/model"
)

// Produce a summary of human-readable information with a minimum of
// redundancy.
type SummaryMessages map[string]int // key is human-readable message, count is how many times it was produced

type Summary map[string]SummaryMessages // key is directory that was inspected

func (summary Summary) summarize(typ string, currentFile string, manifest model.FileManifest, authority Authority) {
	// Produce summary.  Messages repeated for multiple
	// files should appear once in the summary.  So, omit
	// file-specific info.
	currentDir := filepath.Dir(currentFile)

	_, ok := summary[currentDir]
	if !ok {
		summary[currentDir] = make(SummaryMessages)
	}
	timestamp := time.Unix(manifest.Time, 0)

	for _, m := range manifest.Message {
		msg := fmt.Sprintf("%s (%s) by %s: %s", typ, model.QualityString(manifest.Quality), authority.name, m)
		count, _ := summary[currentDir][msg]
		summary[currentDir][msg] = count + 1

		if command.V(1) { // control verbosity
			if currentFile == manifest.Path {
				command.Errorf("%q %s on %s: %s", currentFile, manifest.Role, timestamp.Format(time.RFC850), msg)
			} else {
				command.Errorf("%q (as %q) %s on %s: %s", currentFile, manifest.Path, manifest.Role, timestamp.Format(time.RFC850), msg)
			}
		}
	}
	if len(manifest.Message) == 0 {
		// no explicit message
		msg := fmt.Sprintf("%s (%s) by %s", typ, model.QualityString(manifest.Quality), authority.name)
		count, _ := summary[currentDir][msg]
		summary[currentDir][msg] = count + 1

		if command.V(1) { // control verbosity
			if currentFile == manifest.Path {
				command.Errorf("%q %s on %s: %s", currentFile, manifest.Role, timestamp.Format(time.RFC850), msg)
			} else {
				command.Errorf("%q (as %q) %s on %s: %s", currentFile, manifest.Path, manifest.Role, timestamp.Format(time.RFC850), msg)
			}
		}

	}

}

// authorized keys, from config files
type Authority struct {
	name   string
	public ssh.PublicKey
}

func init() {
	command.RegisterOperation(command.Operation{
		Handler:     verifyMain,
		Name:        "verify",
		Syntax:      "verify -index <URL> [-r] <file or directory> [...]",
		Description: "Check the authenticity of a source file.",
................................................................................
	defaultIndex := cfg.Section("").Key("index").MustString("https://hancock.beyondcentral.com")
	defaultIPFS := cfg.Section("").Key("ipfs").MustString("http://localhost:8080")

	strictFlag := command.OperationFlagSet.Bool("strict", false, "strict mode requires source to be endorsed")
	command.OperationFlagSet.Var(&verifyIndexFlag, "index", fmt.Sprintf("URL of authenticity index (default %s)", defaultIndex))
	ipfsFlag := command.OperationFlagSet.String("ipfs", defaultIPFS, "URL of manifests")
	recurseFlag := command.OperationFlagSet.Bool("r", false, "verify all files in directory")

	err = command.OperationFlagSet.Parse(command.Args()[1:])
	if err != nil {
		return err
	}
	if len(verifyIndexFlag) == 0 && len(defaultIndex) > 0 {
		verifyIndexFlag = append(verifyIndexFlag, strings.Split(defaultIndex, ",")...) // comma separated list allowed in config file
	}
................................................................................
	}
	// TODO(dnc): validate an index is reachable

	if len(command.OperationFlagSet.Args()) == 0 {
		return errors.New("Expected <file or directory> parameter.")
	}







	authorities := make([]Authority, 0)
	for _, key := range cfg.Section("authority").Keys() {
		public, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Value()))
		if err != nil {
			command.Check(errors.Wrapf(err, "failed to parse public key (%q %q)", key, key.Value()))
		}
		authorities = append(authorities, Authority{key.Name(), public})
	}

	httpclient := http.Client{
		Timeout: time.Duration(10 * time.Second),
	}

	noRecurse := errors.New("no recurse") // prepare for filepath.Walk

	// Exit status
	status := 0
	summary := make(Summary)

	// TODO(dnc): goroutines for performance
	for _, arg := range command.OperationFlagSet.Args() {
		err = filepath.Walk(arg, func(currentFile string, info os.FileInfo, err error) error {
			if err != nil {
				command.Error(err) // log error
				return nil         // continue walk
			}
			if info.IsDir() {
				if *recurseFlag == false {
					return noRecurse // end of walk
				} else {
					return nil // noop (we only process files)
				}
			}

			verified := false
			repudiated := false
			_ = repudiated // not yet used

			f, err := os.Open(currentFile)
			if err != nil {
				command.Error(errors.Wrapf(err, "failed to open file (%q)", currentFile))
				return nil // continue walk
			}
			defer f.Close()
			cid := model.NewSha256CID(nil)
			_, err = io.Copy(cid, f)
			if err != nil {
				command.Error(err)
................................................................................
								continue
							}

							// checks
							err = testimony.Verify()
							if err != nil {
								// BUG(dnc): may not be an error, if another valid testimony applies to the same file.
								command.Info(errors.Wrapf(err, "invalid testimony (%q)", currentFile))
								continue
							}

							// path check is disabled because an identical file may appear in more than one place.  TODO(dnc) fix this, or enable strict mode
							//if manifest.Path != currentFile {
							//	command.Infof("path mismatch (local: %q, testimony: %q)", currentFile, manifest.Path)
							//	continue
							//}



							if manifest.Quality&model.Valid == 0 {
								// Testimony is repudiation.

								repudiated = true
								status = 1 // non-zero exit status

								summary.summarize("repudiated", currentFile, manifest, authority)

								continue
							}

							verified = true // TODO(dnc): consider a count of weighted authorities, rather than simple verified or not






							summary.summarize("confirmed", currentFile, manifest, authority)


						default:
							command.V(2).Info(currentFile, u.String(), resp.Status)
							continue
						} // end ipfs status
					default:
						command.V(2).Info(currentFile, u.String(), resp.Status)
					} // end index status
				} // end index loop
			} // end authority loop

			if !verified {
				if *strictFlag {
					// command.Error sets exit status code (not zero)
					command.Errorf("%q not verified", currentFile)
					command.Exit() // exit early when strict mode (should this be only when verbose is off?)
				} else if command.V(2) {
					command.Infof("%q not verified", currentFile)
				}
			}
			return nil // continue file walk
		})
		if err == noRecurse {
			return fmt.Errorf("Expected file name. Use -r to recurse directories (%q).", arg)
		} else if err != nil {
			command.Error(err) // log error, continue arg loop
		}
	}

	// write summary
	for dir, sum := range summary {
		for m, count := range sum {
			if dir == "." {
				fmt.Printf("%d files %s\n", count, m)
			} else {
				fmt.Printf("%d files in %q %s\n", count, dir, m)
			}
		}
	}
	os.Exit(status)
	return nil
}