TCP Dump

Public key:

Message hash:

Signature:

Let’s observe Internet Computer calls on the wire with tcpdump and tcpflow. Assuming dfx start is running on the default port of 8000:

$ tcpdump -i lo port 8000 -w pcap

We call our hello.wasm canister:

$ dfx canister call hello hi

Next, we hit Ctrl-C to stop tcpdump, and run:

$ tcpflow -r pcap

This produces a few files. One has a name like:

127.000.000.001.59628-127.000.000.001.08000

where the first port number is selected by the system.

This log shows the TCP requests made to the local net. It turns out to use the HTTPS interface of the Internet Computer, starting with a status check:

GET /api/v2/status HTTP/1.1
content-type: application/cbor
accept: */*
host: 127.0.0.1:8000

Satisfied with the check, it then sends a POST request to call the hi method:

POST /api/v2/canister/rrkah-fqaaa-aaaaa-aaaaq-cai/call HTTP/1.1
content-type: application/cbor
accept: */*
host: 127.0.0.1:8000
content-length: 346

We hex dump the contents:

00000000: d9d9 f7a3 6763 6f6e 7465 6e74 a76c 7265  ....gcontent.lre
00000010: 7175 6573 745f 7479 7065 6463 616c 6c65  quest_typedcalle
00000020: 6e6f 6e63 6550 a397 34ae 37c9 ebba f7ae  nonceP..4.7.....
00000030: cd7f 9b10 ce0e 6e69 6e67 7265 7373 5f65  ......ningress_e
00000040: 7870 6972 791b 16e2 e030 d9c3 b342 6673  xpiry....0...Bfs
00000050: 656e 6465 7258 1d4f f2c7 9f70 067d 24bb  enderX.O...p.}$.
00000060: ba4a 1647 37e0 eddd 6280 2ac6 0353 1fa0  .J.G7...b.*..S..
00000070: fc85 5b02 6b63 616e 6973 7465 725f 6964  ..[.kcanister_id
00000080: 4a00 0000 0000 0000 0101 016b 6d65 7468  J..........kmeth
00000090: 6f64 5f6e 616d 6562 6869 6361 7267 4644  od_namebhicargFD
000000a0: 4944 4c00 006d 7365 6e64 6572 5f70 7562  IDL..msender_pub
000000b0: 6b65 7958 5830 5630 1006 072a 8648 ce3d  keyXX0V0...*.H.=
000000c0: 0201 0605 2b81 0400 0a03 4200 043c c849  ....+.....B..<.I
000000d0: c77d 5ead 3aea f2ea 821d c85d 6bb1 0483  .}^.:......]k...
000000e0: bbe9 7875 d010 ada2 629e 4a86 3e81 5793  ..xu....b.J.>.W.
000000f0: de69 ae4f fce4 6d52 c4b1 4ed1 a3ae 40e8  .i.O..mR..N...@.
00000100: 5b53 b5cb 6c7e d6de 89d8 0c43 056a 7365  [S..l~.....C.jse
00000110: 6e64 6572 5f73 6967 5840 c1e2 68ea d7f7  nder_sigX@..h...
00000120: f8b5 e42c bb77 c1d7 5f2e e8a5 8f32 94d9  ...,.w.._....2..
00000130: 23fc 9bc1 cc83 6719 9be5 8a4d c285 beb4  #.....g....M....
00000140: ed6f a1be abb0 682e c37a a6b7 7263 6071  .o....h..z..rc`q
00000150: 41a6 9573 3e6b a53d d002                 A..s>k.=..

The d9d9f7 hints that it is a CBOR message, which we decode with cbor.me:

55799(
{ "content":
  { "request_type": "call"
  , "nonce": h'A39734AE37C9EBBAF7AECD7F9B10CE0E'
  , "ingress_expiry": 1649126913987556162
  , "sender": h'4FF2C79F70067D24BBBA4A164737E0EDDD62802AC603531FA0FC855B02'
  , "canister_id": h'00000000000000010101'
  , "method_name": "hi"
  , "arg": h'4449444C0000'
  }
, "sender_pubkey": h'3056301006072A8648CE3D020106052B8104000A034200043CC849C77D5EAD3AEAF2EA821DC85D6BB10483BBE97875D010ADA2629E4A863E815793DE69AE4FFCE46D52C4B14ED1A3AE40E85B53B5CB6C7ED6DE89D80C4305'
, "sender_sig": h'C1E268EAD7F7F8B5E42CBB77C1D75F2EE8A58F3294D923FC9BC1CC8367199BE58A4DC285BEB4ED6FA1BEABB0682EC37AA6B77263607141A695733E6BA53DD002'})

We see that:

  • request_type is call. This is overkill because hi is a query, not an update. On the other hand, we now get to see an update in action, which is more interesting than a query.

  • arg is the Candid message 4449444C0000, the empty message.

  • sender is the (raw) principal ID of the sender; since a user sent the request, it is the SHA-224 hash of the sender_pubkey field followed by 0x02.

  • sender_pubkey is a DER-encoded key.

  • sender_sig is the r and s fields of the signature, both 32 bytes long.

The sender_pubkey is the the "zoo" key we explored. Recall we can decode sender_pubkey online to see some boilerplate followed by 0x04 indicating an uncompressed point, then the x and y coordinates that take 32 bytes each (so the last 130 hex digits represent the raw key).

The message being signed is the Request ID prepended with the domain separator \x0Aic-request.

Because we made a call instead of a query, we see dfx poll for a response via read_state:

POST /api/v2/canister/rrkah-fqaaa-aaaaa-aaaaq-cai/read_state HTTP/1.1
content-type: application/cbor
accept: */*
host: 127.0.0.1:8000
content-length: 337

The decoded contents contain:

"paths": [[h'726571756573745F737461747573', h'E3676A8C10705FCCE9772601485F6CEA00283166411E5FBE827F796B3C779D06']]},

The first blob is request_status, and the second is the request ID of the request we just sent.

We can compute the message hash that is signed by the sender_pubkey:

$ (printf "\x0aic-request"; echo E3676A8C10705FCCE9772601485F6CEA00283166411E5FBE827F796B3C779D06 | xxd -r -p ) | sha256sum
ab01a4005d74c5f86b8b1e7a0e2e7c79286c6f6b6a910585cd822f210139fb2b  -

Use the widget at the top of this page to verify the signature.

We examine another file produced by tcpflow, logging the responses from the local net:

127.000.000.001.08000-127.000.000.001.59628
55799({"certificate": h'D9D9F7A264747265658301820458209BB201590CC0CC99791C2E597995C481E1AAAD237C25465084D27FFAC3EEE0D3830182045820ED5A370EA06DD5C64BBDB9049F9645D703430F0827E39C7AEBF1FE44599120EE83024474696D65820349FC9EA8AA8FFFB7F116697369676E61747572655830B26FAB0E4093F839194990434A1374DCD14DBE4F90EA7A9280B26C8751CC1B9BDD7477AECD054C61A60BA8A6AC6BF94F'})

We see a CBOR message within this CBOR message. The inner message decodes to:

55799({"tree": [1, [4, h'9BB201590CC0CC99791C2E597995C481E1AAAD237C25465084D27FFAC3EEE0D3'], [1, [4, h'ED5A370EA06DD5C64BBDB9049F9645D703430F0827E39C7AEBF1FE44599120EE'], [2, h'74696D65', [3, h'FC9EA8AA8FFFB7F116']]]], "signature": h'B26FAB0E4093F839194990434A1374DCD14DBE4F90EA7A9280B26C8751CC1B9BDD7477AECD054C61A60BA8A6AC6BF94F'})

This certificate is proof that the local net has not yet received the request at a certain time. Eventually, we get a bigger certifiicate and decoding the CBOR message within this CBOR message gives:

55799({"tree": [1, [1, [4, h'883F097A8B478D53D2A3D8C09D9F3463E299705D78B782A4457815FD966120F0'], [1, [2, h'726571756573745F737461747573', [1, [4, h'16353093030AD98645FEC08958223DDD3ED211DB1C8619BB465EDD88712F1CDF'], [2, h'E3676A8C10705FCCE9772601485F6CEA00283166411E5FBE827F796B3C779D06', [1, [2, h'7265706C79', [3, h'48656C6C6F2C20576F726C64210A']], [2, h'737461747573', [3, h'7265706C696564']]]]]], [4, h'BD1E8E6FCF5F6150EF65C180344A88CCEA77C12967D52A41E65B6A2D6E7504BB']]], [1, [4, h'ED5A370EA06DD5C64BBDB9049F9645D703430F0827E39C7AEBF1FE44599120EE'], [2, h'74696D65', [3, h'A58CAFE291FFB7F116']]]], "signature": h'81E173DAFAA7D9D480107BFC9A38F7060F23FD9E5B7F97A0CA64BB2A065821A80742BBA81E7FF9F9EA0A6D632FA13887'})

Many of the blobs are strings encoded in ASCII, and it turns out this certificate is proof that our call resulted in a reply of "Hello, World!\n".


Ben Lynn 💡