Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
Comment: | cmd/hancock: support testimony to local file, publish later introducing publish operation |
---|---|
Downloads: | Tarball | ZIP archive | SQL archive |
Timelines: | family | ancestors | descendants | both | trunk |
Files: | files | file ages | folders |
SHA3-256: |
0cefd98c324c2ebd85f04c0edefd0549 |
User & Date: | dnc 2019-06-07 00:03:54 |
Context
2019-06-07
| ||
13:32 | cmd/hancock: introduce strict mode check-in: 15250ce5f7 user: dnc tags: trunk | |
00:03 | cmd/hancock: support testimony to local file, publish later introducing publish operation check-in: 0cefd98c32 user: dnc tags: trunk | |
2019-06-05
| ||
01:40 | cmd/hancock: upstream api change (command) check-in: 147f7dbace user: dnc tags: trunk | |
Changes
Changes to cmd/hancock/README.md.
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
Construct a manifest representing a source file with: hancock manifest /path/to/source/file [...] Output is JSON-encoded data about the source file(s), in the format expected by `hancock-sign`. ## Operation testimony Publish signed testimony to an index with: hancock testimony The operation expects as input the output of `hancock-sign`. ## Operation verify Check that a copy of source matches the signed testimony of a trusted authority. hancock verify /path/to/source/copy [...] |
| > > > > > > > > | > |
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 |
Construct a manifest representing a source file with: hancock manifest /path/to/source/file [...] Output is JSON-encoded data about the source file(s), in the format expected by `hancock-sign`. ## Operation publish Publish signed testimony to an index with: hancock publish This operation published the files produced by `hancock testimony`. ## Operation testimony Produce signed testimony with: hancock testimony This operation expects as input the output of `hancock-sign`. Testimony is saved locally, or optionally published to an index. ## Operation verify Check that a copy of source matches the signed testimony of a trusted authority. hancock verify /path/to/source/copy [...] |
Added cmd/hancock/publish.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 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 |
// Copyright (C) 2019 David N. Cohen // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. // Operation publish // // Publish signed testimony to an index with: // hancock publish // // This operation published the files produced by `hancock testimony`. package main import ( "bytes" "encoding/json" "fmt" "io/ioutil" "net/http" "net/url" "os" "path/filepath" "github.com/pkg/errors" "src.d10.dev/command" "src.d10.dev/hancock/model" ) var ( // Flags that may be used by both publish and testimony operations. // single IPFS gateway ipfsFlag *string // one or more indexes indexFlag command.StringSet ) func validateIPFSFlag() error { if ipfsFlag == nil || *ipfsFlag == "" { return nil } u, err := url.Parse(*ipfsFlag) if err != nil { command.Error(errors.Wrapf(err, "bad IPFS gateway URL (%s)", *ipfsFlag)) command.Exit() } switch u.Scheme { case "http", "https": // supported default: command.Errorf("bad IPFS gateway (%q): scheme must be http(s)://", *ipfsFlag) command.Exit() } return err } func validateIndexFlag(defaultIndex string) error { if len(indexFlag) == 0 && defaultIndex != "" { indexFlag = append(indexFlag, defaultIndex) } for _, val := range indexFlag { if val == "" { continue } u, err := url.Parse(val) if err != nil { command.Error(errors.Wrapf(err, "bad index URL (%s)", val)) command.Exit() } switch u.Scheme { case "http", "https", "file": // supported scheme default: command.Errorf("bad index (%q): scheme must be http(s):// or file://", val) command.Exit() } } return nil } func init() { command.RegisterOperation(command.Operation{ Handler: publishMain, Name: "publish", Syntax: "publish [-index=<url>] [-ipfs=<url>] [in=<directory] [testimony ...]", Description: "Publish signed manifest to an index. Expects signed activity on stdin, as encoded by `hancock-sign`.", }) } func publishMain() error { cfg, err := command.Config() command.Check(err) // defaults cfgTop := cfg.Section("") cfgOp := cfg.Section("publish") defaultIndex := cfgOp.Key("index").MustString(cfgTop.Key("index").MustString("https://hancock.beyondcentral.com")) defaultIPFS := cfgOp.Key("ipfs").MustString(cfgTop.Key("ipfs").MustString("http://localhost:8080")) defaultDirectory := cfgOp.Key("testimony").MustString(cfgTop.Key("testimony").String()) // args command.OperationFlagSet.Var(&indexFlag, "index", fmt.Sprintf("URL of testimony index (default %s)", defaultIndex)) ipfsFlag = command.OperationFlagSet.String("ipfs", defaultIPFS, "URL of IPFS gateway") inFlag := command.OperationFlagSet.String("in", defaultDirectory, "directory containing testimony files") // parse flags err = command.OperationFlagSet.Parse(command.Args()[1:]) if err != nil { return err } // validate flags err = validateIndexFlag(defaultIndex) if err != nil { return err } if len(indexFlag) == 0 { return errors.New("Index URL required. Use flag `-index <URL>` or see configuration file.") } if len(command.OperationFlagSet.Args()) == 0 && *inFlag == "" { return errors.New("Testimony required. Use `-in <directory>` or specify testimony files.") } if *inFlag != "" { inStat, err := os.Stat(*inFlag) if os.IsNotExist(err) { return fmt.Errorf("Input directory does not exist (%q).", *inFlag) } if !inStat.IsDir() { return fmt.Errorf("Not a directory (%q).", *inFlag) } } in := command.OperationFlagSet.Args() if len(in) == 0 { in = append(in, *inFlag) } count := 0 // TODO(dnc): concurrency for _, fileOrDir := range in { err = filepath.Walk(fileOrDir, func(filepath string, info os.FileInfo, err error) error { if err != nil { return err } if info.IsDir() { return nil // noop, continue walk (we only process files, not directories) } f, err := os.Open(filepath) if err != nil { return err } defer f.Close() dec := json.NewDecoder(f) var testimony fileTestimony err = dec.Decode(&testimony) if err != nil { return err } // validate testimony here? indexed := false for _, idxURL := range indexFlag { err = publishTestimonyToIndex(idxURL, testimony) if err != nil { command.Error(err) } else { indexed = true } } if indexed { count++ } return err }) if err != nil { command.Error(err) } } command.V(1).Infof("Published %d items.", count) return nil } func publishTestimonyToIPFS(ipfsURL string, ft fileTestimony) error { err := ipfsAdd(ipfsURL, ft.Bytes(), ft.FileName()) return err } // publish testimony either to local filesystem or via POST to online index. func publishTestimonyToIndex(idxURL string, ft fileTestimony) error { u, err := url.Parse(idxURL) if err != nil { return err } switch u.Scheme { case "http", "https": // post to URL urlPath := idxURL + "/testimony" resp, err := http.Post(urlPath, "application/json", bytes.NewBuffer(ft.Bytes())) if err != nil { return err } defer resp.Body.Close() switch resp.StatusCode { case http.StatusOK, http.StatusCreated, http.StatusAccepted: // success default: command.V(1).Infof("Failed to post testimony: %s", string(ft.Bytes())) b, _ := ioutil.ReadAll(resp.Body) return fmt.Errorf("failed to publish testimony (%q) to index (%s): %s %q", ft.Path, idxURL, resp.Status, string(b)) } case "file": // save to directory indexValue := model.NewSha256CID(ft.Bytes()) indexKey, err := model.IndexKey(ft.CID, ft.Public) path := filepath.Join(u.Path, filepath.Join(indexKey...)) err = os.MkdirAll(filepath.Dir(path), os.ModePerm) if err != nil { return errors.Wrapf(err, "failed to save index file (%q)", path) } err = ioutil.WriteFile(path, []byte(indexValue.String()), 0644) if err != nil { return errors.Wrapf(err, "failed to save index file (%q)", path) } else { command.V(2).Infof("wrote testimony (%q) index file (%q)", ft.Path, path) } default: return fmt.Errorf("unexpected index URL scheme (%q)", u.Scheme) } return nil } |
Changes to cmd/hancock/testimony.go.
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 .. 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 .. 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 ... 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 224 225 226 227 228 |
// GNU Affero General Public License for more details. // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. // Operation testimony // // Publish signed testimony to an index with: // hancock testimony // // The operation expects as input the output of `hancock-sign`. package main import ( "bytes" "encoding/json" "fmt" "io" "io/ioutil" "net/http" "net/url" "os" "path/filepath" "github.com/pkg/errors" "golang.org/x/crypto/ssh" "src.d10.dev/command" "src.d10.dev/hancock/model" ) var ( testimonyIndexFlag command.StringSet ) func init() { command.RegisterOperation(command.Operation{ Handler: testimonyMain, Name: "testimony", Syntax: "testimony [-out <directory>] [-index <url>]", Description: "Publish signed manifest to an index. Expects signed activity on stdin, as encoded by `hancock-sign`.", }) } type fileTestimony struct { model.Testimony model.FileManifest `json:"manifest"` } func testimonyMain() error { cfg, err := command.Config() command.Check(err) // defaults ................................................................................ cfgTestimony := cfg.Section("testimony") defaultDirectory := cfgTestimony.Key("directory").MustString(cfgTop.Key("testimony").String()) defaultIndex := cfgTestimony.Key("index").MustString(cfgTop.Key("index").String()) defaultIPFS := cfgTestimony.Key("ipfs").MustString(cfgTop.Key("ipfs").String()) // define args outFlag := command.OperationFlagSet.String("out", defaultDirectory, "directory where testimony files are written") // testimonyIndexFlag := command.OperationFlagSet.String("index", "localhost:3344", "post testimony to index server") command.OperationFlagSet.Var(&testimonyIndexFlag, "index", fmt.Sprintf("URL of authenticity index; use \"file://<path>\" for a local directory (default: %s", defaultIndex)) ipfsFlag := command.OperationFlagSet.String("ipfs", defaultIPFS, "add testimony via IPFS gateway") noopFlag := command.OperationFlagSet.Bool("n", false, "do not save or publish") // parse args err = command.OperationFlagSet.Parse(command.Args()[1:]) if err != nil { return err } ................................................................................ outStat, err := os.Stat(*outFlag) if os.IsNotExist(err) { return fmt.Errorf("Output directory does not exist (%q).", *outFlag) } if !outStat.IsDir() { return fmt.Errorf("Not a directory (%q).", *outFlag) } if len(testimonyIndexFlag) == 0 && defaultIndex != "" { testimonyIndexFlag = append(testimonyIndexFlag, defaultIndex) } if len(testimonyIndexFlag) == 0 { return errors.New("Index URL required. Use flag `-index <URL>` or see configuration file.") } for _, val := range testimonyIndexFlag { u, err := url.Parse(val) if err != nil { command.Error(errors.Wrapf(err, "bad index URL (%s)", val)) command.Exit() } switch u.Scheme { case "http", "https", "file": // supported scheme default: command.Errorf("bad index (%q) - scheme must be http(s):// or file://", val) command.Exit() } } // decode input dec := json.NewDecoder(os.Stdin) // first input, the public key (corresponding to signing private key) var publicEncoded string ................................................................................ testimony := model.Testimony{ Public: publicEncoded, Encoded: rawMan, Signature: sig, } if !*noopFlag { full := fileTestimony{testimony, man} // with expanded manifest enc, _ := model.Encode(full) cid := model.NewSha256CID(enc) // TODO(dnc): goroutines for performance filename := fmt.Sprintf("%s.testimony", cid) if *outFlag != "" { // name the testimony file based on the original file, and the signing key //filename := fmt.Sprintf("%s-%x.testimony", man.CID, public.Marshal()) filepath := filepath.Join(*outFlag, filename) err = ioutil.WriteFile(filepath, enc, 0644) command.Check(err) command.V(2).Infof("%q wrote %s", man.Path, filepath) } if *ipfsFlag != "" { err = ipfsAdd(*ipfsFlag, enc, filename) if err != nil { command.Error(errors.Wrapf(err, "%q IPFS add failed (%s)", man.Path, *ipfsFlag)) } } for _, index := range testimonyIndexFlag { u, err := url.Parse(index) if err != nil { // validated earlier command.Error(err) command.Exit() } switch u.Scheme { case "http", "https": // post to URL urlPath := index + "/testimony" resp, err := http.Post(urlPath, "application/json", bytes.NewBuffer(enc)) command.Check(err) defer resp.Body.Close() switch resp.StatusCode { case http.StatusOK, http.StatusCreated, http.StatusAccepted: // success default: command.Errorf("failed to post testimony (%q) to index (%s %s)", man.Path, urlPath, resp.Status) } if command.V(1) { command.Infof("%q testimony published", man.Path) } else if command.V(2) { command.V(1).Infof("%q testimony posted to index (%s)", man.Path, index) } case "file": // save to directory indexValue := model.NewSha256CID(enc) indexKey, err := model.IndexKey(man.CID, testimony.Public) path := filepath.Join(u.Path, filepath.Join(indexKey...)) err = os.MkdirAll(filepath.Dir(path), os.ModePerm) if err != nil { command.Error(errors.Wrapf(err, "failed to save index file (%q)", path)) } err = ioutil.WriteFile(path, []byte(indexValue.String()), 0644) if err != nil { command.Error(errors.Wrapf(err, "failed to save index file (%q)", path)) } else { command.V(1).Infof("wrote testimony (%q) index file (%q)", man.Path, path) } default: command.Errorf("unexpected index URL scheme (%q)", u.Scheme) } } } } return nil } |
| | > > < | < < < < < | > > > > > > > > > > > > > > > > > > > > > > > > > > > > | < | < < | < < < < < > | < < > | < < < < < < | > > > | < < < < < < < | | | | | | < < < < < < < < < < < < < < < < < < < < < < < < < < < < > | < < < < < > | < > | | < < < < < > |
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 .. 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 ... 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 ... 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 |
// GNU Affero General Public License for more details. // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. // Operation testimony // // Produce signed testimony with: // hancock testimony // // This operation expects as input the output of // `hancock-sign`. Testimony is saved locally, or optionally published // to an index. package main import ( "encoding/json" "fmt" "io" "io/ioutil" "log" "os" "path/filepath" "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: testimonyMain, Name: "testimony", Syntax: "testimony [-out <directory>] [-index=<url>] [-ipfs=<url>]", Description: "Publish signed manifest to an index. Expects signed activity on stdin, as encoded by `hancock-sign`.", }) } // fileTestimony bundles manifest and signature, including (redundant) // encoding of manifest to ensure its CID does not change. type fileTestimony struct { model.Testimony model.FileManifest `json:"manifest"` // calculate these once, use when publishing to multiple indexes and IPFS. encoded []byte filename string } func (this *fileTestimony) FileName() string { if this.filename == "" { pub, err := this.PublicKey() if err != nil { log.Panic(err) } this.filename = fmt.Sprintf("%s-%x.testimony", this.CID, pub.Marshal()) // convention: CID of source, pubkey of sig. } return this.filename } func (this *fileTestimony) Bytes() []byte { if this.encoded == nil { var err error this.encoded, err = model.Encode(this) if err != nil { log.Panic(err) } } return this.encoded } func testimonyMain() error { cfg, err := command.Config() command.Check(err) // defaults ................................................................................ cfgTestimony := cfg.Section("testimony") defaultDirectory := cfgTestimony.Key("directory").MustString(cfgTop.Key("testimony").String()) defaultIndex := cfgTestimony.Key("index").MustString(cfgTop.Key("index").String()) defaultIPFS := cfgTestimony.Key("ipfs").MustString(cfgTop.Key("ipfs").String()) // define args outFlag := command.OperationFlagSet.String("out", defaultDirectory, "directory where testimony files are written") command.OperationFlagSet.Var(&indexFlag, "index", fmt.Sprintf("URL of testimony index (default %s)", defaultIndex)) ipfsFlag = command.OperationFlagSet.String("ipfs", defaultIPFS, "add testimony via IPFS gateway") noopFlag := command.OperationFlagSet.Bool("n", false, "do not save or publish") // parse args err = command.OperationFlagSet.Parse(command.Args()[1:]) if err != nil { return err } ................................................................................ outStat, err := os.Stat(*outFlag) if os.IsNotExist(err) { return fmt.Errorf("Output directory does not exist (%q).", *outFlag) } if !outStat.IsDir() { return fmt.Errorf("Not a directory (%q).", *outFlag) } err = validateIndexFlag(defaultIndex) if err != nil { return err } err = validateIPFSFlag() if err != nil { return err } // decode input dec := json.NewDecoder(os.Stdin) // first input, the public key (corresponding to signing private key) var publicEncoded string ................................................................................ testimony := model.Testimony{ Public: publicEncoded, Encoded: rawMan, Signature: sig, } if !*noopFlag { ft := fileTestimony{Testimony: testimony, FileManifest: man} // testimony with decoded manifest if *outFlag != "" { filepath := filepath.Join(*outFlag, ft.FileName()) err = ioutil.WriteFile(filepath, ft.Bytes(), 0644) command.Check(err) command.V(2).Infof("%q wrote %s", man.Path, filepath) } if *ipfsFlag != "" { err = publishTestimonyToIPFS(*ipfsFlag, ft) if err != nil { command.Error(errors.Wrapf(err, "%q IPFS add failed (%s)", man.Path, *ipfsFlag)) } } for _, idxURL := range indexFlag { if idxURL == "" { continue // explicit -index="" on command line } err = publishTestimonyToIndex(idxURL, ft) if err != nil { command.Errorf("%q failed to publish testimony to index (%s): %s", man.Path, idxURL, err) } else { command.V(2).Infof("%q testimony posted to index (%s)", man.Path, idxURL) } } command.V(1).Infof("%q testimony published", man.Path) } } return nil } |