master
go 310 lines 11 KB
Raw
1 package cli
2
3 import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 "github.com/ipfs/boxo/verifcid"
11 "github.com/ipfs/kubo/config"
12 "github.com/ipfs/kubo/test/cli/harness"
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/require"
15 )
16
17 func TestIdentityCIDOverflowProtection(t *testing.T) {
18 t.Parallel()
19
20 t.Run("ipfs add --hash=identity with small data succeeds", func(t *testing.T) {
21 t.Parallel()
22 node := harness.NewT(t).NewNode().Init().StartDaemon()
23 defer node.StopDaemon()
24
25 // small data that fits in identity CID
26 smallData := "small data"
27 tempFile := filepath.Join(node.Dir, "small.txt")
28 err := os.WriteFile(tempFile, []byte(smallData), 0644)
29 require.NoError(t, err)
30
31 res := node.IPFS("add", "--hash=identity", tempFile)
32 assert.NoError(t, res.Err)
33 cid := strings.Fields(res.Stdout.String())[1]
34
35 // verify it's actually using identity hash
36 res = node.IPFS("cid", "format", "-f", "%h", cid)
37 assert.NoError(t, res.Err)
38 assert.Equal(t, "identity", res.Stdout.Trimmed())
39 })
40
41 t.Run("ipfs add --hash=identity with large data fails", func(t *testing.T) {
42 t.Parallel()
43 node := harness.NewT(t).NewNode().Init().StartDaemon()
44 defer node.StopDaemon()
45
46 // data larger than verifcid.DefaultMaxIdentityDigestSize
47 largeData := strings.Repeat("x", verifcid.DefaultMaxIdentityDigestSize+50)
48 tempFile := filepath.Join(node.Dir, "large.txt")
49 err := os.WriteFile(tempFile, []byte(largeData), 0644)
50 require.NoError(t, err)
51
52 res := node.RunIPFS("add", "--hash=identity", tempFile)
53 assert.NotEqual(t, 0, res.ExitErr.ExitCode())
54 // should error with digest too large message
55 assert.Contains(t, res.Stderr.String(), "digest too large")
56 })
57
58 t.Run("ipfs add --inline with valid --inline-limit succeeds", func(t *testing.T) {
59 t.Parallel()
60 node := harness.NewT(t).NewNode().Init().StartDaemon()
61 defer node.StopDaemon()
62
63 smallData := "small inline data"
64 tempFile := filepath.Join(node.Dir, "inline.txt")
65 err := os.WriteFile(tempFile, []byte(smallData), 0644)
66 require.NoError(t, err)
67
68 // use limit just under the maximum
69 limit := verifcid.DefaultMaxIdentityDigestSize - 10
70 res := node.IPFS("add", "--inline", fmt.Sprintf("--inline-limit=%d", limit), tempFile)
71 assert.NoError(t, res.Err)
72 cid := strings.Fields(res.Stdout.String())[1]
73
74 // verify the CID is using identity hash (inline)
75 res = node.IPFS("cid", "format", "-f", "%h", cid)
76 assert.NoError(t, res.Err)
77 assert.Equal(t, "identity", res.Stdout.Trimmed())
78
79 // verify the codec (may be dag-pb or raw depending on kubo version)
80 res = node.IPFS("cid", "format", "-f", "%c", cid)
81 assert.NoError(t, res.Err)
82 // Accept either raw or dag-pb as both are valid for inline data
83 codec := res.Stdout.Trimmed()
84 assert.True(t, codec == "raw" || codec == "dag-pb", "expected raw or dag-pb codec, got %s", codec)
85 })
86
87 t.Run("ipfs add --inline with excessive --inline-limit fails", func(t *testing.T) {
88 t.Parallel()
89 node := harness.NewT(t).NewNode().Init().StartDaemon()
90 defer node.StopDaemon()
91
92 smallData := "data"
93 tempFile := filepath.Join(node.Dir, "inline2.txt")
94 err := os.WriteFile(tempFile, []byte(smallData), 0644)
95 require.NoError(t, err)
96
97 excessiveLimit := verifcid.DefaultMaxIdentityDigestSize + 50
98 res := node.RunIPFS("add", "--inline", fmt.Sprintf("--inline-limit=%d", excessiveLimit), tempFile)
99 assert.NotEqual(t, 0, res.ExitErr.ExitCode())
100 assert.Contains(t, res.Stderr.String(), fmt.Sprintf("inline-limit %d exceeds maximum allowed size of %d bytes", excessiveLimit, verifcid.DefaultMaxIdentityDigestSize))
101 })
102
103 t.Run("ipfs files write --hash=identity appending to identity CID switches to configured hash", func(t *testing.T) {
104 t.Parallel()
105 node := harness.NewT(t).NewNode().Init().StartDaemon()
106 defer node.StopDaemon()
107
108 // create initial small file with identity CID
109 initialData := "initial"
110 tempFile := filepath.Join(node.Dir, "initial.txt")
111 err := os.WriteFile(tempFile, []byte(initialData), 0644)
112 require.NoError(t, err)
113
114 res := node.IPFS("add", "--hash=identity", tempFile)
115 assert.NoError(t, res.Err)
116 cid1 := strings.Fields(res.Stdout.String())[1]
117
118 // verify initial CID uses identity
119 res = node.IPFS("cid", "format", "-f", "%h", cid1)
120 assert.NoError(t, res.Err)
121 assert.Equal(t, "identity", res.Stdout.Trimmed())
122
123 // copy to MFS
124 res = node.IPFS("files", "cp", fmt.Sprintf("/ipfs/%s", cid1), "/identity-file")
125 assert.NoError(t, res.Err)
126
127 // append data that would exceed identity CID limit
128 appendData := strings.Repeat("a", verifcid.DefaultMaxIdentityDigestSize)
129 appendFile := filepath.Join(node.Dir, "append.txt")
130 err = os.WriteFile(appendFile, []byte(appendData), 0644)
131 require.NoError(t, err)
132
133 // append to the end of the file
134 // get the current data size
135 res = node.IPFS("files", "stat", "--format", "<size>", "/identity-file")
136 assert.NoError(t, res.Err)
137 size := res.Stdout.Trimmed()
138 // this should succeed because DagModifier in boxo handles the overflow
139 res = node.IPFS("files", "write", "--hash=identity", "--offset="+size, "/identity-file", appendFile)
140 assert.NoError(t, res.Err)
141
142 // check that the file now uses non-identity hash
143 res = node.IPFS("files", "stat", "--hash", "/identity-file")
144 assert.NoError(t, res.Err)
145 newCid := res.Stdout.Trimmed()
146
147 // verify new CID does NOT use identity
148 res = node.IPFS("cid", "format", "-f", "%h", newCid)
149 assert.NoError(t, res.Err)
150 assert.NotEqual(t, "identity", res.Stdout.Trimmed())
151
152 // verify it switched to a cryptographic hash
153 assert.Equal(t, config.DefaultHashFunction, res.Stdout.Trimmed())
154 })
155
156 t.Run("ipfs files write --hash=identity with small write creates identity CID", func(t *testing.T) {
157 t.Parallel()
158 node := harness.NewT(t).NewNode().Init().StartDaemon()
159 defer node.StopDaemon()
160
161 // create a small file with identity hash directly in MFS
162 smallData := "small"
163 tempFile := filepath.Join(node.Dir, "small.txt")
164 err := os.WriteFile(tempFile, []byte(smallData), 0644)
165 require.NoError(t, err)
166
167 // write to MFS with identity hash
168 res := node.IPFS("files", "write", "--create", "--hash=identity", "/mfs-identity", tempFile)
169 assert.NoError(t, res.Err)
170
171 // verify using identity CID
172 res = node.IPFS("files", "stat", "--hash", "/mfs-identity")
173 assert.NoError(t, res.Err)
174 cid := res.Stdout.Trimmed()
175
176 // verify CID uses identity hash
177 res = node.IPFS("cid", "format", "-f", "%h", cid)
178 assert.NoError(t, res.Err)
179 assert.Equal(t, "identity", res.Stdout.Trimmed())
180
181 // verify content
182 res = node.IPFS("files", "read", "/mfs-identity")
183 assert.NoError(t, res.Err)
184 assert.Equal(t, smallData, res.Stdout.Trimmed())
185 })
186
187 t.Run("raw node with identity CID converts to UnixFS when appending", func(t *testing.T) {
188 t.Parallel()
189 node := harness.NewT(t).NewNode().Init().StartDaemon()
190 defer node.StopDaemon()
191
192 // create raw block with identity CID
193 rawData := "raw"
194 tempFile := filepath.Join(node.Dir, "raw.txt")
195 err := os.WriteFile(tempFile, []byte(rawData), 0644)
196 require.NoError(t, err)
197
198 res := node.IPFS("block", "put", "--format=raw", "--mhtype=identity", tempFile)
199 assert.NoError(t, res.Err)
200 rawCid := res.Stdout.Trimmed()
201
202 // verify initial CID uses identity hash and raw codec
203 res = node.IPFS("cid", "format", "-f", "%h", rawCid)
204 assert.NoError(t, res.Err)
205 assert.Equal(t, "identity", res.Stdout.Trimmed())
206
207 res = node.IPFS("cid", "format", "-f", "%c", rawCid)
208 assert.NoError(t, res.Err)
209 assert.Equal(t, "raw", res.Stdout.Trimmed())
210
211 // copy to MFS
212 res = node.IPFS("files", "cp", fmt.Sprintf("/ipfs/%s", rawCid), "/raw-identity")
213 assert.NoError(t, res.Err)
214
215 // append data
216 appendData := "appended"
217 appendFile := filepath.Join(node.Dir, "append-raw.txt")
218 err = os.WriteFile(appendFile, []byte(appendData), 0644)
219 require.NoError(t, err)
220
221 // get current data size for appending
222 res = node.IPFS("files", "stat", "--format", "<size>", "/raw-identity")
223 assert.NoError(t, res.Err)
224 size := res.Stdout.Trimmed()
225 res = node.IPFS("files", "write", "--hash=identity", "--offset="+size, "/raw-identity", appendFile)
226 assert.NoError(t, res.Err)
227
228 // verify content
229 res = node.IPFS("files", "read", "/raw-identity")
230 assert.NoError(t, res.Err)
231 assert.Equal(t, rawData+appendData, res.Stdout.Trimmed())
232
233 // check that it's now a UnixFS structure (dag-pb)
234 res = node.IPFS("files", "stat", "--hash", "/raw-identity")
235 assert.NoError(t, res.Err)
236 newCid := res.Stdout.Trimmed()
237
238 res = node.IPFS("cid", "format", "-f", "%c", newCid)
239 assert.NoError(t, res.Err)
240 assert.Equal(t, "dag-pb", res.Stdout.Trimmed())
241
242 res = node.IPFS("files", "stat", "/raw-identity")
243 assert.NoError(t, res.Err)
244 assert.Contains(t, res.Stdout.String(), "Type: file")
245 })
246
247 t.Run("ipfs add --inline-limit at exactly max size succeeds", func(t *testing.T) {
248 t.Parallel()
249 node := harness.NewT(t).NewNode().Init().StartDaemon()
250 defer node.StopDaemon()
251
252 // create small data that will be inlined
253 smallData := "test data for inline"
254 tempFile := filepath.Join(node.Dir, "exact.txt")
255 err := os.WriteFile(tempFile, []byte(smallData), 0644)
256 require.NoError(t, err)
257
258 // exactly at the limit should succeed
259 res := node.IPFS("add", "--inline", fmt.Sprintf("--inline-limit=%d", verifcid.DefaultMaxIdentityDigestSize), tempFile)
260 assert.NoError(t, res.Err)
261 cid := strings.Fields(res.Stdout.String())[1]
262
263 // verify it uses identity hash (inline) since data is small enough
264 res = node.IPFS("cid", "format", "-f", "%h", cid)
265 assert.NoError(t, res.Err)
266 assert.Equal(t, "identity", res.Stdout.Trimmed())
267 })
268
269 t.Run("ipfs add --inline-limit one byte over max fails", func(t *testing.T) {
270 t.Parallel()
271 node := harness.NewT(t).NewNode().Init().StartDaemon()
272 defer node.StopDaemon()
273
274 smallData := "test"
275 tempFile := filepath.Join(node.Dir, "oneover.txt")
276 err := os.WriteFile(tempFile, []byte(smallData), 0644)
277 require.NoError(t, err)
278
279 // one byte over should fail
280 overLimit := verifcid.DefaultMaxIdentityDigestSize + 1
281 res := node.RunIPFS("add", "--inline", fmt.Sprintf("--inline-limit=%d", overLimit), tempFile)
282 assert.NotEqual(t, 0, res.ExitErr.ExitCode())
283 assert.Contains(t, res.Stderr.String(), fmt.Sprintf("inline-limit %d exceeds maximum allowed size of %d bytes", overLimit, verifcid.DefaultMaxIdentityDigestSize))
284 })
285
286 t.Run("ipfs add --inline with data larger than limit uses configured hash", func(t *testing.T) {
287 t.Parallel()
288 node := harness.NewT(t).NewNode().Init().StartDaemon()
289 defer node.StopDaemon()
290
291 // data larger than inline limit
292 largeData := strings.Repeat("y", 100)
293 tempFile := filepath.Join(node.Dir, "toolarge.txt")
294 err := os.WriteFile(tempFile, []byte(largeData), 0644)
295 require.NoError(t, err)
296
297 // set inline limit smaller than data
298 res := node.IPFS("add", "--inline", "--inline-limit=50", tempFile)
299 assert.NoError(t, res.Err)
300 cid := strings.Fields(res.Stdout.String())[1]
301
302 // verify it's NOT using identity hash (data too large for inline)
303 res = node.IPFS("cid", "format", "-f", "%h", cid)
304 assert.NoError(t, res.Err)
305 assert.NotEqual(t, "identity", res.Stdout.Trimmed())
306
307 // should use configured hash
308 assert.Equal(t, config.DefaultHashFunction, res.Stdout.Trimmed())
309 })
310 }