From bbe979245d14a1c1c5092b967817de3b93539b62 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Thu, 7 Sep 2023 14:33:14 +0200 Subject: [PATCH 1/4] feat: ipns v2 and v2 record combination and tests --- fixtures/ipns_records/README.md | 25 +-- fixtures/ipns_records/fixtures.car | Bin 107 -> 0 bytes fixtures/ipns_records/generator/go.mod | 39 ++++ fixtures/ipns_records/generator/go.sum | 164 ++++++++++++++ fixtures/ipns_records/generator/main.go | 207 ++++++++++++++++++ ...i88nsady6qgd1dhjcyfsaqmpp143ab.ipns-record | Bin 392 -> 0 bytes ...dm8c_v1-v2-broken-signature-v2.ipns-record | Bin 0 -> 334 bytes ...ph4y_v1-v2-broken-signature-v1.ipns-record | Bin 0 -> 334 bytes ...8kd10m97m36bjt66my99hb6103f_v2.ipns-record | Bin 0 -> 188 bytes ...eauqdc6a7c3y7d5i9fi8mk9w_v1-v2.ipns-record | Bin 0 -> 326 bytes ...ozae9kpw_v1-v2-broken-v1-value.ipns-record | Bin 0 -> 377 bytes ...wjimndrkydqm81cqtlb5ak6p7ku_v1.ipns-record | Bin 0 -> 144 bytes tests/path_gateway_ipns_test.go | 68 ++++++ tests/trustless_gateway_ipns_test.go | 63 ++++-- tooling/test/sugar.go | 14 +- tooling/test/validate.go | 4 + 16 files changed, 543 insertions(+), 41 deletions(-) delete mode 100644 fixtures/ipns_records/fixtures.car create mode 100644 fixtures/ipns_records/generator/go.mod create mode 100644 fixtures/ipns_records/generator/go.sum create mode 100644 fixtures/ipns_records/generator/main.go delete mode 100644 fixtures/ipns_records/k51qzi5uqu5dh71qgwangrt6r0nd4094i88nsady6qgd1dhjcyfsaqmpp143ab.ipns-record create mode 100644 fixtures/ipns_records/k51qzi5uqu5diamp7qnnvs1p1gzmku3eijkeijs3418j23j077zrkok63xdm8c_v1-v2-broken-signature-v2.ipns-record create mode 100644 fixtures/ipns_records/k51qzi5uqu5dilgf7gorsh9vcqqq4myo6jd4zmqkuy9pxyxi5fua3uf7axph4y_v1-v2-broken-signature-v1.ipns-record create mode 100644 fixtures/ipns_records/k51qzi5uqu5dit2ku9mutlfgwyz8u730on38kd10m97m36bjt66my99hb6103f_v2.ipns-record create mode 100644 fixtures/ipns_records/k51qzi5uqu5dlkw8pxuw9qmqayfdeh4kfebhmreauqdc6a7c3y7d5i9fi8mk9w_v1-v2.ipns-record create mode 100644 fixtures/ipns_records/k51qzi5uqu5dlmit2tuwdvnx4sbnyqgmvbxftl0eo3f33wwtb9gr7yozae9kpw_v1-v2-broken-v1-value.ipns-record create mode 100644 fixtures/ipns_records/k51qzi5uqu5dm4tm0wt8srkg9h9suud4wuiwjimndrkydqm81cqtlb5ak6p7ku_v1.ipns-record diff --git a/fixtures/ipns_records/README.md b/fixtures/ipns_records/README.md index 772712820..76dfb4335 100644 --- a/fixtures/ipns_records/README.md +++ b/fixtures/ipns_records/README.md @@ -4,20 +4,13 @@ ### Fixtures and ipns-record -```sh -# using Kubo CLI version 0.21.0-rc3 (https://dist.ipfs.tech/kubo/v0.21.0-rc3/) -FILE_CID=$(echo "Hello IPFS" | ipfs add --cid-version 1 -q) -IPNS_KEY=$(ipfs key gen ipns-record) +```shell +> go run generator/main.go -ipfs dag export ${FILE_CID} > fixtures.car - -# publish a record valid for a 100 years -ipfs name publish --key=ipns-record --quieter --ttl=876600h --lifetime=876600h /ipfs/${FILE_CID} -ipfs routing get /ipns/${IPNS_KEY} > ${IPNS_KEY}.ipns-record - -echo IPNS_KEY=${IPNS_KEY} -echo FILE_CID=${FILE_CID} # A file containing "Hello IPFS" - -# IPNS_KEY=k51qzi5uqu5dh71qgwangrt6r0nd4094i88nsady6qgd1dhjcyfsaqmpp143ab -# FILE_CID=bafkreidfdrlkeq4m4xnxuyx6iae76fdm4wgl5d4xzsb77ixhyqwumhz244 # A file containing Hello IPFS -``` +k51qzi5uqu5dm4tm0wt8srkg9h9suud4wuiwjimndrkydqm81cqtlb5ak6p7ku_v1.ipns-record -> /ipfs/bafkqadtwgeww63tmpeqhezldn5zgi +k51qzi5uqu5dlkw8pxuw9qmqayfdeh4kfebhmreauqdc6a7c3y7d5i9fi8mk9w_v1-v2.ipns-record -> /ipfs/bafkqaddwgevxmmraojswg33smq +k51qzi5uqu5dlmit2tuwdvnx4sbnyqgmvbxftl0eo3f33wwtb9gr7yozae9kpw_v1-v2-broken-v1-value.ipns-record -> /ipfs/bafkqahtwgevxmmraojswg33smqqho2lunaqge4tpnnsw4idwmfwhkzi +k51qzi5uqu5dilgf7gorsh9vcqqq4myo6jd4zmqkuy9pxyxi5fua3uf7axph4y_v1-v2-broken-signature-v1.ipns-record -> /ipfs/bafkqahtwgevxmmrao5uxi2bamjzg623fnyqhg2lhnzqxi5lsmuqhmmi +k51qzi5uqu5diamp7qnnvs1p1gzmku3eijkeijs3418j23j077zrkok63xdm8c_v1-v2-broken-signature-v2.ipns-record -> /ipfs/bafkqahtwgevxmmrao5uxi2bamjzg623fnyqhg2lhnzqxi5lsmuqhmmq +k51qzi5uqu5dit2ku9mutlfgwyz8u730on38kd10m97m36bjt66my99hb6103f_v2.ipns-record -> /ipfs/bafkqadtwgiww63tmpeqhezldn5zgi +``` \ No newline at end of file diff --git a/fixtures/ipns_records/fixtures.car b/fixtures/ipns_records/fixtures.car deleted file mode 100644 index 5c541e430ea6d1aed7c20545c40c9a56994ac546..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 107 zcmcColv %s\n", filename, value.String()) +} + +func makeV1Only() { + sk, _, name := makeKeyPair() + + v := makeRawPath("v1-only record") + + // Create working record + rec, err := ipns.NewRecord(sk, v, seq, eol, ttl, ipns.WithV1Compatibility(true)) + panicOnErr(err) + + // Marshal + raw, err := ipns.MarshalRecord(rec) + panicOnErr(err) + + // Unmarshal into raw structure + pb := ipns_pb.IpnsRecord{} + err = proto.Unmarshal(raw, &pb) + panicOnErr(err) + + // Make it V1-only + pb.Data = nil + pb.SignatureV2 = nil + + // Marshal again and store it + raw, err = proto.Marshal(&pb) + panicOnErr(err) + + saveToFile(raw, name.String()+"_v1.ipns-record", v) +} + +func makeV1V2() { + sk, _, name := makeKeyPair() + + v := makeRawPath("v1+v2 record") + + rec, err := ipns.NewRecord(sk, v, seq, eol, ttl, ipns.WithV1Compatibility(true)) + panicOnErr(err) + + raw, err := ipns.MarshalRecord(rec) + panicOnErr(err) + + saveToFile(raw, name.String()+"_v1-v2.ipns-record", v) +} + +func makeV1V2WithBrokenValue() { + sk, _, name := makeKeyPair() + + v := makeRawPath("v1+v2 record with broken value") + + // Create working record + rec, err := ipns.NewRecord(sk, v, seq, eol, ttl, ipns.WithV1Compatibility(true)) + panicOnErr(err) + + // Marshal + raw, err := ipns.MarshalRecord(rec) + panicOnErr(err) + + // Unmarshal into raw structure + pb := ipns_pb.IpnsRecord{} + err = proto.Unmarshal(raw, &pb) + panicOnErr(err) + + // Make Value different + pb.Value = []byte("/ipfs/bafkqaglumvzxi2lom4qgeyleebuxa3ttebzgky3pojshgcq") + + // Marshal again and store it + raw, err = proto.Marshal(&pb) + panicOnErr(err) + + saveToFile(raw, name.String()+"_v1-v2-broken-v1-value.ipns-record", v) +} + +func makeV1V2WithBrokenSignatureV1() { + sk, _, name := makeKeyPair() + + v := makeRawPath("v1+v2 with broken signature v1") + + // Create working record + rec, err := ipns.NewRecord(sk, v, seq, eol, ttl, ipns.WithV1Compatibility(true)) + panicOnErr(err) + + // Marshal + raw, err := ipns.MarshalRecord(rec) + panicOnErr(err) + + // Unmarshal into raw structure + pb := ipns_pb.IpnsRecord{} + err = proto.Unmarshal(raw, &pb) + panicOnErr(err) + + // Break Signature V1 + pb.SignatureV1 = []byte("invalid stuff") + + // Marshal again and store it + raw, err = proto.Marshal(&pb) + panicOnErr(err) + + saveToFile(raw, name.String()+"_v1-v2-broken-signature-v1.ipns-record", v) +} + +func makeV1V2WithBrokenSignatureV2() { + sk, _, name := makeKeyPair() + + v := makeRawPath("v1+v2 with broken signature v2") + + // Create working record + rec, err := ipns.NewRecord(sk, v, seq, eol, ttl, ipns.WithV1Compatibility(true)) + panicOnErr(err) + + // Marshal + raw, err := ipns.MarshalRecord(rec) + panicOnErr(err) + + // Unmarshal into raw structure + pb := ipns_pb.IpnsRecord{} + err = proto.Unmarshal(raw, &pb) + panicOnErr(err) + + // Break Signature V2 + pb.SignatureV2 = []byte("invalid stuff") + + // Marshal again and store it + raw, err = proto.Marshal(&pb) + panicOnErr(err) + + saveToFile(raw, name.String()+"_v1-v2-broken-signature-v2.ipns-record", v) +} + +func makeV2Only() { + sk, _, name := makeKeyPair() + + v := makeRawPath("v2-only record") + + rec, err := ipns.NewRecord(sk, v, seq, eol, ttl, ipns.WithV1Compatibility(false)) + panicOnErr(err) + + raw, err := ipns.MarshalRecord(rec) + panicOnErr(err) + + saveToFile(raw, name.String()+"_v2.ipns-record", v) +} + +func main() { + makeV1Only() + makeV1V2() + makeV1V2WithBrokenValue() + makeV1V2WithBrokenSignatureV1() + makeV1V2WithBrokenSignatureV2() + makeV2Only() +} diff --git a/fixtures/ipns_records/k51qzi5uqu5dh71qgwangrt6r0nd4094i88nsady6qgd1dhjcyfsaqmpp143ab.ipns-record b/fixtures/ipns_records/k51qzi5uqu5dh71qgwangrt6r0nd4094i88nsady6qgd1dhjcyfsaqmpp143ab.ipns-record deleted file mode 100644 index ec13ec550162b81f44b35a6ab54b4dd9cdd11707..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 392 zcmd;b)XywPE7ng+Ov^4x%}hy4Day%CEi}nBsmQA+t*kK1OiVR5OH0W$DNoNaO);sc zDo!#t&#cI(EG#e0&8RXmF%fe3WvzU>SFJ04j^dIZkA-4vOEfq6H0ie39ZWWO|7T-n zhTD&u+CmfaOSEsE-5&R2o=I`u(JdVUdxTlHr4_DQ%+Dynpd@8vXk@HwV4`bi6k=#> zWo%|;YN2OjY;I&8rNLm((9pBE;lYEa+D;DZwog8P;JtWuR2%!>xl6YFEuU6)>E){C z#}a3ym{+MAwLQ!K{b1W$zOv&|S}J#Rzj~K(y{i5eAFDB8+QsA7xBcYvn!>m=IV8kK zTKjY}14}XkLuy!JPHAcc<>8PKoLX3#nwOl)kO4F@GbOX6G6Ijca$t%=DhpB>023mb A4*&oF diff --git a/fixtures/ipns_records/k51qzi5uqu5diamp7qnnvs1p1gzmku3eijkeijs3418j23j077zrkok63xdm8c_v1-v2-broken-signature-v2.ipns-record b/fixtures/ipns_records/k51qzi5uqu5diamp7qnnvs1p1gzmku3eijkeijs3418j23j077zrkok63xdm8c_v1-v2-broken-signature-v2.ipns-record new file mode 100644 index 0000000000000000000000000000000000000000..0f278c868ba26c92efacfaf040f198372fc1b057 GIT binary patch literal 334 zcmdG|ef_EiKH*%`Fsi=za8K-t7|2`{7T-&*Z4SoSC*KQnW9i!Qks_LHHmR)a!81eGy?0GCjE(f|Me literal 0 HcmV?d00001 diff --git a/fixtures/ipns_records/k51qzi5uqu5dilgf7gorsh9vcqqq4myo6jd4zmqkuy9pxyxi5fua3uf7axph4y_v1-v2-broken-signature-v1.ipns-record b/fixtures/ipns_records/k51qzi5uqu5dilgf7gorsh9vcqqq4myo6jd4zmqkuy9pxyxi5fua3uf7axph4y_v1-v2-broken-signature-v1.ipns-record new file mode 100644 index 0000000000000000000000000000000000000000..cb914e98935285d36324920ed73cf82a2f3bf39a GIT binary patch literal 334 zcmdG|ef_EiKH*&CL|z&CDxH%*jkqC@v{YOOs$wk~T6lGS)S)&^0s(F*LF=G`BJ^ z)-$s-F)%fX(qJ%XSn%=CMiVCoqoaLU>++`c72KF_xNyNnKZ%D%w)3t@otPav#XYft zuY5~)Hs@-)ja$BnBxHvMS4S*elkTixbTwsWZ%Zw^%vN@)WXu#yyR4d44`?LDVZge5%_$R15*`JS&+&A01K#iGXMYp literal 0 HcmV?d00001 diff --git a/fixtures/ipns_records/k51qzi5uqu5dit2ku9mutlfgwyz8u730on38kd10m97m36bjt66my99hb6103f_v2.ipns-record b/fixtures/ipns_records/k51qzi5uqu5dit2ku9mutlfgwyz8u730on38kd10m97m36bjt66my99hb6103f_v2.ipns-record new file mode 100644 index 0000000000000000000000000000000000000000..ddce7382d79027426e6c02ea1bdabfd5078e8468 GIT binary patch literal 188 zcmZ>A$SRu=5vMKA&Us}@ok+LDMuqFSTTDgYuK6_K{ori3Ku+HTpM&2J0OO znRQR?%c*A5FGu8b{{P#!i)$&PSH;rgkPshf1_s8(5-|Y`sbPsZrKu4r`k4i3#rjE! zY1xH|DJA9UndRkX#wEE0sf8J-RXHhnrd8>g8NsQArKx$zsSFuFU70DFC6y7Zq9KFs-}ipe6I< z-fhKtq3<)8&OJ-L=6a9qNXV-9=h;F^xlj1bc2wmyeR4&1spZk7PW6ZRIPOeYviDR2 z^AA1=1|?}DLnC8d0}EY4lMq8AD?@WD17kfiOA`ZAqbLmqgN6kk4{bDYa_|b=k>cFx z{ra+^8`B#dGj%?n3jtkexxY-q5{~@pUNm7{?GxX!)j>y!=UdhlI!^b?a$I29Ae3d{Udom+9wXifbFFBPV1E?i4C9|Y5 P0-u|6V5&kY3sM;X9A9~{ literal 0 HcmV?d00001 diff --git a/fixtures/ipns_records/k51qzi5uqu5dlmit2tuwdvnx4sbnyqgmvbxftl0eo3f33wwtb9gr7yozae9kpw_v1-v2-broken-v1-value.ipns-record b/fixtures/ipns_records/k51qzi5uqu5dlmit2tuwdvnx4sbnyqgmvbxftl0eo3f33wwtb9gr7yozae9kpw_v1-v2-broken-v1-value.ipns-record new file mode 100644 index 0000000000000000000000000000000000000000..9b4953e9440c39508eb798c0d477b730c1918a43 GIT binary patch literal 377 zcmd-w)6XnOE7ng+Ov^4zOwTFJEvu@?G|I`(H7QI_t;|VHO)9NOG%hJgO{z-It~4&l z&nnJHPc9U4@XO7%eJ|>F$M=6C4|BsZXQK#ni^GwZ>Nf0H(eiTgZH~Fi*z*nfJEy7? z$gYT7?E7T(r>SM4vxS^)eHU?7tW4vRU{I1aGBh&QHL%b%Gzl>@vNANcGBDONvotX< zHHy+;Flbot@z6#SCkL;2r?=LpIlTMd=vlLIZNQEB?$4i!F@+`X`^J)>-?r`LejBYH zSFF5trLK{ZR=dV~=tp2OYn=NhwrgQ*)wR;wS-d7OE=>*z@sVa=U|cK_6TpxfmY7qT z8exa*^^B78^whG7+}xr>pcl*2jg5Iw0W>c&C9|Y50-uj^V5&kY3sM;X1?i5@ literal 0 HcmV?d00001 diff --git a/fixtures/ipns_records/k51qzi5uqu5dm4tm0wt8srkg9h9suud4wuiwjimndrkydqm81cqtlb5ak6p7ku_v1.ipns-record b/fixtures/ipns_records/k51qzi5uqu5dm4tm0wt8srkg9h9suud4wuiwjimndrkydqm81cqtlb5ak6p7ku_v1.ipns-record new file mode 100644 index 0000000000000000000000000000000000000000..c6e2a0f5c5a7482bb42367715f262e5f85044b37 GIT binary patch literal 144 zcmV;B0B`>aBrj=jW^*rMVPaAk35WqNF6ZZ&#mX%aw3oqH(c zT>wh5G^qr9f#GHkR1G}+apD(BEi>AoF4(LlER1|>po#OObsy8PYkTL*I#d`GdbIAB yYOV&%>sSXE03sVQF)}kPFgPtSG*mG%Ix#moFf%STIW#aeGFm7AFo2-(!m%{xT{cJn literal 0 HcmV?d00001 diff --git a/tests/path_gateway_ipns_test.go b/tests/path_gateway_ipns_test.go index f705dec94..351e70f00 100644 --- a/tests/path_gateway_ipns_test.go +++ b/tests/path_gateway_ipns_test.go @@ -7,6 +7,74 @@ import ( . "github.com/ipfs/gateway-conformance/tooling/test" ) +var ( + // See fixtures/ipns_records/README.md for information + ipnsV1 = "k51qzi5uqu5dm4tm0wt8srkg9h9suud4wuiwjimndrkydqm81cqtlb5ak6p7ku" + ipnsV1V2BrokenValueV1 = "k51qzi5uqu5dlmit2tuwdvnx4sbnyqgmvbxftl0eo3f33wwtb9gr7yozae9kpw" + ipnsV1V2BrokenSigV2 = "k51qzi5uqu5diamp7qnnvs1p1gzmku3eijkeijs3418j23j077zrkok63xdm8c" + + ipnsV1V2BrokenSigV1 = "k51qzi5uqu5dilgf7gorsh9vcqqq4myo6jd4zmqkuy9pxyxi5fua3uf7axph4y" + bodyIPNSV1V2BrokenSigV1 = []byte("v1+v2 with broken signature v1") + + ipnsV1V2 = "k51qzi5uqu5dlkw8pxuw9qmqayfdeh4kfebhmreauqdc6a7c3y7d5i9fi8mk9w" + bodyIPNSV1V2 = []byte("v1+v2 record") + cidIPNSV1V2 = "bafkqaddwgevxmmraojswg33smq" + + ipnsV2 = "k51qzi5uqu5dit2ku9mutlfgwyz8u730on38kd10m97m36bjt66my99hb6103f" + bodyIPNSV2 = []byte("v2-only record") + cidIPNSV2 = "bafkqadtwgiww63tmpeqhezldn5zgi" +) + +func TestGatewayIPNSPath(t *testing.T) { + tests := SugarTests{ + { + Name: "GET an IPNS Path (V1) from the gateway fails with 5xx", + Request: Request(). + Path("/ipns/{{name}}", ipnsV1), + Response: Expect(). + StatusRange(500, 599), + }, + { + Name: "GET an IPNS Path (V1+V2) with broken ValueV1 from the gateway fails with 5xx", + Request: Request(). + Path("/ipns/{{name}}", ipnsV1V2BrokenValueV1), + Response: Expect(). + StatusRange(500, 599), + }, + { + Name: "GET an IPNS Path (V1+V2) with broken SignatureV1, but valid SignatureV2 succeeds", + Request: Request(). + Path("/ipns/{{name}}", ipnsV1V2BrokenSigV1), + Response: Expect(). + Status(200). + Body(bodyIPNSV1V2BrokenSigV1), + }, + { + Name: "GET an IPNS Path (V1+V2) from the gateway", + Request: Request(). + Path("/ipns/{{name}}", ipnsV1V2), + Response: Expect(). + Body(bodyIPNSV1V2), + }, + { + Name: "GET an IPNS Path (V2) from the gateway", + Request: Request(). + Path("/ipns/{{name}}", ipnsV2), + Response: Expect(). + Body(bodyIPNSV2), + }, + { + Name: "GET an IPNS Path (V1+V2) with broken SignatureV2 from the gateway fails with 5xx", + Request: Request(). + Path("/ipns/{{name}}", ipnsV1V2BrokenSigV2), + Response: Expect(). + StatusRange(500, 599), + }, + } + + RunWithSpecs(t, tests, specs.PathGatewayIPNS) +} + func TestRedirectCanonicalIPNS(t *testing.T) { tests := SugarTests{ { diff --git a/tests/trustless_gateway_ipns_test.go b/tests/trustless_gateway_ipns_test.go index e99449fbf..e2af21350 100644 --- a/tests/trustless_gateway_ipns_test.go +++ b/tests/trustless_gateway_ipns_test.go @@ -3,66 +3,85 @@ package tests import ( "testing" - "github.com/ipfs/gateway-conformance/tooling/car" . "github.com/ipfs/gateway-conformance/tooling/ipns" "github.com/ipfs/gateway-conformance/tooling/specs" . "github.com/ipfs/gateway-conformance/tooling/test" ) func TestGatewayIPNSRecord(t *testing.T) { - fixture := car.MustOpenUnixfsCar("ipns_records/fixtures.car") - file := fixture.MustGetRoot() - fileCID := file.Cid() - - ipns := MustOpenIPNSRecordWithKey("ipns_records/k51qzi5uqu5dh71qgwangrt6r0nd4094i88nsady6qgd1dhjcyfsaqmpp143ab.ipns-record") - ipnsName := ipns.Key() - tests := SugarTests{ { - Name: "GET an IPNS path from the gateway", + Name: "GET IPNS Record (V1+V2) with format=ipns-record has expected HTTP headers and valid key", Request: Request(). - Path("/ipns/{{name}}", ipnsName), + Path("/ipns/{{name}}", ipnsV1V2). + Query("format", "ipns-record"), Response: Expect(). - Body(file.RawData()), + Headers( + Header("Content-Disposition").Contains("attachment;"), + Header("Content-Type").Contains("application/vnd.ipfs.ipns-record"), + Header("Cache-Control").Contains("public, max-age=1800"), + ). + Body( + IsIPNSRecord(ipnsV1V2). + IsValid(). + PointsTo("/ipfs/{{cid}}", cidIPNSV1V2), + ), }, { - Name: "GET IPNS Record with format=ipns-record has expected HTTP headers and valid key", + Name: "GET IPNS Record (V2) with format=ipns-record has expected HTTP headers and valid key", Request: Request(). - Path("/ipns/{{name}}", ipnsName). + Path("/ipns/{{name}}", ipnsV2). Query("format", "ipns-record"), Response: Expect(). Headers( Header("Content-Disposition").Contains("attachment;"), Header("Content-Type").Contains("application/vnd.ipfs.ipns-record"), - Header("Cache-Control").Contains("public, max-age=3155760000"), + Header("Cache-Control").Contains("public, max-age=1800"), + ). + Body( + IsIPNSRecord(ipnsV2). + IsValid(). + PointsTo("/ipfs/{{cid}}", cidIPNSV2), + ), + }, + { + Name: "GET IPNS Record (V1+V2) with 'Accept: application/vnd.ipfs.ipns-record' has expected HTTP headers and valid key", + Request: Request(). + Path("/ipns/{{name}}", ipnsV1V2). + Header("Accept", "application/vnd.ipfs.ipns-record"), + Response: Expect(). + Headers( + Header("Content-Disposition").Contains("attachment;"), + Header("Content-Type").Contains("application/vnd.ipfs.ipns-record"), + Header("Cache-Control").Contains("public, max-age=1800"), ). Body( - IsIPNSRecord(ipnsName). + IsIPNSRecord(ipnsV1V2). IsValid(). - PointsTo("/ipfs/{{cid}}", fileCID.String()), + PointsTo("/ipfs/{{cid}}", cidIPNSV1V2), ), }, { - Name: "GET IPNS Record with 'Accept: application/vnd.ipfs.ipns-record' has expected HTTP headers and valid key", + Name: "GET IPNS Record (V2) with 'Accept: application/vnd.ipfs.ipns-record' has expected HTTP headers and valid key", Request: Request(). - Path("/ipns/{{name}}", ipnsName). + Path("/ipns/{{name}}", ipnsV2). Header("Accept", "application/vnd.ipfs.ipns-record"), Response: Expect(). Headers( Header("Content-Disposition").Contains("attachment;"), Header("Content-Type").Contains("application/vnd.ipfs.ipns-record"), - Header("Cache-Control").Contains("public, max-age=3155760000"), + Header("Cache-Control").Contains("public, max-age=1800"), ). Body( - IsIPNSRecord(ipnsName). + IsIPNSRecord(ipnsV2). IsValid(). - PointsTo("/ipfs/{{cid}}", fileCID.String()), + PointsTo("/ipfs/{{cid}}", cidIPNSV2), ), }, { Name: "GET IPNS Record with explicit ?filename= succeeds with modified Content-Disposition header", Request: Request(). - Path("/ipns/{{name}}", ipnsName). + Path("/ipns/{{name}}", ipnsV1V2). Query("format", "ipns-record"). Query("filename", "testтест.ipns-record"), Response: Expect(). diff --git a/tooling/test/sugar.go b/tooling/test/sugar.go index 966c96859..ab8c04b0a 100644 --- a/tooling/test/sugar.go +++ b/tooling/test/sugar.go @@ -134,9 +134,11 @@ func (r RequestBuilder) Clone() RequestBuilder { } type ExpectBuilder struct { - StatusCode_ int `json:"statusCode,omitempty"` - Headers_ []HeaderBuilder `json:"headers,omitempty"` - Body_ interface{} `json:"body,omitempty"` + StatusCode_ int `json:"statusCode,omitempty"` + StatusCodeFrom_ int `json:"statusCodeFrom,omitempty"` + StatusCodeTo_ int `json:"statusCodeTo,omitempty"` + Headers_ []HeaderBuilder `json:"headers,omitempty"` + Body_ interface{} `json:"body,omitempty"` } func Expect() ExpectBuilder { @@ -152,6 +154,12 @@ func (e ExpectBuilder) Status(statusCode int) ExpectBuilder { return e } +func (e ExpectBuilder) StatusRange(from, to int) ExpectBuilder { + e.StatusCodeFrom_ = from + e.StatusCodeTo_ = to + return e +} + func (e ExpectBuilder) Header(h HeaderBuilder) ExpectBuilder { e.Headers_ = append(e.Headers_, h) return e diff --git a/tooling/test/validate.go b/tooling/test/validate.go index 2187d06d4..c14a85c98 100644 --- a/tooling/test/validate.go +++ b/tooling/test/validate.go @@ -21,6 +21,10 @@ func validateResponse( if res.StatusCode != expected.StatusCode_ { localReport(t, "Status code is not %d. It is %d", expected.StatusCode_, res.StatusCode) } + } else if expected.StatusCodeFrom_ != 0 && expected.StatusCodeTo_ != 0 { + if res.StatusCode < expected.StatusCodeFrom_ || res.StatusCode > expected.StatusCodeTo_ { + localReport(t, "Status code is not between %d and %d. It is %d", expected.StatusCodeFrom_, expected.StatusCodeTo_, res.StatusCode) + } } for _, header := range expected.Headers_ { From d978d7e037780eaec363d0aec5298f17f61f20b0 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Thu, 7 Sep 2023 16:44:24 +0200 Subject: [PATCH 2/4] refactor: rename to StatusBetween --- tests/path_gateway_ipns_test.go | 6 +++--- tooling/test/sugar.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/path_gateway_ipns_test.go b/tests/path_gateway_ipns_test.go index 351e70f00..d97a84459 100644 --- a/tests/path_gateway_ipns_test.go +++ b/tests/path_gateway_ipns_test.go @@ -32,14 +32,14 @@ func TestGatewayIPNSPath(t *testing.T) { Request: Request(). Path("/ipns/{{name}}", ipnsV1), Response: Expect(). - StatusRange(500, 599), + StatusBetween(500, 599), }, { Name: "GET an IPNS Path (V1+V2) with broken ValueV1 from the gateway fails with 5xx", Request: Request(). Path("/ipns/{{name}}", ipnsV1V2BrokenValueV1), Response: Expect(). - StatusRange(500, 599), + StatusBetween(500, 599), }, { Name: "GET an IPNS Path (V1+V2) with broken SignatureV1, but valid SignatureV2 succeeds", @@ -68,7 +68,7 @@ func TestGatewayIPNSPath(t *testing.T) { Request: Request(). Path("/ipns/{{name}}", ipnsV1V2BrokenSigV2), Response: Expect(). - StatusRange(500, 599), + StatusBetween(500, 599), }, } diff --git a/tooling/test/sugar.go b/tooling/test/sugar.go index ab8c04b0a..62b577b10 100644 --- a/tooling/test/sugar.go +++ b/tooling/test/sugar.go @@ -154,7 +154,7 @@ func (e ExpectBuilder) Status(statusCode int) ExpectBuilder { return e } -func (e ExpectBuilder) StatusRange(from, to int) ExpectBuilder { +func (e ExpectBuilder) StatusBetween(from, to int) ExpectBuilder { e.StatusCodeFrom_ = from e.StatusCodeTo_ = to return e From 96bfa5c13b0f3ad93618adb8bd39916606262ad4 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Fri, 8 Sep 2023 14:42:11 +0200 Subject: [PATCH 3/4] refactor: load all ipns records as fixtures as possible --- tests/path_gateway_ipns_test.go | 34 ++++++++++++++++++---------- tests/trustless_gateway_ipns_test.go | 26 ++++++++++----------- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/tests/path_gateway_ipns_test.go b/tests/path_gateway_ipns_test.go index d97a84459..acfa42d96 100644 --- a/tests/path_gateway_ipns_test.go +++ b/tests/path_gateway_ipns_test.go @@ -1,30 +1,40 @@ package tests import ( + "strings" "testing" + . "github.com/ipfs/gateway-conformance/tooling/ipns" "github.com/ipfs/gateway-conformance/tooling/specs" . "github.com/ipfs/gateway-conformance/tooling/test" + "github.com/ipfs/go-cid" + "github.com/multiformats/go-multihash" ) var ( - // See fixtures/ipns_records/README.md for information + // See fixtures/ipns_records/README.md for information. These are invalid records, so we cannot load them with MustOpenIPNSRecordWithKey ipnsV1 = "k51qzi5uqu5dm4tm0wt8srkg9h9suud4wuiwjimndrkydqm81cqtlb5ak6p7ku" ipnsV1V2BrokenValueV1 = "k51qzi5uqu5dlmit2tuwdvnx4sbnyqgmvbxftl0eo3f33wwtb9gr7yozae9kpw" ipnsV1V2BrokenSigV2 = "k51qzi5uqu5diamp7qnnvs1p1gzmku3eijkeijs3418j23j077zrkok63xdm8c" - ipnsV1V2BrokenSigV1 = "k51qzi5uqu5dilgf7gorsh9vcqqq4myo6jd4zmqkuy9pxyxi5fua3uf7axph4y" - bodyIPNSV1V2BrokenSigV1 = []byte("v1+v2 with broken signature v1") + ipnsV1V2BrokenSigV1 = MustOpenIPNSRecordWithKey("ipns_records/k51qzi5uqu5dilgf7gorsh9vcqqq4myo6jd4zmqkuy9pxyxi5fua3uf7axph4y_v1-v2-broken-signature-v1.ipns-record") + bodyIPNSV1V2BrokenSigV1 = mustBytesFromRawCID(strings.TrimPrefix(ipnsV1V2BrokenSigV1.Value(), "/ipfs/")) - ipnsV1V2 = "k51qzi5uqu5dlkw8pxuw9qmqayfdeh4kfebhmreauqdc6a7c3y7d5i9fi8mk9w" - bodyIPNSV1V2 = []byte("v1+v2 record") - cidIPNSV1V2 = "bafkqaddwgevxmmraojswg33smq" + ipnsV1V2 = MustOpenIPNSRecordWithKey("ipns_records/k51qzi5uqu5dlkw8pxuw9qmqayfdeh4kfebhmreauqdc6a7c3y7d5i9fi8mk9w_v1-v2.ipns-record") + bodyIPNSV1V2 = mustBytesFromRawCID(strings.TrimPrefix(ipnsV1V2.Value(), "/ipfs/")) - ipnsV2 = "k51qzi5uqu5dit2ku9mutlfgwyz8u730on38kd10m97m36bjt66my99hb6103f" - bodyIPNSV2 = []byte("v2-only record") - cidIPNSV2 = "bafkqadtwgiww63tmpeqhezldn5zgi" + ipnsV2 = MustOpenIPNSRecordWithKey("ipns_records/k51qzi5uqu5dit2ku9mutlfgwyz8u730on38kd10m97m36bjt66my99hb6103f_v2.ipns-record") + bodyIPNSV2 = mustBytesFromRawCID(strings.TrimPrefix(ipnsV2.Value(), "/ipfs/")) ) +func mustBytesFromRawCID(c string) []byte { + mh, err := multihash.Decode(cid.MustParse(c).Hash()) + if err != nil { + panic(err) + } + return mh.Digest +} + func TestGatewayIPNSPath(t *testing.T) { tests := SugarTests{ { @@ -44,7 +54,7 @@ func TestGatewayIPNSPath(t *testing.T) { { Name: "GET an IPNS Path (V1+V2) with broken SignatureV1, but valid SignatureV2 succeeds", Request: Request(). - Path("/ipns/{{name}}", ipnsV1V2BrokenSigV1), + Path("/ipns/{{name}}", ipnsV1V2BrokenSigV1.Key()), Response: Expect(). Status(200). Body(bodyIPNSV1V2BrokenSigV1), @@ -52,14 +62,14 @@ func TestGatewayIPNSPath(t *testing.T) { { Name: "GET an IPNS Path (V1+V2) from the gateway", Request: Request(). - Path("/ipns/{{name}}", ipnsV1V2), + Path("/ipns/{{name}}", ipnsV1V2.Key()), Response: Expect(). Body(bodyIPNSV1V2), }, { Name: "GET an IPNS Path (V2) from the gateway", Request: Request(). - Path("/ipns/{{name}}", ipnsV2), + Path("/ipns/{{name}}", ipnsV2.Key()), Response: Expect(). Body(bodyIPNSV2), }, diff --git a/tests/trustless_gateway_ipns_test.go b/tests/trustless_gateway_ipns_test.go index e2af21350..9751f3c76 100644 --- a/tests/trustless_gateway_ipns_test.go +++ b/tests/trustless_gateway_ipns_test.go @@ -13,7 +13,7 @@ func TestGatewayIPNSRecord(t *testing.T) { { Name: "GET IPNS Record (V1+V2) with format=ipns-record has expected HTTP headers and valid key", Request: Request(). - Path("/ipns/{{name}}", ipnsV1V2). + Path("/ipns/{{name}}", ipnsV1V2.Key()). Query("format", "ipns-record"), Response: Expect(). Headers( @@ -22,15 +22,15 @@ func TestGatewayIPNSRecord(t *testing.T) { Header("Cache-Control").Contains("public, max-age=1800"), ). Body( - IsIPNSRecord(ipnsV1V2). + IsIPNSRecord(ipnsV1V2.Key()). IsValid(). - PointsTo("/ipfs/{{cid}}", cidIPNSV1V2), + PointsTo(ipnsV1V2.Value()), ), }, { Name: "GET IPNS Record (V2) with format=ipns-record has expected HTTP headers and valid key", Request: Request(). - Path("/ipns/{{name}}", ipnsV2). + Path("/ipns/{{name}}", ipnsV2.Key()). Query("format", "ipns-record"), Response: Expect(). Headers( @@ -39,15 +39,15 @@ func TestGatewayIPNSRecord(t *testing.T) { Header("Cache-Control").Contains("public, max-age=1800"), ). Body( - IsIPNSRecord(ipnsV2). + IsIPNSRecord(ipnsV2.Key()). IsValid(). - PointsTo("/ipfs/{{cid}}", cidIPNSV2), + PointsTo(ipnsV2.Value()), ), }, { Name: "GET IPNS Record (V1+V2) with 'Accept: application/vnd.ipfs.ipns-record' has expected HTTP headers and valid key", Request: Request(). - Path("/ipns/{{name}}", ipnsV1V2). + Path("/ipns/{{name}}", ipnsV1V2.Key()). Header("Accept", "application/vnd.ipfs.ipns-record"), Response: Expect(). Headers( @@ -56,15 +56,15 @@ func TestGatewayIPNSRecord(t *testing.T) { Header("Cache-Control").Contains("public, max-age=1800"), ). Body( - IsIPNSRecord(ipnsV1V2). + IsIPNSRecord(ipnsV1V2.Key()). IsValid(). - PointsTo("/ipfs/{{cid}}", cidIPNSV1V2), + PointsTo(ipnsV1V2.Value()), ), }, { Name: "GET IPNS Record (V2) with 'Accept: application/vnd.ipfs.ipns-record' has expected HTTP headers and valid key", Request: Request(). - Path("/ipns/{{name}}", ipnsV2). + Path("/ipns/{{name}}", ipnsV2.Key()). Header("Accept", "application/vnd.ipfs.ipns-record"), Response: Expect(). Headers( @@ -73,15 +73,15 @@ func TestGatewayIPNSRecord(t *testing.T) { Header("Cache-Control").Contains("public, max-age=1800"), ). Body( - IsIPNSRecord(ipnsV2). + IsIPNSRecord(ipnsV2.Key()). IsValid(). - PointsTo("/ipfs/{{cid}}", cidIPNSV2), + PointsTo(ipnsV2.Value()), ), }, { Name: "GET IPNS Record with explicit ?filename= succeeds with modified Content-Disposition header", Request: Request(). - Path("/ipns/{{name}}", ipnsV1V2). + Path("/ipns/{{name}}", ipnsV1V2.Key()). Query("format", "ipns-record"). Query("filename", "testтест.ipns-record"), Response: Expect(). From ea745fa18973429f7f742e172f0d5e31be114b55 Mon Sep 17 00:00:00 2001 From: Marcin Rataj Date: Tue, 12 Sep 2023 20:23:32 +0200 Subject: [PATCH 4/4] docs: add Hint to IPNS V1/V2 tests it may be not obvious why we have these checks or why certain edge case fails/passes, added Hints to elaborate --- tests/path_gateway_ipns_test.go | 63 +++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/tests/path_gateway_ipns_test.go b/tests/path_gateway_ipns_test.go index acfa42d96..f74514bc5 100644 --- a/tests/path_gateway_ipns_test.go +++ b/tests/path_gateway_ipns_test.go @@ -38,21 +38,45 @@ func mustBytesFromRawCID(c string) []byte { func TestGatewayIPNSPath(t *testing.T) { tests := SugarTests{ { - Name: "GET an IPNS Path (V1) from the gateway fails with 5xx", + Name: "GET for /ipns/name with V1-only signature MUST fail with 5XX", + Hint: ` + Legacy V1 IPNS records are considered insecure. A gateway should + never return data when IPNS Record is missing V2 signature, EVEN + when V1 signature matches the payload. + More details in IPIP-428. + `, Request: Request(). Path("/ipns/{{name}}", ipnsV1), Response: Expect(). StatusBetween(500, 599), }, { - Name: "GET an IPNS Path (V1+V2) with broken ValueV1 from the gateway fails with 5xx", + Name: "GET for /ipns/name with valid V1+V2 signatures with V1-vs-V2 value mismatch MUST fail with 5XX", + Hint: ` + Legacy V1 signatures in IPNS records are considered insecure and + got replaced with V2 that sings entire CBOR in the data field. + Producing records with both V1 and V2 signatures is valid for + backward-compatibility, but validation logic requires V1 (legacy + protobuf fields) and V2 (CBOR in data field) to match. This means + that even when both signatures are valid, if V1 and V2 values do + not match, the IPNS record should not be considered valid, as it + could allow signature reuse attacks against V1 users. + More details in IPIP-428. + `, Request: Request(). Path("/ipns/{{name}}", ipnsV1V2BrokenValueV1), Response: Expect(). StatusBetween(500, 599), }, { - Name: "GET an IPNS Path (V1+V2) with broken SignatureV1, but valid SignatureV2 succeeds", + Name: "GET for /ipns/name with valid V2 and broken V1 signature succeeds", + Hint: ` + Legacy V1 signatures in IPNS records are considered insecure and + got replaced with V2 that sings entire CBOR in the data field. + Integrity of the record is protected by SignatureV2, V1 can be + ignored as long V1 values match V2 ones in CBOR. + More details in IPIP-428. + `, Request: Request(). Path("/ipns/{{name}}", ipnsV1V2BrokenSigV1.Key()), Response: Expect(). @@ -60,21 +84,38 @@ func TestGatewayIPNSPath(t *testing.T) { Body(bodyIPNSV1V2BrokenSigV1), }, { - Name: "GET an IPNS Path (V1+V2) from the gateway", + Name: "GET for /ipns/name with valid V1+V2 signatures succeeds", + Hint: ` + Records with legacy V1 signatures should not impact V2 verification. + The payload should match the content path from IPNS Record's Value field. + More details in IPIP-428. + `, Request: Request(). Path("/ipns/{{name}}", ipnsV1V2.Key()), Response: Expect(). Body(bodyIPNSV1V2), }, { - Name: "GET an IPNS Path (V2) from the gateway", + Name: "GET for /ipns/name with valid V2-only signature succeeds", + Hint: ` + Legacy V1 signatures in IPNS records are considered insecure and + got replaced with V2 that sings entire CBOR in the data field. + Gateway MUST correctly resolve IPNS records without V1 fields. + More details in IPIP-428. + `, Request: Request(). Path("/ipns/{{name}}", ipnsV2.Key()), Response: Expect(). Body(bodyIPNSV2), }, { - Name: "GET an IPNS Path (V1+V2) with broken SignatureV2 from the gateway fails with 5xx", + Name: "GET for /ipns/name with valid V1 and broken V2 signature MUST fail with 5XX", + Hint: ` + Legacy V1 IPNS records are considered insecure. A gateway should + never return data when IPNS Record is missing a valid V2 signature, + EVEN when V1 signature is valid. + More details in IPIP-428. + `, Request: Request(). Path("/ipns/{{name}}", ipnsV1V2BrokenSigV2), Response: Expect(). @@ -89,6 +130,12 @@ func TestRedirectCanonicalIPNS(t *testing.T) { tests := SugarTests{ { Name: "GET for /ipns/{b58-multihash-of-ed25519-key} redirects to /ipns/{cidv1-libp2p-key-base36}", + Hint: ` + CIDv1 in case-insensitive encoding ensures it works in contexts + such as authority component of URL. Base36 ensures ED25519 + libp2p-key fits in a single DNS label, making the IPNS name + compatible with subdomain gateways. + `, Request: Request(). Path("/ipns/12D3KooWRBy97UB99e3J6hiPesre1MZeuNQvfan4gBziswrRJsNK/root2/"), Response: Expect(). @@ -99,6 +146,10 @@ func TestRedirectCanonicalIPNS(t *testing.T) { }, { Name: "GET for /ipns/{cidv0-like-b58-multihash-of-rsa-key} redirects to /ipns/{cidv1-libp2p-key-base36}", + Hint: ` + CIDv1 in case-insensitive encoding ensures it works in contexts + such as authority component of URL. + `, Request: Request(). Path("/ipns/QmcJM7PRfkSbcM5cf1QugM5R37TLRKyJGgBEhXjLTB8uA2/root2/"), Response: Expect().