From 9f9d58141817450ccc52464c1c223c51c1de2e87 Mon Sep 17 00:00:00 2001 From: Robert Merget Date: Wed, 11 Jun 2025 10:41:08 +0400 Subject: [PATCH 1/7] Redesign ModifiableVariableProperty with generic structure and length constraints - Replaced Type enum with Purpose enum for protocol-agnostic semantic classification - Replaced Format enum with Encoding enum for comprehensive data encoding formats - Added length constraint parameters: minLength, maxLength, expectedLength - Removed protocol-specific fields for better generalization - Updated ModifiableVariableAnalyzer with new analysis methods for length constraints - Enhanced test suite to cover new features and length validation capabilities - Improved documentation with comprehensive usage examples This design provides more structural information about fields while being applicable across different protocols and data formats, not just TLS-specific. Resolves: tls-attacker/ModifiableVariable#203 --- .../ModifiableVariableProperty.java | 188 ++++++++--- .../util/ModifiableVariableAnalyzer.java | 310 +++++++++++++++++- .../ModifiableVariablePropertyTest.java | 181 +++++++++- 3 files changed, 616 insertions(+), 63 deletions(-) diff --git a/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java b/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java index 0cdcad4c..84c69e38 100644 --- a/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java +++ b/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java @@ -16,81 +16,183 @@ * Annotation interface for marking and categorizing modifiable variables within a class. * *

This annotation provides metadata about a modifiable variable field, including its semantic - * type (what kind of data it represents) and format (how the data is encoded). This information can - * be used for reflection-based analysis, serialization, or other operations that need to understand - * the purpose of different variables. + * type (what kind of data it represents) and format (how the data is encoded). This information is + * extensively used throughout the TLS-Attacker ecosystem for reflection-based analysis, + * serialization, testing scenarios, and attack implementations that need to understand the purpose + * and encoding of different protocol variables. * - *

//TODO This class has not been touched or used much for a while and needs refactoring. + *

The annotation is retained at runtime and can only be applied to fields. It serves as the + * foundation for semantic classification of protocol message fields, enabling automated analysis + * and manipulation of protocol implementations. * - *

The annotation is retained at runtime and can only be applied to fields. + *

Usage examples: + * + *

