|
| 1 | +\section{SHRINCS} |
| 2 | +This section specifies the complete SHRINCS signature scheme by composing the building blocks defined in previous sections. |
| 3 | + |
| 4 | +\subsection{Key Generation} |
| 5 | +The \texttt{SHRINCS.KeyGen} function generates a fresh key pair and initializes the signing state. It samples a 3n-byte random seed, partitions it into \texttt{SK.seed} (for key derivation), \texttt{SK.prf} (for message randomization), and \texttt{PK.seed} (for domain separation). It then computes \texttt{PK.sf} (the stateful UXMSS root) and \texttt{PK.sl} (the stateless hypertree root), combining them into \texttt{PK.root} via a tweakable hash. The initial state sets \texttt{q=0} with \texttt{valid=true}, indicating the stateful path is available starting from signature number 1. |
| 6 | + |
| 7 | +\begin{verbatim} |
| 8 | + Algorithm: SHRINCS.KeyGen() |
| 9 | + Output: |
| 10 | + SK: secret key |
| 11 | + PK: public key |
| 12 | + state: initial signing state |
| 13 | +
|
| 14 | + 1. seed ←$ {0,1}^(3n) |
| 15 | + 2. SK.seed ← seed[0:n] |
| 16 | + 3. SK.prf ← seed[n:2n] |
| 17 | + 4. PK.seed ← seed[2n:3n] |
| 18 | + 5. ADRS ← new_ADRS() |
| 19 | + 6. PK.sf ← uxmss_root(SK.seed, PK.seed, ADRS) |
| 20 | + 7. setLayerAddress(ADRS, d - 1) |
| 21 | + 8. setTreeAddress(ADRS, 0) |
| 22 | + 9. PK.sl ← xmss_root(SK.seed, PK.seed, ADRS, h_prime) |
| 23 | + 10. setLayerAddress(ADRS, 0) |
| 24 | + 11. setTreeAddress(ADRS, 0) |
| 25 | + 12. setTypeAndClear(ADRS, ROOT) |
| 26 | + 13. PK.root ← Tw(PK.seed, ADRS, PK.sf || PK.sl) |
| 27 | + 14. SK ← (SK.seed, SK.prf, PK.seed, PK.sf, PK.sl, PK.root) |
| 28 | + 15. PK ← (PK.seed, PK.root) |
| 29 | + 16. state ← { q: 0, valid: true } |
| 30 | + 17. return (SK, PK, state) |
| 31 | +\end{verbatim} |
| 32 | + |
| 33 | +\subsection{Recovery from Seed} |
| 34 | +The \texttt{SHRINCS.Restore} function recovers a key pair from a backed-up seed. This enables the standard Bitcoin workflow of restoring a wallet from a seed phrase. However, since the signing state (which \texttt{q} values have been used) cannot be recovered from the seed alone, the stateful path is disabled by setting \texttt{valid=false}. The recovered wallet can only use stateless signatures, which are larger but do not require state tracking. This is the key tradeoff that enables static backups while maintaining security. |
| 35 | +\begin{verbatim} |
| 36 | + Algorithm: SHRINCS.Restore(seed) |
| 37 | + Input: |
| 38 | + seed: master seed |
| 39 | + Output: |
| 40 | + SK: secret key |
| 41 | + PK: public key |
| 42 | + state: invalid state (stateless only) |
| 43 | +
|
| 44 | + 1. SK.seed ← seed[0:n] |
| 45 | + 2. SK.prf ← seed[n:2n] |
| 46 | + 3. PK.seed ← seed[2n:3n] |
| 47 | + 4. ADRS ← new_ADRS() |
| 48 | + 5. PK.sf ← uxmss_root(SK.seed, PK.seed, ADRS) |
| 49 | + 6. setLayerAddress(ADRS, d - 1) |
| 50 | + 7. setTreeAddress(ADRS, 0) |
| 51 | + 8. PK.sl ← xmss_root(SK.seed, PK.seed, ADRS, h_prime) |
| 52 | + 9. setLayerAddress(ADRS, 0) |
| 53 | + 10. setTreeAddress(ADRS, 0) |
| 54 | + 11. setTypeAndClear(ADRS, ROOT) |
| 55 | + 12. PK.root ← Tw(PK.seed, ADRS, PK.sf || PK.sl) |
| 56 | + 13. SK ← (SK.seed, SK.prf, PK.seed, PK.sf, PK.sl, PK.root) |
| 57 | + 14. PK ← (PK.seed, PK.root) |
| 58 | + 15. state ← { q: false, valid: false } |
| 59 | + 16. return (SK, PK, state) |
| 60 | +\end{verbatim} |
| 61 | + |
| 62 | +\subsection{Stateful signing} |
| 63 | +The \texttt{SHRINCS.SignStateful} function signs a message using the efficient stateful (UXMSS) path. It first checks that state is valid and that signature slots remain (\texttt{q <= hsf + 1}). It then increments \texttt{q} and updates the state before generating the signature. This ordering is critical: if the process is interrupted after signing but before state persistence, the one-time key would be consumed without the state reflecting it, risking catastrophic key reuse on retry (see Section~1.2). |
| 64 | + |
| 65 | +The signature includes \texttt{PK.sl} prepended to enable unified verification. This produces minimal signatures (324 bytes for q = 1 in SHRINCS-B) that grow linearly with \texttt{q}. If state is invalid or exhausted, the function returns "false" and the caller should use stateless signing instead. |
| 66 | +\begin{verbatim} |
| 67 | + Algorithm: SHRINCS.SignStateful(m, SK, state) |
| 68 | + Input: |
| 69 | + m: message to sign |
| 70 | + SK: secret key (SK.seed, SK.prf, PK.seed, PK.sf, PK.sl, PK.root) |
| 71 | + state: current signing state { q, valid } |
| 72 | + Output: |
| 73 | + sig: stateful signature, or $\bot$ if state is invalid/exhausted |
| 74 | + state_prime: updated state |
| 75 | +
|
| 76 | + 1. if not state.valid: |
| 77 | + return (false, state) |
| 78 | + 2. q ← state.q + 1 |
| 79 | + 3. if q > hsf + 1: |
| 80 | + return (false, state) |
| 81 | + 4. state_prime ← { q: q, valid: true } |
| 82 | + 5. ADRS ← new_ADRS() |
| 83 | + 6. uxmss_sig ← uxmss_sign(m, SK.seed, SK.prf, PK.seed, PK.root, ADRS, q) |
| 84 | + 7. sig ← PK.sl || uxmss_sig |
| 85 | + 8. return (sig, state_prime) |
| 86 | +\end{verbatim} |
| 87 | + |
| 88 | +\subsection{Stateless signing} |
| 89 | +The \texttt{SHRINCS.SignStateless} function signs a message using the stateless (SPHINCS+C) path. This is used when state is lost, corrupted, or exhausted. |
| 90 | + |
| 91 | +SHRINCS uses a unified digest for the stateless path: a single call to \texttt{H\_msg\_fors} produces a digest that encodes both the FORS leaf indices (determining which leaves to reveal in the few-time signature) and the hypertree tree/leaf indices (determining which XMSS trees and WOTS+C key pairs to use at each layer). The grinding counter \texttt{ctr} is iterated until the grinding condition is satisfied on this unified digest. |
| 92 | + |
| 93 | +The digest output length is \texttt{(roundup(k*a + hsl) / 8)} bytes. The first \texttt{k*a} bits encode the \texttt{k} FORS leaf indices (with the last a bits required to be zero by grinding). The remaining \texttt{hsl} bits encode the hypertree path indices. |
| 94 | + |
| 95 | +The signature is constructed bottom-up: first a FORS+C signature on the message, then \texttt{d} XMSS signatures on successive layer roots ascending through the hypertree. The signature includes \texttt{PK.sf} prepended to enable unified verification. |
| 96 | + |
| 97 | +\begin{verbatim} |
| 98 | + Algorithm: SHRINCS.SignStateless(m, SK) |
| 99 | + Input: |
| 100 | + m: message to sign |
| 101 | + SK: secret key |
| 102 | + Output: |
| 103 | + sig: stateless signature |
| 104 | +
|
| 105 | + 1. ADRS ← new_ADRS() |
| 106 | + 2. setLayerAddress(ADRS, 0) |
| 107 | + 3. setTreeAddress(ADRS, 0) |
| 108 | + 4. (fors_sig, digest) ← fors_sign(m, SK.seed, SK.prf, PK.seed, PK.root, ADRS) |
| 109 | + 5. (tree_idx, leaf_idx) ← parse_idx(digest) |
| 110 | + 6. indices ← fors_msg_to_indices(digest) |
| 111 | + 7. setLayerAddress(ADRS, 0) |
| 112 | + 8. setTreeAddress(ADRS, tree_idx[0] * 2^h_prime + leaf_idx[0]) |
| 113 | + 9. fors_pk ← fors_pkFromSig(fors_sig, indices, PK.seed, ADRS) |
| 114 | + 10. ht_sig ← [] |
| 115 | + 11. msg ← fors_pk |
| 116 | + 12. for layer from 0 to d - 1: |
| 117 | + a. setLayerAddress(ADRS, layer) |
| 118 | + b. setTreeAddress(ADRS, tree_idx[layer]) |
| 119 | + c. xmss_sig ← xmss_sign(msg, SK.seed, SK.prf, PK.seed, PK.root, ADRS, h', leaf_idx[layer]) |
| 120 | + d. ht_sig ← ht_sig || xmss_sig |
| 121 | + e. if layer < d - 1: |
| 122 | + msg ← xmss_root(SK.seed, PK.seed, ADRS, h') // we may cache or reuse the root computed during xmss_sign |
| 123 | + 13. sig ← PK.sf || fors_sig || ht_sig |
| 124 | + 14. return sig |
| 125 | +\end{verbatim} |
| 126 | + |
| 127 | +\subsection{Index Parsing} |
| 128 | +The \texttt{parse\_idx} function extracts tree and leaf indices for each hypertree layer from the stateless digest. In the unified digest scheme, these bits follow the \texttt{k*a} bits used for FORS indices. The \texttt{hsl} bits are partitioned into \texttt{d} pairs of \texttt{(leaf\_idx, tree\_idx)}, extracted from least significant (layer 0) to most significant (layer \texttt{d-1}), with \texttt{h'} bits per leaf index. |
| 129 | +%\mknote{This is a bit confusin, that we extract the indices of the OTS/FTS schemes used, but not the indices of the revealed leaves in FORS. With the Hashing changes, it needs a double-check that everything is computed correctly and the same data is not used for two purposes. } |
| 130 | +\begin{verbatim} |
| 131 | + Algorithm: parse_idx(digest) |
| 132 | + Input: |
| 133 | + digest: message digest |
| 134 | + Output: |
| 135 | + tree_idx: array of d tree indices |
| 136 | + leaf_idx: array of d leaf indices |
| 137 | +
|
| 138 | + 1. ht_offset ← k * a |
| 139 | + 2. ht_bits ← extract_bits(digest, ht_offset, hsl) |
| 140 | + 3. idx ← ht_bits |
| 141 | + 4. tree_idx ← [] |
| 142 | + 5. leaf_idx ← [] |
| 143 | + 6. for layer from 0 to d - 1: |
| 144 | + a. leaf_idx[layer] ← idx AND (2^h_prime - 1) |
| 145 | + b. idx ← idx >> h_prime |
| 146 | + c. tree_idx[layer] ← idx |
| 147 | + 7. return (tree_idx, leaf_idx) |
| 148 | +\end{verbatim} |
| 149 | +\paragraph{Note:} The total digest must encode \texttt{k*a + hsl = 6*22 + 24 = 156} bits. The \texttt{H\_msg\_fors} output length (Section~5.4.2) must be set accordingly: \texttt{roundup((k*a + hsl) / 8) = 20} bytes. This is a correction from the original \texttt{roundup((k*a) / 8) = 17} bytes, which was insufficient for the unified digest. |
| 150 | + |
| 151 | +\subsection{Stateful verification} |
| 152 | +The \texttt{SHRINCS.VerifyStateful} function verifies a stateful signature. It extracts \texttt{PK.sl} from the signature, parses the UXMSS signature, and infers \texttt{q} from the authentication path length. It then uses the two-phase WOTS+C hashing to recover \texttt{PK.sf} via \texttt{uxmss\_pkFromSig} and verifies that \texttt{Tw(PK.seed, ADRS, PK.sf || PK.sl)} equals \texttt{PK.root}. |
| 153 | +\begin{verbatim} |
| 154 | + Algorithm: SHRINCS.VerifyStateful(m, sig, PK) |
| 155 | + Input: |
| 156 | + m: message |
| 157 | + sig: stateful signature |
| 158 | + PK: public key |
| 159 | + Output: |
| 160 | + valid: boolean |
| 161 | +
|
| 162 | + 1. ADRS ← new_ADRS() |
| 163 | + 2. PK.sl ← sig[0 : n] |
| 164 | + 3. uxmss_sig ← sig[n : ] |
| 165 | + 4. wots_sig_size ← R_SIZE + c + l * n |
| 166 | + 5. auth_len ← len(uxmss_sig) - wots_sig_size |
| 167 | + 6. if auth_len < 0 or (auth_len mod n) != 0: |
| 168 | + return false |
| 169 | + 7. q_raw ← auth_len / n |
| 170 | + 8. if q_raw < 1 or q_raw > hsf: |
| 171 | + return false |
| 172 | + 9. wots_sig ← uxmss_sig[0 : wots_sig_size] |
| 173 | + 10. auth ← uxmss_sig[wots_sig_size : ] |
| 174 | + 11. if q_raw < hsf: |
| 175 | + candidates ← {q_raw} |
| 176 | + else: |
| 177 | + candidates ← {hsf, hsf + 1} |
| 178 | + 12. for each q in candidates: |
| 179 | + a. ADRS ← new_ADRS() |
| 180 | + b. PK.sf ← uxmss_pkFromSig(wots_sig, auth, m, PK.seed, PK.root, ADRS, q) |
| 181 | + c. if PK.sf == false: |
| 182 | + continue |
| 183 | + d. setLayerAddress(ADRS, 0) |
| 184 | + setTreeAddress(ADRS, 0) |
| 185 | + setTypeAndClear(ADRS, ROOT) |
| 186 | + expected_root ← Tw(PK.seed, ADRS, PK.sf || PK.sl) |
| 187 | + e. if expected_root == PK.root: |
| 188 | + return true |
| 189 | + 13. return false |
| 190 | +\end{verbatim} |
| 191 | + |
| 192 | +\subsection{Stateless verification} |
| 193 | +The \texttt{SHRINCS.VerifyStateless} function verifies a stateless signature. It extracts \texttt{PK.sf} from the signature, recomputes the unified digest from the FORS+C signature's \texttt{R} and \texttt{ctr}, extracts both FORS indices and hypertree indices from the same digest, then verifies bottom-up through the hypertree. |
| 194 | +\begin{verbatim} |
| 195 | + Algorithm: SHRINCS.VerifyStateless(m, sig, PK) |
| 196 | + Input: |
| 197 | + m: message |
| 198 | + sig: stateless signature |
| 199 | + PK: public key |
| 200 | + Output: |
| 201 | + valid: boolean |
| 202 | +
|
| 203 | + 1. ADRS ← new_ADRS() |
| 204 | + 2. PK.sf ← sig[0 : n] |
| 205 | + 3. offset ← n |
| 206 | + 4. fors_sig_size ← R_SIZE + (k - 1) * (n + a * n) |
| 207 | + 5. fors_sig ← sig[offset : offset + fors_sig_size] |
| 208 | + 6. offset ← offset + fors_sig_size |
| 209 | + 7. R ← fors_sig[0 : R_SIZE] |
| 210 | + 8. setTypeAndClear(ADRS, SL_H_MSG) |
| 211 | + 9. digest ← H_msg_fors(ADRS, R, PK.seed, PK.root, m) |
| 212 | + 10. indices ← fors_msg_to_indices(digest) |
| 213 | + 11. if indices[k-1] != 0: |
| 214 | + return false |
| 215 | + 12. (tree_idx, leaf_idx) ← parse_idx(digest) |
| 216 | + 13. setLayerAddress(ADRS, 0) |
| 217 | + 14. setTreeAddress(ADRS, tree_idx[0] * 2^h_prime + leaf_idx[0]) |
| 218 | + 15. fors_pk ← fors_pkFromSig(fors_sig, indices, PK.seed, ADRS) |
| 219 | + 16. msg ← fors_pk |
| 220 | + 17. xmss_sig_size ← R_SIZE + c + l * n + h_prime * n |
| 221 | + 18. for layer from 0 to d - 1: |
| 222 | + a. xmss_sig ← sig[offset : offset + xmss_sig_size] |
| 223 | + b. offset ← offset + xmss_sig_size |
| 224 | + c. wots_sig ← xmss_sig[0 : R_SIZE + c + l * n] |
| 225 | + d. auth ← xmss_sig[R_SIZE + c + l * n : R_SIZE + c + l * n + h' * n] |
| 226 | + e. setLayerAddress(ADRS, layer) |
| 227 | + f. setTreeAddress(ADRS, tree_idx[layer]) |
| 228 | + g. msg ← xmss_pkFromSig(wots_sig, auth, msg, PK.seed, PK.root, ADRS, h_prime, leaf_idx[layer]) |
| 229 | + h. if msg == false: |
| 230 | + return false |
| 231 | + 19. PK.sl ← msg |
| 232 | + 20. setLayerAddress(ADRS, 0) |
| 233 | + 21. setTreeAddress(ADRS, 0) |
| 234 | + 22. setTypeAndClear(ADRS, ROOT) |
| 235 | + 23. expected_root ← Tw(PK.seed, ADRS, PK.sf || PK.sl) |
| 236 | + 24. return (expected_root == PK.root) |
| 237 | +\end{verbatim} |
| 238 | + |
| 239 | +\subsection{Unified Verification} |
| 240 | +The \texttt{SHRINCS.Verify} function provides a single entry point for verifying either stateful or stateless signatures. It distinguishes between the two types based on signature length: stateful signatures have a maximum size of \texttt{max\_sf\_size = n + R\_SIZE + c + l*n + hsf*n} bytes (both \texttt{q = hsf} and \texttt{q = hsf + 1} produce hsf auth-path nodes), while stateless signatures are strictly larger (see Section~8.7 for the derivation that this bound always holds). |
| 241 | +\begin{verbatim} |
| 242 | + Algorithm: SHRINCS.Verify(m, sig, PK) |
| 243 | + Input: |
| 244 | + m: message |
| 245 | + sig: signature |
| 246 | + PK: public key |
| 247 | + Output: |
| 248 | + valid: boolean |
| 249 | +
|
| 250 | + 1. max_sf_size ← n + R_SIZE + c + l * n + hsf * n |
| 251 | + 2. if len(sig) <= max_sf_size: |
| 252 | + return SHRINCS.VerifyStateful(m, sig, PK) |
| 253 | + else: |
| 254 | + return SHRINCS.VerifyStateless(m, sig, PK) |
| 255 | +\end{verbatim} |
0 commit comments