hancock

Check-in [0cefd98c32]
Login

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: 0cefd98c324c2ebd85f04c0edefd0549cf6027ab9560676d214a6c4e311a63d2
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
Hide Diffs Unified Diffs Ignore Whitespace Patch

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
}