Skip to content

Commit ac0bab3

Browse files
authored
fix(routing/http): delegated routing of legacy RSA Peer IDs (#609)
This ensures /routing/v1/peers endpoint accepts all variants of PeerIDs we see in the wild, namely: - cidv1-libp2p-key-ed25519-peerid - base58-ed25519-peerid - cidv1-libp2p-key-rsa-peerid - base58-rsa-peerid
1 parent 0f223aa commit ac0bab3

File tree

3 files changed

+101
-75
lines changed

3 files changed

+101
-75
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ The following emojis are used to highlight certain changes:
3030
### Fixed
3131

3232
* `routing/http/server` now returns 404 Status Not Found when no records can be found.
33+
* `routing/http/server` now supports legacy RSA PeerIDs encoded as Base58 Multihash
3334

3435
### Security
3536

routing/http/server/server.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -254,20 +254,26 @@ func (s *server) findPeers(w http.ResponseWriter, r *http.Request) {
254254
// See https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md#string-representation
255255
// We are liberal in inputs here, and uplift legacy PeerID to CID if necessary.
256256
// Rationale: it is better to fix this common mistake than to error and break peer routing.
257-
cid, err := cid.Decode(pidStr)
257+
258+
// Attempt to parse PeerID
259+
pid, err := peer.Decode(pidStr)
260+
258261
if err != nil {
259-
// check if input is peer ID in legacy format
260-
if pid, err2 := peer.Decode(pidStr); err2 == nil {
261-
cid = peer.ToCid(pid)
262-
} else {
263-
writeErr(w, "FindPeers", http.StatusBadRequest, fmt.Errorf("unable to parse peer ID as libp2p-key CID: %w", err))
264-
return
262+
// Retry by parsing PeerID as CID, then setting codec to libp2p-key
263+
// and turning that back to PeerID.
264+
// This is necessary to make sure legacy keys like:
265+
// - RSA QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N
266+
// - ED25519 12D3KooWD3eckifWpRn9wQpMG9R9hX3sD158z7EqHWmweQAJU5SA
267+
// are parsed correctly.
268+
pidAsCid, err2 := cid.Decode(pidStr)
269+
if err2 == nil {
270+
pidAsCid = cid.NewCidV1(cid.Libp2pKey, pidAsCid.Hash())
271+
pid, err = peer.FromCid(pidAsCid)
265272
}
266273
}
267274

268-
pid, err := peer.FromCid(cid)
269275
if err != nil {
270-
writeErr(w, "FindPeers", http.StatusBadRequest, fmt.Errorf("unable to parse peer ID: %w", err))
276+
writeErr(w, "FindPeers", http.StatusBadRequest, fmt.Errorf("unable to parse PeerID %q: %w", pidStr, err))
271277
return
272278
}
273279

routing/http/server/server_test.go

Lines changed: 85 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func TestHeaders(t *testing.T) {
6161
require.Equal(t, "text/plain; charset=utf-8", header)
6262
}
6363

64-
func makePeerID(t *testing.T) (crypto.PrivKey, peer.ID) {
64+
func makeEd25519PeerID(t *testing.T) (crypto.PrivKey, peer.ID) {
6565
sk, _, err := crypto.GenerateEd25519Key(rand.Reader)
6666
require.NoError(t, err)
6767

@@ -71,6 +71,16 @@ func makePeerID(t *testing.T) (crypto.PrivKey, peer.ID) {
7171
return sk, pid
7272
}
7373

74+
func makeLegacyRSAPeerID(t *testing.T) (crypto.PrivKey, peer.ID) {
75+
sk, _, err := crypto.GenerateRSAKeyPair(2048, rand.Reader)
76+
require.NoError(t, err)
77+
78+
pid, err := peer.IDFromPrivateKey(sk)
79+
require.NoError(t, err)
80+
81+
return sk, pid
82+
}
83+
7484
func requireCloseToNow(t *testing.T, lastModified string) {
7585
// inspecting fields like 'Last-Modified' is prone to one-off errors, we test with 1m buffer
7686
lastModifiedTime, err := time.Parse(http.TimeFormat, lastModified)
@@ -220,22 +230,22 @@ func TestPeers(t *testing.T) {
220230
return resp
221231
}
222232

223-
t.Run("GET /routing/v1/peers/{non-peer-valid-cid} returns 400", func(t *testing.T) {
233+
t.Run("GET /routing/v1/peers/{non-peerid} returns 400", func(t *testing.T) {
224234
t.Parallel()
225235

226236
router := &mockContentRouter{}
227-
resp := makeRequest(t, router, mediaTypeJSON, "bafkqaaa")
237+
resp := makeRequest(t, router, mediaTypeJSON, "nonpeerid")
228238
require.Equal(t, 400, resp.StatusCode)
229239
})
230240

231241
t.Run("GET /routing/v1/peers/{cid-libp2p-key-peer-id} returns 404 with correct body and headers (No Results, explicit JSON)", func(t *testing.T) {
232242
t.Parallel()
233243

234-
_, pid := makePeerID(t)
244+
_, pid := makeEd25519PeerID(t)
235245
results := iter.FromSlice([]iter.Result[*types.PeerRecord]{})
236246

237247
router := &mockContentRouter{}
238-
router.On("FindPeers", mock.Anything, pid, 20).Return(results, nil)
248+
router.On("FindPeers", mock.Anything, pid, DefaultRecordsLimit).Return(results, nil)
239249

240250
resp := makeRequest(t, router, mediaTypeJSON, peer.ToCid(pid).String())
241251
require.Equal(t, 404, resp.StatusCode)
@@ -250,11 +260,11 @@ func TestPeers(t *testing.T) {
250260
t.Run("GET /routing/v1/peers/{cid-libp2p-key-peer-id} returns 404 with correct body and headers (No Results, implicit JSON, wildcard Accept header)", func(t *testing.T) {
251261
t.Parallel()
252262

253-
_, pid := makePeerID(t)
263+
_, pid := makeEd25519PeerID(t)
254264
results := iter.FromSlice([]iter.Result[*types.PeerRecord]{})
255265

256266
router := &mockContentRouter{}
257-
router.On("FindPeers", mock.Anything, pid, 20).Return(results, nil)
267+
router.On("FindPeers", mock.Anything, pid, DefaultRecordsLimit).Return(results, nil)
258268

259269
// Simulate request with Accept header that includes wildcard match
260270
resp := makeRequest(t, router, "text/html,*/*", peer.ToCid(pid).String())
@@ -268,11 +278,11 @@ func TestPeers(t *testing.T) {
268278
t.Run("GET /routing/v1/peers/{cid-libp2p-key-peer-id} returns 404 with correct body and headers (No Results, implicit JSON, no Accept header)", func(t *testing.T) {
269279
t.Parallel()
270280

271-
_, pid := makePeerID(t)
281+
_, pid := makeEd25519PeerID(t)
272282
results := iter.FromSlice([]iter.Result[*types.PeerRecord]{})
273283

274284
router := &mockContentRouter{}
275-
router.On("FindPeers", mock.Anything, pid, 20).Return(results, nil)
285+
router.On("FindPeers", mock.Anything, pid, DefaultRecordsLimit).Return(results, nil)
276286

277287
// Simulate request without Accept header
278288
resp := makeRequest(t, router, "", peer.ToCid(pid).String())
@@ -285,10 +295,10 @@ func TestPeers(t *testing.T) {
285295
t.Run("GET /routing/v1/peers/{cid-libp2p-key-peer-id} returns 404 when router returns routing.ErrNotFound", func(t *testing.T) {
286296
t.Parallel()
287297

288-
_, pid := makePeerID(t)
298+
_, pid := makeEd25519PeerID(t)
289299

290300
router := &mockContentRouter{}
291-
router.On("FindPeers", mock.Anything, pid, 20).Return(nil, routing.ErrNotFound)
301+
router.On("FindPeers", mock.Anything, pid, DefaultRecordsLimit).Return(nil, routing.ErrNotFound)
292302

293303
// Simulate request without Accept header
294304
resp := makeRequest(t, router, "", peer.ToCid(pid).String())
@@ -301,7 +311,7 @@ func TestPeers(t *testing.T) {
301311
t.Run("GET /routing/v1/peers/{cid-libp2p-key-peer-id} returns 200 with correct body and headers (JSON)", func(t *testing.T) {
302312
t.Parallel()
303313

304-
_, pid := makePeerID(t)
314+
_, pid := makeEd25519PeerID(t)
305315
results := iter.FromSlice([]iter.Result[*types.PeerRecord]{
306316
{Val: &types.PeerRecord{
307317
Schema: types.SchemaPeer,
@@ -318,7 +328,7 @@ func TestPeers(t *testing.T) {
318328
})
319329

320330
router := &mockContentRouter{}
321-
router.On("FindPeers", mock.Anything, pid, 20).Return(results, nil)
331+
router.On("FindPeers", mock.Anything, pid, DefaultRecordsLimit).Return(results, nil)
322332

323333
libp2pKeyCID := peer.ToCid(pid).String()
324334
resp := makeRequest(t, router, mediaTypeJSON, libp2pKeyCID)
@@ -340,11 +350,11 @@ func TestPeers(t *testing.T) {
340350
t.Run("GET /routing/v1/peers/{cid-libp2p-key-peer-id} returns 404 with correct body and headers (No Results, NDJSON)", func(t *testing.T) {
341351
t.Parallel()
342352

343-
_, pid := makePeerID(t)
353+
_, pid := makeEd25519PeerID(t)
344354
results := iter.FromSlice([]iter.Result[*types.PeerRecord]{})
345355

346356
router := &mockContentRouter{}
347-
router.On("FindPeers", mock.Anything, pid, 0).Return(results, nil)
357+
router.On("FindPeers", mock.Anything, pid, DefaultStreamingRecordsLimit).Return(results, nil)
348358

349359
resp := makeRequest(t, router, mediaTypeNDJSON, peer.ToCid(pid).String())
350360
require.Equal(t, 404, resp.StatusCode)
@@ -359,7 +369,7 @@ func TestPeers(t *testing.T) {
359369
t.Run("GET /routing/v1/peers/{cid-libp2p-key-peer-id} returns 200 with correct body and headers (NDJSON)", func(t *testing.T) {
360370
t.Parallel()
361371

362-
_, pid := makePeerID(t)
372+
_, pid := makeEd25519PeerID(t)
363373
results := iter.FromSlice([]iter.Result[*types.PeerRecord]{
364374
{Val: &types.PeerRecord{
365375
Schema: types.SchemaPeer,
@@ -376,7 +386,7 @@ func TestPeers(t *testing.T) {
376386
})
377387

378388
router := &mockContentRouter{}
379-
router.On("FindPeers", mock.Anything, pid, 0).Return(results, nil)
389+
router.On("FindPeers", mock.Anything, pid, DefaultStreamingRecordsLimit).Return(results, nil)
380390

381391
libp2pKeyCID := peer.ToCid(pid).String()
382392
resp := makeRequest(t, router, mediaTypeNDJSON, libp2pKeyCID)
@@ -393,11 +403,34 @@ func TestPeers(t *testing.T) {
393403
require.Equal(t, expectedBody, string(body))
394404
})
395405

396-
t.Run("GET /routing/v1/peers/{legacy-base58-peer-id} returns 200 with correct body (JSON)", func(t *testing.T) {
397-
t.Parallel()
406+
// Test matrix that runs the HTTP 200 scenario against different flavours of PeerID
407+
// to ensure consistent behavior
408+
peerIdtestCases := []struct {
409+
peerIdType string
410+
makePeerId func(t *testing.T) (crypto.PrivKey, peer.ID)
411+
peerIdAsCidV1 bool
412+
}{
413+
// Test against current and past PeerID key types and string representations.
414+
// https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md#string-representation
415+
{"cidv1-libp2p-key-ed25519-peerid", makeEd25519PeerID, true},
416+
{"base58-ed25519-peerid", makeEd25519PeerID, false},
417+
{"cidv1-libp2p-key-rsa-peerid", makeLegacyRSAPeerID, true},
418+
{"base58-rsa-peerid", makeLegacyRSAPeerID, false},
419+
}
398420

399-
_, pid := makePeerID(t)
400-
results := iter.FromSlice([]iter.Result[*types.PeerRecord]{
421+
for _, tc := range peerIdtestCases {
422+
_, pid := tc.makePeerId(t)
423+
var peerIDStr string
424+
if tc.peerIdAsCidV1 {
425+
// PeerID represented by CIDv1 with libp2p-key codec
426+
// https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md#string-representation
427+
peerIDStr = peer.ToCid(pid).String()
428+
} else {
429+
// Legacy PeerID starting with "123..." or "Qm.."
430+
// https://github.com/libp2p/specs/blob/master/peer-ids/peer-ids.md#string-representation
431+
peerIDStr = b58.Encode([]byte(pid))
432+
}
433+
results := []iter.Result[*types.PeerRecord]{
401434
{Val: &types.PeerRecord{
402435
Schema: types.SchemaPeer,
403436
ID: &pid,
@@ -410,67 +443,53 @@ func TestPeers(t *testing.T) {
410443
Protocols: []string{"transport-foo"},
411444
Addrs: []types.Multiaddr{},
412445
}},
413-
})
414-
415-
router := &mockContentRouter{}
416-
router.On("FindPeers", mock.Anything, pid, 20).Return(results, nil)
446+
}
417447

418-
legacyPeerID := b58.Encode([]byte(pid))
419-
resp := makeRequest(t, router, mediaTypeJSON, legacyPeerID)
420-
require.Equal(t, 200, resp.StatusCode)
448+
t.Run("GET /routing/v1/peers/{"+tc.peerIdType+"} returns 200 with correct body and headers (NDJSON streaming response)", func(t *testing.T) {
449+
t.Parallel()
421450

422-
header := resp.Header.Get("Content-Type")
423-
require.Equal(t, "Accept", resp.Header.Get("Vary"))
424-
require.Equal(t, mediaTypeJSON, header)
451+
router := &mockContentRouter{}
452+
router.On("FindPeers", mock.Anything, pid, DefaultStreamingRecordsLimit).Return(iter.FromSlice(results), nil)
425453

426-
body, err := io.ReadAll(resp.Body)
427-
require.NoError(t, err)
454+
resp := makeRequest(t, router, mediaTypeNDJSON, peerIDStr)
455+
require.Equal(t, 200, resp.StatusCode)
428456

429-
expectedBody := `{"Peers":[{"Addrs":[],"ID":"` + pid.String() + `","Protocols":["transport-bitswap","transport-foo"],"Schema":"peer"},{"Addrs":[],"ID":"` + pid.String() + `","Protocols":["transport-foo"],"Schema":"peer"}]}`
430-
require.Equal(t, expectedBody, string(body))
431-
})
457+
require.Equal(t, mediaTypeNDJSON, resp.Header.Get("Content-Type"))
458+
require.Equal(t, "Accept", resp.Header.Get("Vary"))
459+
require.Equal(t, "public, max-age=300, stale-while-revalidate=172800, stale-if-error=172800", resp.Header.Get("Cache-Control"))
432460

433-
t.Run("GET /routing/v1/peers/{legacy-base58-peer-id} returns 200 with correct body (NDJSON)", func(t *testing.T) {
434-
t.Parallel()
461+
body, err := io.ReadAll(resp.Body)
462+
require.NoError(t, err)
435463

436-
_, pid := makePeerID(t)
437-
results := iter.FromSlice([]iter.Result[*types.PeerRecord]{
438-
{Val: &types.PeerRecord{
439-
Schema: types.SchemaPeer,
440-
ID: &pid,
441-
Protocols: []string{"transport-bitswap", "transport-foo"},
442-
Addrs: []types.Multiaddr{},
443-
}},
444-
{Val: &types.PeerRecord{
445-
Schema: types.SchemaPeer,
446-
ID: &pid,
447-
Protocols: []string{"transport-foo"},
448-
Addrs: []types.Multiaddr{},
449-
}},
464+
expectedBody := `{"Addrs":[],"ID":"` + pid.String() + `","Protocols":["transport-bitswap","transport-foo"],"Schema":"peer"}` + "\n" + `{"Addrs":[],"ID":"` + pid.String() + `","Protocols":["transport-foo"],"Schema":"peer"}` + "\n"
465+
require.Equal(t, expectedBody, string(body))
450466
})
451467

452-
router := &mockContentRouter{}
453-
router.On("FindPeers", mock.Anything, pid, 0).Return(results, nil)
468+
t.Run("GET /routing/v1/peers/{"+tc.peerIdType+"} returns 200 with correct body and headers (JSON response)", func(t *testing.T) {
469+
t.Parallel()
454470

455-
legacyPeerID := b58.Encode([]byte(pid))
456-
resp := makeRequest(t, router, mediaTypeNDJSON, legacyPeerID)
457-
require.Equal(t, 200, resp.StatusCode)
471+
router := &mockContentRouter{}
472+
router.On("FindPeers", mock.Anything, pid, DefaultRecordsLimit).Return(iter.FromSlice(results), nil)
458473

459-
header := resp.Header.Get("Content-Type")
460-
require.Equal(t, "Accept", resp.Header.Get("Vary"))
461-
require.Equal(t, mediaTypeNDJSON, header)
474+
resp := makeRequest(t, router, mediaTypeJSON, peerIDStr)
475+
require.Equal(t, 200, resp.StatusCode)
462476

463-
body, err := io.ReadAll(resp.Body)
464-
require.NoError(t, err)
477+
require.Equal(t, mediaTypeJSON, resp.Header.Get("Content-Type"))
478+
require.Equal(t, "Accept", resp.Header.Get("Vary"))
479+
require.Equal(t, "public, max-age=300, stale-while-revalidate=172800, stale-if-error=172800", resp.Header.Get("Cache-Control"))
465480

466-
expectedBody := `{"Addrs":[],"ID":"` + pid.String() + `","Protocols":["transport-bitswap","transport-foo"],"Schema":"peer"}` + "\n" + `{"Addrs":[],"ID":"` + pid.String() + `","Protocols":["transport-foo"],"Schema":"peer"}` + "\n"
467-
require.Equal(t, expectedBody, string(body))
468-
})
481+
body, err := io.ReadAll(resp.Body)
482+
require.NoError(t, err)
483+
484+
expectedBody := `{"Peers":[{"Addrs":[],"ID":"` + pid.String() + `","Protocols":["transport-bitswap","transport-foo"],"Schema":"peer"},{"Addrs":[],"ID":"` + pid.String() + `","Protocols":["transport-foo"],"Schema":"peer"}]}`
485+
require.Equal(t, expectedBody, string(body))
486+
})
487+
}
469488

470489
}
471490

472491
func makeName(t *testing.T) (crypto.PrivKey, ipns.Name) {
473-
sk, pid := makePeerID(t)
492+
sk, pid := makeEd25519PeerID(t)
474493
return sk, ipns.NameFromPeer(pid)
475494
}
476495

0 commit comments

Comments
 (0)