{@code
+ * // Simple length field with constraints
+ * @ModifiableVariableProperty(purpose = Purpose.LENGTH, minLength = 1, maxLength = 4)
+ * private ModifiableInteger messageLength;
+ *
+ * // Signature with specific encoding and length
+ * @ModifiableVariableProperty(
+ *     purpose = Purpose.SIGNATURE,
+ *     encoding = Encoding.ASN1_DER,
+ *     minLength = 64,
+ *     maxLength = 256,
+ *     critical = true)
+ * private ModifiableByteArray digitalSignature;
+ *
+ * // Fixed-length random value
+ * @ModifiableVariableProperty(purpose = Purpose.RANDOM, expectedLength = 32)
+ * private ModifiableByteArray nonce;
+ *
+ * // Variable-length payload with description
+ * @ModifiableVariableProperty(
+ *     purpose = Purpose.PAYLOAD,
+ *     encoding = Encoding.UTF8,
+ *     maxLength = 1024,
+ *     description = "Application data payload")
+ * private ModifiableByteArray applicationData;
+ * }
*/ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface ModifiableVariableProperty { /** - * Semantic types that can be assigned to modifiable variables. These values describe the - * purpose or role of the variable. + * Semantic purpose categories for modifiable variables. These describe the role or function of + * the variable within any protocol or data structure. */ - enum Type { + enum Purpose { /** Variable representing a length field */ LENGTH, - /** Variable representing a count field */ + /** Variable representing a count or quantity field */ COUNT, - /** Variable representing padding */ + /** Variable representing padding or filler bytes */ PADDING, - /** Variable representing one or more (array of) TLS constants */ - TLS_CONSTANT, + /** Variable representing a protocol constant or enumerated value */ + CONSTANT, /** Variable representing a cryptographic signature */ SIGNATURE, - /** Variable representing encrypted data */ + /** Variable representing encrypted or ciphered data */ CIPHERTEXT, - /** Variable representing a message authentication code */ - HMAC, - /** Variable representing a public key */ - PUBLIC_KEY, - /** Variable representing a private key */ - PRIVATE_KEY, - /** Variable representing key material */ + /** Variable representing a message authentication code or hash */ + MAC, + /** Variable representing cryptographic key material */ KEY_MATERIAL, - /** Variable representing a certificate */ + /** Variable representing a certificate or credential */ CERTIFICATE, - /** Variable representing a plain protocol message, always in a decrypted state */ - PLAIN_PROTOCOL_MESSAGE, - /** Variable representing a plain record */ - PLAIN_RECORD, - /** Variable representing a cookie */ - COOKIE, - /** Default type when no specific type is applicable */ - NONE, - /** Variable that switches behavior */ - BEHAVIOR_SWITCH + /** Variable representing plaintext protocol data */ + PLAINTEXT, + /** Variable representing a random value, nonce, or salt */ + RANDOM, + /** Variable representing a protocol version or format identifier */ + VERSION, + /** Variable representing a session or connection identifier */ + IDENTIFIER, + /** Variable representing a timestamp or temporal value */ + TIMESTAMP, + /** Variable representing an extension or optional component */ + EXTENSION, + /** Variable that controls protocol behavior or flags */ + CONTROL, + /** Variable representing user or application data */ + PAYLOAD, + /** Default purpose when no specific category applies */ + NONE } /** - * Encoding formats that can be used for modifiable variables. These values describe how the - * data is encoded. + * Encoding formats for modifiable variables. These describe how the data is encoded, + * structured, or represented. */ - enum Format { - /** ASN.1 encoding format */ - ASN1, - /** PKCS#1 encoding format */ + enum Encoding { + /** ASN.1 Distinguished Encoding Rules format */ + ASN1_DER, + /** ASN.1 Basic Encoding Rules format */ + ASN1_BER, + /** PKCS#1 format for RSA key encoding */ PKCS1, - /** Default format when no specific format is applicable */ + /** PKCS#8 format for private key information */ + PKCS8, + /** X.509 format for certificates */ + X509, + /** PEM (Privacy-Enhanced Mail) text format */ + PEM, + /** Base64 text encoding */ + BASE64, + /** Hexadecimal string representation */ + HEX, + /** Raw binary data */ + BINARY, + /** UTF-8 text encoding */ + UTF8, + /** Big-endian byte order */ + BIG_ENDIAN, + /** Little-endian byte order */ + LITTLE_ENDIAN, + /** Variable-length encoding (e.g., varint) */ + VARINT, + /** Length-prefixed format */ + LENGTH_PREFIXED, + /** JSON text format */ + JSON, + /** XML text format */ + XML, + /** Default encoding when not specified */ NONE } /** - * Specifies the semantic type of the annotated variable. + * Specifies the semantic purpose of the annotated variable. * - * @return The type of the variable + * @return The purpose category of the variable */ - Type type() default Type.NONE; + Purpose purpose() default Purpose.NONE; /** * Specifies the encoding format of the annotated variable. * - * @return The format of the variable + * @return The encoding format of the variable + */ + Encoding encoding() default Encoding.NONE; + + /** + * Specifies the minimum length (in bytes) of the variable's value. Use -1 to indicate no + * minimum constraint. + * + * @return The minimum length in bytes, or -1 if unconstrained + */ + int minLength() default -1; + + /** + * Specifies the maximum length (in bytes) of the variable's value. Use -1 to indicate no + * maximum constraint. + * + * @return The maximum length in bytes, or -1 if unconstrained + */ + int maxLength() default -1; + + /** + * Specifies the expected or default length (in bytes) of the variable's value. Use -1 to + * indicate no expected length. + * + * @return The expected length in bytes, or -1 if variable + */ + int expectedLength() default -1; + + /** + * Provides a human-readable description of the variable's purpose. This can be used for + * documentation generation or debugging. + * + * @return A description of the variable's purpose + */ + String description() default ""; + + /** + * Indicates whether this variable is critical for correct operation. Critical variables may + * require special handling during modification. + * + * @return true if the variable is critical, false otherwise */ - Format format() default Format.NONE; + boolean critical() default false; } diff --git a/src/main/java/de/rub/nds/modifiablevariable/util/ModifiableVariableAnalyzer.java b/src/main/java/de/rub/nds/modifiablevariable/util/ModifiableVariableAnalyzer.java index 326c129f..4634b988 100644 --- a/src/main/java/de/rub/nds/modifiablevariable/util/ModifiableVariableAnalyzer.java +++ b/src/main/java/de/rub/nds/modifiablevariable/util/ModifiableVariableAnalyzer.java @@ -9,9 +9,12 @@ import de.rub.nds.modifiablevariable.HoldsModifiableVariable; import de.rub.nds.modifiablevariable.ModifiableVariable; +import de.rub.nds.modifiablevariable.ModifiableVariableProperty; +import de.rub.nds.modifiablevariable.ModifiableVariableProperty.Encoding; +import de.rub.nds.modifiablevariable.ModifiableVariableProperty.Purpose; import java.lang.reflect.Field; -import java.util.LinkedList; -import java.util.List; +import java.util.*; +import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -23,6 +26,10 @@ * locate fields that are either of type ModifiableVariable or are annotated with {@link * HoldsModifiableVariable}. * + *

Additionally, this class provides specialized methods for working with the {@link + * ModifiableVariableProperty} annotation to categorize and analyze fields by their semantic + * meaning, encoding format, and protocol context. + * *

This class cannot be instantiated and all methods are static. */ public final class ModifiableVariableAnalyzer { @@ -205,4 +212,303 @@ public static List getAllModifiableVariableHolders } return result; } + + // ===== ModifiableVariableProperty annotation analysis methods ===== + + /** + * Retrieves all fields in a class hierarchy that are annotated with ModifiableVariableProperty. + * + * @param clazz The class to analyze + * @return A list of fields annotated with ModifiableVariableProperty + */ + public static List getAnnotatedFields(Class clazz) { + List annotatedFields = new ArrayList<>(); + Class currentClass = clazz; + + while (currentClass != null && currentClass != Object.class) { + for (Field field : currentClass.getDeclaredFields()) { + if (field.isAnnotationPresent(ModifiableVariableProperty.class)) { + annotatedFields.add(field); + } + } + currentClass = currentClass.getSuperclass(); + } + + return annotatedFields; + } + + /** + * Groups annotated fields by their semantic purpose. + * + * @param clazz The class to analyze + * @return A map where keys are Purpose enum values and values are lists of fields + */ + public static Map> groupFieldsByPurpose(Class clazz) { + return getAnnotatedFields(clazz).stream() + .collect( + Collectors.groupingBy( + field -> + field.getAnnotation(ModifiableVariableProperty.class) + .purpose())); + } + + /** + * Groups annotated fields by their encoding format. + * + * @param clazz The class to analyze + * @return A map where keys are Encoding enum values and values are lists of fields + */ + public static Map> groupFieldsByEncoding(Class clazz) { + return getAnnotatedFields(clazz).stream() + .collect( + Collectors.groupingBy( + field -> + field.getAnnotation(ModifiableVariableProperty.class) + .encoding())); + } + + /** + * Groups annotated fields by their length constraints. + * + * @param clazz The class to analyze + * @return A map where keys describe length constraints and values are lists of fields + */ + public static Map> groupFieldsByLengthConstraints(Class clazz) { + return getAnnotatedFields(clazz).stream() + .collect( + Collectors.groupingBy( + field -> { + ModifiableVariableProperty annotation = + field.getAnnotation(ModifiableVariableProperty.class); + int minLen = annotation.minLength(); + int maxLen = annotation.maxLength(); + int expectedLen = annotation.expectedLength(); + + if (expectedLen > 0) { + return "Fixed length (" + expectedLen + " bytes)"; + } else if (minLen > 0 && maxLen > 0) { + return "Variable length (" + + minLen + + "-" + + maxLen + + " bytes)"; + } else if (minLen > 0) { + return "Minimum length (" + minLen + "+ bytes)"; + } else if (maxLen > 0) { + return "Maximum length (≤" + maxLen + " bytes)"; + } else { + return "Unconstrained length"; + } + })); + } + + /** + * Finds all fields marked as critical. + * + * @param clazz The class to analyze + * @return A list of fields marked as critical + */ + public static List getCriticalFields(Class clazz) { + return getAnnotatedFields(clazz).stream() + .filter(field -> field.getAnnotation(ModifiableVariableProperty.class).critical()) + .collect(Collectors.toList()); + } + + /** + * Finds all fields of a specific semantic purpose. + * + * @param clazz The class to analyze + * @param purpose The semantic purpose to search for + * @return A list of fields with the specified purpose + */ + public static List getFieldsByPurpose(Class clazz, Purpose purpose) { + return getAnnotatedFields(clazz).stream() + .filter( + field -> + field.getAnnotation(ModifiableVariableProperty.class).purpose() + == purpose) + .collect(Collectors.toList()); + } + + /** + * Finds all fields with a specific encoding format. + * + * @param clazz The class to analyze + * @param encoding The encoding format to search for + * @return A list of fields with the specified encoding + */ + public static List getFieldsByEncoding(Class clazz, Encoding encoding) { + return getAnnotatedFields(clazz).stream() + .filter( + field -> + field.getAnnotation(ModifiableVariableProperty.class).encoding() + == encoding) + .collect(Collectors.toList()); + } + + /** + * Finds all fields with length constraints. + * + * @param clazz The class to analyze + * @return A list of fields that have any length constraints defined + */ + public static List getFieldsWithLengthConstraints(Class clazz) { + return getAnnotatedFields(clazz).stream() + .filter( + field -> { + ModifiableVariableProperty annotation = + field.getAnnotation(ModifiableVariableProperty.class); + return annotation.minLength() > 0 + || annotation.maxLength() > 0 + || annotation.expectedLength() > 0; + }) + .collect(Collectors.toList()); + } + + /** + * Finds all fields with a specific expected length. + * + * @param clazz The class to analyze + * @param expectedLength The expected length to search for + * @return A list of fields with the specified expected length + */ + public static List getFieldsByExpectedLength(Class clazz, int expectedLength) { + return getAnnotatedFields(clazz).stream() + .filter( + field -> + field.getAnnotation(ModifiableVariableProperty.class) + .expectedLength() + == expectedLength) + .collect(Collectors.toList()); + } + + /** + * Checks if a field has a ModifiableVariableProperty annotation. + * + * @param field The field to check + * @return true if the field is annotated, false otherwise + */ + public static boolean isAnnotated(Field field) { + return field.isAnnotationPresent(ModifiableVariableProperty.class); + } + + /** + * Gets the ModifiableVariableProperty annotation from a field. + * + * @param field The field to examine + * @return The annotation or null if not present + */ + public static ModifiableVariableProperty getAnnotation(Field field) { + return field.getAnnotation(ModifiableVariableProperty.class); + } + + /** + * Validates that all ModifiableVariable fields in a class have proper annotations. This is + * useful for ensuring coding standards compliance. + * + * @param clazz The class to validate + * @return A list of field names that are ModifiableVariable but lack annotations + */ + public static List getUnannotatedModifiableVariables(Class clazz) { + List allModifiableFields = + ReflectionHelper.getFieldsUpTo(clazz, null, ModifiableVariable.class); + + return allModifiableFields.stream() + .filter(field -> !isAnnotated(field)) + .map(Field::getName) + .collect(Collectors.toList()); + } + + /** + * Generates a summary report of all annotated fields in a class. + * + * @param clazz The class to analyze + * @return A formatted string containing analysis results + */ + public static String generatePropertyAnalysisReport(Class clazz) { + StringBuilder report = new StringBuilder(); + report.append("ModifiableVariableProperty Analysis Report for ") + .append(clazz.getSimpleName()) + .append("\n"); + report.append("=".repeat(60)).append("\n\n"); + + List annotatedFields = getAnnotatedFields(clazz); + List allModifiableFields = + ReflectionHelper.getFieldsUpTo(clazz, null, ModifiableVariable.class); + + report.append("Total ModifiableVariable fields: ") + .append(allModifiableFields.size()) + .append("\n"); + report.append("Annotated fields: ").append(annotatedFields.size()).append("\n"); + report.append("Unannotated fields: ") + .append(allModifiableFields.size() - annotatedFields.size()) + .append("\n\n"); + + if (!annotatedFields.isEmpty()) { + report.append("Fields by Purpose:\n"); + Map> byPurpose = groupFieldsByPurpose(clazz); + byPurpose.entrySet().stream() + .sorted(Map.Entry.>comparingByKey()) + .forEach( + entry -> { + report.append(" ").append(entry.getKey()).append(": "); + report.append( + entry.getValue().stream() + .map(Field::getName) + .collect(Collectors.joining(", "))); + report.append("\n"); + }); + + Map> byEncoding = groupFieldsByEncoding(clazz); + if (byEncoding.size() > 1 || !byEncoding.containsKey(Encoding.NONE)) { + report.append("\nFields by Encoding:\n"); + byEncoding.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .forEach( + entry -> { + report.append(" ").append(entry.getKey()).append(": "); + report.append( + entry.getValue().stream() + .map(Field::getName) + .collect(Collectors.joining(", "))); + report.append("\n"); + }); + } + + List criticalFields = getCriticalFields(clazz); + if (!criticalFields.isEmpty()) { + report.append("\nCritical fields: "); + report.append( + criticalFields.stream() + .map(Field::getName) + .collect(Collectors.joining(", "))); + report.append("\n"); + } + + Map> byLengthConstraints = groupFieldsByLengthConstraints(clazz); + if (byLengthConstraints.size() > 1 + || !byLengthConstraints.containsKey("Unconstrained length")) { + report.append("\nFields by Length Constraints:\n"); + byLengthConstraints.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .forEach( + entry -> { + report.append(" ").append(entry.getKey()).append(": "); + report.append( + entry.getValue().stream() + .map(Field::getName) + .collect(Collectors.joining(", "))); + report.append("\n"); + }); + } + } + + List unannotated = getUnannotatedModifiableVariables(clazz); + if (!unannotated.isEmpty()) { + report.append("\nUnannotated ModifiableVariable fields:\n"); + unannotated.forEach(name -> report.append(" - ").append(name).append("\n")); + } + + return report.toString(); + } } diff --git a/src/test/java/de/rub/nds/modifiablevariable/ModifiableVariablePropertyTest.java b/src/test/java/de/rub/nds/modifiablevariable/ModifiableVariablePropertyTest.java index 058d28e4..62a1354c 100644 --- a/src/test/java/de/rub/nds/modifiablevariable/ModifiableVariablePropertyTest.java +++ b/src/test/java/de/rub/nds/modifiablevariable/ModifiableVariablePropertyTest.java @@ -7,29 +7,52 @@ */ package de.rub.nds.modifiablevariable; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.*; -import de.rub.nds.modifiablevariable.ModifiableVariableProperty.Format; -import de.rub.nds.modifiablevariable.ModifiableVariableProperty.Type; +import de.rub.nds.modifiablevariable.ModifiableVariableProperty.Encoding; +import de.rub.nds.modifiablevariable.ModifiableVariableProperty.Purpose; import de.rub.nds.modifiablevariable.integer.ModifiableInteger; +import de.rub.nds.modifiablevariable.util.ModifiableVariableAnalyzer; import java.lang.reflect.Field; +import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; public class ModifiableVariablePropertyTest { // Test class with annotated fields private static class TestClass { - @ModifiableVariableProperty(type = Type.LENGTH) + @ModifiableVariableProperty(purpose = Purpose.LENGTH, minLength = 1, maxLength = 4) private ModifiableInteger length; - @ModifiableVariableProperty(type = Type.COUNT) + @ModifiableVariableProperty(purpose = Purpose.COUNT, expectedLength = 2) private ModifiableInteger count; - @ModifiableVariableProperty(type = Type.SIGNATURE, format = Format.ASN1) - private ModifiableInteger signatureWithFormat; + @ModifiableVariableProperty(purpose = Purpose.SIGNATURE, encoding = Encoding.ASN1_DER) + private ModifiableInteger signatureWithEncoding; @ModifiableVariableProperty private ModifiableInteger defaultProperty; + + @ModifiableVariableProperty( + purpose = Purpose.KEY_MATERIAL, + encoding = Encoding.X509, + description = "Server's public key for ECDH", + critical = true, + minLength = 32, + maxLength = 65) + private ModifiableInteger enhancedProperty; + + @ModifiableVariableProperty(purpose = Purpose.VERSION, expectedLength = 2) + private ModifiableInteger protocolVersion; + + @ModifiableVariableProperty(purpose = Purpose.RANDOM, expectedLength = 32) + private ModifiableInteger randomValue; + + @ModifiableVariableProperty(purpose = Purpose.PADDING, minLength = 0, maxLength = 255) + private ModifiableInteger paddingLength; + + // Unannotated ModifiableVariable field for testing + private ModifiableInteger unannotatedField; } @Test @@ -39,8 +62,11 @@ public void testLengthProperty() throws NoSuchFieldException { lengthField.getAnnotation(ModifiableVariableProperty.class); assertNotNull(annotation); - assertEquals(Type.LENGTH, annotation.type()); - assertEquals(Format.NONE, annotation.format()); + assertEquals(Purpose.LENGTH, annotation.purpose()); + assertEquals(Encoding.NONE, annotation.encoding()); + assertEquals(1, annotation.minLength()); + assertEquals(4, annotation.maxLength()); + assertEquals(-1, annotation.expectedLength()); } @Test @@ -50,19 +76,22 @@ public void testCountProperty() throws NoSuchFieldException { countField.getAnnotation(ModifiableVariableProperty.class); assertNotNull(annotation); - assertEquals(Type.COUNT, annotation.type()); - assertEquals(Format.NONE, annotation.format()); + assertEquals(Purpose.COUNT, annotation.purpose()); + assertEquals(Encoding.NONE, annotation.encoding()); + assertEquals(-1, annotation.minLength()); + assertEquals(-1, annotation.maxLength()); + assertEquals(2, annotation.expectedLength()); } @Test - public void testSignatureWithFormatProperty() throws NoSuchFieldException { - Field signatureField = TestClass.class.getDeclaredField("signatureWithFormat"); + public void testSignatureWithEncodingProperty() throws NoSuchFieldException { + Field signatureField = TestClass.class.getDeclaredField("signatureWithEncoding"); ModifiableVariableProperty annotation = signatureField.getAnnotation(ModifiableVariableProperty.class); assertNotNull(annotation); - assertEquals(Type.SIGNATURE, annotation.type()); - assertEquals(Format.ASN1, annotation.format()); + assertEquals(Purpose.SIGNATURE, annotation.purpose()); + assertEquals(Encoding.ASN1_DER, annotation.encoding()); } @Test @@ -72,7 +101,123 @@ public void testDefaultProperty() throws NoSuchFieldException { defaultField.getAnnotation(ModifiableVariableProperty.class); assertNotNull(annotation); - assertEquals(Type.NONE, annotation.type()); - assertEquals(Format.NONE, annotation.format()); + assertEquals(Purpose.NONE, annotation.purpose()); + assertEquals(Encoding.NONE, annotation.encoding()); + assertEquals(-1, annotation.minLength()); + assertEquals(-1, annotation.maxLength()); + assertEquals(-1, annotation.expectedLength()); + } + + @Test + public void testEnhancedProperty() throws NoSuchFieldException { + Field enhancedField = TestClass.class.getDeclaredField("enhancedProperty"); + ModifiableVariableProperty annotation = + enhancedField.getAnnotation(ModifiableVariableProperty.class); + + assertNotNull(annotation); + assertEquals(Purpose.KEY_MATERIAL, annotation.purpose()); + assertEquals(Encoding.X509, annotation.encoding()); + assertEquals("Server's public key for ECDH", annotation.description()); + assertTrue(annotation.critical()); + assertEquals(32, annotation.minLength()); + assertEquals(65, annotation.maxLength()); + assertEquals(-1, annotation.expectedLength()); + } + + @Test + public void testNewPurposeEnums() throws NoSuchFieldException { + Field versionField = TestClass.class.getDeclaredField("protocolVersion"); + ModifiableVariableProperty versionAnnotation = + versionField.getAnnotation(ModifiableVariableProperty.class); + + assertNotNull(versionAnnotation); + assertEquals(Purpose.VERSION, versionAnnotation.purpose()); + assertEquals(2, versionAnnotation.expectedLength()); + + Field randomField = TestClass.class.getDeclaredField("randomValue"); + ModifiableVariableProperty randomAnnotation = + randomField.getAnnotation(ModifiableVariableProperty.class); + + assertNotNull(randomAnnotation); + assertEquals(Purpose.RANDOM, randomAnnotation.purpose()); + assertEquals(32, randomAnnotation.expectedLength()); + + Field paddingField = TestClass.class.getDeclaredField("paddingLength"); + ModifiableVariableProperty paddingAnnotation = + paddingField.getAnnotation(ModifiableVariableProperty.class); + + assertNotNull(paddingAnnotation); + assertEquals(Purpose.PADDING, paddingAnnotation.purpose()); + assertEquals(0, paddingAnnotation.minLength()); + assertEquals(255, paddingAnnotation.maxLength()); + } + + @Test + public void testModifiableVariableAnalyzer() { + List annotatedFields = + ModifiableVariableAnalyzer.getAnnotatedFields(TestClass.class); + assertEquals(8, annotatedFields.size()); // 8 annotated fields in TestClass + + Map> byPurpose = + ModifiableVariableAnalyzer.groupFieldsByPurpose(TestClass.class); + assertTrue(byPurpose.containsKey(Purpose.LENGTH)); + assertTrue(byPurpose.containsKey(Purpose.KEY_MATERIAL)); + assertTrue(byPurpose.containsKey(Purpose.VERSION)); + assertTrue(byPurpose.containsKey(Purpose.RANDOM)); + assertTrue(byPurpose.containsKey(Purpose.PADDING)); + + List criticalFields = ModifiableVariableAnalyzer.getCriticalFields(TestClass.class); + assertEquals(1, criticalFields.size()); + assertEquals("enhancedProperty", criticalFields.get(0).getName()); + + Map> byLengthConstraints = + ModifiableVariableAnalyzer.groupFieldsByLengthConstraints(TestClass.class); + assertTrue(byLengthConstraints.containsKey("Fixed length (2 bytes)")); + assertTrue(byLengthConstraints.containsKey("Fixed length (32 bytes)")); + assertTrue(byLengthConstraints.containsKey("Variable length (1-4 bytes)")); + + List lengthFields = + ModifiableVariableAnalyzer.getFieldsByPurpose(TestClass.class, Purpose.LENGTH); + assertEquals(1, lengthFields.size()); + assertEquals("length", lengthFields.get(0).getName()); + + List x509Fields = + ModifiableVariableAnalyzer.getFieldsByEncoding(TestClass.class, Encoding.X509); + assertEquals(1, x509Fields.size()); + assertEquals("enhancedProperty", x509Fields.get(0).getName()); + + List withLengthConstraints = + ModifiableVariableAnalyzer.getFieldsWithLengthConstraints(TestClass.class); + assertEquals( + 6, withLengthConstraints.size()); // All except defaultProperty and unannotatedField + + List expectedLength32 = + ModifiableVariableAnalyzer.getFieldsByExpectedLength(TestClass.class, 32); + assertEquals(1, expectedLength32.size()); + assertEquals("randomValue", expectedLength32.get(0).getName()); + + List unannotated = + ModifiableVariableAnalyzer.getUnannotatedModifiableVariables(TestClass.class); + assertEquals(1, unannotated.size()); + assertEquals("unannotatedField", unannotated.get(0)); + } + + @Test + public void testAnalysisReport() { + String report = ModifiableVariableAnalyzer.generatePropertyAnalysisReport(TestClass.class); + assertNotNull(report); + assertTrue(report.contains("ModifiableVariableProperty Analysis Report")); + assertTrue(report.contains("TestClass")); + assertTrue( + report.contains( + "Total ModifiableVariable fields: 9")); // 8 annotated + 1 unannotated + assertTrue(report.contains("Annotated fields: 8")); + assertTrue(report.contains("Unannotated fields: 1")); + assertTrue(report.contains("Critical fields: enhancedProperty")); + assertTrue(report.contains("Fields by Purpose:")); + assertTrue(report.contains("Fields by Length Constraints:")); + assertTrue(report.contains("Fixed length (32 bytes): randomValue")); + assertTrue(report.contains("Variable length (1-4 bytes): length")); + assertTrue(report.contains("unannotatedField")); } } From 653f1bfaca8371b86f781845207f7844be24a484 Mon Sep 17 00:00:00 2001 From: Robert Merget Date: Wed, 11 Jun 2025 14:01:04 +0400 Subject: [PATCH 2/7] Address PR review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Simplify verbose JavaDoc descriptions - Update signature example with plausible length values (70-73 bytes) - Remove critical parameter from annotation and all related usage - Remove "or credential" from CERTIFICATE documentation - Clarify expectedLength as hint for typical length - Update example comments for clarity 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../ModifiableVariableProperty.java | 33 ++++++------------- .../util/ModifiableVariableAnalyzer.java | 22 ------------- .../ModifiableVariablePropertyTest.java | 7 ---- 3 files changed, 10 insertions(+), 52 deletions(-) diff --git a/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java b/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java index 84c69e38..4afbb8cf 100644 --- a/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java +++ b/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java @@ -17,18 +17,14 @@ * *

This annotation provides metadata about a modifiable variable field, including its semantic * type (what kind of data it represents) and format (how the data is encoded). This information is - * extensively used throughout the TLS-Attacker ecosystem for reflection-based analysis, - * serialization, testing scenarios, and attack implementations that need to understand the purpose - * and encoding of different protocol variables. + * used for reflection-based analysis, serialization, and testing scenarios. * - *

The annotation is retained at runtime and can only be applied to fields. It serves as the - * foundation for semantic classification of protocol message fields, enabling automated analysis - * and manipulation of protocol implementations. + *

The annotation is retained at runtime and can only be applied to fields. * *

Usage examples: * *

{@code
- * // Simple length field with constraints
+ * // Variable length field
  * @ModifiableVariableProperty(purpose = Purpose.LENGTH, minLength = 1, maxLength = 4)
  * private ModifiableInteger messageLength;
  *
@@ -36,9 +32,8 @@
  * @ModifiableVariableProperty(
  *     purpose = Purpose.SIGNATURE,
  *     encoding = Encoding.ASN1_DER,
- *     minLength = 64,
- *     maxLength = 256,
- *     critical = true)
+ *     minLength = 70,
+ *     maxLength = 73)
  * private ModifiableByteArray digitalSignature;
  *
  * // Fixed-length random value
@@ -49,8 +44,7 @@
  * @ModifiableVariableProperty(
  *     purpose = Purpose.PAYLOAD,
  *     encoding = Encoding.UTF8,
- *     maxLength = 1024,
- *     description = "Application data payload")
+ *     maxLength = 1024)
  * private ModifiableByteArray applicationData;
  * }
