Skip to content

Commit 0acf1fd

Browse files
First (incomplete) draft of ml-dsa signing scheme.
Signed-off-by: Fredrik Skogman <kommendorkapten@github.com>
1 parent 61a7d76 commit 0acf1fd

1 file changed

Lines changed: 181 additions & 0 deletions

File tree

tap21.md

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
* TAP: 21
2+
* Title: ML-DSA signing scheme for TUF metadata
3+
* Last-Modified: 2026-04-30
4+
* Author: Fredrik Skogman
5+
* Status: Draft
6+
* Content-Type: text/markdown
7+
* Created: 2026-04-29
8+
9+
# Abstract
10+
11+
This TAP proposes an application-level pre-hashing scheme to use with
12+
ML-DSA that minimizes the data sent to the signing device. Instead of
13+
passing the full canonicalized metadata, the application computes a
14+
cryptographic hash over the metadata and computes a pre-signing byte
15+
string (including domain separator and protocol version). The
16+
pre-signing byte string is what is sent to the signing device. This
17+
approach keeps the HSM interface simple and bounded while preserving
18+
the security properties required by FIPS 204.
19+
20+
# Motivation
21+
22+
TUF metadata can be large, particularly for targets metadata in
23+
repositories with many artifacts. ML-DSA pure mode signing requires
24+
the entire message to be available to the signing device. When private
25+
keys are held in hardware security modules (HSMs), the HSM must
26+
receive the full message to produce a pure mode signature.
27+
Transmitting large metadata payloads to an HSM introduces practical
28+
limitations on message size, and potential interface constraints that
29+
make pure mode ML-DSA unsuitable for some TUF deployments.
30+
31+
# Specification
32+
33+
Conventions used:
34+
35+
* `0x__`: a raw byte value, specified as a hexadecimal number
36+
* `||`: byte concatenation
37+
38+
The raw TUF metadata is NEVER signed, instead a pre-signing byte string is
39+
created with the following format, offering domain separation and
40+
versioning at the protocol level:
41+
42+
```
43+
0x74 || 0x75 || 0x66 || <version> || H(MSG)
44+
```
45+
46+
The domain separators are the ASCII codes for `tuf`.
47+
48+
Version must be a single byte specifying the version. `0x01` is
49+
currently the only defined version.
50+
51+
The hash function and the canonicalization scheme for the message are
52+
specified by the version.
53+
54+
Pure ML-DSA MUST be used with an **empty context**.
55+
56+
## Rationale
57+
58+
Why not use the `scheme` to specify the hash algorithm and instead use
59+
the version? The version specifies the entire set of choices and
60+
cryptographically binds those choices to the signature which would
61+
reduce risks of misuse. This allows for easier updates of the
62+
versioning scheme as it's an all-or-nothing approach. By providing
63+
some details via the scheme and others via some versioning opens up
64+
for possible confusion.
65+
66+
Why not use HashML-DSA? With Ed25519 the ecosystem support has been
67+
much better for the pure version, and it's likely it will be the same
68+
for ML-DSA. Based on this an application specific protocol is better
69+
suited for wider adoption.
70+
71+
## Protocol versions
72+
73+
To allow for future updates on hash algorithm selection to mitigate
74+
any collision or preimage attack the selection of hash algorithm is
75+
specified via a protocol version. This provides a layer of indirection
76+
where certain details can change over time without encoding too much
77+
information into the `scheme` parameter.
78+
79+
### v1
80+
81+
* Version byte: `0x01`
82+
* Hash algorithm: SHA-512
83+
* Implementations MUST support
84+
* `ML-DSA-44`
85+
* `ML-DSA-65`
86+
* `ML-DSA-87`
87+
* Metadata canonicalization scheme: encoded as "canonical JSON" as described
88+
in the [TUF
89+
Specification](https://theupdateframework.github.io/specification/v1.0.34/index.html#metaformat).
90+
91+
## Signature generation
92+
93+
1. Load the public key from TUF metadata
94+
2. Parse the version from the public key's `scheme` and prepare the
95+
hash function `H`
96+
3. Compute the canonicalized metadata representation `MSG`
97+
4. Create the pre-signing byte string:
98+
```
99+
0x74 || 0x75 || 0x66 || version || H(MSG)
100+
```
101+
5. Sign the pre-signing byte string using an empty context
102+
103+
## Verification steps:
104+
105+
1. Compute canonical metadata representation
106+
2. Load up the public key for verification
107+
3. Parse `scheme` into parameter set and version
108+
* Reject if the protocol version is not supported
109+
* Implementations MUST NOT infer or select an ML-DSA
110+
parameter set or version from the signature bytes alone
111+
4. Verifier must reconstruct the exact signed bytes itself
112+
* It should not accept a caller-supplied digest/prefix blindly
113+
* The `version` MUST be taken from the trusted TUF metadata's
114+
`scheme` parameter
115+
* The version binds hash function to use
116+
* It should compute: `digest = H(MSG)`
117+
* Then verify over `0x74 || 0x75 || 0x66 || version || digest`
118+
using an empty context
119+
5. Reject unknown or mismatched versions
120+
* Do not fall back
121+
* Do not try multiple interpretations
122+
* Do not accept the same signature under HashML-DSA or another scheme
123+
124+
## TUF metadata parameters:
125+
126+
* `keytype`: `ml-dsa`
127+
* `scheme`: (`ml-dsa-<parameter set>/<version>`)
128+
* `ml-dsa-44/1`
129+
* `ml-dsa-65/1`
130+
* `ml-dsa-87/1`
131+
* `keyval.public`: PEM encoding of DER-encoded `SubjectPublicKeyInfo`
132+
structures as defined for ML-DSA in RFC 9881
133+
* `signature.sig`: Hex-encoded signature byte string as per FIPS 204
134+
§7.2
135+
136+
# Security considerations
137+
138+
1. SHA-512 length extension: Not a concern. The signature is made over `domain
139+
|| version || digest`. Length extension is more a concern when the
140+
digest is computed to be used as a MAC
141+
2. SHA-512 vs ML-DSA-87 margin. SHA-512 has zero margin, but is
142+
valid. Future versions can increase the margin if deemed necessary
143+
3. Signature confusion/replay: With the domain separation a valid
144+
signature over the raw digest from another domain would not be
145+
valid in the TUF metadata domain (collisions on the domain and
146+
version bytes are _very_ unlikely)
147+
148+
# Appendix
149+
150+
## ML-DSA parameter sets
151+
152+
This is an excerpt only, see FIPS-204 §4 for a complete parameter set.
153+
154+
| Parameter | ML-DSA-44 | ML-DSA-65 | ML-DSA-87 |
155+
|-----------|-----------|-----------|-----------|
156+
| 𝜆 | 128 | 192 | 256 |
157+
158+
## Notes on application level hashing
159+
160+
From FIPS 204 on application level hashing (§5.4):
161+
162+
> In order to maintain the same level of security strength when the
163+
> content is hashed at the application level or using HashML-DSA , the
164+
> digest that is signed needs to be generated using an approved hash
165+
> function or XOF (e.g., from FIPS 180 [8] or FIPS 202 [7]) that
166+
> provides at least 𝜆 bits of classical security strength against both
167+
> collision and second preimage attacks [7, Table 4]<sup>6</sup>.
168+
>
169+
> The verification of a signature that is created in this way will
170+
> require the verify function to generate a digest from the message in
171+
> the same way to be used as input for the verification function.
172+
>
173+
> 6. Obtaining at least 𝜆 bits of classical security strength against
174+
> collision attacks requires that the digest to be signed be at least 2𝜆
175+
> bits in length.
176+
177+
# References
178+
179+
* [FIPS-204](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.204.pdf)
180+
* [TUF Specification](https://theupdateframework.github.io/specification/v1.0.34/index.html)
181+
* [RFC 9881](https://datatracker.ietf.org/doc/html/rfc9881)

0 commit comments

Comments
 (0)