Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
Comment: | cmd/hancock: refactor to use Store interface |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | trunk |
Files: | files | file ages | folders |
SHA3-256: |
ef3dc07f7ec205bd485dd92be792dc03 |
User & Date: | dnc 2019-12-13 04:22:14 |
Context
2020-01-03
| ||
02:29 | store: consistent error when testimony not found; expose closer check-in: d22c07e59b user: dnc tags: trunk | |
2019-12-13
| ||
04:22 | cmd/hancock: refactor to use Store interface check-in: ef3dc07f7e user: dnc tags: trunk | |
04:19 | model: NewAuthority helper formats authority string consistently check-in: e0fadea8cc user: dnc tags: trunk | |
Changes
Changes to cmd/hancock/hancock.go.
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
...
110
111
112
113
114
115
116
|
//go:generate sh -c "go doc | dumbdown > README.md"
import (
"flag"
"fmt"
"log"
"github.com/pkg/errors"
"src.d10.dev/command"
"src.d10.dev/command/config"
)
func main() {
command.RegisterCommand(command.Command{
Application: `hancock`,
Description: `Create and inspect Hancock Protocol activity.`,
})
................................................................................
log.SetPrefix(fmt.Sprintf("hancock %s: ", flag.CommandLine.Args()[0]))
err = command.CurrentOperation().Operate()
command.CheckUsage(err)
command.Exit()
}
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
...
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
|
//go:generate sh -c "go doc | dumbdown > README.md" import ( "flag" "fmt" "log" "net/url" "os" "github.com/boltdb/bolt" "github.com/pkg/errors" "src.d10.dev/command" "src.d10.dev/command/config" "src.d10.dev/hancock/store" "src.d10.dev/hancock/store/boltstore" ) func main() { command.RegisterCommand(command.Command{ Application: `hancock`, Description: `Create and inspect Hancock Protocol activity.`, }) ................................................................................ log.SetPrefix(fmt.Sprintf("hancock %s: ", flag.CommandLine.Args()[0])) err = command.CurrentOperation().Operate() command.CheckUsage(err) command.Exit() } var stor map[string]store.Store // TODO(dnc): close all stores that implement io.Closer // helper to construct a store from a command line flag func storeFromFlag(flag string) (store.Store, error) { if stor == nil { stor = make(map[string]store.Store) } s, ok := stor[flag] if ok { return s, nil } u, err := url.Parse(flag) if err != nil { return nil, err } switch u.Scheme { case "file": fi, err := os.Stat(u.Path) if err != nil { return nil, err } switch mode := fi.Mode(); { case mode.IsDir(): s = store.NewFileStore(u.Path) case mode.IsRegular(): db, err := bolt.Open(u.Path, 0600, nil) if err != nil { return nil, err } s, err = boltstore.NewBoltStore(db) if err != nil { return nil, err } } // TODO(dnc): case "http(s)" default: return nil, fmt.Errorf("unexpected store sheme (%q)", u.Scheme) } return s, nil } |
Changes to cmd/hancock/testimony.go.
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 |
Authority: authorityEncoded, Content: man.Content, Encoded: rawMan, Signature: sig, } if !*noopFlag { ft := fileTestimony{Testimony: testimony, FileManifest: man} // testimony with decoded manifest stored := false for _, storeURL := range storeFlag { if storeURL == "" { continue // explicit -index="" on command line } err = publishTestimonyToStore(storeURL, ft) if err != nil { command.Errorf("%q failed to publish testimony to index (%s): %s", man.Path, storeURL, err) } else { stored = true command.V(2).Infof("%q testimony stored (%s)", man.Path, storeURL) } } if stored { command.V(1).Infof("%q testimony published", man.Path) } } } return nil } |
| | | < < > > | > > > > > |
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 |
Authority: authorityEncoded, Content: man.Content, Encoded: rawMan, Signature: sig, } if !*noopFlag { //ft := fileTestimony{Testimony: testimony, FileManifest: man} // testimony with decoded manifest stored := false for _, storeURL := range storeFlag { if storeURL == "" { continue // explicit -index="" on command line } stor, err := storeFromFlag(storeURL) if err != nil { command.Errorf("failed to publish testimony (%q) to %q: %s", man.Path, storeURL, err) continue } tkey, err := stor.PutTestimony(testimony) if err != nil { command.Errorf("failed to publish testimony (%q) to %q: %s", man.Path, storeURL, err) continue } stored = true command.V(2).Infof("published testimony (%q) to %q; key: %v", man.Path, storeURL, tkey) } if stored { command.V(1).Infof("%q testimony published", man.Path) } } } return nil } |
Changes to cmd/hancock/verify.go.
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 .. 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 ... 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 ... 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 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 |
// [authority] // d10 = ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEMLF+xyqVxGP9iK5UK/v/PFqGJbnmKZ6LRK3qmr8tEi // // (values are in the format of "~/ssh/.authorized_keys") package main import ( "encoding/json" "fmt" "io" "io/ioutil" "net/http" "net/url" "os" "path" "path/filepath" "strings" "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 { ................................................................................ } } } } // 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.", }) } var ( verifyIndexFlag command.StringSet ) func verifyMain() error { cfg, err := command.Config() command.Check(err) 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 } if len(verifyIndexFlag) == 0 { return errors.New("Index is required. Use `-index <URL>` or configuration 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 { ................................................................................ cid := model.NewSha256CID(nil) _, err = io.Copy(cid, f) if err != nil { command.Error(err) return nil // continue walk } for _, authority := range authorities { key, err := model.IndexKey(cid.Encode(), authority.public) command.Check(err) for _, index := range verifyIndexFlag { // TODO(dnc): support filesystem index u, err := url.Parse(index) command.Check(err) // TODO(dnc): validate earlier u.Path = path.Join(u.Path, "index", path.Join(key...)) resp, err := http.Get(u.String()) if err != nil { command.Info(u.Path, err) continue } defer resp.Body.Close() switch resp.StatusCode { case http.StatusOK: body, err := ioutil.ReadAll(resp.Body) if err != nil { command.Error(err) continue } // body is the CID of testimony regarding the source CID u, err = url.Parse(*ipfsFlag) command.Check(err) // TODO(dnc): validate earlier u.Path = path.Join(u.Path, "ipfs", string(body)) resp, err = httpclient.Get(u.String()) if err != nil { command.Info(err) continue } defer resp.Body.Close() switch resp.StatusCode { case http.StatusOK: // decode the body dec := json.NewDecoder(resp.Body) var testimony model.Testimony err = dec.Decode(&testimony) if err != nil { command.Info(err) continue } var manifest model.FileManifest err = json.Unmarshal(testimony.Encoded, &manifest) if err != nil { command.Info(err) continue } err = manifest.Check() if err != nil { command.Info(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?) |
< < < < < < | | | < < < < < < | | < | < < > | | | | | > | | | < > | < > > > | | < > | | < < < < < < < | < < < < > > > | < < < < < < > | < < < < < | < < < < < < < < < < | < < < > > | < > > | | < < < < | < | | | | < > | | > > > | | | | | | | | | | | | | | < < < < | < < < < |
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 .. 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 ... 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 ... 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 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 |
// [authority] // d10 = ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEMLF+xyqVxGP9iK5UK/v/PFqGJbnmKZ6LRK3qmr8tEi // // (values are in the format of "~/ssh/.authorized_keys") package main import ( "fmt" "io" "os" "path/filepath" "strings" "time" "github.com/pkg/errors" "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, authorityName string) { // 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), authorityName, 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), authorityName) 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 { ................................................................................ } } } } func init() { command.RegisterOperation(command.Operation{ Handler: verifyMain, Name: "verify", Syntax: "verify -store <URL> [-r] <file or directory> [...]", Description: "Check the authenticity of a source file.", }) } var ( verifyStoreFlag command.StringSet ) func verifyMain() error { cfg, err := command.Config() command.Check(err) defaultStore := cfg.Section("").Key("store").MustString("http://localhost:3344") strictFlag := command.OperationFlagSet.Bool("strict", false, "strict mode requires source to be endorsed") command.OperationFlagSet.Var(&verifyStoreFlag, "store", fmt.Sprintf("URL of testimony store (default %s)", defaultStore)) 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(verifyStoreFlag) == 0 && len(defaultStore) > 0 { verifyStoreFlag = append(verifyStoreFlag, strings.Split(defaultStore, ",")...) // comma separated list allowed in config file } if len(verifyStoreFlag) == 0 { return errors.New("Testimony store is required. Use `-store <URL>` or configuration file") } // TODO(dnc): confirm store is reachable command.V(3).Info("testimony store:", verifyStoreFlag) if len(command.OperationFlagSet.Args()) == 0 { return errors.New("Expected <file or directory> parameter.") } authority := make(map[string]model.Authority) for _, key := range cfg.Section("authority").Keys() { a, err := model.NewAuthority(key.Value()) if err != nil { command.Check(errors.Wrapf(err, "failed to parse public key (%q %q)", key, key.Value())) } authority[key.Name()] = a } if len(authority) == 0 { return errors.New("Trusted authority must be configured.") } 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) } } command.V(3).Infof("visiting file %q", currentFile) // verbose verified := false repudiated := false _ = repudiated // not yet used f, err := os.Open(currentFile) if err != nil { ................................................................................ cid := model.NewSha256CID(nil) _, err = io.Copy(cid, f) if err != nil { command.Error(err) return nil // continue walk } for authorityName, auth := range authority { command.V(3).Infof("file %q; authority %q (%q)", currentFile, authorityName, auth) // verbose tkey := model.TestimonyKey{ Authority: auth, Content: cid.Encode(), } for _, storeFlag := range verifyStoreFlag { command.V(3).Infof("querying %q for %q (%v)", storeFlag, currentFile, tkey) store, err := storeFromFlag(storeFlag) if err != nil { err = fmt.Errorf("failed to access testimony store (%q): %w", storeFlag, err) command.Check(err) } // TODO(dnc): concurrency for performance testimony, err := store.GetTestimony(tkey) if err != nil { // don't exit, as the testimony may be retrieved from another store command.Infof("failed to retrieve testimony via %q: %s", storeFlag, err) continue } // checks err = testimony.Verify() if err != nil { // may not be an error, if another valid testimony applies to the same file. command.Info(fmt.Errorf("invalid testimony (authority %q; file %q) ignored", authorityName, currentFile)) continue } var manifest model.FileManifest err = model.Decode(&manifest, testimony.Encoded) // 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, authorityName) continue } verified = true // TODO(dnc): consider a count of weighted authorities, rather than simple verified or not summary.summarize("confirmed", currentFile, manifest, authorityName) } // end store 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?) |