Skip to content

RFC-0004: ML-DSA Key Support for libp2p Peer Identities#710

Open
dozyio wants to merge 5 commits intolibp2p:masterfrom
dozyio:docs/ml-dsa
Open

RFC-0004: ML-DSA Key Support for libp2p Peer Identities#710
dozyio wants to merge 5 commits intolibp2p:masterfrom
dozyio:docs/ml-dsa

Conversation

@dozyio
Copy link
Copy Markdown

@dozyio dozyio commented Apr 11, 2026

RFC to kick of some discussion around a post quantum strategy for libp2p

@paschal533
Copy link
Copy Markdown

Hi @dozyio , great to see this as a formal RFC. I have been working on the handshake side of the PQC picture (XXhfs + X-Wing KEM, ChainSafe/js-libp2p-noise#665)
From my implementation and benchmarks, the combined overhead when XXhfs + ML-DSA-65 identity are both enabled on both sides is roughly:

Scenario Approx. wire size
Classical XX + Ed25519 ~500 B
XXhfs + Ed25519 (KEM upgrade only) ~2,852 B
XXhfs + ML-DSA-65 both sides ~9,400 B

The ML-DSA-65 identity cost (~6,600 B across both sides: 1,952 B public key + 3,309 B signature + overhead, per side) is 2.7x larger than the KEM overhead (~2,352 B). Worth factoring in when discussing whether to require hybrid signatures or allow a phased migration.

One argument for deferring hybrid signatures: the threat timelines are different. For the KEM, XXhfs hybrid makes sense today because of store-now-decrypt-later... someone recording traffic now can decrypt it later. For identity signatures, the threat only materialises once quantum computers actually exist, so a clean migration from Ed25519 to ML-DSA could be reasonable rather than carrying both indefinitely. That said, downgrade resistance is a real concern your security considerations section mentions
I have working test vectors from the JS XXhfs implementation that cover ML-DSA-65 signing and verification. Happy to contribute those if it helps progress

Comment thread RFC/0004-mldsa-peer-ids.md Outdated
Comment on lines +63 to +67
Where `variant-prefix` is one byte:

- `0x01` = `ML-DSA-44`
- `0x02` = `ML-DSA-65`
- `0x03` = `ML-DSA-87`
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For public keys: length alone identifies the variant (1312 / 1952 / 2592 bytes are unique), so the prefix is redundant. iiuc the JS PoC implementation already falls back to length-based detection?

For private keys: necessity depends on the format choice:

  • Seed (32 bytes for all variants): prefix is required to identify the variant.
  • Expanded (unique sizes per variant): prefix is redundant, consistent with the multicodec approach.

I think if you go with expanded private key format for this RFC, then this prefix is redundant, all we need is length of the expanded priv key alone to tell which version it is

Comment thread RFC/0004-mldsa-peer-ids.md Outdated
Comment on lines +83 to +86
- Input to signing is the exact message bytes.
- No additional pre-hash step is applied by libp2p at the key API boundary.
- Verification is performed over the same message bytes with the same parameter
set.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FIPS 204 §5.2 defines ML-DSA.Sign(sk, M, ctx) where ctx is a caller-supplied context string (0–255 bytes). The spec says nothing about what value libp2p uses for ctx. Two compliant implementations using different values produce incompatible signatures.

Existing libp2p signing contexts use message-level string prefixes prepended
before calling Sign(), not algorithm-level context parameters:

  • Noise: "noise-libp2p-static-key:" || static_pub_key (handshake.go)
  • TLS: "libp2p-tls-handshake:" || cert_pub_key (crypto.go)
  • Record envelope: varint(len(domain)) || domain || varint(len(type)) || type || varint(len(payload)) || payload (envelope.go)

So.. iiuc to stay consistent with this pattern, the RFC should explicitly state what ctx is? ctx = "" ? ctx = "libp2p-identity"?

Suggested change
- Input to signing is the exact message bytes.
- No additional pre-hash step is applied by libp2p at the key API boundary.
- Verification is performed over the same message bytes with the same parameter
set.
- Input to signing is the exact message bytes.
- Implementations MUST pass an empty context string (`ctx = "??TODO DECIDE??"`) to `ML-DSA.Sign`. Domain separation is the responsibility of the caller via message prefixes, as with all other libp2p key types.
- No additional pre-hash step is applied by libp2p at the key API boundary.
- Verification is performed over the same message bytes with the same parameter
set.

Note I'm not a cryptographer, but it may be safer to do belt-and-supenders and also require ctx = "libp2p-identity".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The context string should be a unique based on what the signature is for. Similar to the context in Ed25519ctx. Applications should be be able to pass this in.

Ed25519 = 1;
Secp256k1 = 2;
ECDSA = 3;
MLDSA = 4;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MLDSA = 4 covers all three parameter sets; the variant is only in Data.

Multicodec assigns separate code points per variant (multiformats/multicodec#392).

With this design, the security level is not visible from the protobuf envelope without parsing the key data.

The choice is defensible, but this is an opportunity to remove indirection for new key types and just use the same number as in multicodec table:

Assuming we use expanded key format, we could do:

Suggested change
MLDSA = 4;
MLDSA44 = 0x1317;
MLDSA65 = 0x1318;
MLDSA87 = 0x1319;

Copy link
Copy Markdown

@dennis-tra dennis-tra Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't the type be inferred from the length of the data field? This would be similar to the RSA case, no?

Edit: I see that this is being discussed in another comment.

Copy link
Copy Markdown

@dennis-tra dennis-tra Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If my understanding of the protobuf wire format is correct I would limit the enum values to a maximum of 7 (as long as there's space) because going beyond that would then require two bytes on the wire due to varint encoding.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you mean 7 bits, right? or values less than 0x80/128 https://protobuf.dev/programming-guides/encoding/#varints.

Copy link
Copy Markdown

@dennis-tra dennis-tra Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh of course, my brain slipped. For some reason I thought the number seven requires seven bits (so that the maximum enum value is 7). The argument still holds because the proposed multicodec values are larger than 127 and thus would still require multiple bytes to encode

Copy link
Copy Markdown

@dennis-tra dennis-tra Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One could argue that it doesn't matter because MLDSA keys are large anyway. One thing to consider could be how they will fit in MTUs (just to rule out that these values don't push the encoded data into an additional packet). At the same time they are not sent in isolation anyway. So nevermind...

Comment thread RFC/0004-mldsa-peer-ids.md
Comment thread RFC/0004-mldsa-peer-ids.md Outdated
@MarcoPolo
Copy link
Copy Markdown
Contributor

A different tact would be to add a PKIX key type, and the data is the PKIX key encoding. Then we would get ML-DSA for free with https://www.rfc-editor.org/rfc/rfc9881.html#name-ml-dsa-public-keys-in-pkix

@MarcoPolo
Copy link
Copy Markdown
Contributor

A different tact would be to add a PKIX key type, and the data is the PKIX key encoding. Then we would get ML-DSA for free with https://www.rfc-editor.org/rfc/rfc9881.html#name-ml-dsa-public-keys-in-pkix

To be concrete: #711

dozyio and others added 4 commits April 15, 2026 19:26
add expanded to private key size

Co-authored-by: Marcin Rataj <lidel@lidel.org>
Co-authored-by: Marcin Rataj <lidel@lidel.org>
Co-authored-by: Marcin Rataj <lidel@lidel.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

5 participants