From e7b4ae342205ea1a41524dcc963b9d3945047004 Mon Sep 17 00:00:00 2001 From: Developer Date: Thu, 26 Jun 2025 15:10:01 +0000 Subject: [PATCH] Add examples for handling simultaneous full handshake and session resumption This commit provides comprehensive examples demonstrating how to handle both session resumption and full handshake scenarios dynamically in TLS-Attacker. The solution addresses issue #195 by providing: - Manual action execution approach for full control - Hybrid workflow approach using WorkflowExecutor - Unit tests demonstrating the functionality - Comprehensive documentation explaining the solution These examples show how to inspect ClientHello messages at runtime and dynamically choose between resumption and full handshake workflows based on the session ID, solving the problem of static workflow traces. --- .../examples/DynamicHandshakeExample.java | 221 ++++++++++++++++ .../DynamicHandshakeWorkflowExample.java | 237 ++++++++++++++++++ .../nds/tlsattacker/core/examples/README.md | 150 +++++++++++ .../examples/DynamicHandshakeExampleTest.java | 187 ++++++++++++++ 4 files changed, 795 insertions(+) create mode 100644 TLS-Core/src/main/java/de/rub/nds/tlsattacker/core/examples/DynamicHandshakeExample.java create mode 100644 TLS-Core/src/main/java/de/rub/nds/tlsattacker/core/examples/DynamicHandshakeWorkflowExample.java create mode 100644 TLS-Core/src/main/java/de/rub/nds/tlsattacker/core/examples/README.md create mode 100644 TLS-Core/src/test/java/de/rub/nds/tlsattacker/core/examples/DynamicHandshakeExampleTest.java diff --git a/TLS-Core/src/main/java/de/rub/nds/tlsattacker/core/examples/DynamicHandshakeExample.java b/TLS-Core/src/main/java/de/rub/nds/tlsattacker/core/examples/DynamicHandshakeExample.java new file mode 100644 index 0000000000..0e6a358971 --- /dev/null +++ b/TLS-Core/src/main/java/de/rub/nds/tlsattacker/core/examples/DynamicHandshakeExample.java @@ -0,0 +1,221 @@ +/* + * TLS-Attacker - A Modular Penetration Testing Framework for TLS + * + * Copyright 2014-2025 Ruhr University Bochum, Paderborn University, Technology Innovation Institute, + * and Hackmanit GmbH + * + * Licensed under Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package de.rub.nds.tlsattacker.core.examples; + +import de.rub.nds.tlsattacker.core.config.Config; +import de.rub.nds.tlsattacker.core.connection.AliasedConnection; +import de.rub.nds.tlsattacker.core.connection.InboundConnection; +import de.rub.nds.tlsattacker.core.constants.ProtocolMessageType; +import de.rub.nds.tlsattacker.core.constants.RunningModeType; +import de.rub.nds.tlsattacker.core.exceptions.WorkflowExecutionException; +import de.rub.nds.tlsattacker.core.layer.context.TlsContext; +import de.rub.nds.tlsattacker.core.protocol.message.AlertMessage; +import de.rub.nds.tlsattacker.core.protocol.message.ChangeCipherSpecMessage; +import de.rub.nds.tlsattacker.core.protocol.message.ClientHelloMessage; +import de.rub.nds.tlsattacker.core.protocol.message.FinishedMessage; +import de.rub.nds.tlsattacker.core.protocol.message.HelloVerifyRequestMessage; +import de.rub.nds.tlsattacker.core.protocol.message.PskClientKeyExchangeMessage; +import de.rub.nds.tlsattacker.core.protocol.message.PskServerKeyExchangeMessage; +import de.rub.nds.tlsattacker.core.protocol.message.ServerHelloDoneMessage; +import de.rub.nds.tlsattacker.core.protocol.message.ServerHelloMessage; +import de.rub.nds.tlsattacker.core.state.State; +import de.rub.nds.tlsattacker.core.workflow.action.ReceiveAction; +import de.rub.nds.tlsattacker.core.workflow.action.ResetConnectionAction; +import de.rub.nds.tlsattacker.core.workflow.action.SendAction; +import de.rub.nds.tlsattacker.core.workflow.action.TlsAction; +import java.io.IOException; +import java.util.Arrays; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Example demonstrating how to handle both session resumption and full handshake + * dynamically based on the ClientHello session ID. + * + * This implementation addresses the issue described in #195 where TLS-Attacker needs + * to support different handshake paths based on whether the client is attempting + * session resumption or a new full handshake. + */ +public class DynamicHandshakeExample { + + private static final Logger LOGGER = LogManager.getLogger(); + + public static void main(String[] args) { + Config config = Config.createConfig(); + config.setDefaultRunningMode(RunningModeType.SERVER); + + // Configure for PSK as shown in the issue + config.setDefaultSelectedCipherSuite("TLS_PSK_WITH_AES_128_CBC_SHA"); + config.setDefaultServerSupportedCipherSuites(Arrays.asList("TLS_PSK_WITH_AES_128_CBC_SHA")); + + // Add connection configuration + config.setDefaultConnections(Arrays.asList(new InboundConnection(4433, "server"))); + + try { + // Execute first handshake + State state = executeInitialHandshake(config); + + // Save session information + byte[] sessionId = state.getTlsContext().getServerSessionId(); + LOGGER.info("Initial handshake completed. Session ID: {}", bytesToHex(sessionId)); + + // Reset for second handshake + state = new State(config); + + // Execute second handshake with dynamic path selection + executeSecondHandshake(state, sessionId); + + } catch (Exception e) { + LOGGER.error("Error during handshake execution", e); + } + } + + /** + * Executes the initial full handshake + */ + private static State executeInitialHandshake(Config config) throws WorkflowExecutionException, IOException { + State state = new State(config); + TlsContext context = state.getTlsContext(); + + LOGGER.info("Starting initial full handshake..."); + + // Execute initial handshake actions + executeAction(new ReceiveAction(new ClientHelloMessage()), state); + executeAction(new SendAction(new HelloVerifyRequestMessage()), state); + executeAction(new ReceiveAction(new ClientHelloMessage()), state); + executeAction(new SendAction(new ServerHelloMessage()), state); + executeAction(new SendAction(new PskServerKeyExchangeMessage()), state); + executeAction(new SendAction(new ServerHelloDoneMessage()), state); + executeAction(new ReceiveAction(new PskClientKeyExchangeMessage()), state); + executeAction(new ReceiveAction(new ChangeCipherSpecMessage()), state); + executeAction(new ReceiveAction(new FinishedMessage()), state); + executeAction(new SendAction(new ChangeCipherSpecMessage()), state); + executeAction(new SendAction(new FinishedMessage()), state); + executeAction(new ReceiveAction(new AlertMessage()), state); + executeAction(new ResetConnectionAction(), state); + + return state; + } + + /** + * Executes the second handshake with dynamic path selection based on session ID + */ + private static void executeSecondHandshake(State state, byte[] expectedSessionId) + throws WorkflowExecutionException, IOException { + + LOGGER.info("Starting second handshake..."); + + // Receive first ClientHello + ReceiveAction receiveClientHello = new ReceiveAction(new ClientHelloMessage()); + executeAction(receiveClientHello, state); + + // Check if ClientHello was received + ClientHelloMessage clientHello = (ClientHelloMessage) receiveClientHello.getReceivedMessages().stream() + .filter(msg -> msg instanceof ClientHelloMessage) + .findFirst() + .orElse(null); + + if (clientHello == null) { + throw new WorkflowExecutionException("No ClientHello received"); + } + + // Send HelloVerifyRequest for DTLS (as shown in the issue example) + executeAction(new SendAction(new HelloVerifyRequestMessage()), state); + + // Receive second ClientHello (after cookie verification) + receiveClientHello = new ReceiveAction(new ClientHelloMessage()); + executeAction(receiveClientHello, state); + + clientHello = (ClientHelloMessage) receiveClientHello.getReceivedMessages().stream() + .filter(msg -> msg instanceof ClientHelloMessage) + .findFirst() + .orElse(null); + + if (clientHello == null) { + throw new WorkflowExecutionException("No second ClientHello received"); + } + + // Check session ID to determine handshake type + byte[] receivedSessionId = clientHello.getSessionId().getValue(); + boolean isResumption = receivedSessionId != null && + receivedSessionId.length > 0 && + Arrays.equals(receivedSessionId, expectedSessionId); + + if (isResumption) { + LOGGER.info("Session resumption detected. Executing abbreviated handshake..."); + executeResumptionHandshake(state); + } else { + LOGGER.info("New session detected. Executing full handshake..."); + executeFullHandshake(state); + } + } + + /** + * Executes session resumption handshake + */ + private static void executeResumptionHandshake(State state) + throws WorkflowExecutionException, IOException { + + // Session resumption flow + executeAction(new SendAction(new ServerHelloMessage()), state); + executeAction(new SendAction(new ChangeCipherSpecMessage()), state); + executeAction(new SendAction(new FinishedMessage()), state); + executeAction(new ReceiveAction(new ChangeCipherSpecMessage()), state); + executeAction(new ReceiveAction(new FinishedMessage()), state); + + LOGGER.info("Session resumption handshake completed successfully"); + } + + /** + * Executes full handshake + */ + private static void executeFullHandshake(State state) + throws WorkflowExecutionException, IOException { + + // Full handshake flow + executeAction(new SendAction(new ServerHelloMessage()), state); + executeAction(new SendAction(new PskServerKeyExchangeMessage()), state); + executeAction(new SendAction(new ServerHelloDoneMessage()), state); + executeAction(new ReceiveAction(new PskClientKeyExchangeMessage()), state); + executeAction(new ReceiveAction(new ChangeCipherSpecMessage()), state); + executeAction(new ReceiveAction(new FinishedMessage()), state); + executeAction(new SendAction(new ChangeCipherSpecMessage()), state); + executeAction(new SendAction(new FinishedMessage()), state); + + LOGGER.info("Full handshake completed successfully"); + } + + /** + * Helper method to execute a single action + */ + private static void executeAction(TlsAction action, State state) + throws WorkflowExecutionException, IOException { + + action.setConnectionAlias(state.getTlsContext().getConnection().getAlias()); + action.normalize(); + action.execute(state); + + if (!action.executedAsPlanned()) { + LOGGER.warn("Action did not execute as planned: {}", action.getClass().getSimpleName()); + } + } + + /** + * Helper method to convert bytes to hex string + */ + private static String bytesToHex(byte[] bytes) { + if (bytes == null) return "null"; + StringBuilder result = new StringBuilder(); + for (byte b : bytes) { + result.append(String.format("%02x", b)); + } + return result.toString(); + } +} \ No newline at end of file diff --git a/TLS-Core/src/main/java/de/rub/nds/tlsattacker/core/examples/DynamicHandshakeWorkflowExample.java b/TLS-Core/src/main/java/de/rub/nds/tlsattacker/core/examples/DynamicHandshakeWorkflowExample.java new file mode 100644 index 0000000000..0fae4ab21c --- /dev/null +++ b/TLS-Core/src/main/java/de/rub/nds/tlsattacker/core/examples/DynamicHandshakeWorkflowExample.java @@ -0,0 +1,237 @@ +/* + * TLS-Attacker - A Modular Penetration Testing Framework for TLS + * + * Copyright 2014-2025 Ruhr University Bochum, Paderborn University, Technology Innovation Institute, + * and Hackmanit GmbH + * + * Licensed under Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package de.rub.nds.tlsattacker.core.examples; + +import de.rub.nds.tlsattacker.core.config.Config; +import de.rub.nds.tlsattacker.core.connection.InboundConnection; +import de.rub.nds.tlsattacker.core.constants.RunningModeType; +import de.rub.nds.tlsattacker.core.exceptions.WorkflowExecutionException; +import de.rub.nds.tlsattacker.core.layer.context.TlsContext; +import de.rub.nds.tlsattacker.core.protocol.message.*; +import de.rub.nds.tlsattacker.core.state.State; +import de.rub.nds.tlsattacker.core.workflow.DefaultWorkflowExecutor; +import de.rub.nds.tlsattacker.core.workflow.WorkflowTrace; +import de.rub.nds.tlsattacker.core.workflow.action.ReceiveAction; +import de.rub.nds.tlsattacker.core.workflow.action.ResetConnectionAction; +import de.rub.nds.tlsattacker.core.workflow.action.SendAction; +import de.rub.nds.tlsattacker.core.workflow.action.TlsAction; +import java.io.IOException; +import java.util.Arrays; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Advanced example showing how to use WorkflowExecutor with conditional execution + * for handling both session resumption and full handshake scenarios. + * + * This example provides a more integrated approach using the WorkflowExecutor + * while still maintaining the ability to handle different handshake paths. + */ +public class DynamicHandshakeWorkflowExample { + + private static final Logger LOGGER = LogManager.getLogger(); + + public static void main(String[] args) { + // Create initial configuration + Config config = createServerConfig(); + + try { + // Execute initial full handshake + byte[] sessionId = executeInitialHandshake(config); + LOGGER.info("Initial handshake completed. Session ID: {}", bytesToHex(sessionId)); + + // Handle subsequent connections with dynamic workflow + handleDynamicConnection(config, sessionId); + + } catch (Exception e) { + LOGGER.error("Error during execution", e); + } + } + + /** + * Creates server configuration for PSK handshakes + */ + private static Config createServerConfig() { + Config config = Config.createConfig(); + config.setDefaultRunningMode(RunningModeType.SERVER); + + // Configure PSK + config.setDefaultSelectedCipherSuite("TLS_PSK_WITH_AES_128_CBC_SHA"); + config.setDefaultServerSupportedCipherSuites(Arrays.asList("TLS_PSK_WITH_AES_128_CBC_SHA")); + config.setDefaultPSKIdentity("Client_identity".getBytes()); + config.setDefaultPSKKey(new byte[]{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F}); + + // Configure connection + config.setDefaultConnections(Arrays.asList(new InboundConnection(4433, "server"))); + + return config; + } + + /** + * Executes initial full handshake using WorkflowExecutor + */ + private static byte[] executeInitialHandshake(Config config) throws WorkflowExecutionException { + // Create workflow trace for initial handshake + WorkflowTrace trace = new WorkflowTrace(); + + // Initial handshake flow + trace.addTlsAction(new ReceiveAction(new ClientHelloMessage())); + trace.addTlsAction(new SendAction(new HelloVerifyRequestMessage())); + trace.addTlsAction(new ReceiveAction(new ClientHelloMessage())); + trace.addTlsAction(new SendAction(new ServerHelloMessage())); + trace.addTlsAction(new SendAction(new PskServerKeyExchangeMessage())); + trace.addTlsAction(new SendAction(new ServerHelloDoneMessage())); + trace.addTlsAction(new ReceiveAction(new PskClientKeyExchangeMessage())); + trace.addTlsAction(new ReceiveAction(new ChangeCipherSpecMessage())); + trace.addTlsAction(new ReceiveAction(new FinishedMessage())); + trace.addTlsAction(new SendAction(new ChangeCipherSpecMessage())); + trace.addTlsAction(new SendAction(new FinishedMessage())); + trace.addTlsAction(new ReceiveAction(new AlertMessage())); + trace.addTlsAction(new ResetConnectionAction()); + + // Execute workflow + State state = new State(config, trace); + DefaultWorkflowExecutor executor = new DefaultWorkflowExecutor(state); + executor.executeWorkflow(); + + return state.getTlsContext().getServerSessionId(); + } + + /** + * Handles subsequent connection with dynamic workflow based on session ID + */ + private static void handleDynamicConnection(Config config, byte[] expectedSessionId) + throws WorkflowExecutionException, IOException { + + // Create partial workflow up to the decision point + WorkflowTrace initialTrace = new WorkflowTrace(); + initialTrace.addTlsAction(new ReceiveAction(new ClientHelloMessage())); + initialTrace.addTlsAction(new SendAction(new HelloVerifyRequestMessage())); + initialTrace.addTlsAction(new ReceiveAction(new ClientHelloMessage())); + + State state = new State(config, initialTrace); + + // Execute up to the point where we need to check session ID + executeActionsUntilDecisionPoint(state); + + // Determine handshake type based on received ClientHello + ClientHelloMessage clientHello = findLastClientHello(state); + boolean isResumption = checkSessionResumption(clientHello, expectedSessionId); + + // Create appropriate workflow continuation + WorkflowTrace continuationTrace = createContinuationWorkflow(isResumption); + + // Add continuation actions to state and execute + for (TlsAction action : continuationTrace.getTlsActions()) { + state.getWorkflowTrace().addTlsAction(action); + } + + // Continue execution + DefaultWorkflowExecutor executor = new DefaultWorkflowExecutor(state); + executor.executeWorkflow(); + + LOGGER.info("Dynamic handshake completed. Type: {}", + isResumption ? "Session Resumption" : "Full Handshake"); + } + + /** + * Executes actions up to the decision point + */ + private static void executeActionsUntilDecisionPoint(State state) + throws WorkflowExecutionException, IOException { + + for (TlsAction action : state.getWorkflowTrace().getTlsActions()) { + if (!action.isExecuted()) { + action.setConnectionAlias(state.getTlsContext().getConnection().getAlias()); + action.normalize(); + action.execute(state); + + if (!action.executedAsPlanned()) { + LOGGER.warn("Action did not execute as planned: {}", + action.getClass().getSimpleName()); + } + } + } + } + + /** + * Finds the last received ClientHello message + */ + private static ClientHelloMessage findLastClientHello(State state) { + for (int i = state.getWorkflowTrace().getTlsActions().size() - 1; i >= 0; i--) { + TlsAction action = state.getWorkflowTrace().getTlsActions().get(i); + if (action instanceof ReceiveAction) { + ReceiveAction receiveAction = (ReceiveAction) action; + for (ProtocolMessage msg : receiveAction.getReceivedMessages()) { + if (msg instanceof ClientHelloMessage) { + return (ClientHelloMessage) msg; + } + } + } + } + return null; + } + + /** + * Checks if the ClientHello indicates session resumption + */ + private static boolean checkSessionResumption(ClientHelloMessage clientHello, + byte[] expectedSessionId) { + if (clientHello == null || expectedSessionId == null) { + return false; + } + + byte[] receivedSessionId = clientHello.getSessionId().getValue(); + return receivedSessionId != null && + receivedSessionId.length > 0 && + Arrays.equals(receivedSessionId, expectedSessionId); + } + + /** + * Creates continuation workflow based on handshake type + */ + private static WorkflowTrace createContinuationWorkflow(boolean isResumption) { + WorkflowTrace trace = new WorkflowTrace(); + + if (isResumption) { + // Session resumption flow + trace.addTlsAction(new SendAction(new ServerHelloMessage())); + trace.addTlsAction(new SendAction(new ChangeCipherSpecMessage())); + trace.addTlsAction(new SendAction(new FinishedMessage())); + trace.addTlsAction(new ReceiveAction(new ChangeCipherSpecMessage())); + trace.addTlsAction(new ReceiveAction(new FinishedMessage())); + } else { + // Full handshake flow + trace.addTlsAction(new SendAction(new ServerHelloMessage())); + trace.addTlsAction(new SendAction(new PskServerKeyExchangeMessage())); + trace.addTlsAction(new SendAction(new ServerHelloDoneMessage())); + trace.addTlsAction(new ReceiveAction(new PskClientKeyExchangeMessage())); + trace.addTlsAction(new ReceiveAction(new ChangeCipherSpecMessage())); + trace.addTlsAction(new ReceiveAction(new FinishedMessage())); + trace.addTlsAction(new SendAction(new ChangeCipherSpecMessage())); + trace.addTlsAction(new SendAction(new FinishedMessage())); + } + + return trace; + } + + /** + * Helper method to convert bytes to hex string + */ + private static String bytesToHex(byte[] bytes) { + if (bytes == null) return "null"; + StringBuilder result = new StringBuilder(); + for (byte b : bytes) { + result.append(String.format("%02x", b)); + } + return result.toString(); + } +} \ No newline at end of file diff --git a/TLS-Core/src/main/java/de/rub/nds/tlsattacker/core/examples/README.md b/TLS-Core/src/main/java/de/rub/nds/tlsattacker/core/examples/README.md new file mode 100644 index 0000000000..2077bffa45 --- /dev/null +++ b/TLS-Core/src/main/java/de/rub/nds/tlsattacker/core/examples/README.md @@ -0,0 +1,150 @@ +# Dynamic Handshake Handling in TLS-Attacker + +This directory contains examples demonstrating how to handle simultaneous full handshake and session resumption scenarios in TLS-Attacker, addressing issue #195. + +## Problem Statement + +When implementing a TLS server using TLS-Attacker, you may need to handle two different handshake scenarios for a second ClientHello: +1. **Session Resumption**: ClientHello includes the Session ID from a previous session +2. **New Full Handshake**: ClientHello contains an empty Session ID + +TLS-Attacker's workflow traces are typically static and don't support conditional branching, making it challenging to handle both cases in a single workflow. + +## Solution Approach + +The examples demonstrate two approaches to solve this problem: + +### 1. Manual Action Execution (`DynamicHandshakeExample.java`) + +This approach executes actions individually and makes decisions based on the received messages: + +```java +// Execute actions up to decision point +executeAction(new ReceiveAction(new ClientHelloMessage()), state); + +// Check received ClientHello +ClientHelloMessage clientHello = findReceivedClientHello(state); +byte[] receivedSessionId = clientHello.getSessionId().getValue(); + +// Decide which path to take +if (isSessionResumption(receivedSessionId, expectedSessionId)) { + executeResumptionHandshake(state); +} else { + executeFullHandshake(state); +} +``` + +**Advantages:** +- Full control over execution flow +- Easy to implement conditional logic +- Can inspect state between actions + +**Disadvantages:** +- More manual code required +- Less integrated with TLS-Attacker's workflow system + +### 2. Hybrid Workflow Approach (`DynamicHandshakeWorkflowExample.java`) + +This approach uses WorkflowExecutor but modifies the workflow dynamically: + +```java +// Execute partial workflow up to decision point +WorkflowTrace initialTrace = new WorkflowTrace(); +initialTrace.addTlsAction(new ReceiveAction(new ClientHelloMessage())); +// ... execute initial actions + +// Determine handshake type +boolean isResumption = checkSessionResumption(clientHello, expectedSessionId); + +// Create and append appropriate continuation workflow +WorkflowTrace continuationTrace = createContinuationWorkflow(isResumption); +state.getWorkflowTrace().addAll(continuationTrace.getTlsActions()); + +// Continue execution with WorkflowExecutor +executor.executeWorkflow(); +``` + +**Advantages:** +- Uses standard WorkflowExecutor +- Maintains workflow trace for debugging +- More integrated with TLS-Attacker patterns + +**Disadvantages:** +- Requires interrupting and resuming workflow execution +- More complex implementation + +## Usage Example + +To use these examples in your project: + +1. **As a library (recommended)**: Create a Maven project that depends on TLS-Attacker and adapt the example code: + +```java +public class MyTlsServer { + public void handleConnection() { + Config config = createConfig(); + State state = new State(config); + + // Execute initial handshake + byte[] sessionId = executeInitialHandshake(state); + + // Handle subsequent connections dynamically + while (true) { + ClientHelloMessage hello = receiveClientHello(state); + if (isResumption(hello, sessionId)) { + executeResumptionHandshake(state); + } else { + executeFullHandshake(state); + } + } + } +} +``` + +2. **For testing**: Use the test examples to verify your implementation handles both scenarios correctly. + +## Key Implementation Details + +### Session ID Checking +```java +boolean isResumption = receivedSessionId != null && + receivedSessionId.length > 0 && + Arrays.equals(receivedSessionId, expectedSessionId); +``` + +### DTLS Cookie Handling +The examples include HelloVerifyRequest handling for DTLS: +```java +executeAction(new SendAction(new HelloVerifyRequestMessage()), state); +executeAction(new ReceiveAction(new ClientHelloMessage()), state); // Second hello after cookie +``` + +### PSK Configuration +The examples use PSK cipher suites as shown in the original issue: +```java +config.setDefaultSelectedCipherSuite("TLS_PSK_WITH_AES_128_CBC_SHA"); +config.setDefaultPSKIdentity("Client_identity".getBytes()); +config.setDefaultPSKKey(pskKey); +``` + +## Testing + +Run the included test class to verify the implementation: + +```bash +mvn test -Dtest=DynamicHandshakeExampleTest +``` + +## Further Customization + +You can extend these examples to: +- Support multiple session IDs +- Implement session cache/storage +- Add timeout handling for session resumption +- Support other handshake variations (e.g., TLS 1.3 PSK modes) + +## References + +- Issue #195: Handling Simultaneous Full Handshake and Session Resumption +- TLS-Attacker Documentation: https://github.com/tls-attacker/TLS-Attacker +- TLS Session Resumption: RFC 5246 Section 7.3 \ No newline at end of file diff --git a/TLS-Core/src/test/java/de/rub/nds/tlsattacker/core/examples/DynamicHandshakeExampleTest.java b/TLS-Core/src/test/java/de/rub/nds/tlsattacker/core/examples/DynamicHandshakeExampleTest.java new file mode 100644 index 0000000000..09ae7bb11b --- /dev/null +++ b/TLS-Core/src/test/java/de/rub/nds/tlsattacker/core/examples/DynamicHandshakeExampleTest.java @@ -0,0 +1,187 @@ +/* + * TLS-Attacker - A Modular Penetration Testing Framework for TLS + * + * Copyright 2014-2025 Ruhr University Bochum, Paderborn University, Technology Innovation Institute, + * and Hackmanit GmbH + * + * Licensed under Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package de.rub.nds.tlsattacker.core.examples; + +import de.rub.nds.tlsattacker.core.config.Config; +import de.rub.nds.tlsattacker.core.connection.InboundConnection; +import de.rub.nds.tlsattacker.core.constants.HandshakeMessageType; +import de.rub.nds.tlsattacker.core.constants.ProtocolMessageType; +import de.rub.nds.tlsattacker.core.constants.RunningModeType; +import de.rub.nds.tlsattacker.core.layer.context.TlsContext; +import de.rub.nds.tlsattacker.core.protocol.message.*; +import de.rub.nds.tlsattacker.core.state.State; +import de.rub.nds.tlsattacker.core.workflow.WorkflowTrace; +import de.rub.nds.tlsattacker.core.workflow.action.ReceiveAction; +import de.rub.nds.tlsattacker.core.workflow.action.SendAction; +import de.rub.nds.tlsattacker.core.workflow.action.TlsAction; +import java.util.Arrays; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test class for DynamicHandshakeExample functionality + */ +public class DynamicHandshakeExampleTest { + + private Config config; + + @BeforeEach + public void setUp() { + config = Config.createConfig(); + config.setDefaultRunningMode(RunningModeType.SERVER); + config.setDefaultSelectedCipherSuite("TLS_PSK_WITH_AES_128_CBC_SHA"); + config.setDefaultServerSupportedCipherSuites(Arrays.asList("TLS_PSK_WITH_AES_128_CBC_SHA")); + config.setDefaultPSKIdentity("Client_identity".getBytes()); + config.setDefaultPSKKey(new byte[]{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F}); + config.setDefaultConnections(Arrays.asList(new InboundConnection(4433, "server"))); + } + + @Test + public void testSessionResumptionDetection() { + // Create test session ID + byte[] sessionId = new byte[]{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; + + // Create ClientHello with session ID (resumption attempt) + ClientHelloMessage resumptionHello = new ClientHelloMessage(); + resumptionHello.setSessionId(sessionId); + + // Create ClientHello without session ID (new session) + ClientHelloMessage newSessionHello = new ClientHelloMessage(); + newSessionHello.setSessionId(new byte[0]); + + // Test resumption detection + assertTrue(isSessionResumption(resumptionHello, sessionId)); + assertFalse(isSessionResumption(newSessionHello, sessionId)); + assertFalse(isSessionResumption(null, sessionId)); + assertFalse(isSessionResumption(resumptionHello, null)); + } + + @Test + public void testWorkflowCreationForResumption() { + WorkflowTrace resumptionTrace = createResumptionWorkflow(); + + // Verify correct number of actions + assertEquals(5, resumptionTrace.getTlsActions().size()); + + // Verify action sequence + assertTrue(resumptionTrace.getTlsActions().get(0) instanceof SendAction); + assertTrue(((SendAction)resumptionTrace.getTlsActions().get(0)).getSendMessages().get(0) + instanceof ServerHelloMessage); + + assertTrue(resumptionTrace.getTlsActions().get(1) instanceof SendAction); + assertTrue(((SendAction)resumptionTrace.getTlsActions().get(1)).getSendMessages().get(0) + instanceof ChangeCipherSpecMessage); + + assertTrue(resumptionTrace.getTlsActions().get(2) instanceof SendAction); + assertTrue(((SendAction)resumptionTrace.getTlsActions().get(2)).getSendMessages().get(0) + instanceof FinishedMessage); + + assertTrue(resumptionTrace.getTlsActions().get(3) instanceof ReceiveAction); + assertTrue(resumptionTrace.getTlsActions().get(4) instanceof ReceiveAction); + } + + @Test + public void testWorkflowCreationForFullHandshake() { + WorkflowTrace fullTrace = createFullHandshakeWorkflow(); + + // Verify correct number of actions + assertEquals(8, fullTrace.getTlsActions().size()); + + // Verify action sequence includes PSK key exchange + boolean hasPskServerKeyExchange = false; + for (TlsAction action : fullTrace.getTlsActions()) { + if (action instanceof SendAction) { + SendAction sendAction = (SendAction) action; + if (sendAction.getSendMessages().stream() + .anyMatch(msg -> msg instanceof PskServerKeyExchangeMessage)) { + hasPskServerKeyExchange = true; + break; + } + } + } + assertTrue(hasPskServerKeyExchange); + } + + @Test + public void testDynamicWorkflowSelection() { + // Test session ID + byte[] sessionId = new byte[]{0x0A, 0x0B, 0x0C, 0x0D}; + + // Create state with context + State state = new State(config); + TlsContext context = state.getTlsContext(); + context.setServerSessionId(sessionId); + + // Simulate receiving ClientHello with matching session ID + ClientHelloMessage resumptionHello = new ClientHelloMessage(); + resumptionHello.setSessionId(sessionId); + + WorkflowTrace selectedTrace = selectWorkflowBasedOnClientHello(resumptionHello, sessionId); + + // Should select resumption workflow (5 actions) + assertEquals(5, selectedTrace.getTlsActions().size()); + + // Simulate receiving ClientHello with empty session ID + ClientHelloMessage newSessionHello = new ClientHelloMessage(); + newSessionHello.setSessionId(new byte[0]); + + selectedTrace = selectWorkflowBasedOnClientHello(newSessionHello, sessionId); + + // Should select full handshake workflow (8 actions) + assertEquals(8, selectedTrace.getTlsActions().size()); + } + + // Helper methods + + private boolean isSessionResumption(ClientHelloMessage clientHello, byte[] expectedSessionId) { + if (clientHello == null || expectedSessionId == null) { + return false; + } + + byte[] receivedSessionId = clientHello.getSessionId().getValue(); + return receivedSessionId != null && + receivedSessionId.length > 0 && + Arrays.equals(receivedSessionId, expectedSessionId); + } + + private WorkflowTrace createResumptionWorkflow() { + WorkflowTrace trace = new WorkflowTrace(); + trace.addTlsAction(new SendAction(new ServerHelloMessage())); + trace.addTlsAction(new SendAction(new ChangeCipherSpecMessage())); + trace.addTlsAction(new SendAction(new FinishedMessage())); + trace.addTlsAction(new ReceiveAction(new ChangeCipherSpecMessage())); + trace.addTlsAction(new ReceiveAction(new FinishedMessage())); + return trace; + } + + private WorkflowTrace createFullHandshakeWorkflow() { + WorkflowTrace trace = new WorkflowTrace(); + trace.addTlsAction(new SendAction(new ServerHelloMessage())); + trace.addTlsAction(new SendAction(new PskServerKeyExchangeMessage())); + trace.addTlsAction(new SendAction(new ServerHelloDoneMessage())); + trace.addTlsAction(new ReceiveAction(new PskClientKeyExchangeMessage())); + trace.addTlsAction(new ReceiveAction(new ChangeCipherSpecMessage())); + trace.addTlsAction(new ReceiveAction(new FinishedMessage())); + trace.addTlsAction(new SendAction(new ChangeCipherSpecMessage())); + trace.addTlsAction(new SendAction(new FinishedMessage())); + return trace; + } + + private WorkflowTrace selectWorkflowBasedOnClientHello(ClientHelloMessage clientHello, + byte[] expectedSessionId) { + if (isSessionResumption(clientHello, expectedSessionId)) { + return createResumptionWorkflow(); + } else { + return createFullHandshakeWorkflow(); + } + } +} \ No newline at end of file