Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions RFC/0004-mldsa-peer-ids.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
- Start Date: 2026-04-11
- Related PRs: [js-libp2p/pull/3432](https://github.com/libp2p/js-libp2p/pull/3432)
- Related Spec: [peer-ids/peer-ids.md](../peer-ids/peer-ids.md)

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

## Abstract

This RFC proposes adding support for post-quantum ML-DSA identity keys to
libp2p.

It defines a new `KeyType` value for ML-DSA in libp2p key protobufs, a wire
format for serializing ML-DSA public/private keys, and how peer IDs are derived
from ML-DSA public keys.

## Motivation

libp2p peer identities are currently based on classical signature schemes (RSA,
Ed25519, secp256k1, ECDSA).

To prepare libp2p identity and signature systems for post-quantum migration, we
need an interoperable encoding and verification story for ML-DSA keys.

Without a shared specification:

- Implementations may choose incompatible key encodings.
- Cross-implementation interoperability is not guaranteed.
- Future migration to hybrid or fully post-quantum deployments becomes harder.

## Design

### 1. `KeyType` extension

The key protobuf enum in the peer-id spec is extended with:

```protobuf
enum KeyType {
RSA = 0;
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...

}
```

### 2. ML-DSA parameter sets

This RFC supports the three standardized ML-DSA parameter sets:

- `ML-DSA-44`
- `ML-DSA-65`
- `ML-DSA-87`

Parameter names and sizes are defined by [FIPS 204](#references).

### 3. Key serialization in protobuf `Data`

For `PublicKey` and `PrivateKey` messages where `Type = MLDSA`, the `Data`
field is:

```
<variant-prefix><raw-key-bytes>
```

Where `variant-prefix` is one byte:

- `0x01` = `ML-DSA-44`
- `0x02` = `ML-DSA-65`
- `0x03` = `ML-DSA-87`
Comment on lines +65 to +69
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


Raw key and signature lengths are fixed per ML-DSA variant:

| Variant | Public key (bytes) | Expanded private key (bytes) | Signature (bytes) |
| --- | ---: | ---: | ---: |
| `ML-DSA-44` | 1312 | 2560 | 2420 |
| `ML-DSA-65` | 1952 | 4032 | 3309 |
| `ML-DSA-87` | 2592 | 4896 | 4627 |

Implementations MUST reject malformed key payloads, including unknown
`variant-prefix`, missing prefix, or length mismatch against the table above.

### 4. Signature semantics

ML-DSA signatures are generated and verified using the standard ML-DSA
algorithm for the corresponding parameter set.

- Implementations MUST sign the exact message bytes.
- Implementations MUST NOT apply an additional pre-hash at the libp2p key API
boundary.
- Implementations MUST verify signatures over the exact same message bytes and
ML-DSA parameter set.

### 5. Peer ID derivation

Peer ID derivation remains unchanged:

1. Protobuf-encode `PublicKey` with `Type = MLDSA` and `Data` as specified
above.
2. Compute peer ID multihash according to the existing rule:
- identity multihash if encoded key is `<= 42` bytes
- SHA-256 multihash otherwise

Because all ML-DSA public key encodings are much larger than 42 bytes, ML-DSA
peer IDs always use SHA-256 multihash.

## Backward Compatibility

- Existing peers and key types are unaffected.
- Implementations that do not support `KeyType = MLDSA` will reject those keys
as unsupported.
- Text and CID peer ID formats are unchanged.

## Security Considerations

- This RFC only defines identity key representation and signature verification
semantics.
- It does not by itself provide hybrid authentication or downgrade resistance
between classical and post-quantum identities.
- Deployments should continue evaluating algorithm maturity, implementation
quality, and runtime support in their target environments.

## Implementation Status (Non-Normative)

Current language support for ML-DSA is still evolving:

- Node.js runtime support is experimental.
- Browser WebCrypto support is unavailable.
- Go has an internal implementation for ML-DSA but no public API as of Go 1.26.
Comment thread
dozyio marked this conversation as resolved.
- Worth noting: `crypto/mlkem` went public in Go 1.24, `crypto/mldsa` is in
proposal phase [golang/go#77626](https://github.com/golang/go/issues/77626).
For now, `github.com/cloudflare/circl/sign/mldsa` is available.

This RFC specifies interoperability behavior independent of implementation
maturity.
Production deployments SHOULD evaluate runtime support, performance, and audit
status before enabling ML-DSA identities.

## Open Questions

The following questions should be resolved before promoting this to a candidate
recommendation:

1. Default variant selection for new key generation (`ML-DSA-44`, `-65`, or
`-87`).
2. Whether any libp2p subsystem should require dual-signature or hybrid
identity strategies.
3. Whether to define mandatory test vectors in the peer-id spec for ML-DSA
encodings.
4. Canonical private key format: should it be FIPS 204 expanded form
[multiformats/multicodec#399](https://github.com/multiformats/multicodec/pull/399),
W3C DI Quantum-Safe Cryptosuite)?

## References

- [FIPS 204] NIST, *Module-Lattice-Based Digital Signature Standard* (ML-DSA),
https://csrc.nist.gov/pubs/fips/204/final
39 changes: 36 additions & 3 deletions peer-ids/peer-ids.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ about maturity level and spec status.
- [Ed25519](#ed25519)
- [Secp256k1](#secp256k1)
- [ECDSA](#ecdsa)
- [ML-DSA](#ml-dsa)
- [Peer Ids](#peer-ids)
- [String representation](#string-representation)
- [Encoding](#encoding)
Expand Down Expand Up @@ -63,6 +64,7 @@ enum KeyType {
Ed25519 = 1;
Secp256k1 = 2;
ECDSA = 3;
MLDSA = 4;
}

message PublicKey {
Expand Down Expand Up @@ -110,16 +112,17 @@ The second is for generating peer ids; this is discussed in the section below.

### Key Types

Four key types are supported:
Five key types are supported:
- RSA
- Ed25519
- Secp256k1
- ECDSA
- ML-DSA

Implementations MUST support Ed25519. Implementations SHOULD support RSA if they wish to
interoperate with the mainline IPFS DHT and the default IPFS bootstrap nodes. Implementations MAY
support Secp256k1 and ECDSA, but nodes using those keys may not be able to connect to all other
nodes.
support Secp256k1, ECDSA, and ML-DSA, but nodes using those keys may not be able to connect to all
other nodes.

In all cases, implementation MAY allow the user to enable/disable specific key
types via configuration. Note that disabling support for compulsory key types
Expand Down Expand Up @@ -185,6 +188,33 @@ To sign a message, we hash the message with SHA 256, and then sign it with the
[ECDSA standard algorithm](https://tools.ietf.org/html/rfc6979), then we encode
it using [DER-encoded ASN.1.](https://wiki.openssl.org/index.php/DER)

#### ML-DSA

For `MLDSA` keys, the serialized `Data` field includes a one-byte variant prefix
followed by raw key bytes:

```
<variant-prefix><raw-key-bytes>
```

Variant prefixes are:

- `0x01`: ML-DSA-44
- `0x02`: ML-DSA-65
- `0x03`: ML-DSA-87

Public/private key lengths are the standard lengths for each variant:

- ML-DSA-44: public key `1312` bytes, private key `2560` bytes
- ML-DSA-65: public key `1952` bytes, private key `4032` bytes
- ML-DSA-87: public key `2592` bytes, private key `4896` bytes

Implementations MUST reject malformed key encodings (unknown variant prefix,
missing prefix, or key length mismatch).

ML-DSA signatures follow the normal ML-DSA standard. Signatures are generated
and verified over the exact message bytes.

### Test vectors

The following test vectors are hex-encoded bytes of the above described protobuf encoding.
Expand Down Expand Up @@ -221,6 +251,9 @@ Specifically, to compute a peer ID of a key:
5. If the length is greater than 42, then hash it using the SHA256
multihash.

Because ML-DSA public keys are larger than 42 bytes, ML-DSA peer IDs always use
the SHA256 multihash.

### String representation

There are two ways to represent peer IDs in text: as a raw
Expand Down