@@ -27,6 +27,7 @@ public class DTLSTranscriptHashTest
2727 private static final byte [] CLIENT_HELLO_BODY = body (0x10 , 40 );
2828 private static final byte [] SERVER_HELLO_BODY = body (0x20 , 24 );
2929 private static final byte [] CERTIFICATE_BODY = body (0x30 , 11 );
30+ private static final byte [] HELLO_RETRY_REQUEST_BODY = body (0x50 , 18 );
3031
3132 /**
3233 * A DTLS 1.3 transcript hashes msg_type || uint24 length || body, and nothing else.
@@ -124,6 +125,100 @@ public void testClientHelloHashedBeforeTheVersionIsKnownUsesTheDTLSv12Form() thr
124125 Arrays .areEqual (sha256 (expected .toByteArray ()), fixture .transcript ()));
125126 }
126127
128+ /**
129+ * RFC 8446 4.4.1. Where a HelloRetryRequest is used the transcript is not ClientHello1 itself but a
130+ * synthetic handshake message standing in for it:
131+ * <pre>
132+ * Transcript-Hash(ClientHello1, HelloRetryRequest, ... Mn) =
133+ * Hash(message_hash || (= 254)
134+ * 00 00 Hash.length ||
135+ * Hash(ClientHello1) ||
136+ * HelloRetryRequest || ... || Mn)
137+ * </pre>
138+ * The expected bytes below are assembled from those literal field values, NOT from
139+ * {@link TlsUtils#adjustTranscriptForRetry(TlsHandshakeHash)}: an error in the synthetic message's own
140+ * encoding is invisible to any handshake between two Bouncy Castle peers, which would make the identical
141+ * error on both sides, agree with each other, and fail only against another implementation.
142+ */
143+ public void testHelloRetryRequestTranscriptUsesTheSyntheticMessageHash () throws Exception
144+ {
145+ Fixture fixture = new Fixture (ProtocolVersion .DTLSv13 );
146+
147+ fixture .digest (HandshakeType .client_hello , 0 , CLIENT_HELLO_BODY );
148+ fixture .adjustForRetry ();
149+ fixture .digest (HandshakeType .server_hello , 1 , HELLO_RETRY_REQUEST_BODY );
150+
151+ byte [] transcript = fixture .transcript ();
152+
153+ byte [] clientHello1Hash = sha256 (tlsHeaderForm (HandshakeType .client_hello , CLIENT_HELLO_BODY ));
154+ assertEquals ("SHA-256 output length" , 32 , clientHello1Hash .length );
155+ assertEquals ("RFC 8446 4.4.1 message_hash HandshakeType" , 254 , HandshakeType .message_hash );
156+
157+ ByteArrayOutputStream expected = new ByteArrayOutputStream ();
158+ expected .write (254 );
159+ expected .write (0x00 );
160+ expected .write (0x00 );
161+ expected .write (32 );
162+ expected .write (clientHello1Hash );
163+ expected .write (tlsHeaderForm (HandshakeType .server_hello , HELLO_RETRY_REQUEST_BODY ));
164+
165+ assertTrue ("the transcript must be over the RFC 8446 4.4.1 synthetic message_hash" ,
166+ Arrays .areEqual (sha256 (expected .toByteArray ()), transcript ));
167+
168+ /*
169+ * Negative controls. Each is a plausible way to get the synthetic message wrong that leaves the
170+ * ordering - the only thing the existing inverted-ordering test can catch - intact.
171+ */
172+
173+ // no substitution at all: ClientHello1 left in the transcript verbatim
174+ ByteArrayOutputStream noSubstitution = new ByteArrayOutputStream ();
175+ noSubstitution .write (tlsHeaderForm (HandshakeType .client_hello , CLIENT_HELLO_BODY ));
176+ noSubstitution .write (tlsHeaderForm (HandshakeType .server_hello , HELLO_RETRY_REQUEST_BODY ));
177+
178+ assertFalse ("ClientHello1 must not be left in the transcript" ,
179+ Arrays .areEqual (sha256 (noSubstitution .toByteArray ()), transcript ));
180+
181+ // the bare digest, with no synthetic message header at all
182+ ByteArrayOutputStream noHeader = new ByteArrayOutputStream ();
183+ noHeader .write (clientHello1Hash );
184+ noHeader .write (tlsHeaderForm (HandshakeType .server_hello , HELLO_RETRY_REQUEST_BODY ));
185+
186+ assertFalse ("the synthetic message_hash header must be present" ,
187+ Arrays .areEqual (sha256 (noHeader .toByteArray ()), transcript ));
188+
189+ // some other msg_type in the synthetic message
190+ ByteArrayOutputStream wrongType = new ByteArrayOutputStream ();
191+ wrongType .write (HandshakeType .client_hello );
192+ wrongType .write (0x00 );
193+ wrongType .write (0x00 );
194+ wrongType .write (32 );
195+ wrongType .write (clientHello1Hash );
196+ wrongType .write (tlsHeaderForm (HandshakeType .server_hello , HELLO_RETRY_REQUEST_BODY ));
197+
198+ assertFalse ("the synthetic message's msg_type must be message_hash (254)" ,
199+ Arrays .areEqual (sha256 (wrongType .toByteArray ()), transcript ));
200+
201+ // the 12-byte DTLS header form for the synthetic message, which RFC 9147 5.2 excludes
202+ ByteArrayOutputStream dtlsHeader = new ByteArrayOutputStream ();
203+ dtlsHeader .write (dtlsHeaderForm (HandshakeType .message_hash , 0 , clientHello1Hash ));
204+ dtlsHeader .write (tlsHeaderForm (HandshakeType .server_hello , HELLO_RETRY_REQUEST_BODY ));
205+
206+ assertFalse ("the synthetic message must use the 4-byte TLS header form" ,
207+ Arrays .areEqual (sha256 (dtlsHeader .toByteArray ()), transcript ));
208+
209+ // ClientHello1 digested in the 12-byte DTLS form before being substituted
210+ ByteArrayOutputStream dtlsFormClientHello = new ByteArrayOutputStream ();
211+ dtlsFormClientHello .write (254 );
212+ dtlsFormClientHello .write (0x00 );
213+ dtlsFormClientHello .write (0x00 );
214+ dtlsFormClientHello .write (32 );
215+ dtlsFormClientHello .write (sha256 (dtlsHeaderForm (HandshakeType .client_hello , 0 , CLIENT_HELLO_BODY )));
216+ dtlsFormClientHello .write (tlsHeaderForm (HandshakeType .server_hello , HELLO_RETRY_REQUEST_BODY ));
217+
218+ assertFalse ("Hash(ClientHello1) must be over the 4-byte TLS header form" ,
219+ Arrays .areEqual (sha256 (dtlsFormClientHello .toByteArray ()), transcript ));
220+ }
221+
127222 /**
128223 * HelloVerifyRequest stays out of the transcript in every version.
129224 */
@@ -202,6 +297,19 @@ void negotiate(ProtocolVersion negotiatedVersion)
202297 context .getSecurityParametersHandshake ().negotiatedVersion = negotiatedVersion ;
203298 }
204299
300+ /**
301+ * The HelloRetryRequest substitution, driven exactly as DTLSServerProtocol.serverHandshake and
302+ * DTLSClientProtocol.clientHandshake drive it: the PRF is determined, the transcript is replaced, and
303+ * the HelloRetryRequest is then digested on top of the replacement.
304+ */
305+ void adjustForRetry () throws IOException
306+ {
307+ TlsHandshakeHash handshakeHash = handshake .getHandshakeHash ();
308+ handshakeHash .notifyPRFDetermined ();
309+
310+ TlsUtils .adjustTranscriptForRetry (handshakeHash );
311+ }
312+
205313 void digest (short msgType , int messageSeq , byte [] msgBody ) throws IOException
206314 {
207315 handshake .updateHandshakeMessagesDigest (
0 commit comments