*/ @@ -79,7 +73,7 @@ enum Purpose { MAC, /** Variable representing cryptographic key material */ KEY_MATERIAL, - /** Variable representing a certificate or credential */ + /** Variable representing a certificate */ CERTIFICATE, /** Variable representing plaintext protocol data */ PLAINTEXT, @@ -173,8 +167,9 @@ enum Encoding { int maxLength() default -1; /** - * Specifies the expected or default length (in bytes) of the variable's value. Use -1 to - * indicate no expected length. + * Specifies the expected or typical length (in bytes) of the variable's value. This is a hint + * for the most common case, but the field may still accept other lengths within minLength and + * maxLength bounds. Use -1 to indicate no expected length. * * @return The expected length in bytes, or -1 if variable */ @@ -187,12 +182,4 @@ enum Encoding { * @return A description of the variable's purpose */ String description() default ""; - - /** - * Indicates whether this variable is critical for correct operation. Critical variables may - * require special handling during modification. - * - * @return true if the variable is critical, false otherwise - */ - boolean critical() default false; } diff --git a/src/main/java/de/rub/nds/modifiablevariable/util/ModifiableVariableAnalyzer.java b/src/main/java/de/rub/nds/modifiablevariable/util/ModifiableVariableAnalyzer.java index 4634b988..f13c24e8 100644 --- a/src/main/java/de/rub/nds/modifiablevariable/util/ModifiableVariableAnalyzer.java +++ b/src/main/java/de/rub/nds/modifiablevariable/util/ModifiableVariableAnalyzer.java @@ -302,18 +302,6 @@ public static Map> groupFieldsByLengthConstraints(Class c })); } - /** - * Finds all fields marked as critical. - * - * @param clazz The class to analyze - * @return A list of fields marked as critical - */ - public static List getCriticalFields(Class clazz) { - return getAnnotatedFields(clazz).stream() - .filter(field -> field.getAnnotation(ModifiableVariableProperty.class).critical()) - .collect(Collectors.toList()); - } - /** * Finds all fields of a specific semantic purpose. * @@ -475,16 +463,6 @@ public static String generatePropertyAnalysisReport(Class clazz) { }); } - List criticalFields = getCriticalFields(clazz); - if (!criticalFields.isEmpty()) { - report.append("\nCritical fields: "); - report.append( - criticalFields.stream() - .map(Field::getName) - .collect(Collectors.joining(", "))); - report.append("\n"); - } - Map> byLengthConstraints = groupFieldsByLengthConstraints(clazz); if (byLengthConstraints.size() > 1 || !byLengthConstraints.containsKey("Unconstrained length")) { diff --git a/src/test/java/de/rub/nds/modifiablevariable/ModifiableVariablePropertyTest.java b/src/test/java/de/rub/nds/modifiablevariable/ModifiableVariablePropertyTest.java index 62a1354c..1027178d 100644 --- a/src/test/java/de/rub/nds/modifiablevariable/ModifiableVariablePropertyTest.java +++ b/src/test/java/de/rub/nds/modifiablevariable/ModifiableVariablePropertyTest.java @@ -37,7 +37,6 @@ private static class TestClass { purpose = Purpose.KEY_MATERIAL, encoding = Encoding.X509, description = "Server's public key for ECDH", - critical = true, minLength = 32, maxLength = 65) private ModifiableInteger enhancedProperty; @@ -118,7 +117,6 @@ public void testEnhancedProperty() throws NoSuchFieldException { assertEquals(Purpose.KEY_MATERIAL, annotation.purpose()); assertEquals(Encoding.X509, annotation.encoding()); assertEquals("Server's public key for ECDH", annotation.description()); - assertTrue(annotation.critical()); assertEquals(32, annotation.minLength()); assertEquals(65, annotation.maxLength()); assertEquals(-1, annotation.expectedLength()); @@ -166,10 +164,6 @@ public void testModifiableVariableAnalyzer() { assertTrue(byPurpose.containsKey(Purpose.RANDOM)); assertTrue(byPurpose.containsKey(Purpose.PADDING)); - List criticalFields = ModifiableVariableAnalyzer.getCriticalFields(TestClass.class); - assertEquals(1, criticalFields.size()); - assertEquals("enhancedProperty", criticalFields.get(0).getName()); - Map> byLengthConstraints = ModifiableVariableAnalyzer.groupFieldsByLengthConstraints(TestClass.class); assertTrue(byLengthConstraints.containsKey("Fixed length (2 bytes)")); @@ -213,7 +207,6 @@ public void testAnalysisReport() { "Total ModifiableVariable fields: 9")); // 8 annotated + 1 unannotated assertTrue(report.contains("Annotated fields: 8")); assertTrue(report.contains("Unannotated fields: 1")); - assertTrue(report.contains("Critical fields: enhancedProperty")); assertTrue(report.contains("Fields by Purpose:")); assertTrue(report.contains("Fields by Length Constraints:")); assertTrue(report.contains("Fixed length (32 bytes): randomValue")); From 076d6cebcda4cc7425a980c8e4063435fa698b36 Mon Sep 17 00:00:00 2001 From: Robert Merget Date: Wed, 11 Jun 2025 14:04:28 +0400 Subject: [PATCH 3/7] Remove expectedLength from JavaDoc example Remove expectedLength usage from random value example in JavaDoc to address reviewer feedback about its semantics. --- .../nds/modifiablevariable/ModifiableVariableProperty.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java b/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java index 4afbb8cf..d4665dab 100644 --- a/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java +++ b/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java @@ -36,8 +36,8 @@ * maxLength = 73) * private ModifiableByteArray digitalSignature; * - * // Fixed-length random value - * @ModifiableVariableProperty(purpose = Purpose.RANDOM, expectedLength = 32) + * // Random value + * @ModifiableVariableProperty(purpose = Purpose.RANDOM) * private ModifiableByteArray nonce; * * // Variable-length payload with description From 98d874a1195d8b36396bdf4178eb8b51a5fbf858 Mon Sep 17 00:00:00 2001 From: Robert Merget Date: Wed, 11 Jun 2025 14:07:55 +0400 Subject: [PATCH 4/7] Remove VERSION purpose and use CONSTANT instead VERSION was too protocol-specific. Replace with existing CONSTANT purpose to maintain protocol-agnostic design. --- .../nds/modifiablevariable/ModifiableVariableProperty.java | 2 -- .../modifiablevariable/ModifiableVariablePropertyTest.java | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java b/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java index d4665dab..2ae0243f 100644 --- a/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java +++ b/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java @@ -79,8 +79,6 @@ enum Purpose { PLAINTEXT, /** Variable representing a random value, nonce, or salt */ RANDOM, - /** Variable representing a protocol version or format identifier */ - VERSION, /** Variable representing a session or connection identifier */ IDENTIFIER, /** Variable representing a timestamp or temporal value */ diff --git a/src/test/java/de/rub/nds/modifiablevariable/ModifiableVariablePropertyTest.java b/src/test/java/de/rub/nds/modifiablevariable/ModifiableVariablePropertyTest.java index 1027178d..00dd0a54 100644 --- a/src/test/java/de/rub/nds/modifiablevariable/ModifiableVariablePropertyTest.java +++ b/src/test/java/de/rub/nds/modifiablevariable/ModifiableVariablePropertyTest.java @@ -41,7 +41,7 @@ private static class TestClass { maxLength = 65) private ModifiableInteger enhancedProperty; - @ModifiableVariableProperty(purpose = Purpose.VERSION, expectedLength = 2) + @ModifiableVariableProperty(purpose = Purpose.CONSTANT, expectedLength = 2) private ModifiableInteger protocolVersion; @ModifiableVariableProperty(purpose = Purpose.RANDOM, expectedLength = 32) @@ -129,7 +129,7 @@ public void testNewPurposeEnums() throws NoSuchFieldException { versionField.getAnnotation(ModifiableVariableProperty.class); assertNotNull(versionAnnotation); - assertEquals(Purpose.VERSION, versionAnnotation.purpose()); + assertEquals(Purpose.CONSTANT, versionAnnotation.purpose()); assertEquals(2, versionAnnotation.expectedLength()); Field randomField = TestClass.class.getDeclaredField("randomValue"); @@ -160,7 +160,7 @@ public void testModifiableVariableAnalyzer() { ModifiableVariableAnalyzer.groupFieldsByPurpose(TestClass.class); assertTrue(byPurpose.containsKey(Purpose.LENGTH)); assertTrue(byPurpose.containsKey(Purpose.KEY_MATERIAL)); - assertTrue(byPurpose.containsKey(Purpose.VERSION)); + assertTrue(byPurpose.containsKey(Purpose.CONSTANT)); assertTrue(byPurpose.containsKey(Purpose.RANDOM)); assertTrue(byPurpose.containsKey(Purpose.PADDING)); From 37633b101e76510d726f78e1bc9c43689a9bcd7b Mon Sep 17 00:00:00 2001 From: Robert Merget Date: Wed, 11 Jun 2025 14:09:34 +0400 Subject: [PATCH 5/7] Remove protocol-specific purposes: EXTENSION, CONTROL, PAYLOAD Remove EXTENSION, CONTROL, and PAYLOAD purposes to make the annotation more protocol-agnostic. Updated JavaDoc example to use PLAINTEXT instead. --- .../modifiablevariable/ModifiableVariableProperty.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java b/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java index 2ae0243f..892bac1e 100644 --- a/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java +++ b/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java @@ -40,9 +40,9 @@ * @ModifiableVariableProperty(purpose = Purpose.RANDOM) * private ModifiableByteArray nonce; * - * // Variable-length payload with description + * // Variable-length data with description * @ModifiableVariableProperty( - * purpose = Purpose.PAYLOAD, + * purpose = Purpose.PLAINTEXT, * encoding = Encoding.UTF8, * maxLength = 1024) * private ModifiableByteArray applicationData; @@ -83,12 +83,6 @@ enum Purpose { IDENTIFIER, /** Variable representing a timestamp or temporal value */ TIMESTAMP, - /** Variable representing an extension or optional component */ - EXTENSION, - /** Variable that controls protocol behavior or flags */ - CONTROL, - /** Variable representing user or application data */ - PAYLOAD, /** Default purpose when no specific category applies */ NONE } From 3b1ad317f5f8b44fe994e82752967ec863999875 Mon Sep 17 00:00:00 2001 From: Robert Merget Date: Wed, 11 Jun 2025 14:12:05 +0400 Subject: [PATCH 6/7] Rename HEX encoding to HEX_STRING for clarity --- .../rub/nds/modifiablevariable/ModifiableVariableProperty.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java b/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java index 892bac1e..a7458021 100644 --- a/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java +++ b/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java @@ -107,7 +107,7 @@ enum Encoding { /** Base64 text encoding */ BASE64, /** Hexadecimal string representation */ - HEX, + HEX_STRING, /** Raw binary data */ BINARY, /** UTF-8 text encoding */ From 3bfbc446dd0cadbcf13b9a1194e44821ec6cecb2 Mon Sep 17 00:00:00 2001 From: Robert Merget Date: Wed, 11 Jun 2025 15:01:33 +0400 Subject: [PATCH 7/7] addressed review --- .../ModifiableVariableProperty.java | 42 ++--- .../util/ModifiableVariableAnalyzer.java | 156 ------------------ .../ModifiableVariablePropertyTest.java | 56 +------ 3 files changed, 20 insertions(+), 234 deletions(-) diff --git a/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java b/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java index a7458021..320a20d2 100644 --- a/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java +++ b/src/main/java/de/rub/nds/modifiablevariable/ModifiableVariableProperty.java @@ -84,7 +84,7 @@ enum Purpose { /** Variable representing a timestamp or temporal value */ TIMESTAMP, /** Default purpose when no specific category applies */ - NONE + UNSPECIFIED } /** @@ -112,20 +112,21 @@ enum Encoding { BINARY, /** UTF-8 text encoding */ UTF8, - /** Big-endian byte order */ - BIG_ENDIAN, - /** Little-endian byte order */ - LITTLE_ENDIAN, - /** Variable-length encoding (e.g., varint) */ - VARINT, - /** Length-prefixed format */ - LENGTH_PREFIXED, + /** Unsigned big-endian encoding */ + UNSIGNED_BIG_ENDIAN, + /** Unsigned little-endian encoding */ + UNSIGNED_LITTLE_ENDIAN, + /** Signed big-endian encoding */ + SIGNED_BIG_ENDIAN, + /** Signed little-endian encoding */ + SIGNED_LITTLE_ENDIAN, + /** JSON text format */ JSON, /** XML text format */ XML, /** Default encoding when not specified */ - NONE + UNSPECIFIED } /** @@ -133,14 +134,14 @@ enum Encoding { * * @return The purpose category of the variable */ - Purpose purpose() default Purpose.NONE; + Purpose purpose() default Purpose.UNSPECIFIED; /** * Specifies the encoding format of the annotated variable. * * @return The encoding format of the variable */ - Encoding encoding() default Encoding.NONE; + Encoding encoding() default Encoding.UNSPECIFIED; /** * Specifies the minimum length (in bytes) of the variable's value. Use -1 to indicate no @@ -157,21 +158,4 @@ enum Encoding { * @return The maximum length in bytes, or -1 if unconstrained */ int maxLength() default -1; - - /** - * Specifies the expected or typical length (in bytes) of the variable's value. This is a hint - * for the most common case, but the field may still accept other lengths within minLength and - * maxLength bounds. Use -1 to indicate no expected length. - * - * @return The expected length in bytes, or -1 if variable - */ - int expectedLength() default -1; - - /** - * Provides a human-readable description of the variable's purpose. This can be used for - * documentation generation or debugging. - * - * @return A description of the variable's purpose - */ - String description() default ""; } diff --git a/src/main/java/de/rub/nds/modifiablevariable/util/ModifiableVariableAnalyzer.java b/src/main/java/de/rub/nds/modifiablevariable/util/ModifiableVariableAnalyzer.java index f13c24e8..03d4f9ff 100644 --- a/src/main/java/de/rub/nds/modifiablevariable/util/ModifiableVariableAnalyzer.java +++ b/src/main/java/de/rub/nds/modifiablevariable/util/ModifiableVariableAnalyzer.java @@ -213,8 +213,6 @@ public static List getAllModifiableVariableHolders return result; } - // ===== ModifiableVariableProperty annotation analysis methods ===== - /** * Retrieves all fields in a class hierarchy that are annotated with ModifiableVariableProperty. * @@ -267,41 +265,6 @@ public static Map> groupFieldsByEncoding(Class clazz) { .encoding())); } - /** - * Groups annotated fields by their length constraints. - * - * @param clazz The class to analyze - * @return A map where keys describe length constraints and values are lists of fields - */ - public static Map> groupFieldsByLengthConstraints(Class clazz) { - return getAnnotatedFields(clazz).stream() - .collect( - Collectors.groupingBy( - field -> { - ModifiableVariableProperty annotation = - field.getAnnotation(ModifiableVariableProperty.class); - int minLen = annotation.minLength(); - int maxLen = annotation.maxLength(); - int expectedLen = annotation.expectedLength(); - - if (expectedLen > 0) { - return "Fixed length (" + expectedLen + " bytes)"; - } else if (minLen > 0 && maxLen > 0) { - return "Variable length (" - + minLen - + "-" - + maxLen - + " bytes)"; - } else if (minLen > 0) { - return "Minimum length (" + minLen + "+ bytes)"; - } else if (maxLen > 0) { - return "Maximum length (≤" + maxLen + " bytes)"; - } else { - return "Unconstrained length"; - } - })); - } - /** * Finds all fields of a specific semantic purpose. * @@ -334,42 +297,6 @@ public static List getFieldsByEncoding(Class clazz, Encoding encoding) .collect(Collectors.toList()); } - /** - * Finds all fields with length constraints. - * - * @param clazz The class to analyze - * @return A list of fields that have any length constraints defined - */ - public static List getFieldsWithLengthConstraints(Class clazz) { - return getAnnotatedFields(clazz).stream() - .filter( - field -> { - ModifiableVariableProperty annotation = - field.getAnnotation(ModifiableVariableProperty.class); - return annotation.minLength() > 0 - || annotation.maxLength() > 0 - || annotation.expectedLength() > 0; - }) - .collect(Collectors.toList()); - } - - /** - * Finds all fields with a specific expected length. - * - * @param clazz The class to analyze - * @param expectedLength The expected length to search for - * @return A list of fields with the specified expected length - */ - public static List getFieldsByExpectedLength(Class clazz, int expectedLength) { - return getAnnotatedFields(clazz).stream() - .filter( - field -> - field.getAnnotation(ModifiableVariableProperty.class) - .expectedLength() - == expectedLength) - .collect(Collectors.toList()); - } - /** * Checks if a field has a ModifiableVariableProperty annotation. * @@ -406,87 +333,4 @@ public static List getUnannotatedModifiableVariables(Class clazz) { .map(Field::getName) .collect(Collectors.toList()); } - - /** - * Generates a summary report of all annotated fields in a class. - * - * @param clazz The class to analyze - * @return A formatted string containing analysis results - */ - public static String generatePropertyAnalysisReport(Class clazz) { - StringBuilder report = new StringBuilder(); - report.append("ModifiableVariableProperty Analysis Report for ") - .append(clazz.getSimpleName()) - .append("\n"); - report.append("=".repeat(60)).append("\n\n"); - - List annotatedFields = getAnnotatedFields(clazz); - List allModifiableFields = - ReflectionHelper.getFieldsUpTo(clazz, null, ModifiableVariable.class); - - report.append("Total ModifiableVariable fields: ") - .append(allModifiableFields.size()) - .append("\n"); - report.append("Annotated fields: ").append(annotatedFields.size()).append("\n"); - report.append("Unannotated fields: ") - .append(allModifiableFields.size() - annotatedFields.size()) - .append("\n\n"); - - if (!annotatedFields.isEmpty()) { - report.append("Fields by Purpose:\n"); - Map> byPurpose = groupFieldsByPurpose(clazz); - byPurpose.entrySet().stream() - .sorted(Map.Entry.>comparingByKey()) - .forEach( - entry -> { - report.append(" ").append(entry.getKey()).append(": "); - report.append( - entry.getValue().stream() - .map(Field::getName) - .collect(Collectors.joining(", "))); - report.append("\n"); - }); - - Map> byEncoding = groupFieldsByEncoding(clazz); - if (byEncoding.size() > 1 || !byEncoding.containsKey(Encoding.NONE)) { - report.append("\nFields by Encoding:\n"); - byEncoding.entrySet().stream() - .sorted(Map.Entry.comparingByKey()) - .forEach( - entry -> { - report.append(" ").append(entry.getKey()).append(": "); - report.append( - entry.getValue().stream() - .map(Field::getName) - .collect(Collectors.joining(", "))); - report.append("\n"); - }); - } - - Map> byLengthConstraints = groupFieldsByLengthConstraints(clazz); - if (byLengthConstraints.size() > 1 - || !byLengthConstraints.containsKey("Unconstrained length")) { - report.append("\nFields by Length Constraints:\n"); - byLengthConstraints.entrySet().stream() - .sorted(Map.Entry.comparingByKey()) - .forEach( - entry -> { - report.append(" ").append(entry.getKey()).append(": "); - report.append( - entry.getValue().stream() - .map(Field::getName) - .collect(Collectors.joining(", "))); - report.append("\n"); - }); - } - } - - List unannotated = getUnannotatedModifiableVariables(clazz); - if (!unannotated.isEmpty()) { - report.append("\nUnannotated ModifiableVariable fields:\n"); - unannotated.forEach(name -> report.append(" - ").append(name).append("\n")); - } - - return report.toString(); - } } diff --git a/src/test/java/de/rub/nds/modifiablevariable/ModifiableVariablePropertyTest.java b/src/test/java/de/rub/nds/modifiablevariable/ModifiableVariablePropertyTest.java index 00dd0a54..cedc2a3a 100644 --- a/src/test/java/de/rub/nds/modifiablevariable/ModifiableVariablePropertyTest.java +++ b/src/test/java/de/rub/nds/modifiablevariable/ModifiableVariablePropertyTest.java @@ -25,7 +25,7 @@ private static class TestClass { @ModifiableVariableProperty(purpose = Purpose.LENGTH, minLength = 1, maxLength = 4) private ModifiableInteger length; - @ModifiableVariableProperty(purpose = Purpose.COUNT, expectedLength = 2) + @ModifiableVariableProperty(purpose = Purpose.COUNT) private ModifiableInteger count; @ModifiableVariableProperty(purpose = Purpose.SIGNATURE, encoding = Encoding.ASN1_DER) @@ -36,15 +36,14 @@ private static class TestClass { @ModifiableVariableProperty( purpose = Purpose.KEY_MATERIAL, encoding = Encoding.X509, - description = "Server's public key for ECDH", minLength = 32, maxLength = 65) private ModifiableInteger enhancedProperty; - @ModifiableVariableProperty(purpose = Purpose.CONSTANT, expectedLength = 2) + @ModifiableVariableProperty(purpose = Purpose.CONSTANT) private ModifiableInteger protocolVersion; - @ModifiableVariableProperty(purpose = Purpose.RANDOM, expectedLength = 32) + @ModifiableVariableProperty(purpose = Purpose.RANDOM) private ModifiableInteger randomValue; @ModifiableVariableProperty(purpose = Purpose.PADDING, minLength = 0, maxLength = 255) @@ -62,10 +61,9 @@ public void testLengthProperty() throws NoSuchFieldException { assertNotNull(annotation); assertEquals(Purpose.LENGTH, annotation.purpose()); - assertEquals(Encoding.NONE, annotation.encoding()); + assertEquals(Encoding.UNSPECIFIED, annotation.encoding()); assertEquals(1, annotation.minLength()); assertEquals(4, annotation.maxLength()); - assertEquals(-1, annotation.expectedLength()); } @Test @@ -76,10 +74,9 @@ public void testCountProperty() throws NoSuchFieldException { assertNotNull(annotation); assertEquals(Purpose.COUNT, annotation.purpose()); - assertEquals(Encoding.NONE, annotation.encoding()); + assertEquals(Encoding.UNSPECIFIED, annotation.encoding()); assertEquals(-1, annotation.minLength()); assertEquals(-1, annotation.maxLength()); - assertEquals(2, annotation.expectedLength()); } @Test @@ -100,11 +97,10 @@ public void testDefaultProperty() throws NoSuchFieldException { defaultField.getAnnotation(ModifiableVariableProperty.class); assertNotNull(annotation); - assertEquals(Purpose.NONE, annotation.purpose()); - assertEquals(Encoding.NONE, annotation.encoding()); + assertEquals(Purpose.UNSPECIFIED, annotation.purpose()); + assertEquals(Encoding.UNSPECIFIED, annotation.encoding()); assertEquals(-1, annotation.minLength()); assertEquals(-1, annotation.maxLength()); - assertEquals(-1, annotation.expectedLength()); } @Test @@ -116,10 +112,8 @@ public void testEnhancedProperty() throws NoSuchFieldException { assertNotNull(annotation); assertEquals(Purpose.KEY_MATERIAL, annotation.purpose()); assertEquals(Encoding.X509, annotation.encoding()); - assertEquals("Server's public key for ECDH", annotation.description()); assertEquals(32, annotation.minLength()); assertEquals(65, annotation.maxLength()); - assertEquals(-1, annotation.expectedLength()); } @Test @@ -130,7 +124,6 @@ public void testNewPurposeEnums() throws NoSuchFieldException { assertNotNull(versionAnnotation); assertEquals(Purpose.CONSTANT, versionAnnotation.purpose()); - assertEquals(2, versionAnnotation.expectedLength()); Field randomField = TestClass.class.getDeclaredField("randomValue"); ModifiableVariableProperty randomAnnotation = @@ -138,7 +131,6 @@ public void testNewPurposeEnums() throws NoSuchFieldException { assertNotNull(randomAnnotation); assertEquals(Purpose.RANDOM, randomAnnotation.purpose()); - assertEquals(32, randomAnnotation.expectedLength()); Field paddingField = TestClass.class.getDeclaredField("paddingLength"); ModifiableVariableProperty paddingAnnotation = @@ -164,12 +156,6 @@ public void testModifiableVariableAnalyzer() { assertTrue(byPurpose.containsKey(Purpose.RANDOM)); assertTrue(byPurpose.containsKey(Purpose.PADDING)); - Map> byLengthConstraints = - ModifiableVariableAnalyzer.groupFieldsByLengthConstraints(TestClass.class); - assertTrue(byLengthConstraints.containsKey("Fixed length (2 bytes)")); - assertTrue(byLengthConstraints.containsKey("Fixed length (32 bytes)")); - assertTrue(byLengthConstraints.containsKey("Variable length (1-4 bytes)")); - List lengthFields = ModifiableVariableAnalyzer.getFieldsByPurpose(TestClass.class, Purpose.LENGTH); assertEquals(1, lengthFields.size()); @@ -180,37 +166,9 @@ public void testModifiableVariableAnalyzer() { assertEquals(1, x509Fields.size()); assertEquals("enhancedProperty", x509Fields.get(0).getName()); - List withLengthConstraints = - ModifiableVariableAnalyzer.getFieldsWithLengthConstraints(TestClass.class); - assertEquals( - 6, withLengthConstraints.size()); // All except defaultProperty and unannotatedField - - List expectedLength32 = - ModifiableVariableAnalyzer.getFieldsByExpectedLength(TestClass.class, 32); - assertEquals(1, expectedLength32.size()); - assertEquals("randomValue", expectedLength32.get(0).getName()); - List unannotated = ModifiableVariableAnalyzer.getUnannotatedModifiableVariables(TestClass.class); assertEquals(1, unannotated.size()); assertEquals("unannotatedField", unannotated.get(0)); } - - @Test - public void testAnalysisReport() { - String report = ModifiableVariableAnalyzer.generatePropertyAnalysisReport(TestClass.class); - assertNotNull(report); - assertTrue(report.contains("ModifiableVariableProperty Analysis Report")); - assertTrue(report.contains("TestClass")); - assertTrue( - report.contains( - "Total ModifiableVariable fields: 9")); // 8 annotated + 1 unannotated - assertTrue(report.contains("Annotated fields: 8")); - assertTrue(report.contains("Unannotated fields: 1")); - assertTrue(report.contains("Fields by Purpose:")); - assertTrue(report.contains("Fields by Length Constraints:")); - assertTrue(report.contains("Fixed length (32 bytes): randomValue")); - assertTrue(report.contains("Variable length (1-4 bytes): length")); - assertTrue(report.contains("unannotatedField")); - } }