diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/CSharpDetectionRules.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/CSharpDetectionRules.java index 92576f65a..b7d1b57ae 100755 --- a/csharp/src/main/java/com/ibm/plugin/rules/detection/CSharpDetectionRules.java +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/CSharpDetectionRules.java @@ -22,16 +22,27 @@ import com.ibm.engine.language.csharp.tree.CSharpTree; import com.ibm.engine.rule.IDetectionRule; import com.ibm.plugin.rules.detection.dotnet.DotNetAES; +import com.ibm.plugin.rules.detection.dotnet.DotNetAlgorithmFactory; import com.ibm.plugin.rules.detection.dotnet.DotNetDES; import com.ibm.plugin.rules.detection.dotnet.DotNetDSA; import com.ibm.plugin.rules.detection.dotnet.DotNetECDiffieHellman; import com.ibm.plugin.rules.detection.dotnet.DotNetECDsa; import com.ibm.plugin.rules.detection.dotnet.DotNetHMAC; +import com.ibm.plugin.rules.detection.dotnet.DotNetKMAC; +import com.ibm.plugin.rules.detection.dotnet.DotNetKeyDerivation; +import com.ibm.plugin.rules.detection.dotnet.DotNetLegacyFormatters; +import com.ibm.plugin.rules.detection.dotnet.DotNetMLDsa; +import com.ibm.plugin.rules.detection.dotnet.DotNetMLKem; +import com.ibm.plugin.rules.detection.dotnet.DotNetProtectedData; import com.ibm.plugin.rules.detection.dotnet.DotNetRC2; import com.ibm.plugin.rules.detection.dotnet.DotNetRSA; +import com.ibm.plugin.rules.detection.dotnet.DotNetRandomNumberGenerator; import com.ibm.plugin.rules.detection.dotnet.DotNetRfc2898DeriveBytes; import com.ibm.plugin.rules.detection.dotnet.DotNetSHA; +import com.ibm.plugin.rules.detection.dotnet.DotNetSHA3; +import com.ibm.plugin.rules.detection.dotnet.DotNetSlhDsa; import com.ibm.plugin.rules.detection.dotnet.DotNetTripleDES; +import com.ibm.plugin.rules.detection.dotnet.DotNetX25519DiffieHellman; import java.util.List; import java.util.stream.Stream; import javax.annotation.Nonnull; @@ -53,10 +64,21 @@ public static List> rules() { DotNetRSA.rules().stream(), DotNetECDsa.rules().stream(), DotNetECDiffieHellman.rules().stream(), + DotNetX25519DiffieHellman.rules().stream(), + DotNetMLKem.rules().stream(), + DotNetMLDsa.rules().stream(), + DotNetSlhDsa.rules().stream(), DotNetDSA.rules().stream(), + DotNetLegacyFormatters.rules().stream(), DotNetSHA.rules().stream(), + DotNetSHA3.rules().stream(), DotNetHMAC.rules().stream(), - DotNetRfc2898DeriveBytes.rules().stream()) + DotNetKMAC.rules().stream(), + DotNetRfc2898DeriveBytes.rules().stream(), + DotNetKeyDerivation.rules().stream(), + DotNetRandomNumberGenerator.rules().stream(), + DotNetProtectedData.rules().stream(), + DotNetAlgorithmFactory.rules().stream()) .flatMap(i -> i) .toList(); } diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetAES.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetAES.java index f750ea4e0..e3a17fe55 100755 --- a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetAES.java +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetAES.java @@ -21,8 +21,11 @@ import com.ibm.engine.detection.MethodMatcher; import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.CipherAction; import com.ibm.engine.model.Size; import com.ibm.engine.model.context.CipherContext; +import com.ibm.engine.model.factory.BlockSizeFactory; +import com.ibm.engine.model.factory.CipherActionFactory; import com.ibm.engine.model.factory.KeySizeFactory; import com.ibm.engine.model.factory.ModeFactory; import com.ibm.engine.model.factory.PaddingFactory; @@ -30,19 +33,27 @@ import com.ibm.engine.rule.IDetectionRule; import com.ibm.engine.rule.builder.DetectionRuleBuilder; import java.util.List; +import java.util.stream.Stream; import javax.annotation.Nonnull; /** - * Detection rules for AES usage in System.Security.Cryptography. + * Detection rules for the AES family in System.Security.Cryptography. * - *

Detects: + *

Classes covered: * *

+ * + *

Architecture: all methods inherited from {@code SymmetricAlgorithm} (EncryptCbc, DecryptCbc, + * CreateEncryptor, property setters, etc.) are expressed as depending rules attached to + * each primary creation rule. The detection engine tracks the variable and fires these rules on + * every matching method call, regardless of the concrete Aes subclass. */ @SuppressWarnings("java:S1192") public final class DotNetAES { @@ -51,6 +62,10 @@ private DotNetAES() { // nothing } + // ========================================================================= + // Property setter rules (synthetic set_X method invocations) + // ========================================================================= + // aes.Mode = CipherMode.CBC → synthetic set_Mode(CipherMode.CBC) private static final IDetectionRule AES_SET_MODE = new DetectionRuleBuilder() @@ -87,10 +102,613 @@ private DotNetAES() { .inBundle(() -> "DotNet") .withoutDependingDetectionRules(); + // aes.FeedbackSize = 128 → synthetic set_FeedbackSize(128) + private static final IDetectionRule AES_SET_FEEDBACK_SIZE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_FeedbackSize") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new BlockSizeFactory<>(Size.UnitType.BIT)) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + private static final List> PROPERTY_SETTER_RULES = - List.of(AES_SET_MODE, AES_SET_KEY_SIZE, AES_SET_PADDING); + List.of(AES_SET_MODE, AES_SET_KEY_SIZE, AES_SET_PADDING, AES_SET_FEEDBACK_SIZE); + + // ========================================================================= + // CreateEncryptor / CreateDecryptor rules + // ========================================================================= + + // aes.CreateEncryptor() + private static final IDetectionRule AES_CREATE_ENCRYPTOR = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateEncryptor") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.ENCRYPT)) + .withoutParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // aes.CreateEncryptor(byte[] key, byte[] iv) + private static final IDetectionRule AES_CREATE_ENCRYPTOR_WITH_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateEncryptor") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.ENCRYPT)) + .withMethodParameter(MethodMatcher.ANY) // key bytes + .withMethodParameter(MethodMatcher.ANY) // iv bytes + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // aes.CreateDecryptor() + private static final IDetectionRule AES_CREATE_DECRYPTOR = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateDecryptor") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.DECRYPT)) + .withoutParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // aes.CreateDecryptor(byte[] key, byte[] iv) + private static final IDetectionRule AES_CREATE_DECRYPTOR_WITH_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateDecryptor") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.DECRYPT)) + .withMethodParameter(MethodMatcher.ANY) // key bytes + .withMethodParameter(MethodMatcher.ANY) // iv bytes + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // EncryptCbc / DecryptCbc rules + // Mode is constant "CBC" (from method name); padding is detected from last param. + // Two overloads: 3-param and 4-param (with output buffer). + // ========================================================================= + + // EncryptCbc(plaintext, iv, padding) + private static final IDetectionRule AES_ENCRYPT_CBC_3 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptCbc") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // EncryptCbc(plaintext, iv, destination, padding) [output-buffer overload] + private static final IDetectionRule AES_ENCRYPT_CBC_4 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptCbc") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptCbc(ciphertext, iv, padding) + private static final IDetectionRule AES_DECRYPT_CBC_3 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptCbc") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptCbc(ciphertext, iv, destination, padding) + private static final IDetectionRule AES_DECRYPT_CBC_4 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptCbc") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // EncryptEcb / DecryptEcb rules + // Mode is constant "ECB" (from method name); no IV parameter. + // Two overloads: 2-param and 3-param (with output buffer). + // ========================================================================= + + // EncryptEcb(plaintext, padding) + private static final IDetectionRule AES_ENCRYPT_ECB_2 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptEcb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // EncryptEcb(plaintext, destination, padding) + private static final IDetectionRule AES_ENCRYPT_ECB_3 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptEcb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptEcb(ciphertext, padding) + private static final IDetectionRule AES_DECRYPT_ECB_2 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptEcb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptEcb(ciphertext, destination, padding) + private static final IDetectionRule AES_DECRYPT_ECB_3 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptEcb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // EncryptCfb / DecryptCfb rules + // Mode is constant "CFB"; padding detected from 3rd param; feedbackSize ignored. + // Two overloads: 4-param and 5-param (with output buffer). + // ========================================================================= + + // EncryptCfb(plaintext, iv, padding, feedbackSize) + private static final IDetectionRule AES_ENCRYPT_CFB_4 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptCfb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize (int) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // EncryptCfb(plaintext, iv, destination, padding, feedbackSize) + private static final IDetectionRule AES_ENCRYPT_CFB_5 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptCfb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize (int) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptCfb(ciphertext, iv, padding, feedbackSize) + private static final IDetectionRule AES_DECRYPT_CFB_4 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptCfb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize (int) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptCfb(ciphertext, iv, destination, padding, feedbackSize) + private static final IDetectionRule AES_DECRYPT_CFB_5 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptCfb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize (int) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // TryEncrypt* / TryDecrypt* rules + // Signatures: + // TryEncryptCbc(plaintext, iv, destination, out bytesWritten, padding) — 5 params + // TryDecryptCbc(ciphertext, iv, destination, out bytesWritten, padding) — 5 params + // TryEncryptEcb(plaintext, destination, padding, out bytesWritten) — 4 params + // TryDecryptEcb(ciphertext, destination, padding, out bytesWritten) — 4 params + // TryEncryptCfb(plaintext, iv, destination, out bytesWritten, padding, fs) — 6 params + // TryDecryptCfb(ciphertext, iv, destination, out bytesWritten, padding, fs) — 6 params + // ========================================================================= + + // TryEncryptCbc(plaintext, iv, destination, out bytesWritten, padding) + private static final IDetectionRule AES_TRY_ENCRYPT_CBC = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryEncryptCbc") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); - // Aes.Create() — abstract factory + // TryDecryptCbc(ciphertext, iv, destination, out bytesWritten, padding) + private static final IDetectionRule AES_TRY_DECRYPT_CBC = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryDecryptCbc") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryEncryptEcb(plaintext, destination, padding, out bytesWritten) + private static final IDetectionRule AES_TRY_ENCRYPT_ECB = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryEncryptEcb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryDecryptEcb(ciphertext, destination, padding, out bytesWritten) + private static final IDetectionRule AES_TRY_DECRYPT_ECB = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryDecryptEcb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryEncryptCfb(plaintext, iv, destination, out bytesWritten, padding, feedbackSize) + private static final IDetectionRule AES_TRY_ENCRYPT_CFB = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryEncryptCfb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryDecryptCfb(ciphertext, iv, destination, out bytesWritten, padding, feedbackSize) + private static final IDetectionRule AES_TRY_DECRYPT_CFB = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryDecryptCfb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // EncryptKeyWrapPadded / DecryptKeyWrapPadded / TryDecryptKeyWrapPadded rules + // RFC 5649 AES Key Wrap with Padding — declared on Aes (not SymmetricAlgorithm), + // introduced in .NET 10.0. Unlike EncryptCbc/EncryptEcb/EncryptCfb, this algorithm has + // no CipherMode/PaddingMode concept, so findings are generic Encrypt/Decrypt (same + // modeling as AesGcm/AesCcm below) rather than Mode/Padding-tagged. + // + // Verified overloads (Microsoft Learn, Aes class, net-10.0/net-11.0): + // EncryptKeyWrapPadded(byte[] plaintext) — 1 param + // EncryptKeyWrapPadded(ReadOnlySpan plaintext) — 1 param + // (same arity as above; the engine cannot distinguish by type, so one rule + // covers both overloads) + // EncryptKeyWrapPadded(ReadOnlySpan plaintext, Span destination) — 2 params + // DecryptKeyWrapPadded(byte[] ciphertext) — 1 param + // DecryptKeyWrapPadded(ReadOnlySpan ciphertext) — 1 param + // DecryptKeyWrapPadded(ReadOnlySpan ciphertext, Span destination) — 2 params + // TryDecryptKeyWrapPadded(ReadOnlySpan ciphertext, Span destination, + // out int bytesWritten) — 3 params + // There is no TryEncryptKeyWrapPadded overload (confirmed 404 on Microsoft Learn). + // ========================================================================= + + // aes.EncryptKeyWrapPadded(plaintext) + private static final IDetectionRule AES_ENCRYPT_KEY_WRAP_PADDED_1 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptKeyWrapPadded") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.ENCRYPT)) + .withMethodParameter(MethodMatcher.ANY) // plaintext + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // aes.EncryptKeyWrapPadded(plaintext, destination) + private static final IDetectionRule AES_ENCRYPT_KEY_WRAP_PADDED_2 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptKeyWrapPadded") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.ENCRYPT)) + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // aes.DecryptKeyWrapPadded(ciphertext) + private static final IDetectionRule AES_DECRYPT_KEY_WRAP_PADDED_1 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptKeyWrapPadded") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.DECRYPT)) + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // aes.DecryptKeyWrapPadded(ciphertext, destination) + private static final IDetectionRule AES_DECRYPT_KEY_WRAP_PADDED_2 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptKeyWrapPadded") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.DECRYPT)) + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // aes.TryDecryptKeyWrapPadded(ciphertext, destination, out bytesWritten) + private static final IDetectionRule AES_TRY_DECRYPT_KEY_WRAP_PADDED = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryDecryptKeyWrapPadded") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.DECRYPT)) + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // Key / IV generation rules + // ========================================================================= + + // aes.GenerateKey() — generates a new random key (size determined by KeySize property) + private static final IDetectionRule AES_GENERATE_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("GenerateKey") + .shouldBeDetectedAs(new ValueActionFactory<>("GenerateKey")) + .withoutParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // aes.GenerateIV() — generates a new random initialization vector + private static final IDetectionRule AES_GENERATE_IV = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("GenerateIV") + .shouldBeDetectedAs(new ValueActionFactory<>("GenerateIV")) + .withoutParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // Aggregated depending-rule lists + // ========================================================================= + + /** + * All cipher operation rules that fire on a tracked Aes-family variable. Includes + * CreateEncryptor/CreateDecryptor, direct mode-specific Encrypt/Decrypt methods, Try* variants, + * and key/IV generation. + */ + private static final List> CIPHER_OP_RULES = + List.of( + AES_CREATE_ENCRYPTOR, + AES_CREATE_ENCRYPTOR_WITH_KEY, + AES_CREATE_DECRYPTOR, + AES_CREATE_DECRYPTOR_WITH_KEY, + AES_ENCRYPT_CBC_3, + AES_ENCRYPT_CBC_4, + AES_DECRYPT_CBC_3, + AES_DECRYPT_CBC_4, + AES_ENCRYPT_ECB_2, + AES_ENCRYPT_ECB_3, + AES_DECRYPT_ECB_2, + AES_DECRYPT_ECB_3, + AES_ENCRYPT_CFB_4, + AES_ENCRYPT_CFB_5, + AES_DECRYPT_CFB_4, + AES_DECRYPT_CFB_5, + AES_TRY_ENCRYPT_CBC, + AES_TRY_DECRYPT_CBC, + AES_TRY_ENCRYPT_ECB, + AES_TRY_DECRYPT_ECB, + AES_TRY_ENCRYPT_CFB, + AES_TRY_DECRYPT_CFB, + AES_ENCRYPT_KEY_WRAP_PADDED_1, + AES_ENCRYPT_KEY_WRAP_PADDED_2, + AES_DECRYPT_KEY_WRAP_PADDED_1, + AES_DECRYPT_KEY_WRAP_PADDED_2, + AES_TRY_DECRYPT_KEY_WRAP_PADDED, + AES_GENERATE_KEY, + AES_GENERATE_IV); + + /** Full set of depending rules for all Aes-derived classes. */ + private static final List> AES_DEPENDING_RULES = + Stream.concat(PROPERTY_SETTER_RULES.stream(), CIPHER_OP_RULES.stream()).toList(); + + // ========================================================================= + // AesGcm / AesCcm AEAD operation rules + // ========================================================================= + + // aesGcm.Encrypt(nonce, plaintext, ciphertext, tag [, aad]) + private static final IDetectionRule AES_GCM_ENCRYPT_OP = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("Encrypt") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.ENCRYPT)) + .withAnyParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // aesGcm.Decrypt(nonce, ciphertext, tag, plaintext [, aad]) + private static final IDetectionRule AES_GCM_DECRYPT_OP = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("Decrypt") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.DECRYPT)) + .withAnyParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + private static final List> GCM_OP_RULES = + List.of(AES_GCM_ENCRYPT_OP, AES_GCM_DECRYPT_OP); + + // aesCcm.Encrypt(nonce, plaintext, ciphertext, tag [, aad]) + private static final IDetectionRule AES_CCM_ENCRYPT_OP = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("Encrypt") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.ENCRYPT)) + .withAnyParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // aesCcm.Decrypt(nonce, ciphertext, tag, plaintext [, aad]) + private static final IDetectionRule AES_CCM_DECRYPT_OP = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("Decrypt") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.DECRYPT)) + .withAnyParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + private static final List> CCM_OP_RULES = + List.of(AES_CCM_ENCRYPT_OP, AES_CCM_DECRYPT_OP); + + // ========================================================================= + // Primary creation rules + // ========================================================================= + + // Aes.Create() — abstract factory, no parameters private static final IDetectionRule AES_CREATE = new DetectionRuleBuilder() .createDetectionRule() @@ -100,9 +718,21 @@ private DotNetAES() { .withoutParameters() .buildForContext(new CipherContext()) .inBundle(() -> "DotNet") - .withDependingDetectionRules(PROPERTY_SETTER_RULES); + .withDependingDetectionRules(AES_DEPENDING_RULES); + + // Aes.Create("AES") — named factory (obsolete in .NET 7, still detectable) + private static final IDetectionRule AES_CREATE_NAMED = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("Aes") + .forMethods("Create") + .shouldBeDetectedAs(new ValueActionFactory<>("AES")) + .withMethodParameter(MethodMatcher.ANY) // algorithm name string + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(AES_DEPENDING_RULES); - // new AesManaged() — legacy concrete class + // new AesManaged() — pure-managed implementation private static final IDetectionRule AES_MANAGED = new DetectionRuleBuilder() .createDetectionRule() @@ -112,22 +742,36 @@ private DotNetAES() { .withoutParameters() .buildForContext(new CipherContext()) .inBundle(() -> "DotNet") - .withDependingDetectionRules(PROPERTY_SETTER_RULES); + .withDependingDetectionRules(AES_DEPENDING_RULES); - // new AesCng() — CNG-backed implementation - private static final IDetectionRule AES_CNG = + // new AesCng() / new AesCng("keyName") / new AesCng("keyName", provider) / ... + // Matches all AesCng constructors (ephemeral 0-param and persisted 1–3 params). + // Uses withAnyParameters() to avoid double-detection that would occur if a separate + // withoutParameters() rule were added alongside this one. + private static final IDetectionRule AES_CNG_NAMED = new DetectionRuleBuilder() .createDetectionRule() .forObjectTypes("AesCng") .forMethods("") .shouldBeDetectedAs(new ValueActionFactory<>("AES")) + .withAnyParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(AES_DEPENDING_RULES); + + // new AesCryptoServiceProvider() — legacy CAPI implementation + private static final IDetectionRule AES_CSP = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("AesCryptoServiceProvider") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("AES")) .withoutParameters() .buildForContext(new CipherContext()) .inBundle(() -> "DotNet") - .withDependingDetectionRules(PROPERTY_SETTER_RULES); + .withDependingDetectionRules(AES_DEPENDING_RULES); - // new AesGcm(key) — GCM authenticated encryption - // TODO: capture key parameter (byte[] key) to extract key length as a known gap + // new AesGcm(key) — GCM authenticated encryption (byte[] or ReadOnlySpan, 1 param) private static final IDetectionRule AES_GCM = new DetectionRuleBuilder() .createDetectionRule() @@ -137,10 +781,9 @@ private DotNetAES() { .withAnyParameters() .buildForContext(new CipherContext()) .inBundle(() -> "DotNet") - .withDependingDetectionRules(List.of()); + .withDependingDetectionRules(GCM_OP_RULES); - // new AesCcm(key) — CCM authenticated encryption - // TODO: capture key parameter (byte[] key) to extract key length as a known gap + // new AesCcm(key) — CCM authenticated encryption (byte[] or ReadOnlySpan, 1 param) private static final IDetectionRule AES_CCM = new DetectionRuleBuilder() .createDetectionRule() @@ -150,10 +793,17 @@ private DotNetAES() { .withAnyParameters() .buildForContext(new CipherContext()) .inBundle(() -> "DotNet") - .withDependingDetectionRules(List.of()); + .withDependingDetectionRules(CCM_OP_RULES); @Nonnull public static List> rules() { - return List.of(AES_CREATE, AES_MANAGED, AES_CNG, AES_GCM, AES_CCM); + return List.of( + AES_CREATE, + AES_CREATE_NAMED, + AES_MANAGED, + AES_CNG_NAMED, + AES_CSP, + AES_GCM, + AES_CCM); } } diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetAlgorithmFactory.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetAlgorithmFactory.java new file mode 100644 index 000000000..0c8d5b99c --- /dev/null +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetAlgorithmFactory.java @@ -0,0 +1,721 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import com.ibm.engine.detection.MethodMatcher; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.CipherAction; +import com.ibm.engine.model.Size; +import com.ibm.engine.model.context.CipherContext; +import com.ibm.engine.model.context.DigestContext; +import com.ibm.engine.model.context.KeyContext; +import com.ibm.engine.model.context.MacContext; +import com.ibm.engine.model.factory.AlgorithmFactory; +import com.ibm.engine.model.factory.BlockSizeFactory; +import com.ibm.engine.model.factory.CipherActionFactory; +import com.ibm.engine.model.factory.KeySizeFactory; +import com.ibm.engine.model.factory.ModeFactory; +import com.ibm.engine.model.factory.PaddingFactory; +import com.ibm.engine.model.factory.ValueActionFactory; +import com.ibm.engine.rule.IDetectionRule; +import com.ibm.engine.rule.builder.DetectionRuleBuilder; +import java.util.List; +import java.util.stream.Stream; +import javax.annotation.Nonnull; + +/** + * Detection rules for the generic, string-based factory methods declared directly on the + * abstract base classes of {@code System.Security.Cryptography} — as opposed to the + * per-algorithm {@code Create()}/{@code Create(string)} overloads already covered on concrete + * algorithm classes ({@code RC2.Create(string)} in {@link DotNetRC2}, {@code + * RandomNumberGenerator.Create(string)} in {@link DotNetRandomNumberGenerator}, etc.), where the + * class name itself already fixes the algorithm identity and the string parameter only selects an + * implementation/provider. + * + *

Here, the base class carries no algorithm identity of its own — the algorithm is + * chosen entirely at runtime by the string argument: + * + *

+ * + *

All five overloads are marked {@code Obsolete} (diagnostic {@code SYSLIB0045}) starting with + * .NET 7, in favor of the parameterless, strongly-typed {@code Create()} factory on each concrete + * algorithm type. They remain valid, detectable legacy source across every earlier and current + * .NET/.NET Framework version — the same "obsolete but in scope" precedent already established for + * {@code PasswordDeriveBytes} (see {@link DotNetKeyDerivation}) and {@code + * RandomNumberGenerator.Create(string)}/{@code RNGCryptoServiceProvider} (see {@link + * DotNetRandomNumberGenerator}). + * + *

Architecture — reusing the engine's existing string-value-to-algorithm resolution mechanism + * ({@code AlgorithmFactory}): {@code AlgorithmFactory} ({@code + * com.ibm.engine.model.factory.AlgorithmFactory}) is a generic, language-agnostic engine class + * (parametrized over {@code }, operating purely on {@code ResolvedValue}) already used + * identically by the Java module for the structurally identical problem — a call on an abstract + * base class whose method resolves an algorithm from a runtime string, e.g. {@code + * MessageDigest.getInstance(String)} ({@code JcaMessageDigestGetInstance}) or {@code + * Cipher.getInstance(String)} ({@code JcaCipherGetInstance}). No engine change was required: the + * same {@code .withMethodParameter(type).shouldBeDetectedAs(new AlgorithmFactory<>())} idiom used + * there is reused here verbatim. + * + *

Two things confirm this works for C# without any engine modification: + * + *

+ * + *

Depending rules — deliberately asymmetric across the five factories: + * + *

+ * + *

String tables — verified against the official API reference (learn.microsoft.com), not + * guessed: + * + *

+ */ +public final class DotNetAlgorithmFactory { + + private DotNetAlgorithmFactory() { + // nothing + } + + // ========================================================================= + // SymmetricAlgorithm.Create(string) depending rules + // + // A SymmetricAlgorithm.Create("...") call site is tracked exactly like any other + // SymmetricAlgorithm-derived variable (Aes/RC2/TripleDES/DES): every method inherited from + // SymmetricAlgorithm itself (CreateEncryptor/CreateDecryptor, EncryptCbc/Ecb/Cfb, + // DecryptCbc/Ecb/Cfb, the Try* variants, GenerateKey/GenerateIV, and the Mode/KeySize/ + // Padding/FeedbackSize property setters) is algorithm-independent — it only depends on the + // method name, never on a concrete class name (forObjectTypes(MethodMatcher.ANY)) — so it + // applies unconditionally regardless of which concrete algorithm the runtime string resolves + // to. This is a deliberate, literal duplicate of the identical rule set already defined in + // DotNetAES/DotNetRC2/DotNetTripleDES/DotNetDES (see their PROPERTY_SETTER_RULES/ + // CIPHER_OP_RULES): those fields are private to their own files, and every existing + // per-algorithm depending-rule list in this module is duplicated per file rather than + // centrally shared, so this follows the same established convention instead of inventing a + // new shared abstraction. + // ========================================================================= + + // alg.Mode = CipherMode.CBC → synthetic set_Mode(CipherMode.CBC) + private static final IDetectionRule SYMMETRIC_ALGORITHM_SET_MODE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_Mode") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new ModeFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // alg.KeySize = 256 → synthetic set_KeySize(256) + private static final IDetectionRule SYMMETRIC_ALGORITHM_SET_KEY_SIZE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_KeySize") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new KeySizeFactory<>(Size.UnitType.BIT)) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // alg.Padding = PaddingMode.PKCS7 → synthetic set_Padding(PaddingMode.PKCS7) + private static final IDetectionRule SYMMETRIC_ALGORITHM_SET_PADDING = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_Padding") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // alg.FeedbackSize = 128 → synthetic set_FeedbackSize(128) + private static final IDetectionRule SYMMETRIC_ALGORITHM_SET_FEEDBACK_SIZE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_FeedbackSize") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new BlockSizeFactory<>(Size.UnitType.BIT)) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + private static final List> + SYMMETRIC_ALGORITHM_PROPERTY_SETTER_RULES = + List.of( + SYMMETRIC_ALGORITHM_SET_MODE, + SYMMETRIC_ALGORITHM_SET_KEY_SIZE, + SYMMETRIC_ALGORITHM_SET_PADDING, + SYMMETRIC_ALGORITHM_SET_FEEDBACK_SIZE); + + // alg.CreateEncryptor() + private static final IDetectionRule SYMMETRIC_ALGORITHM_CREATE_ENCRYPTOR = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateEncryptor") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.ENCRYPT)) + .withoutParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // alg.CreateEncryptor(byte[] key, byte[] iv) + private static final IDetectionRule SYMMETRIC_ALGORITHM_CREATE_ENCRYPTOR_WITH_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateEncryptor") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.ENCRYPT)) + .withMethodParameter(MethodMatcher.ANY) // key bytes + .withMethodParameter(MethodMatcher.ANY) // iv bytes + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // alg.CreateDecryptor() + private static final IDetectionRule SYMMETRIC_ALGORITHM_CREATE_DECRYPTOR = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateDecryptor") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.DECRYPT)) + .withoutParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // alg.CreateDecryptor(byte[] key, byte[] iv) + private static final IDetectionRule SYMMETRIC_ALGORITHM_CREATE_DECRYPTOR_WITH_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateDecryptor") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.DECRYPT)) + .withMethodParameter(MethodMatcher.ANY) // key bytes + .withMethodParameter(MethodMatcher.ANY) // iv bytes + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // EncryptCbc(plaintext, iv, padding) + private static final IDetectionRule SYMMETRIC_ALGORITHM_ENCRYPT_CBC_3 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptCbc") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // EncryptCbc(plaintext, iv, destination, padding) [output-buffer overload] + private static final IDetectionRule SYMMETRIC_ALGORITHM_ENCRYPT_CBC_4 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptCbc") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptCbc(ciphertext, iv, padding) + private static final IDetectionRule SYMMETRIC_ALGORITHM_DECRYPT_CBC_3 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptCbc") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptCbc(ciphertext, iv, destination, padding) + private static final IDetectionRule SYMMETRIC_ALGORITHM_DECRYPT_CBC_4 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptCbc") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // EncryptEcb(plaintext, padding) + private static final IDetectionRule SYMMETRIC_ALGORITHM_ENCRYPT_ECB_2 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptEcb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // EncryptEcb(plaintext, destination, padding) + private static final IDetectionRule SYMMETRIC_ALGORITHM_ENCRYPT_ECB_3 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptEcb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptEcb(ciphertext, padding) + private static final IDetectionRule SYMMETRIC_ALGORITHM_DECRYPT_ECB_2 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptEcb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptEcb(ciphertext, destination, padding) + private static final IDetectionRule SYMMETRIC_ALGORITHM_DECRYPT_ECB_3 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptEcb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // EncryptCfb(plaintext, iv, padding, feedbackSize) + private static final IDetectionRule SYMMETRIC_ALGORITHM_ENCRYPT_CFB_4 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptCfb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize (int) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // EncryptCfb(plaintext, iv, destination, padding, feedbackSize) + private static final IDetectionRule SYMMETRIC_ALGORITHM_ENCRYPT_CFB_5 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptCfb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize (int) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptCfb(ciphertext, iv, padding, feedbackSize) + private static final IDetectionRule SYMMETRIC_ALGORITHM_DECRYPT_CFB_4 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptCfb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize (int) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptCfb(ciphertext, iv, destination, padding, feedbackSize) + private static final IDetectionRule SYMMETRIC_ALGORITHM_DECRYPT_CFB_5 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptCfb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize (int) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryEncryptCbc(plaintext, iv, destination, out bytesWritten, padding) + private static final IDetectionRule SYMMETRIC_ALGORITHM_TRY_ENCRYPT_CBC = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryEncryptCbc") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryDecryptCbc(ciphertext, iv, destination, out bytesWritten, padding) + private static final IDetectionRule SYMMETRIC_ALGORITHM_TRY_DECRYPT_CBC = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryDecryptCbc") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryEncryptEcb(plaintext, destination, padding, out bytesWritten) + private static final IDetectionRule SYMMETRIC_ALGORITHM_TRY_ENCRYPT_ECB = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryEncryptEcb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryDecryptEcb(ciphertext, destination, padding, out bytesWritten) + private static final IDetectionRule SYMMETRIC_ALGORITHM_TRY_DECRYPT_ECB = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryDecryptEcb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryEncryptCfb(plaintext, iv, destination, out bytesWritten, padding, feedbackSize) + private static final IDetectionRule SYMMETRIC_ALGORITHM_TRY_ENCRYPT_CFB = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryEncryptCfb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryDecryptCfb(ciphertext, iv, destination, out bytesWritten, padding, feedbackSize) + private static final IDetectionRule SYMMETRIC_ALGORITHM_TRY_DECRYPT_CFB = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryDecryptCfb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // alg.GenerateKey() — generates a new random key (size determined by KeySize property) + private static final IDetectionRule SYMMETRIC_ALGORITHM_GENERATE_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("GenerateKey") + .shouldBeDetectedAs(new ValueActionFactory<>("GenerateKey")) + .withoutParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // alg.GenerateIV() — generates a new random initialization vector + private static final IDetectionRule SYMMETRIC_ALGORITHM_GENERATE_IV = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("GenerateIV") + .shouldBeDetectedAs(new ValueActionFactory<>("GenerateIV")) + .withoutParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + /** + * All cipher operation rules that fire on a tracked SymmetricAlgorithm-family variable. + * Includes CreateEncryptor/CreateDecryptor, direct mode-specific Encrypt/Decrypt methods, Try* + * variants, and key/IV generation. Literal duplicate of the identical list in + * DotNetAES/DotNetRC2/DotNetTripleDES/DotNetDES (see class javadoc above). + */ + private static final List> SYMMETRIC_ALGORITHM_CIPHER_OP_RULES = + List.of( + SYMMETRIC_ALGORITHM_CREATE_ENCRYPTOR, + SYMMETRIC_ALGORITHM_CREATE_ENCRYPTOR_WITH_KEY, + SYMMETRIC_ALGORITHM_CREATE_DECRYPTOR, + SYMMETRIC_ALGORITHM_CREATE_DECRYPTOR_WITH_KEY, + SYMMETRIC_ALGORITHM_ENCRYPT_CBC_3, + SYMMETRIC_ALGORITHM_ENCRYPT_CBC_4, + SYMMETRIC_ALGORITHM_DECRYPT_CBC_3, + SYMMETRIC_ALGORITHM_DECRYPT_CBC_4, + SYMMETRIC_ALGORITHM_ENCRYPT_ECB_2, + SYMMETRIC_ALGORITHM_ENCRYPT_ECB_3, + SYMMETRIC_ALGORITHM_DECRYPT_ECB_2, + SYMMETRIC_ALGORITHM_DECRYPT_ECB_3, + SYMMETRIC_ALGORITHM_ENCRYPT_CFB_4, + SYMMETRIC_ALGORITHM_ENCRYPT_CFB_5, + SYMMETRIC_ALGORITHM_DECRYPT_CFB_4, + SYMMETRIC_ALGORITHM_DECRYPT_CFB_5, + SYMMETRIC_ALGORITHM_TRY_ENCRYPT_CBC, + SYMMETRIC_ALGORITHM_TRY_DECRYPT_CBC, + SYMMETRIC_ALGORITHM_TRY_ENCRYPT_ECB, + SYMMETRIC_ALGORITHM_TRY_DECRYPT_ECB, + SYMMETRIC_ALGORITHM_TRY_ENCRYPT_CFB, + SYMMETRIC_ALGORITHM_TRY_DECRYPT_CFB, + SYMMETRIC_ALGORITHM_GENERATE_KEY, + SYMMETRIC_ALGORITHM_GENERATE_IV); + + /** Full set of depending rules for the SymmetricAlgorithm.Create(string) factory. */ + private static final List> SYMMETRIC_ALGORITHM_DEPENDING_RULES = + Stream.concat( + SYMMETRIC_ALGORITHM_PROPERTY_SETTER_RULES.stream(), + SYMMETRIC_ALGORITHM_CIPHER_OP_RULES.stream()) + .toList(); + + // SymmetricAlgorithm.Create(string algName) — e.g. SymmetricAlgorithm.Create("RC2") + private static final IDetectionRule SYMMETRIC_ALGORITHM_CREATE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("SymmetricAlgorithm") + .forMethods("Create") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new AlgorithmFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(SYMMETRIC_ALGORITHM_DEPENDING_RULES); + + // HashAlgorithm.Create(string hashName) — e.g. HashAlgorithm.Create("SHA256") + private static final IDetectionRule HASH_ALGORITHM_CREATE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("HashAlgorithm") + .forMethods("Create") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new AlgorithmFactory<>()) + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // KeyedHashAlgorithm.Create(string algName) — e.g. KeyedHashAlgorithm.Create("HMACSHA256") + private static final IDetectionRule KEYED_HASH_ALGORITHM_CREATE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("KeyedHashAlgorithm") + .forMethods("Create") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new AlgorithmFactory<>()) + .buildForContext(new MacContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // HMAC.Create(string algorithmName) — e.g. HMAC.Create("HMACSHA256") + private static final IDetectionRule HMAC_CREATE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("HMAC") + .forMethods("Create") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new AlgorithmFactory<>()) + .buildForContext(new MacContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // AsymmetricAlgorithm.Create(string algName) — e.g. AsymmetricAlgorithm.Create("RSA") + // No fixed "kind" property: CSharpKeyContextTranslator's new Algorithm-typed branch resolves + // the concrete algorithm (RSA/DSA/ECDSA/ECDH) purely from the captured string value. + private static final IDetectionRule ASYMMETRIC_ALGORITHM_CREATE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("AsymmetricAlgorithm") + .forMethods("Create") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new AlgorithmFactory<>()) + .buildForContext(new KeyContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + @Nonnull + public static List> rules() { + return List.of( + SYMMETRIC_ALGORITHM_CREATE, + HASH_ALGORITHM_CREATE, + KEYED_HASH_ALGORITHM_CREATE, + HMAC_CREATE, + ASYMMETRIC_ALGORITHM_CREATE); + } +} diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetDES.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetDES.java index f4384f7f7..2c32f1f69 100755 --- a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetDES.java +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetDES.java @@ -19,23 +19,40 @@ */ package com.ibm.plugin.rules.detection.dotnet; +import com.ibm.engine.detection.MethodMatcher; import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.CipherAction; +import com.ibm.engine.model.Size; import com.ibm.engine.model.context.CipherContext; +import com.ibm.engine.model.factory.BlockSizeFactory; +import com.ibm.engine.model.factory.CipherActionFactory; +import com.ibm.engine.model.factory.KeySizeFactory; +import com.ibm.engine.model.factory.ModeFactory; +import com.ibm.engine.model.factory.PaddingFactory; import com.ibm.engine.model.factory.ValueActionFactory; import com.ibm.engine.rule.IDetectionRule; import com.ibm.engine.rule.builder.DetectionRuleBuilder; import java.util.List; +import java.util.stream.Stream; import javax.annotation.Nonnull; /** - * Detection rules for DES usage in System.Security.Cryptography. + * Detection rules for the DES family in System.Security.Cryptography. * - *

Detects: + *

Classes covered: * *

    - *
  • {@code DES.Create()} — abstract factory (deprecated in .NET 5+) - *
  • {@code new DESCryptoServiceProvider()} — CAPI-backed (deprecated) + *
  • {@code DES} — abstract base ({@code DES.Create()}, {@code DES.Create(string)}) + *
  • {@code DESCryptoServiceProvider} — legacy CAPI implementation *
+ * + *

Architecture: all methods inherited from {@code SymmetricAlgorithm} (EncryptCbc, DecryptCbc, + * CreateEncryptor, property setters, etc.) are expressed as depending rules attached to + * each primary creation rule. The detection engine tracks the variable and fires these rules on + * every matching method call, regardless of the concrete DES subclass. Unlike {@code Aes}, {@code + * DES} has no CNG-backed subclass and no AEAD variant, so it has fewer primary creation rules than + * {@code DotNetAES}, but identical depending-rule coverage since both derive from {@code + * SymmetricAlgorithm}. */ @SuppressWarnings("java:S1192") public final class DotNetDES { @@ -44,6 +61,505 @@ private DotNetDES() { // nothing } + // ========================================================================= + // Property setter rules (synthetic set_X method invocations) + // ========================================================================= + + // des.Mode = CipherMode.CBC → synthetic set_Mode(CipherMode.CBC) + private static final IDetectionRule DES_SET_MODE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_Mode") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new ModeFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // des.KeySize = 64 → synthetic set_KeySize(64) + private static final IDetectionRule DES_SET_KEY_SIZE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_KeySize") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new KeySizeFactory<>(Size.UnitType.BIT)) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // des.Padding = PaddingMode.PKCS7 → synthetic set_Padding(PaddingMode.PKCS7) + private static final IDetectionRule DES_SET_PADDING = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_Padding") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // des.FeedbackSize = 8 → synthetic set_FeedbackSize(8) + private static final IDetectionRule DES_SET_FEEDBACK_SIZE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_FeedbackSize") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new BlockSizeFactory<>(Size.UnitType.BIT)) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + private static final List> PROPERTY_SETTER_RULES = + List.of(DES_SET_MODE, DES_SET_KEY_SIZE, DES_SET_PADDING, DES_SET_FEEDBACK_SIZE); + + // ========================================================================= + // CreateEncryptor / CreateDecryptor rules + // ========================================================================= + + // des.CreateEncryptor() + private static final IDetectionRule DES_CREATE_ENCRYPTOR = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateEncryptor") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.ENCRYPT)) + .withoutParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // des.CreateEncryptor(byte[] key, byte[] iv) + private static final IDetectionRule DES_CREATE_ENCRYPTOR_WITH_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateEncryptor") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.ENCRYPT)) + .withMethodParameter(MethodMatcher.ANY) // key bytes + .withMethodParameter(MethodMatcher.ANY) // iv bytes + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // des.CreateDecryptor() + private static final IDetectionRule DES_CREATE_DECRYPTOR = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateDecryptor") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.DECRYPT)) + .withoutParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // des.CreateDecryptor(byte[] key, byte[] iv) + private static final IDetectionRule DES_CREATE_DECRYPTOR_WITH_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateDecryptor") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.DECRYPT)) + .withMethodParameter(MethodMatcher.ANY) // key bytes + .withMethodParameter(MethodMatcher.ANY) // iv bytes + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // EncryptCbc / DecryptCbc rules + // Mode is constant "CBC" (from method name); padding is detected from last param. + // Two overloads: 3-param and 4-param (with output buffer). + // ========================================================================= + + // EncryptCbc(plaintext, iv, padding) + private static final IDetectionRule DES_ENCRYPT_CBC_3 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptCbc") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // EncryptCbc(plaintext, iv, destination, padding) [output-buffer overload] + private static final IDetectionRule DES_ENCRYPT_CBC_4 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptCbc") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptCbc(ciphertext, iv, padding) + private static final IDetectionRule DES_DECRYPT_CBC_3 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptCbc") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptCbc(ciphertext, iv, destination, padding) + private static final IDetectionRule DES_DECRYPT_CBC_4 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptCbc") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // EncryptEcb / DecryptEcb rules + // Mode is constant "ECB" (from method name); no IV parameter. + // Two overloads: 2-param and 3-param (with output buffer). + // ========================================================================= + + // EncryptEcb(plaintext, padding) + private static final IDetectionRule DES_ENCRYPT_ECB_2 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptEcb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // EncryptEcb(plaintext, destination, padding) + private static final IDetectionRule DES_ENCRYPT_ECB_3 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptEcb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptEcb(ciphertext, padding) + private static final IDetectionRule DES_DECRYPT_ECB_2 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptEcb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptEcb(ciphertext, destination, padding) + private static final IDetectionRule DES_DECRYPT_ECB_3 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptEcb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // EncryptCfb / DecryptCfb rules + // Mode is constant "CFB"; padding detected from 3rd param; feedbackSize ignored. + // Two overloads: 4-param and 5-param (with output buffer). + // ========================================================================= + + // EncryptCfb(plaintext, iv, padding, feedbackSize) + private static final IDetectionRule DES_ENCRYPT_CFB_4 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptCfb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize (int) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // EncryptCfb(plaintext, iv, destination, padding, feedbackSize) + private static final IDetectionRule DES_ENCRYPT_CFB_5 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptCfb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize (int) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptCfb(ciphertext, iv, padding, feedbackSize) + private static final IDetectionRule DES_DECRYPT_CFB_4 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptCfb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize (int) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptCfb(ciphertext, iv, destination, padding, feedbackSize) + private static final IDetectionRule DES_DECRYPT_CFB_5 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptCfb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize (int) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // TryEncrypt* / TryDecrypt* rules + // Signatures: + // TryEncryptCbc(plaintext, iv, destination, out bytesWritten, padding) — 5 params + // TryDecryptCbc(ciphertext, iv, destination, out bytesWritten, padding) — 5 params + // TryEncryptEcb(plaintext, destination, padding, out bytesWritten) — 4 params + // TryDecryptEcb(ciphertext, destination, padding, out bytesWritten) — 4 params + // TryEncryptCfb(plaintext, iv, destination, out bytesWritten, padding, fs) — 6 params + // TryDecryptCfb(ciphertext, iv, destination, out bytesWritten, padding, fs) — 6 params + // ========================================================================= + + // TryEncryptCbc(plaintext, iv, destination, out bytesWritten, padding) + private static final IDetectionRule DES_TRY_ENCRYPT_CBC = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryEncryptCbc") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryDecryptCbc(ciphertext, iv, destination, out bytesWritten, padding) + private static final IDetectionRule DES_TRY_DECRYPT_CBC = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryDecryptCbc") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryEncryptEcb(plaintext, destination, padding, out bytesWritten) + private static final IDetectionRule DES_TRY_ENCRYPT_ECB = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryEncryptEcb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryDecryptEcb(ciphertext, destination, padding, out bytesWritten) + private static final IDetectionRule DES_TRY_DECRYPT_ECB = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryDecryptEcb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryEncryptCfb(plaintext, iv, destination, out bytesWritten, padding, feedbackSize) + private static final IDetectionRule DES_TRY_ENCRYPT_CFB = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryEncryptCfb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryDecryptCfb(ciphertext, iv, destination, out bytesWritten, padding, feedbackSize) + private static final IDetectionRule DES_TRY_DECRYPT_CFB = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryDecryptCfb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // Key / IV generation rules + // ========================================================================= + + // des.GenerateKey() — generates a new random key (size determined by KeySize property) + private static final IDetectionRule DES_GENERATE_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("GenerateKey") + .shouldBeDetectedAs(new ValueActionFactory<>("GenerateKey")) + .withoutParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // des.GenerateIV() — generates a new random initialization vector + private static final IDetectionRule DES_GENERATE_IV = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("GenerateIV") + .shouldBeDetectedAs(new ValueActionFactory<>("GenerateIV")) + .withoutParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // Aggregated depending-rule lists + // ========================================================================= + + /** + * All cipher operation rules that fire on a tracked DES-family variable. Includes + * CreateEncryptor/CreateDecryptor, direct mode-specific Encrypt/Decrypt methods, Try* variants, + * and key/IV generation. + */ + private static final List> CIPHER_OP_RULES = + List.of( + DES_CREATE_ENCRYPTOR, + DES_CREATE_ENCRYPTOR_WITH_KEY, + DES_CREATE_DECRYPTOR, + DES_CREATE_DECRYPTOR_WITH_KEY, + DES_ENCRYPT_CBC_3, + DES_ENCRYPT_CBC_4, + DES_DECRYPT_CBC_3, + DES_DECRYPT_CBC_4, + DES_ENCRYPT_ECB_2, + DES_ENCRYPT_ECB_3, + DES_DECRYPT_ECB_2, + DES_DECRYPT_ECB_3, + DES_ENCRYPT_CFB_4, + DES_ENCRYPT_CFB_5, + DES_DECRYPT_CFB_4, + DES_DECRYPT_CFB_5, + DES_TRY_ENCRYPT_CBC, + DES_TRY_DECRYPT_CBC, + DES_TRY_ENCRYPT_ECB, + DES_TRY_DECRYPT_ECB, + DES_TRY_ENCRYPT_CFB, + DES_TRY_DECRYPT_CFB, + DES_GENERATE_KEY, + DES_GENERATE_IV); + + /** Full set of depending rules for all DES-derived classes. */ + private static final List> DES_DEPENDING_RULES = + Stream.concat(PROPERTY_SETTER_RULES.stream(), CIPHER_OP_RULES.stream()).toList(); + + // ========================================================================= + // Primary creation rules + // ========================================================================= + + // DES.Create() — abstract factory, no parameters private static final IDetectionRule DES_CREATE = new DetectionRuleBuilder() .createDetectionRule() @@ -53,8 +569,21 @@ private DotNetDES() { .withoutParameters() .buildForContext(new CipherContext()) .inBundle(() -> "DotNet") - .withDependingDetectionRules(List.of()); + .withDependingDetectionRules(DES_DEPENDING_RULES); + + // DES.Create("DES") — named factory (obsolete, still detectable) + private static final IDetectionRule DES_CREATE_NAMED = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("DES") + .forMethods("Create") + .shouldBeDetectedAs(new ValueActionFactory<>("DES")) + .withMethodParameter(MethodMatcher.ANY) // algorithm name string + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(DES_DEPENDING_RULES); + // new DESCryptoServiceProvider() — legacy CAPI implementation private static final IDetectionRule DES_CSP = new DetectionRuleBuilder() .createDetectionRule() @@ -64,10 +593,10 @@ private DotNetDES() { .withoutParameters() .buildForContext(new CipherContext()) .inBundle(() -> "DotNet") - .withDependingDetectionRules(List.of()); + .withDependingDetectionRules(DES_DEPENDING_RULES); @Nonnull public static List> rules() { - return List.of(DES_CREATE, DES_CSP); + return List.of(DES_CREATE, DES_CREATE_NAMED, DES_CSP); } } diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetDSA.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetDSA.java index 30fdd70fc..12507348f 100755 --- a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetDSA.java +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetDSA.java @@ -19,8 +19,14 @@ */ package com.ibm.plugin.rules.detection.dotnet; +import com.ibm.engine.detection.MethodMatcher; import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.SignatureAction; +import com.ibm.engine.model.Size; import com.ibm.engine.model.context.KeyContext; +import com.ibm.engine.model.context.SignatureContext; +import com.ibm.engine.model.factory.KeySizeFactory; +import com.ibm.engine.model.factory.SignatureActionFactory; import com.ibm.engine.model.factory.ValueActionFactory; import com.ibm.engine.rule.IDetectionRule; import com.ibm.engine.rule.builder.DetectionRuleBuilder; @@ -29,14 +35,25 @@ import javax.annotation.Nonnull; /** - * Detection rules for DSA usage in System.Security.Cryptography. + * Detection rules for the DSA family in System.Security.Cryptography. * - *

Detects: + *

Classes covered: * *

    - *
  • {@code DSA.Create()} — abstract factory - *
  • {@code new DSACryptoServiceProvider()} — CAPI-backed implementation + *
  • {@code DSA} — abstract base ({@code DSA.Create()}, {@code DSA.Create(DSAParameters)}, + * {@code DSA.Create(int)}, {@code DSA.Create(string)}) + *
  • {@code DSACng} — CNG-backed implementation (ephemeral and persisted-key constructors) + *
  • {@code DSACryptoServiceProvider} — legacy CAPI implementation + *
  • {@code DSAOpenSsl} — OpenSSL-backed implementation *
+ * + *

Architecture: all methods inherited from {@code AsymmetricAlgorithm} / {@code DSA} (KeySize + * property, CreateSignature, VerifySignature, SignData, VerifyData, Try* variants, etc.), as well + * as the legacy {@code SignHash}/{@code VerifyHash} methods that exist only on {@code + * DSACryptoServiceProvider} (not on the abstract {@code DSA} base class; {@code DSA} has no {@code + * TrySignHash}), are expressed as depending rules attached to each primary creation rule. + * The detection engine tracks the variable and fires these rules on every matching method call, + * regardless of the concrete DSA subclass. */ @SuppressWarnings("java:S1192") public final class DotNetDSA { @@ -45,30 +62,200 @@ private DotNetDSA() { // nothing } + // ========================================================================= + // Property setter rules (synthetic set_X method invocations) + // ========================================================================= + + // dsa.KeySize = 2048 → synthetic set_KeySize(2048) + private static final IDetectionRule DSA_SET_KEY_SIZE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_KeySize") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new KeySizeFactory<>(Size.UnitType.BIT)) + .buildForContext(new KeyContext(Map.of("kind", "DSA"))) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // Signing / verification operation rules + // Each covers every overload of the given method name (arities vary only by + // hash-algorithm / signature-format / offset-length parameters, which are not + // individually tracked), mirroring the JCA Signature.sign()/verify() rules. + // ========================================================================= + + // dsa.CreateSignature(hash) / dsa.CreateSignature(hash, format) + private static final IDetectionRule DSA_CREATE_SIGNATURE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateSignature") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.SIGN)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // dsa.TryCreateSignature(hash, destination, ...) + private static final IDetectionRule DSA_TRY_CREATE_SIGNATURE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryCreateSignature") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.SIGN)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // dsa.SignData(data, hashAlgorithm[, format]) + private static final IDetectionRule DSA_SIGN_DATA = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("SignData") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.SIGN)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // dsa.TrySignData(data, destination, hashAlgorithm, ...) + private static final IDetectionRule DSA_TRY_SIGN_DATA = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TrySignData") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.SIGN)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // dsa.VerifySignature(hash, signature[, format]) + private static final IDetectionRule DSA_VERIFY_SIGNATURE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("VerifySignature") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.VERIFY)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // dsa.VerifyData(data, signature, hashAlgorithm[, format]) + private static final IDetectionRule DSA_VERIFY_DATA = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("VerifyData") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.VERIFY)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // dsa.SignHash(hash, hashAlgorithmName) — legacy CSP-era method, only real on + // DSACryptoServiceProvider (not present on the abstract DSA base class), mirroring + // RSA_SIGN_HASH in DotNetRSA.java. Note that unlike RSA, DSA has no TrySignHash. + private static final IDetectionRule DSA_SIGN_HASH = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("SignHash") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.SIGN)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // dsa.VerifyHash(hash, hashAlgorithmName, signature) — legacy CSP-era method, only real + // on DSACryptoServiceProvider (not present on the abstract DSA base class), mirroring + // RSA_VERIFY_HASH in DotNetRSA.java. + private static final IDetectionRule DSA_VERIFY_HASH = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("VerifyHash") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.VERIFY)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // Aggregated depending-rule list + // ========================================================================= + + /** Full set of depending rules for all DSA-derived classes. */ + private static final List> DSA_DEPENDING_RULES = + List.of( + DSA_SET_KEY_SIZE, + DSA_CREATE_SIGNATURE, + DSA_TRY_CREATE_SIGNATURE, + DSA_SIGN_DATA, + DSA_TRY_SIGN_DATA, + DSA_VERIFY_SIGNATURE, + DSA_VERIFY_DATA, + DSA_SIGN_HASH, + DSA_VERIFY_HASH); + + // ========================================================================= + // Primary creation rules + // ========================================================================= + + // DSA.Create() / DSA.Create(DSAParameters) / DSA.Create(int) / DSA.Create(string) private static final IDetectionRule DSA_CREATE = new DetectionRuleBuilder() .createDetectionRule() .forObjectTypes("DSA") .forMethods("Create") .shouldBeDetectedAs(new ValueActionFactory<>("DSA")) - .withoutParameters() + .withAnyParameters() .buildForContext(new KeyContext(Map.of("kind", "DSA"))) .inBundle(() -> "DotNet") - .withDependingDetectionRules(List.of()); + .withDependingDetectionRules(DSA_DEPENDING_RULES); + // new DSACng() / new DSACng(CngKey) / new DSACng(int) — CNG-backed implementation + private static final IDetectionRule DSA_CNG = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("DSACng") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("DSA")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "DSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(DSA_DEPENDING_RULES); + + // new DSACryptoServiceProvider() / (CspParameters) / (int) / (int, CspParameters) private static final IDetectionRule DSA_CSP = new DetectionRuleBuilder() .createDetectionRule() .forObjectTypes("DSACryptoServiceProvider") .forMethods("") .shouldBeDetectedAs(new ValueActionFactory<>("DSA")) - .withoutParameters() + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "DSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(DSA_DEPENDING_RULES); + + // new DSAOpenSsl() / (DSAParameters) / (int) / (IntPtr) / (SafeEvpPKeyHandle) + private static final IDetectionRule DSA_OPENSSL = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("DSAOpenSsl") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("DSA")) + .withAnyParameters() .buildForContext(new KeyContext(Map.of("kind", "DSA"))) .inBundle(() -> "DotNet") - .withDependingDetectionRules(List.of()); + .withDependingDetectionRules(DSA_DEPENDING_RULES); @Nonnull public static List> rules() { - return List.of(DSA_CREATE, DSA_CSP); + return List.of(DSA_CREATE, DSA_CNG, DSA_CSP, DSA_OPENSSL); } } diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetECDiffieHellman.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetECDiffieHellman.java index 79ea96196..ab7f54036 100755 --- a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetECDiffieHellman.java +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetECDiffieHellman.java @@ -19,8 +19,11 @@ */ package com.ibm.plugin.rules.detection.dotnet; +import com.ibm.engine.detection.MethodMatcher; import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.Size; import com.ibm.engine.model.context.KeyContext; +import com.ibm.engine.model.factory.KeySizeFactory; import com.ibm.engine.model.factory.ValueActionFactory; import com.ibm.engine.rule.IDetectionRule; import com.ibm.engine.rule.builder.DetectionRuleBuilder; @@ -29,15 +32,66 @@ import javax.annotation.Nonnull; /** - * Detection rules for ECDH usage in System.Security.Cryptography. + * Detection rules for ECDH (Elliptic Curve Diffie-Hellman key agreement) usage in + * System.Security.Cryptography. * - *

Detects: + *

Classes covered: * *

    - *
  • {@code ECDiffieHellman.Create()} — abstract factory - *
  • {@code ECDiffieHellman.Create(curve)} — factory with ECCurve - *
  • {@code new ECDiffieHellmanCng()} — CNG-backed implementation + *
  • {@code ECDiffieHellman} — abstract base ({@code ECDiffieHellman.Create()}, {@code + * ECDiffieHellman.Create(ECCurve)}, {@code ECDiffieHellman.Create(ECParameters)}, {@code + * ECDiffieHellman.Create(string)}) + *
  • {@code ECDiffieHellmanCng} — CNG-backed implementation, Windows-only + *
  • {@code ECDiffieHellmanOpenSsl} — OpenSSL-backed implementation, non-Windows only *
+ * + *

Architecture: all members inherited from {@code ECDiffieHellman} / {@code AsymmetricAlgorithm} + * (the {@code KeySize} property, {@code DeriveKeyMaterial}, {@code DeriveKeyFromHash}, {@code + * DeriveKeyFromHmac}, {@code DeriveKeyTls}, {@code DeriveRawSecretAgreement}) are expressed as + * depending rules attached to each primary creation rule, mirroring {@code + * DotNetECDsa.java} / {@code DotNetAES.java}. Method overloads that only differ by array-vs-{@code + * Span}, {@code CngKey}-vs-{@code ECDiffieHellmanPublicKey} receiver type, optional prepend/append + * byte arrays, or output-buffer parameters are intentionally collapsed into a single {@code + * withAnyParameters()} rule per method name: the ANTLR4-based C# engine cannot resolve parameter + * types (see {@code CSharpLanguageTranslation}), so distinguishing overloads by parameter type is + * not possible. + * + *

None of the key-derivation operations ({@code DeriveKeyMaterial}, {@code DeriveKeyFromHash}, + * {@code DeriveKeyFromHmac}, {@code DeriveKeyTls}, {@code DeriveRawSecretAgreement}) fit any of the + * typed {@code CipherAction.Action} values ({@code WRAP}, {@code HASH}, {@code ENCRYPT}, {@code + * DECRYPT}, {@code PADDING}, {@code MAC}, {@code NONE}) — none of them mean "derive a key". + * Following the existing convention of generic string captures for operations without a typed + * action (e.g. {@code GenerateKey}/{@code GenerateIV} in {@code DotNetAES.java}), each derive + * operation is captured with {@code ValueActionFactory<>()} under its own {@code + * KeyContext} "kind", and translated in {@code CSharpKeyContextTranslator} to the generic {@code + * KeyDerivation} functionality node (or, for {@code DeriveRawSecretAgreement} — which returns the + * raw shared secret with no KDF post-processing applied — to the generic {@code Generate} + * functionality node, mirroring how {@code GenerateIV} is translated). This is a modeling decision + * with real discretion (no dedicated "key agreement derive" functionality node exists yet in the + * mapper model); it is called out here and in the task report rather than decided silently. + * + *

Known gap — {@code PublicKey} property: reading {@code ecdh.PublicKey} (e.g. to send to + * a peer, or to call {@code alice.PublicKey.ToByteArray()}) cannot be detected with the current + * engine. This was verified by tracing the engine code, not assumed: + * + *

    + *
  • A bare property read with no chained call (e.g. {@code var pub = alice.PublicKey;}) never + * reaches the tree converter at all — {@code CSharpTreeConverter.convertPrimaryExpression} + * only emits a node when the {@code primary_expression} contains a {@code method_invocation}; + * a lone member access produces nothing. + *
  • A chained call such as {@code alice.PublicKey.ToByteArray()} *does* produce a {@code + * CSharpMethodInvocationTree} node, but {@code resolveObjectTypeName} resolves its receiver + * ({@code getObjectTypeName()}) to the generic member name {@code "PublicKey"} (the member + * access immediately preceding the call), not the tracked variable name {@code "alice"}. In + * {@code CSharpDetectionEngine.isInvocationOnVariable}, the match is {@code + * invocation.getObjectTypeName().equals(sym.getName())} — so this always fails for the + * tracked {@code alice} symbol, and the depending-rule scan skips the statement before rule + * matching is even attempted (see {@code processStatement}). + *
+ * + *

No rule is added for {@code PublicKey}; forcing one would either never fire (dead code) or + * fire on an unrelated broad pattern (any {@code x.PublicKey.Method()} in the file, disconnected + * from the tracked ECDH variable). */ @SuppressWarnings("java:S1192") public final class DotNetECDiffieHellman { @@ -46,6 +100,109 @@ private DotNetECDiffieHellman() { // nothing } + // ========================================================================= + // Property setter rules (synthetic set_X method invocations) + // ========================================================================= + + // ecdh.KeySize = 384 → synthetic set_KeySize(384) + private static final IDetectionRule ECDH_SET_KEY_SIZE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_KeySize") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new KeySizeFactory<>(Size.UnitType.BIT)) + .buildForContext(new KeyContext(Map.of("kind", "ECDH"))) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // Key-derivation operation rules + // See class javadoc for why ValueActionFactory (not a typed CipherAction) is used, and why + // each method gets its own KeyContext "kind" (dispatched in CSharpKeyContextTranslator). + // ========================================================================= + + // ecdh.DeriveKeyMaterial(otherPartyPublicKey) + private static final IDetectionRule ECDH_DERIVE_KEY_MATERIAL = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DeriveKeyMaterial") + .shouldBeDetectedAs(new ValueActionFactory<>("DeriveKeyMaterial")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "ECDH_DERIVE_KEY_MATERIAL"))) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ecdh.DeriveKeyFromHash(otherPartyPublicKey, hashAlgorithm[, secretPrepend, secretAppend]) + private static final IDetectionRule ECDH_DERIVE_KEY_FROM_HASH = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DeriveKeyFromHash") + .shouldBeDetectedAs(new ValueActionFactory<>("DeriveKeyFromHash")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "ECDH_DERIVE_KEY_FROM_HASH"))) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ecdh.DeriveKeyFromHmac(otherPartyPublicKey, hashAlgorithm, hmacKey[, prepend, append]) + private static final IDetectionRule ECDH_DERIVE_KEY_FROM_HMAC = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DeriveKeyFromHmac") + .shouldBeDetectedAs(new ValueActionFactory<>("DeriveKeyFromHmac")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "ECDH_DERIVE_KEY_FROM_HMAC"))) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ecdh.DeriveKeyTls(otherPartyPublicKey, prfLabel, prfSeed) — TLS 1.1 PRF-based derivation. + // Per the ECDiffieHellman API reference there is only one such method (no "1_2" variant). + private static final IDetectionRule ECDH_DERIVE_KEY_TLS = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DeriveKeyTls") + .shouldBeDetectedAs(new ValueActionFactory<>("DeriveKeyTls")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "ECDH_DERIVE_KEY_TLS"))) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ecdh.DeriveRawSecretAgreement(otherPartyPublicKey) — raw shared secret, no KDF applied. + private static final IDetectionRule ECDH_DERIVE_RAW_SECRET_AGREEMENT = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DeriveRawSecretAgreement") + .shouldBeDetectedAs(new ValueActionFactory<>("DeriveRawSecretAgreement")) + .withAnyParameters() + .buildForContext( + new KeyContext(Map.of("kind", "ECDH_DERIVE_RAW_SECRET_AGREEMENT"))) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // Aggregated depending-rule list + // ========================================================================= + + /** Full set of depending rules for all ECDiffieHellman-derived classes. */ + private static final List> ECDH_DEPENDING_RULES = + List.of( + ECDH_SET_KEY_SIZE, + ECDH_DERIVE_KEY_MATERIAL, + ECDH_DERIVE_KEY_FROM_HASH, + ECDH_DERIVE_KEY_FROM_HMAC, + ECDH_DERIVE_KEY_TLS, + ECDH_DERIVE_RAW_SECRET_AGREEMENT); + + // ========================================================================= + // Primary creation rules + // ========================================================================= + + // ECDiffieHellman.Create() / Create(ECCurve) / Create(ECParameters) / Create(string) private static final IDetectionRule ECDH_CREATE = new DetectionRuleBuilder() .createDetectionRule() @@ -55,21 +212,37 @@ private DotNetECDiffieHellman() { .withAnyParameters() .buildForContext(new KeyContext(Map.of("kind", "ECDH"))) .inBundle(() -> "DotNet") - .withDependingDetectionRules(List.of()); + .withDependingDetectionRules(ECDH_DEPENDING_RULES); + // new ECDiffieHellmanCng() / (CngKey) / (ECCurve) / (int) — CNG-backed implementation. + // Uses withAnyParameters() to cover all constructor overloads in a single rule and avoid + // double-detection (see AES_CNG_NAMED in DotNetAES.java / ECDSA_OPENSSL in DotNetECDsa.java). private static final IDetectionRule ECDH_CNG = new DetectionRuleBuilder() .createDetectionRule() .forObjectTypes("ECDiffieHellmanCng") .forMethods("") .shouldBeDetectedAs(new ValueActionFactory<>("ECDH")) - .withoutParameters() + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "ECDH"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(ECDH_DEPENDING_RULES); + + // new ECDiffieHellmanOpenSsl() / (ECCurve) / (int) / (IntPtr) / (SafeEvpPKeyHandle) — + // OpenSSL-backed implementation. Uses withAnyParameters() for the same reason as ECDH_CNG. + private static final IDetectionRule ECDH_OPENSSL = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("ECDiffieHellmanOpenSsl") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("ECDH")) + .withAnyParameters() .buildForContext(new KeyContext(Map.of("kind", "ECDH"))) .inBundle(() -> "DotNet") - .withDependingDetectionRules(List.of()); + .withDependingDetectionRules(ECDH_DEPENDING_RULES); @Nonnull public static List> rules() { - return List.of(ECDH_CREATE, ECDH_CNG); + return List.of(ECDH_CREATE, ECDH_CNG, ECDH_OPENSSL); } } diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetECDsa.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetECDsa.java index 5c9fb5f54..c0d68c902 100755 --- a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetECDsa.java +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetECDsa.java @@ -19,8 +19,14 @@ */ package com.ibm.plugin.rules.detection.dotnet; +import com.ibm.engine.detection.MethodMatcher; import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.SignatureAction; +import com.ibm.engine.model.Size; import com.ibm.engine.model.context.KeyContext; +import com.ibm.engine.model.context.SignatureContext; +import com.ibm.engine.model.factory.KeySizeFactory; +import com.ibm.engine.model.factory.SignatureActionFactory; import com.ibm.engine.model.factory.ValueActionFactory; import com.ibm.engine.rule.IDetectionRule; import com.ibm.engine.rule.builder.DetectionRuleBuilder; @@ -31,13 +37,29 @@ /** * Detection rules for ECDSA usage in System.Security.Cryptography. * - *

Detects: + *

Classes covered: * *

    - *
  • {@code ECDsa.Create()} — factory without curve - *
  • {@code ECDsa.Create(curve)} — factory with ECCurve parameter - *
  • {@code new ECDsaCng()} — CNG-backed implementation + *
  • {@code ECDsa} — abstract base ({@code ECDsa.Create()}, {@code ECDsa.Create(ECCurve)}, + * {@code ECDsa.Create(ECParameters)}, {@code ECDsa.Create(string)}) + *
  • {@code ECDsaCng} — CNG-backed implementation, Windows-only ({@code ECDsaCng()}, {@code + * ECDsaCng(CngKey)}, {@code ECDsaCng(ECCurve)}, {@code ECDsaCng(int)}) + *
  • {@code ECDsaOpenSsl} — OpenSSL-backed implementation, non-Windows only *
+ * + *

Architecture: all members inherited from {@code ECDsa} / {@code AsymmetricAlgorithm} (the + * {@code KeySize} property, {@code SignData}/{@code VerifyData}, {@code SignHash}/{@code + * VerifyHash}, and their {@code Try*} variants) are expressed as depending rules attached + * to each primary creation rule. The detection engine tracks the variable and fires these rules on + * every matching method call, regardless of the concrete ECDsa subclass. Method overloads that only + * differ by array-vs-{@code Span}, offset/length, {@code DSASignatureFormat}, or output-buffer + * parameters are intentionally collapsed into a single {@code withAnyParameters()} rule per method + * name: the ANTLR4-based C# engine cannot resolve parameter types (see {@code + * CSharpLanguageTranslation}), so distinguishing overloads by parameter type is not possible, and + * none of the extra parameters carry additional cryptographic information worth extracting. Unlike + * RSA, ECDSA has no encrypt/decrypt operations — it is signature-only. Per the {@code ECDsa} API + * reference, there are no {@code TryVerifyData}/{@code TryVerifyHash} methods (verification returns + * a bool directly, so there is no output buffer to size), mirroring RSA. */ @SuppressWarnings("java:S1192") public final class DotNetECDsa { @@ -46,6 +68,125 @@ private DotNetECDsa() { // nothing } + // ========================================================================= + // Property setter rules (synthetic set_X method invocations) + // ========================================================================= + + // ecdsa.KeySize = 256 → synthetic set_KeySize(256) + private static final IDetectionRule ECDSA_SET_KEY_SIZE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_KeySize") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new KeySizeFactory<>(Size.UnitType.BIT)) + .buildForContext(new KeyContext(Map.of("kind", "ECDSA"))) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // Signing / verification operation rules + // Each rule covers every overload of the given method name (arities vary only + // by hash-algorithm / DSASignatureFormat / offset-length / output-buffer + // parameters, which are not individually tracked), mirroring the RSA/DSA + // SignData()/VerifyData() rules. + // ========================================================================= + + // ecdsa.SignData(data, hashAlgorithm[, format]) [+ offset/length or Stream overloads] + private static final IDetectionRule ECDSA_SIGN_DATA = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("SignData") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.SIGN)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ecdsa.TrySignData(data, destination, hashAlgorithm[, format], out bytesWritten) + private static final IDetectionRule ECDSA_TRY_SIGN_DATA = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TrySignData") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.SIGN)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ecdsa.SignHash(hash[, format]) — legacy byte[] overload (ECDsaOpenSsl) and modern + // Span/DSASignatureFormat overloads (ECDsa base) are all covered. + private static final IDetectionRule ECDSA_SIGN_HASH = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("SignHash") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.SIGN)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ecdsa.TrySignHash(hash, destination[, format], out bytesWritten) + private static final IDetectionRule ECDSA_TRY_SIGN_HASH = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TrySignHash") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.SIGN)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ecdsa.VerifyData(data, signature, hashAlgorithm[, format]) [+ offset/length or Stream + // overloads] + private static final IDetectionRule ECDSA_VERIFY_DATA = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("VerifyData") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.VERIFY)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ecdsa.VerifyHash(hash, signature[, format]) — legacy byte[] overload (ECDsaOpenSsl) and + // modern ReadOnlySpan/DSASignatureFormat overloads (ECDsa base) are all covered. + private static final IDetectionRule ECDSA_VERIFY_HASH = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("VerifyHash") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.VERIFY)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // Aggregated depending-rule list + // ========================================================================= + + /** Full set of depending rules for all ECDsa-derived classes. */ + private static final List> ECDSA_DEPENDING_RULES = + List.of( + ECDSA_SET_KEY_SIZE, + ECDSA_SIGN_DATA, + ECDSA_TRY_SIGN_DATA, + ECDSA_SIGN_HASH, + ECDSA_TRY_SIGN_HASH, + ECDSA_VERIFY_DATA, + ECDSA_VERIFY_HASH); + + // ========================================================================= + // Primary creation rules + // ========================================================================= + + // ECDsa.Create() / ECDsa.Create(ECCurve) / ECDsa.Create(ECParameters) / ECDsa.Create(string) private static final IDetectionRule ECDSA_CREATE = new DetectionRuleBuilder() .createDetectionRule() @@ -55,21 +196,39 @@ private DotNetECDsa() { .withAnyParameters() .buildForContext(new KeyContext(Map.of("kind", "ECDSA"))) .inBundle(() -> "DotNet") - .withDependingDetectionRules(List.of()); + .withDependingDetectionRules(ECDSA_DEPENDING_RULES); + // new ECDsaCng() / (CngKey) / (ECCurve) / (int) — CNG-backed implementation. + // Uses withAnyParameters() to cover all constructor overloads in a single rule and avoid + // double-detection (see AES_CNG_NAMED in DotNetAES.java / ECDH_CNG in + // DotNetECDiffieHellman.java). private static final IDetectionRule ECDSA_CNG = new DetectionRuleBuilder() .createDetectionRule() .forObjectTypes("ECDsaCng") .forMethods("") .shouldBeDetectedAs(new ValueActionFactory<>("ECDSA")) - .withoutParameters() + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "ECDSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(ECDSA_DEPENDING_RULES); + + // new ECDsaOpenSsl() / (ECCurve) / (int) / (IntPtr) / (SafeEvpPKeyHandle) — OpenSSL-backed + // implementation. Uses withAnyParameters() to avoid double-detection that would occur if + // separate rules were added per overload (see AES_CNG_NAMED in DotNetAES.java). + private static final IDetectionRule ECDSA_OPENSSL = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("ECDsaOpenSsl") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("ECDSA")) + .withAnyParameters() .buildForContext(new KeyContext(Map.of("kind", "ECDSA"))) .inBundle(() -> "DotNet") - .withDependingDetectionRules(List.of()); + .withDependingDetectionRules(ECDSA_DEPENDING_RULES); @Nonnull public static List> rules() { - return List.of(ECDSA_CREATE, ECDSA_CNG); + return List.of(ECDSA_CREATE, ECDSA_CNG, ECDSA_OPENSSL); } } diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetHMAC.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetHMAC.java index 073cfdf08..d1619ec27 100755 --- a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetHMAC.java +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetHMAC.java @@ -28,11 +28,56 @@ import javax.annotation.Nonnull; /** - * Detection rules for HMAC algorithms in System.Security.Cryptography. + * Detection rules for the HMAC family, and for {@code MACTripleDES}, in {@code + * System.Security.Cryptography}. * - *

Detects constructor calls for all concrete HMAC implementations. The detected value encodes - * the full class name (e.g., {@code "HMACSHA256"}) so the translator can resolve the inner hash. + *

Classes covered: + * + *

    + *
  • {@code HMACMD5}, {@code HMACSHA1}, {@code HMACSHA256}, {@code HMACSHA384}, {@code + * HMACSHA512} — standard HMAC implementations, available across all .NET versions. + *
  • {@code HMACRIPEMD160} — like {@code RIPEMD160}/{@code RIPEMD160Managed} (see {@link + * DotNetSHA}), this class only ever existed in .NET Framework (documented up to + * netframework-4.8.1 only; no netcoreapp/net5+ monikers). It is detected here for legacy .NET + * Framework source code. + *
  • {@code HMACSHA3_256}, {@code HMACSHA3_384}, {@code HMACSHA3_512} — introduced in .NET 8, + * same platform-dependent status ({@code IsSupported}, intentionally not modeled — consistent + * with how other {@code IsSupported} properties are ignored across this rule set) as their + * {@code SHA3_*} digest counterparts in {@link DotNetSHA3}. + *
  • {@code MACTripleDES} — not an {@code HMAC} subclass. Per the official API reference, + * it derives directly from {@code KeyedHashAlgorithm} (the same base class {@code HMAC} + * itself derives from) and computes a TripleDES/CBC-keyed MAC, not a hash-based one. It only + * ever existed in .NET Framework (documented up to netframework-4.8.1 only), same legacy + * status as {@code HMACRIPEMD160}. It is included in this file because it is part of the .NET + * "keyed hash"/MAC surface that this batch covers, not because it shares an implementation + * with HMAC. + *
+ * + *

The detected value encodes the full class name (e.g., {@code "HMACSHA256"}) so the translator + * ({@code CSharpMacContextTranslator}) can resolve the inner hash algorithm. {@code MACTripleDES} + * is translated separately, by reusing the existing {@code DESede} algorithm model reinterpreted + * "as" a {@code Mac} kind — the same idiom already used elsewhere in this codebase to reinterpret + * {@code DESede} as a {@code KeyWrap} (see {@code JcaCipherMapper}/{@code BcWrapperMapper}, and the + * {@code DESede(Class, DESede)} "as-kind" constructor). Note that {@code MACTripleDES} is a plain + * TripleDES-keyed CBC-MAC construction, which is a distinct (and cryptographically weaker) + * construction from the NIST {@code CMAC}/OMAC1 standard also present in this codebase's model — it + * is therefore intentionally not modeled as {@code CMAC}. + * + *

Known gap — the {@code Key} property setter: none of these classes' {@code Key} + * property (inherited from {@code HMAC}/{@code KeyedHashAlgorithm}) is modeled as a depending rule + * here. A grep across every {@code DotNet*.java} rule file in this module found no existing + * precedent for capturing a raw {@code byte[]} property value: unlike {@code Aes.KeySize} (an + * {@code int} literal, capturable via {@code KeySizeFactory}, see {@link DotNetAES}), {@code + * hmac.Key} is virtually always assigned from a variable in realistic code (e.g. {@code hmac.Key = + * keyBytes;}), and per the engine's documented limitations ({@code CSharpSymbol}/{@code + * CSharpTreeConverter}), only literals and bare identifiers are read — the byte contents/length of + * a variable cannot be resolved this way. Recording only that "a key was set" (with no value) would + * require inventing a new marker/translation with no established counterpart elsewhere in this + * codebase, so this is left as a documented, explicit gap (TODO) rather than a guessed modeling + * decision. */ +// TODO: the `Key` property setter (`hmac.Key = ...`) is not modeled as a depending rule — see +// class javadoc "Known gap" section above for the rationale. @SuppressWarnings("java:S1192") public final class DotNetHMAC { @@ -52,6 +97,21 @@ private static IDetectionRule hmacRule(String className) { .withDependingDetectionRules(List.of()); } + // new MACTripleDES() / new MACTripleDES(byte[] key) / new MACTripleDES(string algName, byte[] + // key) + // Not an HMAC subclass (see class javadoc); modeled as a distinct MAC value, resolved to + // DESede-as-Mac by CSharpMacContextTranslator. + private static final IDetectionRule MAC_TRIPLE_DES = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("MACTripleDES") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("MACTRIPLEDES")) + .withAnyParameters() + .buildForContext(new MacContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of()); + @Nonnull public static List> rules() { return List.of( @@ -59,6 +119,11 @@ public static List> rules() { hmacRule("HMACSHA256"), hmacRule("HMACSHA384"), hmacRule("HMACSHA512"), - hmacRule("HMACMD5")); + hmacRule("HMACMD5"), + hmacRule("HMACRIPEMD160"), + hmacRule("HMACSHA3_256"), + hmacRule("HMACSHA3_384"), + hmacRule("HMACSHA3_512"), + MAC_TRIPLE_DES); } } diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetKMAC.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetKMAC.java new file mode 100644 index 000000000..0a6a9f6f3 --- /dev/null +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetKMAC.java @@ -0,0 +1,118 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.context.MacContext; +import com.ibm.engine.model.factory.ValueActionFactory; +import com.ibm.engine.rule.IDetectionRule; +import com.ibm.engine.rule.builder.DetectionRuleBuilder; +import java.util.List; +import javax.annotation.Nonnull; + +/** + * Detection rules for the KMAC family in {@code System.Security.Cryptography}. + * + *

Classes covered (a recent addition to .NET, specified by NIST SP 800-185; documented for the + * net-9.0/net-10.0/net-11.0 monikers — verified against the official API reference, see {@code + * IsSupported}, a platform-availability check intentionally not modeled here, consistent with how + * other {@code IsSupported} properties are ignored across this rule set): + * + *

    + *
  • {@code Kmac128} — fixed-output KMAC128 MAC + *
  • {@code Kmac256} — fixed-output KMAC256 MAC + *
  • {@code KmacXof128} — extendable-output (XOF) variant of KMAC128 + *
  • {@code KmacXof256} — extendable-output (XOF) variant of KMAC256 + *
+ * + *

All four classes are sealed, implement {@code IDisposable}, and share the identical + * constructor shape: {@code Kmac128(byte[] key, byte[]? customizationString = default)} (plus a + * {@code ReadOnlySpan} overload with the same parameter roles). Both the {@code key} and + * {@code customizationString} arguments are matched with {@code .withAnyParameters()}: per the + * engine's documented limitations ({@code CSharpLanguageTranslation} — no parameter-type checking; + * {@code CSharpTreeConverter} — only bare literals/identifiers are read), a {@code byte[]} key + * argument is virtually always a variable reference in realistic code and its contents/length + * cannot be resolved this way. This mirrors the exact precedent set for the {@code HMAC*} family in + * {@link DotNetHMAC} (see that class's "Known gap" javadoc for the equivalent {@code Key} property + * case). + * + *

Mapper reuse: all four classes translate to the existing {@code + * com.ibm.mapper.model.algorithms.KMAC} model class (already used by the BouncyCastle {@code KMAC} + * mapper — see {@code BcMacMapper}/{@code BcDigestMapper}), via its {@code KMAC(int + * parameterSetIdentifier, DetectionLocation)} constructor: {@code KMAC(128, ...)} for {@code + * Kmac128}/{@code KmacXof128}, {@code KMAC(256, ...)} for {@code Kmac256}/{@code KmacXof256}. No + * new mapper model class was introduced for this batch. + * + *

Known modeling gap — fixed vs. XOF variant collapse: the existing {@code KMAC} mapper + * model has no concept distinguishing the fixed-output KMAC128/256 construction (domain-separated + * internally with {@code right_encode(L)}, for a specific requested output length {@code L}) from + * the true extendable-output KMACXOF128/256 construction (domain-separated with {@code + * right_encode(0)}, per NIST SP 800-185) — its constructor always wraps a plain {@code CSHAKE} + * child regardless of which .NET class triggered detection. Consequently {@code Kmac128} and {@code + * KmacXof128} both translate to an identical node ({@code asString() == "KMAC128"}), and likewise + * for the 256-bit pair. The raw detected value string ({@code "KMAC128"} vs. {@code "KMACXOF128"}, + * i.e. {@link com.ibm.engine.model.ValueAction#asString()} before translation) still distinguishes + * the four .NET classes at the detection-store level, so this is a translation-layer precision gap, + * not a detection gap — it is documented here rather than fixed by inventing a new mapper model + * class. See {@code CSharpMacContextTranslator} for the translation switch. + * + *

Why the output-length parameter is not modeled as a depending rule: like {@code + * Shake128}/{@code Shake256} in {@link DotNetSHA3}, all four KMAC classes expose {@code + * GetHashAndReset(int outputLength)}/{@code GetCurrentHash(int outputLength)}/{@code HashData(..., + * int outputLength, ...)} overloads (in addition to span-based overloads) whose length argument is + * an arbitrary per-call output-size request, not additional information about the algorithm itself + * — the "128"/"256" in the class name is the security-strength parameter, already captured by the + * creation rule. No depending rules are attached for these methods, or for {@code + * AppendData}/{@code Verify}/{@code VerifyCurrentHash}/{@code VerifyHashAndReset}/{@code Clone}, + * consistent with the same convention already established for {@code Shake128}/{@code Shake256} and + * the {@code HMAC*} family (see {@link DotNetHMAC}), where no depending rules are attached for the + * equivalent hash-computation methods either. + */ +@SuppressWarnings("java:S1192") +public final class DotNetKMAC { + + private DotNetKMAC() { + // nothing + } + + // new Kmac128(key) / new Kmac128(key, customizationString) — and the KmacXof128/256, Kmac256 + // siblings. Both constructor overloads (byte[], byte[]) and (ReadOnlySpan, + // ReadOnlySpan) are covered by withAnyParameters(). + private static IDetectionRule kmacRule(String className) { + return new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(className) + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>(className.toUpperCase())) + .withAnyParameters() + .buildForContext(new MacContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of()); + } + + @Nonnull + public static List> rules() { + return List.of( + kmacRule("Kmac128"), + kmacRule("Kmac256"), + kmacRule("KmacXof128"), + kmacRule("KmacXof256")); + } +} diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetKeyDerivation.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetKeyDerivation.java new file mode 100644 index 000000000..542f714a8 --- /dev/null +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetKeyDerivation.java @@ -0,0 +1,316 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import com.ibm.engine.detection.MethodMatcher; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.context.DigestContext; +import com.ibm.engine.model.context.KeyContext; +import com.ibm.engine.model.factory.AlgorithmFactory; +import com.ibm.engine.model.factory.IterationCountFactory; +import com.ibm.engine.model.factory.ValueActionFactory; +import com.ibm.engine.rule.IDetectionRule; +import com.ibm.engine.rule.builder.DetectionRuleBuilder; +import java.util.List; +import java.util.Map; +import javax.annotation.Nonnull; + +/** + * Detection rules for the KDF (key derivation function) family in System.Security.Cryptography, + * excluding {@code Rfc2898DeriveBytes} (covered separately in {@link DotNetRfc2898DeriveBytes}). + * + *

Classes covered: + * + *

    + *
  • {@code HKDF} — RFC 5869 HMAC-based Extract-and-Expand KDF. Static-only class (no + * constructor / no instance): {@code HKDF.Extract(...)}, {@code HKDF.Expand(...)}, {@code + * HKDF.DeriveKey(...)} (each with a {@code byte[]} and a {@code Span}-based overload). + *
  • {@code SP800108HmacCounterKdf} — NIST SP 800-108 HMAC counter-mode KDF (KBKDF). Both + * constructor-based ({@code new SP800108HmacCounterKdf(key, hashAlgorithm)} + instance-method + * {@code DeriveKey}/{@code DeriveBytes}) and a fully static one-shot overload ({@code + * SP800108HmacCounterKdf.DeriveBytes(key, hashAlgorithm, label, context, length)} — no + * instance required). + *
  • {@code PasswordDeriveBytes} — legacy PBKDF1 extension (per the official API reference: + * "This class uses an extension of the PBKDF1 algorithm"). Constructor-based, with instance + * methods {@code GetBytes(int)} (marked {@code Obsolete} since .NET Core, still detectable + * source) and {@code CryptDeriveKey(string, string, int, byte[])}. + *
+ * + *

Modeling decision — why HKDF/SP800108's static overloads are top-level rules, not depending + * rules: the Batch 3 pattern established in {@link DotNetECDiffieHellman} (a creation rule for + * the object, with derive-operations as depending rules attached to it, translated to the generic + * {@code KeyDerivation} functionality node) assumes there is an object instance to track between + * "creation" and "operation". {@code HKDF} has no such instance — every method is {@code static}, + * and a single static call (e.g. {@code HKDF.DeriveKey(...)}) is both the "creation" and the + * "operation" at once, with nothing to chain afterwards. The same is true for {@code + * SP800108HmacCounterKdf.DeriveBytes(...)}, which is static and self-contained (unlike its sibling + * instance methods). For these static, self-contained calls this file follows the {@code + * Rfc2898DeriveBytes} idiom instead: the call is captured directly as a top-level {@code + * ValueActionFactory} mapping straight to the KDF algorithm model node ({@code HKDF} / {@code + * KDFCounter}), exactly like {@code new Rfc2898DeriveBytes(...)} maps directly to {@code PBKDF2} + * with no intermediate {@code KeyDerivation} functionality wrapper — because the class itself *is* + * the KDF, unlike {@code ECDiffieHellman} which is a general-purpose key-agreement primitive that + * only sometimes derives a key. + * + *

Where an actual object instance *is* tracked ({@code SP800108HmacCounterKdf}'s constructor + + * instance {@code DeriveKey}, and {@code PasswordDeriveBytes}'s constructor + instance {@code + * GetBytes}/{@code CryptDeriveKey}), the Batch 3 pattern is reused as instructed: each derive + * operation is a depending rule capturing {@code ValueActionFactory<>()} under its own + * {@code KeyContext} "kind", dispatched in {@code CSharpKeyContextTranslator} to the generic {@code + * KeyDerivation} functionality node — mirroring {@code AES_GENERATE_KEY}/{@code AES_GENERATE_IV} in + * {@link DotNetAES} (an operation captured as a child of an already-identified algorithm) more + * closely than the ECDH case, since here the parent node is already a concrete KDF algorithm, not + * an ambiguous key-agreement primitive. + * + *

As with every other file in this rule set, the ANTLR4-based C# engine cannot resolve parameter + * types (see {@code CSharpLanguageTranslation}), so all overloads that only differ by {@code + * byte[]} vs. {@code ReadOnlySpan}/{@code Span}, {@code string} vs. {@code + * ReadOnlySpan}, or the presence of an output-buffer parameter are collapsed into a single + * {@code withAnyParameters()} rule per method name. The {@code HashAlgorithmName} parameter that + * several of these methods take (e.g. {@code HashAlgorithmName.SHA256}) is not decoded into a + * digest child node — consistent with {@code DotNetECDsa}/{@code DotNetECDiffieHellman}, which + * leave the same parameter opaque on {@code SignData}/{@code DeriveKeyFromHash}, since only + * literals and bare identifiers are readable by the engine (see {@code CSharpTreeConverter}), not + * static member-access expressions like {@code HashAlgorithmName.SHA256}. + * + *

Known gap — {@code SP800108HmacCounterKdf.DeriveBytes} (instance overload): only the + * instance {@code DeriveKey} overloads are modeled as depending rules; the instance {@code + * DeriveBytes} overloads (which take the label/context/hash-algorithm again, redundantly with the + * constructor) exist per the API reference but are not separately covered here — {@code DeriveKey} + * already exercises the same depending-rule path and this avoids an unnecessary near-duplicate rule + * for a method that behaves identically for detection purposes (an opaque {@code + * withAnyParameters()} call site). Not a gap in coverage of the class's cryptographic identity or + * of the "a key was derived" signal — only a granularity choice, called out here rather than + * decided silently. + * + *

Modeling decision — {@code PasswordDeriveBytes}'s {@code HashName}/{@code IterationCount}/ + * {@code Salt} property setters: unlike {@code HMAC.Key} (see {@code DotNetHMAC}'s "Known gap" + * javadoc, {@code byte[]}-typed, never a literal in realistic code), {@code HashName} ({@code + * string}) and {@code IterationCount} ({@code int}) are verified (learn.microsoft.com, + * PasswordDeriveBytes class reference) to be plain {@code get; set;} properties of literal-friendly + * types — structurally identical to {@code Aes.KeySize}/{@code Aes.Mode} in {@link DotNetAES}, + * which are modeled as synthetic {@code set_X(literal)} depending rules. {@code pdb.IterationCount + * = 100000;} and {@code pdb.HashName = "SHA256";} are realistic, common patterns (raising the + * iteration count / picking a stronger digest are the two knobs this legacy PBKDF1 API exposes), so + * both are modeled below: {@code PDB_SET_ITERATION_COUNT} reuses {@code IterationCountFactory} (the + * same factory already used by {@code GoCryptoPBKDF2} for the equivalent Go PBKDF2 {@code iter} + * parameter — see {@code IterationCountFactory}/{@code GoKeyContextTranslator}), translated to the + * mapper's {@code NumberOfIterations} node. {@code PDB_SET_HASH_NAME} reuses {@code + * AlgorithmFactory} (already used elsewhere in this module for {@code + * HashAlgorithm.Create(string)}, see {@code DotNetAlgorithmFactory}) under a {@code DigestContext} + * rather than {@code KeyContext} — a depending rule is free to declare any context via {@code + * buildForContext(...)}; dispatch in {@code CSharpTranslator} is by each finding's own declared + * context, not its parent's, and this exact cross-context nesting (a {@code DigestContext} child + * rule attached under a {@code KeyContext} parent) is already precedented by the Go module (see + * {@code GoCryptoPBKDF2}'s {@code KEY_STDLIB} rule, which attaches {@code GoCryptoHash.rules()} — + * all {@code DigestContext}-based — as depending rules of a {@code KeyContext} PBKDF2 rule). This + * lets {@code CSharpDigestContextTranslator} resolve the captured hash-name string (e.g. {@code + * "SHA256"} -> {@code SHA2(256)}) with its existing, already-tested {@code Algorithm} branch, + * with no new translation code needed. + * + *

{@code Salt} ({@code byte[]}), by contrast, is deliberately not modeled: it is + * structurally identical to the excluded {@code HMAC.Key} case above (a byte-array-typed property + * that is realistically always assigned from a variable, e.g. {@code pdb.Salt = saltBytes;}, never + * a literal the ANTLR4 engine can read), so recording only that "a Salt was set" (no value) would + * require the same unprecedented valueless-marker mechanism rejected for {@code HMAC.Key} — this is + * an intentional, documented exclusion, not an oversight. + */ +@SuppressWarnings("java:S1192") +public final class DotNetKeyDerivation { + + private DotNetKeyDerivation() { + // nothing + } + + // ========================================================================= + // HKDF — static-only class, no instance. Each static method is its own + // top-level rule (see class javadoc "Modeling decision"). + // ========================================================================= + + // HKDF.Extract(hashAlgorithmName, ikm, salt) / Extract(hashAlgorithmName, ikm, salt, prk) + private static final IDetectionRule HKDF_EXTRACT = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("HKDF") + .forMethods("Extract") + .shouldBeDetectedAs(new ValueActionFactory<>("HKDF")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "KDF_HKDF"))) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // HKDF.Expand(hashAlgorithmName, prk, outputLength, info) / Expand(..., output, info) + private static final IDetectionRule HKDF_EXPAND = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("HKDF") + .forMethods("Expand") + .shouldBeDetectedAs(new ValueActionFactory<>("HKDF")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "KDF_HKDF"))) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // HKDF.DeriveKey(hashAlgorithmName, ikm, outputLength, salt, info) — full Extract+Expand + private static final IDetectionRule HKDF_DERIVE_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("HKDF") + .forMethods("DeriveKey") + .shouldBeDetectedAs(new ValueActionFactory<>("HKDF")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "KDF_HKDF"))) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // SP800108HmacCounterKdf — instance derive-operation depending rules, reusing the Batch 3 + // KeyDerivation pattern (see class javadoc). + // ========================================================================= + + // kdf.DeriveKey(label, context, keyLengthBytes) — instance method, all overloads + // (byte[]/ReadOnlySpan/ReadOnlySpan/string label+context, Int32/Span output) + private static final IDetectionRule SP800108_DERIVE_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DeriveKey") + .shouldBeDetectedAs(new ValueActionFactory<>("DeriveKey")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "KDF_SP800108_DERIVE_KEY"))) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + private static final List> SP800108_DEPENDING_RULES = + List.of(SP800108_DERIVE_KEY); + + // new SP800108HmacCounterKdf(key, hashAlgorithmName) — byte[] or ReadOnlySpan key + private static final IDetectionRule SP800108_CTOR = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("SP800108HmacCounterKdf") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("SP800108")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "KDF_SP800108"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(SP800108_DEPENDING_RULES); + + // SP800108HmacCounterKdf.DeriveBytes(key, hashAlgorithmName, label, context, keyLengthBytes) — + // fully static one-shot overload, no instance required (see class javadoc "Modeling decision"). + private static final IDetectionRule SP800108_STATIC_DERIVE_BYTES = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("SP800108HmacCounterKdf") + .forMethods("DeriveBytes") + .shouldBeDetectedAs(new ValueActionFactory<>("SP800108")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "KDF_SP800108"))) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // PasswordDeriveBytes — legacy PBKDF1 extension. Constructor-based, with instance + // derive-operation depending rules reusing the Batch 3 KeyDerivation pattern. + // ========================================================================= + + // pdb.GetBytes(cb) — Obsolete since .NET Core, still valid/detectable legacy source + private static final IDetectionRule PDB_GET_BYTES = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("GetBytes") + .shouldBeDetectedAs(new ValueActionFactory<>("GetBytes")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "KDF_PDB_GET_BYTES"))) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // pdb.CryptDeriveKey(algName, algHashName, keySize, rgbIV) + private static final IDetectionRule PDB_CRYPT_DERIVE_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CryptDeriveKey") + .shouldBeDetectedAs(new ValueActionFactory<>("CryptDeriveKey")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "KDF_PDB_CRYPT_DERIVE_KEY"))) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // pdb.IterationCount = 100000 → synthetic set_IterationCount(100000) + // (see class javadoc "Modeling decision" for why this is captured, unlike HMAC.Key/PDB's Salt) + private static final IDetectionRule PDB_SET_ITERATION_COUNT = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_IterationCount") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new IterationCountFactory<>()) + .buildForContext(new KeyContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // pdb.HashName = "SHA256" → synthetic set_HashName("SHA256") + // Uses DigestContext (not KeyContext) so CSharpDigestContextTranslator's existing + // Algorithm branch resolves the digest name — see class javadoc "Modeling decision". + private static final IDetectionRule PDB_SET_HASH_NAME = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_HashName") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new AlgorithmFactory<>()) + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + private static final List> PDB_DEPENDING_RULES = + List.of( + PDB_GET_BYTES, + PDB_CRYPT_DERIVE_KEY, + PDB_SET_ITERATION_COUNT, + PDB_SET_HASH_NAME); + + // new PasswordDeriveBytes(password, salt[, hashName, iterations][, cspParams]) — 8 + // constructor overloads (password as string or byte[]; optional hashName/iterations; + // optional CspParameters), all collapsed via withAnyParameters(). + private static final IDetectionRule PASSWORD_DERIVE_BYTES_CTOR = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("PasswordDeriveBytes") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("PBKDF1")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "KDF_PASSWORD_DERIVE_BYTES"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(PDB_DEPENDING_RULES); + + @Nonnull + public static List> rules() { + return List.of( + HKDF_EXTRACT, + HKDF_EXPAND, + HKDF_DERIVE_KEY, + SP800108_CTOR, + SP800108_STATIC_DERIVE_BYTES, + PASSWORD_DERIVE_BYTES_CTOR); + } +} diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetLegacyFormatters.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetLegacyFormatters.java new file mode 100644 index 000000000..398702487 --- /dev/null +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetLegacyFormatters.java @@ -0,0 +1,450 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import com.ibm.engine.detection.MethodMatcher; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.CipherAction; +import com.ibm.engine.model.SignatureAction; +import com.ibm.engine.model.context.CipherContext; +import com.ibm.engine.model.context.DigestContext; +import com.ibm.engine.model.context.KeyContext; +import com.ibm.engine.model.context.SignatureContext; +import com.ibm.engine.model.factory.AlgorithmFactory; +import com.ibm.engine.model.factory.CipherActionFactory; +import com.ibm.engine.model.factory.PaddingFactory; +import com.ibm.engine.model.factory.SignatureActionFactory; +import com.ibm.engine.model.factory.ValueActionFactory; +import com.ibm.engine.rule.IDetectionRule; +import com.ibm.engine.rule.builder.DetectionRuleBuilder; +import java.util.List; +import java.util.Map; +import javax.annotation.Nonnull; + +/** + * Detection rules for the legacy Formatter/Deformatter and mask-generation classes in {@code + * System.Security.Cryptography}. + * + *

Classes covered (all nine, all still present through net-11.0 per the official API reference — + * none were removed in modern .NET): + * + *

    + *
  • {@code DSASignatureFormatter} / {@code DSASignatureDeformatter} — create/verify a DSA + * signature from a caller-supplied hash + *
  • {@code RSAPKCS1SignatureFormatter} / {@code RSAPKCS1SignatureDeformatter} — create/verify + * an RSA PKCS #1 v1.5 signature from a caller-supplied hash + *
  • {@code RSAOAEPKeyExchangeFormatter} / {@code RSAOAEPKeyExchangeDeformatter} — + * encrypt/decrypt a symmetric key using RSA-OAEP ("key exchange") + *
  • {@code RSAPKCS1KeyExchangeFormatter} / {@code RSAPKCS1KeyExchangeDeformatter} — + * encrypt/decrypt a symmetric key using RSA PKCS #1 v1.5 ("key exchange") + *
  • {@code PKCS1MaskGenerationMethod} — the PKCS #1 (MGF1) mask generation function used + * internally by OAEP implementations + *
+ * + *

Architecture: each Formatter/Deformatter constructor is modeled exactly like the primary + * creation rules in {@link DotNetDSA} / {@link DotNetRSA} — a top-level {@link ValueActionFactory} + * of {@code "DSA"} or {@code "RSA"} under {@link KeyContext}{@code (kind=DSA/RSA)}, which is + * already translated by the existing {@code CSharpKeyContextTranslator} switch (no translator + * changes needed for those two). The class's two constructors ({@code ()} and {@code + * (AsymmetricAlgorithm)}) are collapsed into a single {@code withAnyParameters()} rule, mirroring + * {@code DSA_CNG}/{@code RSA_CNG} in the referenced files. + * + *

{@code CreateSignature}/{@code VerifySignature} are modeled as depending rules using + * {@link SignatureActionFactory} ({@code SIGN}/{@code VERIFY}) under {@link SignatureContext}, with + * {@code withAnyParameters()} — deliberately not attempting to also capture the PKCS #1 v1.5 + * padding implied by the RSA classes' names, mirroring {@link DotNetRSA}'s own {@code + * RSA_SIGN_DATA}/{@code RSA_VERIFY_DATA} rules, which likewise never capture the {@code + * RSASignaturePadding} parameter of the modern {@code RSA.SignData}/{@code VerifyData} API. There + * is also no existing precedent anywhere in this codebase (Java or C#) for attaching a {@code + * Padding} value under a {@code SignatureContext} detection, so doing so here would require + * inventing new translation code for a single, low-priority call site. + * + *

{@code SetHashAlgorithm(string)} (on all four Signature Formatter/Deformatter classes) + * takes a literal hash-algorithm name (e.g. {@code "SHA1"}, {@code "SHA256"}) — unlike most + * parameters elsewhere in this module, this one is directly readable by the ANTLR4-based engine + * (see {@code CSharpTreeConverter}, which resolves string literals). It is captured with the + * existing {@link AlgorithmFactory} under {@link DigestContext}, reusing the {@code Algorithm} + * branch already present in {@code CSharpDigestContextTranslator} (no translator changes needed). + * + *

{@code CreateKeyExchange}/{@code DecryptKeyExchange} (on the four KeyExchange + * Formatter/Deformatter classes) are modeled as {@link CipherActionFactory} ({@code + * CipherAction.Action.ENCRYPT}/{@code DECRYPT}) under {@link CipherContext}, with a constant {@link + * PaddingFactory} ({@code "OAEP"} or {@code "PKCS1Padding"}) attached to the first parameter — + * exactly the same "constant value keyed off which rule/method matched" pattern {@link DotNetAES} + * uses for {@code ModeFactory<>("CBC")} on {@code EncryptCbc}/{@code DecryptCbc}. Both padding + * strings are already understood by the existing {@code JcaPaddingMapper} reused via {@code + * CSharpCipherContextTranslator}'s {@code Padding} branch ({@code "OAEP"} routes to {@code + * JcaOAEPPaddingMapper}, {@code "PKCS1Padding"} matches its {@code PKCS1} case exactly), so no + * translator changes were needed for this either. Unlike the signature padding case above, this is + * not blocked by the "no parameter value resolution" engine limitation at all: the padding scheme + * here is a compile-time fact of which concrete class was instantiated (its class name), not a + * runtime argument that would need to be resolved. + * + *

Open design question — {@code CipherAction.Action.WRAP} vs. {@code ENCRYPT}/{@code DECRYPT} + * for key exchange: the engine's {@code CipherAction.Action} enum already defines {@code WRAP}, + * used by {@code JcaCipherWrap} (Java, {@code Cipher.WRAP_MODE}) and {@code PycaWrapping} (Python). + * However, its translation is not consistent between those two: {@code + * JavaCipherContextTranslator} maps {@code WRAP} to the generic {@code Encapsulate} functionality, + * while {@code PycaCipherContextTranslator} maps it to a {@code KeyWrap}-typed algorithm variant + * instead of an operation node at all. {@code CSharpCipherContextTranslator} does not handle {@code + * WRAP} at present (falls through to {@code default -> Optional.empty()}). Given this pre-existing + * inconsistency, this file deliberately uses the already-wired {@code ENCRYPT}/{@code DECRYPT} + * actions instead — which also matches the fact that both {@code RSAOAEPKeyExchangeFormatter} and + * {@code RSAPKCS1KeyExchangeFormatter}'s reference-source implementations literally delegate to + * {@code RSA.Encrypt}/{@code RSA.Decrypt} internally, the exact same operation {@link DotNetRSA}'s + * own {@code RSA_ENCRYPT}/{@code RSA_DECRYPT} rules already model with {@code ENCRYPT}/{@code + * DECRYPT}. Should a unified cross-language "key wrap" CBOM concept ever be settled on, these four + * rules (the {@code CREATE_KEY_EXCHANGE_*}/{@code DECRYPT_KEY_EXCHANGE_*} constants below) are the + * ones to revisit. + * + *

{@code PKCS1MaskGenerationMethod} reuses the existing MGF1 concept already present in + * the mapper model ({@code mapper/model/algorithms/MGF1.java}, implementing {@code + * MaskGenerationFunction} — already used by {@code JcaMGFMapper}/{@code JcaOAEPPaddingMapper} for + * JCA's OAEP padding translation), rather than inventing a new one. Its sole constructor is + * captured as {@code ValueActionFactory<>("MGF1")} under {@code KeyContext(kind=MGF1)} — reusing + * {@link KeyContext} as the generic "identify which standalone algorithm was just constructed" + * context already used in this module for RSA/DSA/ECDSA/ECDH/X25519/ML-KEM/ML-DSA/SLH-DSA/KDF + * variants (see {@code CSharpKeyContextTranslator}), not because MGF1 is a "key" algorithm in the + * strict sense. This required one small additive case in {@code + * CSharpKeyContextTranslator}'s existing kind switch: {@code case "MGF1" -> Optional.of(new + * MGF1(detectionLocation))}. Its {@code HashName} property (compiles to a synthetic {@code + * set_HashName(string)} call, see {@code CSharpTreeConverter}) is captured with the same {@link + * AlgorithmFactory}/{@link DigestContext} pattern as {@code SetHashAlgorithm} above, producing a + * digest child of the MGF1 node. Per the official reference, "This class is only used by + * implementations of key exchange algorithms for mask generation. Application code does not use + * this class directly" — so {@code GenerateMask(byte[], int)} is intentionally not modeled + * as a depending rule: like {@code DotNetSHA}'s {@code ComputeHash} (see that class's own javadoc), + * invoking it adds no cryptographic information beyond what the constructor already captured (the + * mask-generation algorithm is always MGF1; only the hash feeding it, already captured via {@code + * HashName}, varies). + * + *

Known gap — {@code SetKey(AsymmetricAlgorithm)}: present on all eight + * Formatter/Deformatter classes, this method is typically called with a variable holding a + * previously created {@code DSA}/{@code RSA} instance (e.g. {@code formatter.SetKey(dsa)} where + * {@code dsa} came from {@code DSA.Create()} in an earlier statement). Per the engine's documented + * limitations ({@code CSharpSymbol} — "Full symbol tracking across scopes is not supported"; {@code + * CSharpLanguageTranslation} — no parameter type resolution), the concrete key instance behind such + * a variable cannot be resolved and linked to this call. This is a known, unavoidable gap: {@code + * SetKey} is therefore intentionally not modeled as a detection rule here (no rule would + * ever usefully fire), rather than faking a value. The Formatter/Deformatter class itself is still + * fully detected in isolation via its constructor (which already identifies the algorithm family — + * DSA or RSA — from the class name alone, independent of {@code SetKey}). The same reasoning + * applies to the non-crypto-identity-bearing {@code Rng}/{@code Parameter}/{@code Parameters} + * properties on the KeyExchange formatters ({@code Rng} in particular would need to resolve a + * {@code new SomeRng()} expression assigned via a property setter, which {@code + * CSharpTreeConverter}'s {@code convertPrimaryExpressionStart} does not handle for object-creation + * right-hand sides — only literals and bare identifiers), so they are likewise left undetected. + */ +@SuppressWarnings("java:S1192") +public final class DotNetLegacyFormatters { + + private DotNetLegacyFormatters() { + // nothing + } + + // ========================================================================= + // Shared depending rules: signature operations (DSA + RSA PKCS#1 formatters) + // ========================================================================= + + // formatter.CreateSignature(hash) — byte[] override or inherited HashAlgorithm override, + // indistinguishable to this engine (see class javadoc: no padding capture, mirrors DotNetRSA). + private static final IDetectionRule CREATE_SIGNATURE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateSignature") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.SIGN)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // deformatter.VerifySignature(hash, signature) — byte[] or inherited HashAlgorithm override. + private static final IDetectionRule VERIFY_SIGNATURE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("VerifySignature") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.VERIFY)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // (de)formatter.SetHashAlgorithm("SHA256") — literal string, shared across all four + // Signature Formatter/Deformatter classes (same method name and semantics on each). + private static final IDetectionRule SET_HASH_ALGORITHM = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("SetHashAlgorithm") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new AlgorithmFactory<>()) + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + private static final List> SIGN_DEPENDING_RULES = + List.of(CREATE_SIGNATURE, SET_HASH_ALGORITHM); + + private static final List> VERIFY_DEPENDING_RULES = + List.of(VERIFY_SIGNATURE, SET_HASH_ALGORITHM); + + // ========================================================================= + // Depending rules: RSA key exchange operations (OAEP and PKCS#1 formatters) + // Constant padding value attached from which rule/arity matched (class name), not from a + // resolved parameter value — see class javadoc. + // ========================================================================= + + // oaepFormatter.CreateKeyExchange(rgbData) + private static final IDetectionRule CREATE_KEY_EXCHANGE_OAEP_1 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateKeyExchange") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.ENCRYPT)) + .withMethodParameter(MethodMatcher.ANY) // rgbData (symmetric key material) + .shouldBeDetectedAs(new PaddingFactory<>("OAEP")) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // oaepFormatter.CreateKeyExchange(rgbData, symAlgType) + private static final IDetectionRule CREATE_KEY_EXCHANGE_OAEP_2 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateKeyExchange") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.ENCRYPT)) + .withMethodParameter(MethodMatcher.ANY) // rgbData + .shouldBeDetectedAs(new PaddingFactory<>("OAEP")) + .withMethodParameter(MethodMatcher.ANY) // symAlgType + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // oaepDeformatter.DecryptKeyExchange(rgbData) — single overload per the official reference. + private static final IDetectionRule DECRYPT_KEY_EXCHANGE_OAEP = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptKeyExchange") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.DECRYPT)) + .withMethodParameter(MethodMatcher.ANY) // rgbIn + .shouldBeDetectedAs(new PaddingFactory<>("OAEP")) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // pkcs1Formatter.CreateKeyExchange(rgbData) + private static final IDetectionRule CREATE_KEY_EXCHANGE_PKCS1_1 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateKeyExchange") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.ENCRYPT)) + .withMethodParameter(MethodMatcher.ANY) // rgbData + .shouldBeDetectedAs(new PaddingFactory<>("PKCS1Padding")) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // pkcs1Formatter.CreateKeyExchange(rgbData, symAlgType) + private static final IDetectionRule CREATE_KEY_EXCHANGE_PKCS1_2 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateKeyExchange") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.ENCRYPT)) + .withMethodParameter(MethodMatcher.ANY) // rgbData + .shouldBeDetectedAs(new PaddingFactory<>("PKCS1Padding")) + .withMethodParameter(MethodMatcher.ANY) // symAlgType + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // pkcs1Deformatter.DecryptKeyExchange(rgbIn) — single overload. + private static final IDetectionRule DECRYPT_KEY_EXCHANGE_PKCS1 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptKeyExchange") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.DECRYPT)) + .withMethodParameter(MethodMatcher.ANY) // rgbIn + .shouldBeDetectedAs(new PaddingFactory<>("PKCS1Padding")) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + private static final List> OAEP_FORMATTER_DEPENDING_RULES = + List.of(CREATE_KEY_EXCHANGE_OAEP_1, CREATE_KEY_EXCHANGE_OAEP_2); + + private static final List> OAEP_DEFORMATTER_DEPENDING_RULES = + List.of(DECRYPT_KEY_EXCHANGE_OAEP); + + private static final List> PKCS1_KX_FORMATTER_DEPENDING_RULES = + List.of(CREATE_KEY_EXCHANGE_PKCS1_1, CREATE_KEY_EXCHANGE_PKCS1_2); + + private static final List> PKCS1_KX_DEFORMATTER_DEPENDING_RULES = + List.of(DECRYPT_KEY_EXCHANGE_PKCS1); + + // ========================================================================= + // Depending rule: PKCS1MaskGenerationMethod.HashName property setter + // ========================================================================= + + // mgf.HashName = "SHA256" → synthetic set_HashName("SHA256") + private static final IDetectionRule MGF1_SET_HASH_NAME = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_HashName") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new AlgorithmFactory<>()) + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // Primary creation rules + // ========================================================================= + + // new DSASignatureFormatter() / new DSASignatureFormatter(AsymmetricAlgorithm) + private static final IDetectionRule DSA_SIGNATURE_FORMATTER = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("DSASignatureFormatter") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("DSA")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "DSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(SIGN_DEPENDING_RULES); + + // new DSASignatureDeformatter() / new DSASignatureDeformatter(AsymmetricAlgorithm) + private static final IDetectionRule DSA_SIGNATURE_DEFORMATTER = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("DSASignatureDeformatter") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("DSA")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "DSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(VERIFY_DEPENDING_RULES); + + // new RSAPKCS1SignatureFormatter() / new RSAPKCS1SignatureFormatter(AsymmetricAlgorithm) + private static final IDetectionRule RSA_PKCS1_SIGNATURE_FORMATTER = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("RSAPKCS1SignatureFormatter") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("RSA")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "RSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(SIGN_DEPENDING_RULES); + + // new RSAPKCS1SignatureDeformatter() / new RSAPKCS1SignatureDeformatter(AsymmetricAlgorithm) + private static final IDetectionRule RSA_PKCS1_SIGNATURE_DEFORMATTER = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("RSAPKCS1SignatureDeformatter") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("RSA")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "RSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(VERIFY_DEPENDING_RULES); + + // new RSAOAEPKeyExchangeFormatter() / new RSAOAEPKeyExchangeFormatter(AsymmetricAlgorithm) + private static final IDetectionRule RSA_OAEP_KEY_EXCHANGE_FORMATTER = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("RSAOAEPKeyExchangeFormatter") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("RSA")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "RSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(OAEP_FORMATTER_DEPENDING_RULES); + + // new RSAOAEPKeyExchangeDeformatter() / new RSAOAEPKeyExchangeDeformatter(AsymmetricAlgorithm) + private static final IDetectionRule RSA_OAEP_KEY_EXCHANGE_DEFORMATTER = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("RSAOAEPKeyExchangeDeformatter") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("RSA")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "RSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(OAEP_DEFORMATTER_DEPENDING_RULES); + + // new RSAPKCS1KeyExchangeFormatter() / new RSAPKCS1KeyExchangeFormatter(AsymmetricAlgorithm) + private static final IDetectionRule RSA_PKCS1_KEY_EXCHANGE_FORMATTER = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("RSAPKCS1KeyExchangeFormatter") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("RSA")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "RSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(PKCS1_KX_FORMATTER_DEPENDING_RULES); + + // new RSAPKCS1KeyExchangeDeformatter() / new + // RSAPKCS1KeyExchangeDeformatter(AsymmetricAlgorithm) + private static final IDetectionRule RSA_PKCS1_KEY_EXCHANGE_DEFORMATTER = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("RSAPKCS1KeyExchangeDeformatter") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("RSA")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "RSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(PKCS1_KX_DEFORMATTER_DEPENDING_RULES); + + // new PKCS1MaskGenerationMethod() — single, parameterless constructor. + private static final IDetectionRule PKCS1_MASK_GENERATION_METHOD = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("PKCS1MaskGenerationMethod") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("MGF1")) + .withoutParameters() + .buildForContext(new KeyContext(Map.of("kind", "MGF1"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of(MGF1_SET_HASH_NAME)); + + @Nonnull + public static List> rules() { + return List.of( + DSA_SIGNATURE_FORMATTER, + DSA_SIGNATURE_DEFORMATTER, + RSA_PKCS1_SIGNATURE_FORMATTER, + RSA_PKCS1_SIGNATURE_DEFORMATTER, + RSA_OAEP_KEY_EXCHANGE_FORMATTER, + RSA_OAEP_KEY_EXCHANGE_DEFORMATTER, + RSA_PKCS1_KEY_EXCHANGE_FORMATTER, + RSA_PKCS1_KEY_EXCHANGE_DEFORMATTER, + PKCS1_MASK_GENERATION_METHOD); + } +} diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetMLDsa.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetMLDsa.java new file mode 100644 index 000000000..1b07cbfd9 --- /dev/null +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetMLDsa.java @@ -0,0 +1,555 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import com.ibm.engine.detection.MethodMatcher; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.SignatureAction; +import com.ibm.engine.model.context.KeyContext; +import com.ibm.engine.model.context.SignatureContext; +import com.ibm.engine.model.factory.ParameterIdentifierFactory; +import com.ibm.engine.model.factory.SignatureActionFactory; +import com.ibm.engine.model.factory.ValueActionFactory; +import com.ibm.engine.rule.IDetectionRule; +import com.ibm.engine.rule.builder.DetectionRuleBuilder; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import javax.annotation.Nonnull; + +/** + * Detection rules for ML-DSA (Module-Lattice-Based Digital Signature Algorithm, FIPS 204) and + * Composite ML-DSA usage in {@code System.Security.Cryptography}. + * + *

API availability (verified against the official Microsoft Learn API reference, not + * assumed): {@code MLDsa}, {@code MLDsaCng}, {@code MLDsaOpenSsl}, {@code MLDsaAlgorithm}, + * {@code CompositeMLDsa}, {@code CompositeMLDsaCng} and {@code CompositeMLDsaAlgorithm} are all + * documented for the {@code net-10.0} and {@code net-11.0} monikers (the {@code net-11.0} page + * redirects/renders identically to {@code net-10.0} content — {@code defaultMoniker: net-10.0} — + * both listing "Assembly: System.Security.Cryptography.dll" as in-box, plus "Assembly: + * Microsoft.Bcl.Cryptography.dll" — package {@code Microsoft.Bcl.Cryptography + * v11.0.0-preview.7.26381.103} — as a back-compat NuGet shim for the non-OpenSSL types on older + * TFMs; {@code MLDsaOpenSsl} lists only the in-box assembly, mirroring {@code MLKemOpenSsl}). The + * plain {@code MLDsa}/{@code MLDsaCng}/{@code MLDsaOpenSsl} surface carries no {@code + * [Experimental]} attribute. {@code CompositeMLDsa}, {@code CompositeMLDsaCng} and {@code + * CompositeMLDsaAlgorithm}, by contrast, are each marked {@code + * [System.Diagnostics.CodeAnalysis.Experimental("SYSLIB5006")]} at the class level — confirming + * Composite ML-DSA specifically (unlike plain ML-DSA) is still a preview API as of .NET 10/11. + * + *

Classes covered: + * + *

    + *
  • {@code MLDsa} — abstract base. All of its members are {@code static} factory methods (the + * only listed constructor, {@code MLDsa(MLDsaAlgorithm)}, is for derived classes, not called + * by ordinary consumer code): {@code GenerateKey(MLDsaAlgorithm)}, {@code + * ImportMLDsaPrivateKey(MLDsaAlgorithm, byte[]/ReadOnlySpan<byte>)}, {@code + * ImportMLDsaPrivateSeed(MLDsaAlgorithm, byte[]/ReadOnlySpan<byte>)}, {@code + * ImportMLDsaPublicKey(MLDsaAlgorithm, byte[]/ReadOnlySpan<byte>)}, {@code + * ImportPkcs8PrivateKey(...)}, {@code ImportSubjectPublicKeyInfo(...)}, {@code + * ImportFromPem(...)}, {@code ImportEncryptedPkcs8PrivateKey(...)}, {@code + * ImportFromEncryptedPem(...)}. + *
  • {@code MLDsaCng} — CNG-backed implementation ({@code MLDsaCng(CngKey)} constructor). + *
  • {@code MLDsaOpenSsl} — OpenSSL-backed implementation ({@code + * MLDsaOpenSsl(SafeEvpPKeyHandle)} constructor). + *
  • {@code CompositeMLDsa} — abstract base for Composite ML-DSA (FIPS 204 ML-DSA combined with + * a traditional algorithm — RSA/ECDSA/Ed25519/Ed448). Structurally mirrors {@code MLDsa}: + * {@code GenerateKey(CompositeMLDsaAlgorithm)}, {@code + * ImportCompositeMLDsaPrivateKey(CompositeMLDsaAlgorithm, byte[]/ReadOnlySpan<byte>)}, + * {@code ImportCompositeMLDsaPublicKey(CompositeMLDsaAlgorithm, + * byte[]/ReadOnlySpan<byte>)}, plus the same {@code ImportPkcs8PrivateKey}/{@code + * ImportSubjectPublicKeyInfo}/{@code ImportFromPem}/{@code + * ImportEncryptedPkcs8PrivateKey}/{@code ImportFromEncryptedPem} structural-import family. + *
  • {@code CompositeMLDsaCng} — CNG-backed implementation of Composite ML-DSA ({@code + * CompositeMLDsaCng(CngKey)} constructor). + *
+ * + *

Architecture — {@code Import*} as primary creation rules, not skipped: exactly as + * established for ML-KEM (see {@code DotNetMLKem}'s javadoc for the full rationale), every {@code + * Import*} method verified above on both {@code MLDsa} and {@code CompositeMLDsa} is a {@code + * static} factory that is the only way (besides {@code GenerateKey} or the {@code Cng}/{@code + * OpenSsl} native-interop constructors) to obtain an instance in the first place — unlike the + * inherited instance {@code Import*}/{@code Export*} members on RSA/ECDsa/ECDiffieHellman, which + * are intentionally not modeled. They are therefore treated as primary creation rules here. + * + *

Two creation-rule shapes result from this, for both {@code MLDsa} and {@code CompositeMLDsa}: + * + *

    + *
  • Algorithm-parameterized ({@code GenerateKey}, {@code ImportMLDsaPrivateKey}, {@code + * ImportMLDsaPrivateSeed}, {@code ImportMLDsaPublicKey} / {@code + * ImportCompositeMLDsaPrivateKey}, {@code ImportCompositeMLDsaPublicKey}): take an {@code + * MLDsaAlgorithm}/{@code CompositeMLDsaAlgorithm} argument (e.g. {@code + * MLDsaAlgorithm.MLDsa65}), a member-access expression the engine resolves to the bare + * identifier {@code "MLDsa65"} (see {@code CSharpLanguageTranslation}, the same mechanism + * {@code DotNetMLKem}'s rules rely on for {@code MLKemAlgorithm}). Captured with {@code + * ParameterIdentifierFactory<>()} as a child of the top-level detection. + *
  • Structural imports ({@code ImportPkcs8PrivateKey}, {@code + * ImportSubjectPublicKeyInfo}, {@code ImportFromPem}, {@code ImportEncryptedPkcs8PrivateKey}, + * {@code ImportFromEncryptedPem}): the parameter set is embedded inside the encoded key + * material / PEM text, not present as a separate literal argument, so — exactly as for ML-KEM + * — it cannot be recovered by this engine. These translate to a generic node with no {@code + * ParameterSetIdentifier} child, a known, inherent precision gap, not a bug. + *
+ * + *

{@code MLDsaCng(CngKey)}, {@code MLDsaOpenSsl(SafeEvpPKeyHandle)} and {@code + * CompositeMLDsaCng(CngKey)} wrap an already-existing native key handle and never receive an + * algorithm argument at all, so they always translate to the generic node, mirroring {@code + * MLKemCng(CngKey)}/{@code MLKemOpenSsl(SafeEvpPKeyHandle)}. + * + *

Sign/Verify modeling: unlike {@code DotNetMLKem} (which models KEM-specific {@code + * Encapsulate}/{@code Decapsulate} via {@code KeyActionFactory}), {@code MLDsa} and {@code + * CompositeMLDsa} are ordinary signature primitives, so their operations are modeled with {@code + * SignatureActionFactory} under a {@code SignatureContext} — the exact same shape as {@code + * DotNetDSA}/{@code DotNetECDsa}'s {@code SignData}/{@code VerifyData} rules, reusing the existing + * generic SIGN/VERIFY dispatch in {@code CSharpSignatureContextTranslator} (no changes needed + * there: that translator already maps {@code SignatureAction.Action.SIGN}/{@code VERIFY} to {@code + * Sign}/{@code Verify} functionality nodes regardless of which algorithm produced the action, so + * this rule set is purely additive from its perspective). {@code MLDsa} additionally exposes {@code + * SignMu}/{@code VerifyMu} (signing a pre-computed FIPS 204 "mu" digest) and {@code + * SignPreHash}/{@code VerifyPreHash} (the FIPS 204 pre-hash variant); both are modeled the same way + * as {@code SignData}/{@code VerifyData} since the engine cannot meaningfully distinguish "what was + * hashed before signing" from "what was signed" without value tracking. {@code CompositeMLDsa} only + * exposes {@code SignData}/{@code VerifyData} (no mu/pre-hash variants per the verified API + * surface). The {@code *Core} overrides ({@code SignDataCore}, {@code SignMuCore}, {@code + * SignPreHashCore}, {@code VerifyDataCore}, {@code VerifyMuCore}, {@code VerifyPreHashCore}) are + * protected extensibility hooks for subclassing, not called by ordinary consumer code, and are + * intentionally not modeled — consistent with not modeling {@code EncapsulateCore}/{@code + * DecapsulateCore} in {@code DotNetMLKem}. + * + *

Mapper model reuse: {@code com.ibm.mapper.model.algorithms.MLDSA} (implementing {@code + * Signature}) already existed before this batch (added alongside {@code MLKEM} — see PR "Add + * support for MLKEM and MLDSA #219") and already exposes the exact {@code MLDSA(int + * parameterSetIdentifier, DetectionLocation)} shape {@code DotNetMLKem} relies on for {@code + * MLKEM}, so {@code asString()} yields {@code "ML-DSA-44"}/{@code "ML-DSA-65"}/{@code "ML-DSA-87"}. + * No new mapper model class was introduced for plain ML-DSA. + * + *

Composite ML-DSA modeling — a deliberate, documented compromise, not a fabrication: a + * repository-wide search ({@code grep -rl "Composite\|Hybrid" mapper/}) found no existing + * "composite"/"hybrid" algorithm concept anywhere in the mapper model — {@code CompositeMLDsa} + * combines a post-quantum algorithm (ML-DSA-44/65/87) with a classical one (e.g. RSA-2048-PSS, + * ECDSA-P256, Ed25519) into a single key, which a properly structured model would represent as two + * linked algorithm nodes. Inventing that two-algorithm "Hybrid"/"Composite" model class from + * scratch was judged out of scope for this batch (it would require new base-class/interface design + * in {@code mapper/model/}, affecting more than this one rule file — see class-level "Not covered" + * note below for the recommendation). However, the detection-rule *shape* for {@code + * CompositeMLDsa} turned out to be structurally identical to plain {@code MLDsa} (same static + * factory / structural-import / Cng-constructor pattern, confirmed from the official reference + * above, not assumed by RSA/ECDsa analogy) — there was therefore no reason to skip detecting it + * entirely. The chosen middle ground: {@code CompositeMLDsa}/{@code CompositeMLDsaCng} creation is + * detected and reuses the existing {@code MLDSA} mapper node (via a distinct {@code + * "MLDSA_COMPOSITE"} {@code KeyContext} kind, dispatched in {@code CSharpKeyContextTranslator}), + * with the full, untranslated {@code CompositeMLDsaAlgorithm} member name (e.g. {@code + * "MLDsa44WithECDsaP256"}, one of the 17 combinations verified from the official {@code + * CompositeMLDsaAlgorithm} reference page — {@code MLDsa44With\{ECDsaP256,Ed25519,RSA2048Pkcs15, + * RSA2048Pss\}}, {@code MLDsa65With\{ECDsaBrainpoolP256r1,ECDsaP256,ECDsaP384,Ed25519, + * RSA3072Pkcs15,RSA3072Pss,RSA4096Pkcs15,RSA4096Pss\}}, {@code + * MLDsa87With\{ECDsaBrainpoolP384r1,ECDsaP384,ECDsaP521,Ed448,RSA3072Pss,RSA4096Pss\}}) captured + * verbatim as the {@code ParameterSetIdentifier} string, rather than parsed into an ML-DSA + * parameter set number plus a separate classical-algorithm node. This preserves 100% of the + * information the engine can see (nothing is dropped or guessed) and yields a distinguishable, + * still-informative {@code asString()} such as {@code "ML-DSA-MLDsa44WithECDsaP256"}, at the cost + * of not exposing the classical component as a structured sibling/child node the way a "true" + * composite/hybrid model would. This is flagged here explicitly, not silently: a follow-up + * introducing a proper composite/hybrid algorithm concept in {@code mapper/model/} could later + * parse this same string into two structured nodes without touching this detection rule file. + * + *

Not covered (deliberately, consistent with the rest of this rule set): the {@code + * Algorithm}/{@code IsSupported} getters and {@code CompositeMLDsa.IsAlgorithmSupported(...)} + * (state reads, not configuration — no property setters exist on {@code MLDsa}/{@code + * CompositeMLDsa} at all), {@code MLDsaCng.GetKey()}/{@code CompositeMLDsaCng.GetKey()}/{@code + * MLDsaOpenSsl.DuplicateKeyHandle()} (native-handle export, the {@code MLDsa}-specific equivalent + * of the {@code Export*} methods skipped for RSA/ECDsa/ECDiffieHellman), {@code Dispose()}, and all + * {@code ExportXxx}/{@code TryExportXxx}/{@code ExportXxxPem} instance methods (standard + * PKCS#8/SPKI/PEM export — same convention as RSA/ECDsa/ECDiffieHellman/MLKem). + */ +@SuppressWarnings("java:S1192") +public final class DotNetMLDsa { + + private DotNetMLDsa() { + // nothing + } + + // ========================================================================= + // Sign / Verify operation rules (depending rules on any tracked MLDsa/CompositeMLDsa-family + // variable, mirroring DotNetDSA.java's SignData/VerifyData rules) + // ========================================================================= + + // mldsa.SignData(data, context) — 2 overloads (array-based and Span-based) + private static final IDetectionRule MLDSA_SIGN_DATA = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("SignData") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.SIGN)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // mldsa.VerifyData(data, signature, context) — 2 overloads + private static final IDetectionRule MLDSA_VERIFY_DATA = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("VerifyData") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.VERIFY)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // mldsa.SignMu(mu[, destination]) — 3 overloads (MLDsa only, not CompositeMLDsa) + private static final IDetectionRule MLDSA_SIGN_MU = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("SignMu") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.SIGN)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // mldsa.VerifyMu(mu, signature) — 2 overloads (MLDsa only) + private static final IDetectionRule MLDSA_VERIFY_MU = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("VerifyMu") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.VERIFY)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // mldsa.SignPreHash(hash, hashAlgorithmOid, context) — 2 overloads (MLDsa only) + private static final IDetectionRule MLDSA_SIGN_PRE_HASH = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("SignPreHash") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.SIGN)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // mldsa.VerifyPreHash(hash, signature, hashAlgorithmOid, context) — 2 overloads (MLDsa only) + private static final IDetectionRule MLDSA_VERIFY_PRE_HASH = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("VerifyPreHash") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.VERIFY)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + /** Full set of depending rules for {@code MLDsa}/{@code MLDsaCng}/{@code MLDsaOpenSsl}. */ + private static final List> MLDSA_DEPENDING_RULES = + List.of( + MLDSA_SIGN_DATA, + MLDSA_VERIFY_DATA, + MLDSA_SIGN_MU, + MLDSA_VERIFY_MU, + MLDSA_SIGN_PRE_HASH, + MLDSA_VERIFY_PRE_HASH); + + /** + * Depending rules for {@code CompositeMLDsa}/{@code CompositeMLDsaCng} — only {@code + * SignData}/{@code VerifyData} exist on this type (no mu/pre-hash variants). + */ + private static final List> COMPOSITE_MLDSA_DEPENDING_RULES = + List.of(MLDSA_SIGN_DATA, MLDSA_VERIFY_DATA); + + // ========================================================================= + // MLDsa — primary creation rules, algorithm-parameterized (MLDsaAlgorithm argument captured + // as the parameter set, see class javadoc) + // ========================================================================= + + // MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65) + private static final IDetectionRule MLDSA_GENERATE_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("MLDsa") + .forMethods("GenerateKey") + .shouldBeDetectedAs(new ValueActionFactory<>("ML-DSA")) + .withMethodParameter(MethodMatcher.ANY) // MLDsaAlgorithm + .shouldBeDetectedAs(new ParameterIdentifierFactory<>()) + .asChildOfParameterWithId(-1) + .buildForContext(new KeyContext(Map.of("kind", "MLDSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(MLDSA_DEPENDING_RULES); + + // MLDsa.ImportMLDsaPrivateKey(MLDsaAlgorithm.MLDsa65, privateKeyBytes) + private static final IDetectionRule MLDSA_IMPORT_PRIVATE_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("MLDsa") + .forMethods("ImportMLDsaPrivateKey") + .shouldBeDetectedAs(new ValueActionFactory<>("ML-DSA")) + .withMethodParameter(MethodMatcher.ANY) // MLDsaAlgorithm + .shouldBeDetectedAs(new ParameterIdentifierFactory<>()) + .asChildOfParameterWithId(-1) + .withMethodParameter(MethodMatcher.ANY) // private key bytes + .buildForContext(new KeyContext(Map.of("kind", "MLDSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(MLDSA_DEPENDING_RULES); + + // MLDsa.ImportMLDsaPrivateSeed(MLDsaAlgorithm.MLDsa65, seedBytes) + private static final IDetectionRule MLDSA_IMPORT_PRIVATE_SEED = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("MLDsa") + .forMethods("ImportMLDsaPrivateSeed") + .shouldBeDetectedAs(new ValueActionFactory<>("ML-DSA")) + .withMethodParameter(MethodMatcher.ANY) // MLDsaAlgorithm + .shouldBeDetectedAs(new ParameterIdentifierFactory<>()) + .asChildOfParameterWithId(-1) + .withMethodParameter(MethodMatcher.ANY) // seed bytes + .buildForContext(new KeyContext(Map.of("kind", "MLDSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(MLDSA_DEPENDING_RULES); + + // MLDsa.ImportMLDsaPublicKey(MLDsaAlgorithm.MLDsa65, publicKeyBytes) + private static final IDetectionRule MLDSA_IMPORT_PUBLIC_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("MLDsa") + .forMethods("ImportMLDsaPublicKey") + .shouldBeDetectedAs(new ValueActionFactory<>("ML-DSA")) + .withMethodParameter(MethodMatcher.ANY) // MLDsaAlgorithm + .shouldBeDetectedAs(new ParameterIdentifierFactory<>()) + .asChildOfParameterWithId(-1) + .withMethodParameter(MethodMatcher.ANY) // public key bytes + .buildForContext(new KeyContext(Map.of("kind", "MLDSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(MLDSA_DEPENDING_RULES); + + // ========================================================================= + // MLDsa — primary creation rules, structural imports (no MLDsaAlgorithm argument; see class + // javadoc for why the parameter set cannot be captured for these) + // ========================================================================= + + private static IDetectionRule structuralImportRule( + @Nonnull String objectType, + @Nonnull String methodName, + @Nonnull String kind, + @Nonnull List> dependingRules) { + return new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(objectType) + .forMethods(methodName) + .shouldBeDetectedAs(new ValueActionFactory<>("ML-DSA")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", kind))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(dependingRules); + } + + // MLDsa.ImportPkcs8PrivateKey(source) / MLDsa.ImportSubjectPublicKeyInfo(source) / + // MLDsa.ImportFromPem(pem) / MLDsa.ImportEncryptedPkcs8PrivateKey(...) / + // MLDsa.ImportFromEncryptedPem(...) + private static final IDetectionRule MLDSA_IMPORT_PKCS8_PRIVATE_KEY = + structuralImportRule("MLDsa", "ImportPkcs8PrivateKey", "MLDSA", MLDSA_DEPENDING_RULES); + + private static final IDetectionRule MLDSA_IMPORT_SUBJECT_PUBLIC_KEY_INFO = + structuralImportRule( + "MLDsa", "ImportSubjectPublicKeyInfo", "MLDSA", MLDSA_DEPENDING_RULES); + + private static final IDetectionRule MLDSA_IMPORT_FROM_PEM = + structuralImportRule("MLDsa", "ImportFromPem", "MLDSA", MLDSA_DEPENDING_RULES); + + private static final IDetectionRule MLDSA_IMPORT_ENCRYPTED_PKCS8_PRIVATE_KEY = + structuralImportRule( + "MLDsa", "ImportEncryptedPkcs8PrivateKey", "MLDSA", MLDSA_DEPENDING_RULES); + + private static final IDetectionRule MLDSA_IMPORT_FROM_ENCRYPTED_PEM = + structuralImportRule("MLDsa", "ImportFromEncryptedPem", "MLDSA", MLDSA_DEPENDING_RULES); + + // ========================================================================= + // MLDsa — primary creation rules, native-interop constructors (no MLDsaAlgorithm argument; + // wrap an already-existing native key handle, mirroring MLKemCng/MLKemOpenSsl in DotNetMLKem) + // ========================================================================= + + // new MLDsaCng(cngKey) + private static final IDetectionRule MLDSA_CNG = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("MLDsaCng") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("ML-DSA")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "MLDSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(MLDSA_DEPENDING_RULES); + + // new MLDsaOpenSsl(safeEvpPKeyHandle) + private static final IDetectionRule MLDSA_OPENSSL = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("MLDsaOpenSsl") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("ML-DSA")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "MLDSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(MLDSA_DEPENDING_RULES); + + // ========================================================================= + // CompositeMLDsa — primary creation rules, algorithm-parameterized (CompositeMLDsaAlgorithm + // argument captured verbatim as the parameter set — see class javadoc for why this is not + // parsed into a structured ML-DSA-size + classical-algorithm pair) + // ========================================================================= + + // CompositeMLDsa.GenerateKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256) + private static final IDetectionRule COMPOSITE_MLDSA_GENERATE_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("CompositeMLDsa") + .forMethods("GenerateKey") + .shouldBeDetectedAs(new ValueActionFactory<>("ML-DSA")) + .withMethodParameter(MethodMatcher.ANY) // CompositeMLDsaAlgorithm + .shouldBeDetectedAs(new ParameterIdentifierFactory<>()) + .asChildOfParameterWithId(-1) + .buildForContext(new KeyContext(Map.of("kind", "MLDSA_COMPOSITE"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(COMPOSITE_MLDSA_DEPENDING_RULES); + + // CompositeMLDsa.ImportCompositeMLDsaPrivateKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256, + // privateKeyBytes) + private static final IDetectionRule COMPOSITE_MLDSA_IMPORT_PRIVATE_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("CompositeMLDsa") + .forMethods("ImportCompositeMLDsaPrivateKey") + .shouldBeDetectedAs(new ValueActionFactory<>("ML-DSA")) + .withMethodParameter(MethodMatcher.ANY) // CompositeMLDsaAlgorithm + .shouldBeDetectedAs(new ParameterIdentifierFactory<>()) + .asChildOfParameterWithId(-1) + .withMethodParameter(MethodMatcher.ANY) // private key bytes + .buildForContext(new KeyContext(Map.of("kind", "MLDSA_COMPOSITE"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(COMPOSITE_MLDSA_DEPENDING_RULES); + + // CompositeMLDsa.ImportCompositeMLDsaPublicKey(CompositeMLDsaAlgorithm.MLDsa65WithECDsaP256, + // publicKeyBytes) + private static final IDetectionRule COMPOSITE_MLDSA_IMPORT_PUBLIC_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("CompositeMLDsa") + .forMethods("ImportCompositeMLDsaPublicKey") + .shouldBeDetectedAs(new ValueActionFactory<>("ML-DSA")) + .withMethodParameter(MethodMatcher.ANY) // CompositeMLDsaAlgorithm + .shouldBeDetectedAs(new ParameterIdentifierFactory<>()) + .asChildOfParameterWithId(-1) + .withMethodParameter(MethodMatcher.ANY) // public key bytes + .buildForContext(new KeyContext(Map.of("kind", "MLDSA_COMPOSITE"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(COMPOSITE_MLDSA_DEPENDING_RULES); + + // ========================================================================= + // CompositeMLDsa — primary creation rules, structural imports (no CompositeMLDsaAlgorithm + // argument) + // ========================================================================= + + // CompositeMLDsa.ImportPkcs8PrivateKey(source) / + // CompositeMLDsa.ImportSubjectPublicKeyInfo(source) / CompositeMLDsa.ImportFromPem(pem) / + // CompositeMLDsa.ImportEncryptedPkcs8PrivateKey(...) / + // CompositeMLDsa.ImportFromEncryptedPem(...) + private static final IDetectionRule COMPOSITE_MLDSA_IMPORT_PKCS8_PRIVATE_KEY = + structuralImportRule( + "CompositeMLDsa", + "ImportPkcs8PrivateKey", + "MLDSA_COMPOSITE", + COMPOSITE_MLDSA_DEPENDING_RULES); + + private static final IDetectionRule COMPOSITE_MLDSA_IMPORT_SUBJECT_PUBLIC_KEY_INFO = + structuralImportRule( + "CompositeMLDsa", + "ImportSubjectPublicKeyInfo", + "MLDSA_COMPOSITE", + COMPOSITE_MLDSA_DEPENDING_RULES); + + private static final IDetectionRule COMPOSITE_MLDSA_IMPORT_FROM_PEM = + structuralImportRule( + "CompositeMLDsa", + "ImportFromPem", + "MLDSA_COMPOSITE", + COMPOSITE_MLDSA_DEPENDING_RULES); + + private static final IDetectionRule + COMPOSITE_MLDSA_IMPORT_ENCRYPTED_PKCS8_PRIVATE_KEY = + structuralImportRule( + "CompositeMLDsa", + "ImportEncryptedPkcs8PrivateKey", + "MLDSA_COMPOSITE", + COMPOSITE_MLDSA_DEPENDING_RULES); + + private static final IDetectionRule COMPOSITE_MLDSA_IMPORT_FROM_ENCRYPTED_PEM = + structuralImportRule( + "CompositeMLDsa", + "ImportFromEncryptedPem", + "MLDSA_COMPOSITE", + COMPOSITE_MLDSA_DEPENDING_RULES); + + // ========================================================================= + // CompositeMLDsa — primary creation rule, native-interop constructor (no + // CompositeMLDsaAlgorithm argument; wraps an already-existing native key handle) + // ========================================================================= + + // new CompositeMLDsaCng(cngKey) + private static final IDetectionRule COMPOSITE_MLDSA_CNG = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("CompositeMLDsaCng") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("ML-DSA")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "MLDSA_COMPOSITE"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(COMPOSITE_MLDSA_DEPENDING_RULES); + + @Nonnull + public static List> rules() { + return Stream.of( + MLDSA_GENERATE_KEY, + MLDSA_IMPORT_PRIVATE_KEY, + MLDSA_IMPORT_PRIVATE_SEED, + MLDSA_IMPORT_PUBLIC_KEY, + MLDSA_IMPORT_PKCS8_PRIVATE_KEY, + MLDSA_IMPORT_SUBJECT_PUBLIC_KEY_INFO, + MLDSA_IMPORT_FROM_PEM, + MLDSA_IMPORT_ENCRYPTED_PKCS8_PRIVATE_KEY, + MLDSA_IMPORT_FROM_ENCRYPTED_PEM, + MLDSA_CNG, + MLDSA_OPENSSL, + COMPOSITE_MLDSA_GENERATE_KEY, + COMPOSITE_MLDSA_IMPORT_PRIVATE_KEY, + COMPOSITE_MLDSA_IMPORT_PUBLIC_KEY, + COMPOSITE_MLDSA_IMPORT_PKCS8_PRIVATE_KEY, + COMPOSITE_MLDSA_IMPORT_SUBJECT_PUBLIC_KEY_INFO, + COMPOSITE_MLDSA_IMPORT_FROM_PEM, + COMPOSITE_MLDSA_IMPORT_ENCRYPTED_PKCS8_PRIVATE_KEY, + COMPOSITE_MLDSA_IMPORT_FROM_ENCRYPTED_PEM, + COMPOSITE_MLDSA_CNG) + .toList(); + } +} diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetMLKem.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetMLKem.java new file mode 100644 index 000000000..486807afa --- /dev/null +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetMLKem.java @@ -0,0 +1,327 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import com.ibm.engine.detection.MethodMatcher; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.KeyAction; +import com.ibm.engine.model.context.KeyContext; +import com.ibm.engine.model.factory.KeyActionFactory; +import com.ibm.engine.model.factory.ParameterIdentifierFactory; +import com.ibm.engine.model.factory.ValueActionFactory; +import com.ibm.engine.rule.IDetectionRule; +import com.ibm.engine.rule.builder.DetectionRuleBuilder; +import java.util.List; +import java.util.Map; +import javax.annotation.Nonnull; + +/** + * Detection rules for ML-KEM (Module-Lattice-Based Key-Encapsulation Mechanism, FIPS 203) usage in + * {@code System.Security.Cryptography}. + * + *

API availability (verified against the official Microsoft Learn API reference, not + * assumed): {@code MLKem}, {@code MLKemCng}, {@code MLKemOpenSsl} and {@code MLKemAlgorithm} + * are documented for the {@code net-10.0} and {@code net-11.0} monikers (both listed the "Assembly: + * System.Security.Cryptography.dll" as in-box, plus "Assembly: Microsoft.Bcl.Cryptography.dll" — + * package {@code Microsoft.Bcl.Cryptography v11.0.0-preview.7.26381.103} — as a back-compat NuGet + * shim for {@code MLKem}/{@code MLKemCng}/{@code MLKemAlgorithm} on older TFMs; {@code + * MLKemOpenSsl} lists only the in-box assembly, since its OpenSSL interop has no back-compat shim). + * {@code ImportPkcs8PrivateKey} carries a {@code + * [System.Diagnostics.CodeAnalysis.Experimental("SYSLIB5006")]} attribute on (at least) one of its + * documented overload snapshots, confirming this whole surface is still an actively evolving + * preview API as of .NET 10/11 — not a settled, stable API. + * + *

Classes covered: + * + *

    + *
  • {@code MLKem} — abstract base. All of its members are {@code static} factory methods (no + * public constructor exists — the only listed constructor, {@code MLKem(MLKemAlgorithm)}, is + * for derived classes): {@code GenerateKey(MLKemAlgorithm)}, {@code + * ImportDecapsulationKey(MLKemAlgorithm, byte[])}, {@code + * ImportEncapsulationKey(MLKemAlgorithm, byte[])}, {@code ImportPrivateSeed(MLKemAlgorithm, + * byte[])}, {@code ImportPkcs8PrivateKey(byte[])}, {@code + * ImportSubjectPublicKeyInfo(byte[])}, {@code ImportFromPem(string)}, {@code + * ImportEncryptedPkcs8PrivateKey(...)}, {@code ImportFromEncryptedPem(...)}. + *
  • {@code MLKemCng} — CNG-backed implementation ({@code MLKemCng(CngKey)} constructor). + *
  • {@code MLKemOpenSsl} — OpenSSL-backed implementation ({@code + * MLKemOpenSsl(SafeEvpPKeyHandle)} constructor). + *
+ * + *

Architecture — a "fundamental difference" from RSA/ECDsa/ECDiffieHellman justifies covering + * the {@code Import*} methods here: for RSA/ECDsa/ECDiffieHellman (see {@code DotNetRSA}, + * {@code DotNetECDsa}, {@code DotNetECDiffieHellman}), the {@code Import*}/{@code Export*} members + * inherited from {@code AsymmetricAlgorithm} are instance methods that mutate/read an + * already-constructed key object, and are intentionally not modeled (pure key-material plumbing, + * consistent with not tracking raw byte buffers per the engine's documented limitations). For + * {@code MLKem}, by contrast, every {@code Import*} method verified above is a {@code static} + * factory that is the only way (besides {@code GenerateKey} or the {@code MLKemCng}/{@code + * MLKemOpenSsl} native-interop constructors) to obtain an {@code MLKem} instance in the first place + * — structurally the same role as {@code RSA.Create(RSAParameters)}, which RSA's rule set does + * cover. They are therefore treated as primary creation rules here, not skipped. + * + *

Two creation-rule shapes result from this: + * + *

    + *
  • Algorithm-parameterized ({@code GenerateKey}, {@code ImportDecapsulationKey}, {@code + * ImportEncapsulationKey}, {@code ImportPrivateSeed}): all take an {@code MLKemAlgorithm} + * argument (e.g. {@code MLKemAlgorithm.MLKem768}), a member-access expression the engine + * resolves to the bare identifier {@code "MLKem768"} (see {@code + * CSharpLanguageTranslation#resolveIdentifierAsString} / {@code getEnumIdentifierName}, the + * same mechanism {@code DotNetAES}'s {@code AES_SET_MODE} rule relies on for {@code + * CipherMode.CBC}). This is captured with {@code ParameterIdentifierFactory<>()} as a child + * of the top-level detection, then mapped in {@code CSharpKeyContextTranslator} from {@code + * "MLKem512"/"MLKem768"/"MLKem1024"} to a {@code ParameterSetIdentifier} of {@code + * "512"/"768"/"1024"} — reproducing the exact {@code MLKEM(int, DetectionLocation)} shape + * already used by {@code JcaKemMapper}/{@code GoCryptoKEMMapper}, so {@code asString()} + * yields {@code "ML-KEM-768"} etc., consistent across all three language modules. + *
  • Structural imports ({@code ImportPkcs8PrivateKey}, {@code + * ImportSubjectPublicKeyInfo}, {@code ImportFromPem}, {@code ImportEncryptedPkcs8PrivateKey}, + * {@code ImportFromEncryptedPem}): the parameter set is embedded inside the encoded key + * material / PEM text, not present as a separate literal argument, so it cannot be recovered + * by this engine (no byte-content parsing). These translate to a generic {@code MLKEM} node + * with no {@code ParameterSetIdentifier} child (mirrors {@code MLKEM(DetectionLocation)}, the + * no-argument constructor) — a known, inherent precision gap, not a bug. + *
+ * + *

{@code MLKemCng(CngKey)} and {@code MLKemOpenSsl(SafeEvpPKeyHandle)} wrap an already-existing + * native key handle and never receive an {@code MLKemAlgorithm} argument at all, so they always + * translate to the generic {@code MLKEM} node, mirroring how {@code RSACng(CngKey)} in {@code + * DotNetRSA} captures no key size either. + * + *

Encapsulate / Decapsulate modeling — no missing {@code CipherAction.Action}/{@code + * SignatureAction.Action} enum value needed: unlike the concern raised for {@code + * ECDiffieHellman}'s derive operations (see {@code DotNetECDiffieHellman}'s javadoc), KEM + * encapsulate/decapsulate turned out to already have first-class support in the engine model: + * {@code KeyAction.Action} (in {@code com.ibm.engine.model.KeyAction}, used via {@code + * KeyActionFactory}) already defines {@code ENCAPSULATION} and {@code DECAPSULATION} members, and + * the mapper model already defines {@code com.ibm.mapper.model.functionality.Encapsulate}/{@code + * Decapsulate} functionality nodes. This exact combination is already used by the very recently + * added Go {@code crypto/mlkem} support ({@code GoCryptoMLKEM.java} / {@code + * GoKeyContextTranslator.java}), which this rule set mirrors: {@code MLKem.Encapsulate(...)} / + * {@code MLKem.Decapsulate(...)} (both instance methods, confirmed from the official + * method-reference pages, each with two overloads differing only by array-vs-{@code Span} / + * out-vs-return parameter shape, collapsed with a single {@code withAnyParameters()} rule per + * method name — {@code EncapsulateCore}/{@code DecapsulateCore} are protected extensibility hooks + * for subclassing {@code MLKem} itself, not called by ordinary consumer code, and are intentionally + * not modeled, consistent with not modeling other protected virtual hooks elsewhere in this rule + * set) are captured with {@code KeyActionFactory<>(KeyAction .Action.ENCAPSULATION / + * .DECAPSULATION)} under a {@code KeyContext} of {@code kind = "KEM"}, dispatched in {@code + * CSharpKeyContextTranslator} exactly like the Go translator does. No detection rule had to fake or + * omit an action type. + * + *

Mapper model reuse: {@code com.ibm.mapper.model.algorithms.MLKEM} (implementing {@code + * KeyEncapsulationMechanism}) already existed before this batch — it is used by the JCA, + * BouncyCastle and Go {@code crypto/mlkem} translations. No new mapper model class was introduced + * for ML-KEM itself; this rule set is purely additive (new detection rules plus new {@code "KEM"} + * dispatch branches in {@code CSharpKeyContextTranslator}). + * + *

Not covered (deliberately, consistent with the rest of this rule set): the {@code + * Algorithm}/{@code IsSupported} getters (state reads, not configuration — no property setters + * exist on {@code MLKem} at all), {@code MLKemCng.GetKey()} / {@code + * MLKemOpenSsl.DuplicateKeyHandle()} (native-handle export, the {@code MLKem}-specific equivalent + * of the {@code Export*} methods skipped for RSA/ECDsa/ECDiffieHellman), {@code Dispose()}, and all + * {@code ExportXxx}/{@code TryExportXxx}/{@code ExportXxxPem} instance methods (standard + * PKCS#8/SPKI/PEM export — same convention as RSA/ECDsa/ECDiffieHellman). + */ +@SuppressWarnings("java:S1192") +public final class DotNetMLKem { + + private DotNetMLKem() { + // nothing + } + + // ========================================================================= + // Encapsulate / Decapsulate operation rules (depending rules on any tracked MLKem-family + // variable, mirroring GoCryptoMLKEM.java's ENCAPSULATE_768/DECAPSULATE_768 rules) + // ========================================================================= + + // mlkem.Encapsulate(out ciphertext, out sharedSecret) / mlkem.Encapsulate(ciphertext, + // sharedSecret) + private static final IDetectionRule MLKEM_ENCAPSULATE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("Encapsulate") + .shouldBeDetectedAs(new KeyActionFactory<>(KeyAction.Action.ENCAPSULATION)) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "KEM"))) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // mlkem.Decapsulate(ciphertext) / mlkem.Decapsulate(ciphertext, sharedSecret) + private static final IDetectionRule MLKEM_DECAPSULATE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("Decapsulate") + .shouldBeDetectedAs(new KeyActionFactory<>(KeyAction.Action.DECAPSULATION)) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "KEM"))) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + private static final List> MLKEM_DEPENDING_RULES = + List.of(MLKEM_ENCAPSULATE, MLKEM_DECAPSULATE); + + // ========================================================================= + // Primary creation rules — algorithm-parameterized (MLKemAlgorithm argument captured as the + // parameter set, see class javadoc) + // ========================================================================= + + // MLKem.GenerateKey(MLKemAlgorithm.MLKem768) + private static final IDetectionRule MLKEM_GENERATE_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("MLKem") + .forMethods("GenerateKey") + .shouldBeDetectedAs(new ValueActionFactory<>("ML-KEM")) + .withMethodParameter(MethodMatcher.ANY) // MLKemAlgorithm + .shouldBeDetectedAs(new ParameterIdentifierFactory<>()) + .asChildOfParameterWithId(-1) + .buildForContext(new KeyContext(Map.of("kind", "KEM"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(MLKEM_DEPENDING_RULES); + + // MLKem.ImportDecapsulationKey(MLKemAlgorithm.MLKem768, decapsulationKeyBytes) + private static final IDetectionRule MLKEM_IMPORT_DECAPSULATION_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("MLKem") + .forMethods("ImportDecapsulationKey") + .shouldBeDetectedAs(new ValueActionFactory<>("ML-KEM")) + .withMethodParameter(MethodMatcher.ANY) // MLKemAlgorithm + .shouldBeDetectedAs(new ParameterIdentifierFactory<>()) + .asChildOfParameterWithId(-1) + .withMethodParameter(MethodMatcher.ANY) // decapsulation key bytes + .buildForContext(new KeyContext(Map.of("kind", "KEM"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(MLKEM_DEPENDING_RULES); + + // MLKem.ImportEncapsulationKey(MLKemAlgorithm.MLKem768, encapsulationKeyBytes) + private static final IDetectionRule MLKEM_IMPORT_ENCAPSULATION_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("MLKem") + .forMethods("ImportEncapsulationKey") + .shouldBeDetectedAs(new ValueActionFactory<>("ML-KEM")) + .withMethodParameter(MethodMatcher.ANY) // MLKemAlgorithm + .shouldBeDetectedAs(new ParameterIdentifierFactory<>()) + .asChildOfParameterWithId(-1) + .withMethodParameter(MethodMatcher.ANY) // encapsulation key bytes + .buildForContext(new KeyContext(Map.of("kind", "KEM"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(MLKEM_DEPENDING_RULES); + + // MLKem.ImportPrivateSeed(MLKemAlgorithm.MLKem768, seedBytes) + private static final IDetectionRule MLKEM_IMPORT_PRIVATE_SEED = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("MLKem") + .forMethods("ImportPrivateSeed") + .shouldBeDetectedAs(new ValueActionFactory<>("ML-KEM")) + .withMethodParameter(MethodMatcher.ANY) // MLKemAlgorithm + .shouldBeDetectedAs(new ParameterIdentifierFactory<>()) + .asChildOfParameterWithId(-1) + .withMethodParameter(MethodMatcher.ANY) // seed bytes + .buildForContext(new KeyContext(Map.of("kind", "KEM"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(MLKEM_DEPENDING_RULES); + + // ========================================================================= + // Primary creation rules — structural imports (no MLKemAlgorithm argument; see class javadoc + // for why the parameter set cannot be captured for these) + // ========================================================================= + + // MLKem.ImportPkcs8PrivateKey(source) / MLKem.ImportSubjectPublicKeyInfo(source) / + // MLKem.ImportFromPem(pem) / MLKem.ImportEncryptedPkcs8PrivateKey(...) / + // MLKem.ImportFromEncryptedPem(...) + private static IDetectionRule structuralImportRule(@Nonnull String methodName) { + return new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("MLKem") + .forMethods(methodName) + .shouldBeDetectedAs(new ValueActionFactory<>("ML-KEM")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "KEM"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(MLKEM_DEPENDING_RULES); + } + + private static final IDetectionRule MLKEM_IMPORT_PKCS8_PRIVATE_KEY = + structuralImportRule("ImportPkcs8PrivateKey"); + + private static final IDetectionRule MLKEM_IMPORT_SUBJECT_PUBLIC_KEY_INFO = + structuralImportRule("ImportSubjectPublicKeyInfo"); + + private static final IDetectionRule MLKEM_IMPORT_FROM_PEM = + structuralImportRule("ImportFromPem"); + + private static final IDetectionRule MLKEM_IMPORT_ENCRYPTED_PKCS8_PRIVATE_KEY = + structuralImportRule("ImportEncryptedPkcs8PrivateKey"); + + private static final IDetectionRule MLKEM_IMPORT_FROM_ENCRYPTED_PEM = + structuralImportRule("ImportFromEncryptedPem"); + + // ========================================================================= + // Primary creation rules — native-interop constructors (no MLKemAlgorithm argument; wrap an + // already-existing native key handle, mirroring RSACng(CngKey)/RSAOpenSsl(SafeEvpPKeyHandle) in + // DotNetRSA) + // ========================================================================= + + // new MLKemCng(cngKey) + private static final IDetectionRule MLKEM_CNG = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("MLKemCng") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("ML-KEM")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "KEM"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(MLKEM_DEPENDING_RULES); + + // new MLKemOpenSsl(safeEvpPKeyHandle) + private static final IDetectionRule MLKEM_OPENSSL = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("MLKemOpenSsl") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("ML-KEM")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "KEM"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(MLKEM_DEPENDING_RULES); + + @Nonnull + public static List> rules() { + return List.of( + MLKEM_GENERATE_KEY, + MLKEM_IMPORT_DECAPSULATION_KEY, + MLKEM_IMPORT_ENCAPSULATION_KEY, + MLKEM_IMPORT_PRIVATE_SEED, + MLKEM_IMPORT_PKCS8_PRIVATE_KEY, + MLKEM_IMPORT_SUBJECT_PUBLIC_KEY_INFO, + MLKEM_IMPORT_FROM_PEM, + MLKEM_IMPORT_ENCRYPTED_PKCS8_PRIVATE_KEY, + MLKEM_IMPORT_FROM_ENCRYPTED_PEM, + MLKEM_CNG, + MLKEM_OPENSSL); + } +} diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetProtectedData.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetProtectedData.java new file mode 100644 index 000000000..d85ab0211 --- /dev/null +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetProtectedData.java @@ -0,0 +1,292 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import com.ibm.engine.detection.MethodMatcher; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.CipherAction; +import com.ibm.engine.model.context.CipherContext; +import com.ibm.engine.model.factory.CipherActionFactory; +import com.ibm.engine.model.factory.ValueActionFactory; +import com.ibm.engine.rule.IDetectionRule; +import com.ibm.engine.rule.builder.DetectionRuleBuilder; +import java.util.List; +import javax.annotation.Nonnull; + +/** + * Detection rules for the Windows Data Protection API (DPAPI) surface of {@code + * System.Security.Cryptography}. + * + *

Classes covered (per the official API reference, verified via WebFetch of learn.microsoft.com + * — not guessed): + * + *

    + *
  • {@code ProtectedData} — static-only class ({@code System.Security.Cryptography} namespace, + * assembly {@code System.Security.Cryptography.ProtectedData.dll} / {@code + * System.Security.dll}). Methods: {@code Protect(byte[], byte[], DataProtectionScope)}, + * {@code Protect(ReadOnlySpan, DataProtectionScope, ReadOnlySpan)}, {@code + * Protect(ReadOnlySpan, DataProtectionScope, Span, ReadOnlySpan)}, {@code + * TryProtect(...)}, and the {@code Unprotect}/{@code TryUnprotect} mirror overloads. + *
  • {@code ProtectedMemory} — static-only class (assembly {@code System.Security.dll}). + * Methods: {@code Protect(byte[], MemoryProtectionScope)}, {@code Unprotect(byte[], + * MemoryProtectionScope)}. No {@code Try*} variants are published for this class. + *
  • {@code DpapiDataProtector} — a real, documented class ({@code System.Security.Cryptography} + * namespace, assembly {@code System.Security.dll}), sealed and deriving from the abstract + * {@code DataProtector} base class. Verified via WebFetch to exist as an actual API member + * (not a misnaming of something from {@code Microsoft.AspNetCore.DataProtection}, which is an + * entirely different, unrelated API — the ASP.NET Core Data Protection stack has its own + * {@code IDataProtector}/{@code IDataProtectionProvider} types, not this class). Constructor + * {@code DpapiDataProtector(string appName, string primaryPurpose, params string[] + * specificPurpose)}; instance methods {@code Protect(byte[])}/{@code Unprotect(byte[])} + * (inherited, non-overridable, from {@code DataProtector}) delegate internally to {@code + * ProtectedData.Protect}/{@code Unprotect}. Important availability caveat found during + * verification: unlike {@code ProtectedData}/{@code ProtectedMemory} (which ship for + * modern .NET on Windows via the {@code System.Security.Cryptography.ProtectedData} NuGet + * package, per the moniker list on their doc pages: {@code netcore-1.0} onward, {@code + * windowsdesktop-*}, {@code net-11.0-pp}), {@code DpapiDataProtector}'s doc page lists + * only {@code netframework-4.5} through {@code netframework-4.8.1} monikers — it is + * .NET Framework legacy-only and has no modern-.NET equivalent. It remains valid, detectable + * legacy source, so it is covered here. + *
+ * + *

Platform note: per the official remarks on both {@code ProtectedData} and {@code + * ProtectedMemory}, "this class provides access to the Data Protection API (DPAPI) available in + * Windows operating systems" and its use "on platforms other than Windows throws a {@code + * PlatformNotSupportedException}". This does not affect detection: these are source-level patterns + * matched independently of the runtime platform the scanned code would actually execute on. + * + *

Modeling decision — algorithm identity: DPAPI deliberately does not expose which + * concrete symmetric algorithm it uses underneath (this has changed across Windows versions and is + * not selectable by the caller), so translating it as a specific algorithm such as {@code AES} or + * {@code DESede} would be inventing information that is not present in the source code. Instead, + * following the same precedent as {@code CSharpPRNGContextTranslator}'s {@code "NATIVEPRNG"} (used + * for .NET's {@code RandomNumberGenerator}, an equally vendor/platform-opaque primitive — see + * {@link DotNetRandomNumberGenerator}), this file captures the vendor-specific value {@code + * "DPAPI"} and dispatches it in {@code CSharpCipherContextTranslator} to the generic mapper {@code + * Algorithm} constructor ({@code new Algorithm("DPAPI", Cipher.class, detectionLocation)}), using + * the existing generic {@code Cipher} primitive marker interface (already present in the mapper + * model, parent of {@code BlockCipher}/{@code StreamCipher}) as the closest honest classification: + * "used to encrypt/decrypt, kind and identity unspecified". No new mapper model class was created + * for this batch — the existing generic {@code Algorithm(name, kind, detectionLocation)} concept is + * sufficient, exactly as it already was for {@code NATIVEPRNG}. + * + *

Modeling decision — static one-shot calls vs. instance depending-rule operations: + * mirrors the {@code HKDF}/{@code RandomNumberGenerator} static-methods precedent (see {@link + * DotNetKeyDerivation}, {@link DotNetRandomNumberGenerator}). {@code ProtectedData.Protect}/{@code + * TryProtect}/{@code Unprotect}/{@code TryUnprotect} and {@code ProtectedMemory.Protect}/{@code + * Unprotect} are each a complete, self-contained, fully static call with no instance to track — the + * "algorithm identity" (DPAPI) and the "operation" (encrypt/decrypt) happen at once. Because a + * detection rule's top-level {@code shouldBeDetectedAs} can only capture one thing (see + * DETECTION_RULE_STRUCTURE.md), and {@code CipherContext}'s existing translation branches already + * distinguish a {@code ValueAction} (algorithm identity) from a {@code CipherAction} + * (encrypt/decrypt functionality) as two independent, mutually exclusive top-level captures, these + * static calls capture a single merged {@code ValueActionFactory<>("DPAPI_PROTECT")} / {@code + * ValueActionFactory<>("DPAPI_UNPROTECT")} value instead. {@code CSharpCipherContextTranslator} + * then builds the composite node directly (the {@code DPAPI} {@code Algorithm} node with an {@code + * Encrypt}/{@code Decrypt} functionality child already attached) — the same "single detected string + * encodes both identity and purpose" idiom already used elsewhere in this codebase (e.g. {@code + * JavaKeyContextTranslator}'s {@code algo.put(new KeyGeneration(detectionLocation))} pattern). + * + *

By contrast, {@code DpapiDataProtector} does have a real tracked instance (created + * via {@code new DpapiDataProtector(...)}), so it follows the AES-family creation/depending-rule + * pattern instead (see {@link DotNetAES}): the constructor is a top-level rule capturing the {@code + * "DPAPI"} identity, with the instance {@code Protect}/{@code Unprotect} calls as depending rules + * capturing {@code CipherActionFactory<>(CipherAction.Action.ENCRYPT/DECRYPT)} — translated as + * children of the already-identified {@code DPAPI} node by the existing (unmodified) {@code + * CipherAction} branch of {@code CSharpCipherContextTranslator}. + * + *

As with every other file in this rule set, the ANTLR4-based C# engine cannot resolve parameter + * types (see {@code CSharpLanguageTranslation}), so all overloads that only differ by {@code + * byte[]} vs. {@code ReadOnlySpan}/{@code Span}, or by the presence of an output-buffer + * / {@code out} parameter, are collapsed into a single {@code withAnyParameters()} rule per method + * name — exactly like {@code TryEncryptCbc} etc. in {@link DotNetAES}. + * + *

Known gap — {@code DataProtectionScope}/{@code MemoryProtectionScope} parameter, and {@code + * DpapiDataProtector.Scope} property setter, are not decoded: unlike {@code CipherMode}/{@code + * PaddingMode} (decoded by {@code AES_SET_MODE}/{@code AES_SET_PADDING} in {@link DotNetAES} + * because the mapper model already has generic, cross-library {@code Mode}/{@code Padding} concepts + * that {@code CipherMode.CBC}/{@code PaddingMode.PKCS7} map onto), {@code DataProtectionScope} + * ({@code CurrentUser}/{@code LocalMachine}) and {@code MemoryProtectionScope} ({@code + * SameProcess}/{@code CrossProcess}/{@code SameLogon}) describe who can decrypt the data + * (a key-custody / access-scope concept), not a cipher mode, padding, or any other concept the + * mapper model currently represents. Forcing it through {@code ModeFactory} or a similar existing + * factory would silently mislabel an access-scope value as a block-cipher mode once it reaches + * {@code JcaModeMapper}, which would not recognize e.g. {@code "CurrentUser"} and would just drop + * it — arguably worse than not capturing it at all. The engine can technically resolve + * these enum member-access expressions to their member name (see {@code + * CSharpDetectionEngine#resolveValues}, which handles {@code CSharpMemberAccessTree} generically, + * not only for {@code Mode}/{@code Padding}), so this is a genuine modeling gap — not an engine + * limitation — flagged here per the "document, don't force" principle rather than silently deciding + * one way. The scope parameter position is still captured (via {@code MethodMatcher.ANY}, + * uninterpreted) so the rule's parameter arity/shape stays accurate; only its value is left + * uncaptured. For the same reason, {@code DpapiDataProtector}'s {@code appName}/{@code + * primaryPurpose}/{@code specificPurpose} constructor arguments (arbitrary caller-chosen strings, + * not cryptographic identifiers) and its {@code Scope} property setter are not decoded either. + */ +@SuppressWarnings("java:S1192") +public final class DotNetProtectedData { + + private DotNetProtectedData() { + // nothing + } + + // ========================================================================= + // ProtectedData — static-only class. Each static call is both the "creation" and + // the "operation" at once (see class javadoc "Modeling decision"). + // ========================================================================= + + // ProtectedData.Protect(byte[] userData, byte[] optionalEntropy, DataProtectionScope scope) + // / Protect(ReadOnlySpan, DataProtectionScope, ReadOnlySpan) + // / Protect(ReadOnlySpan, DataProtectionScope, Span, ReadOnlySpan) + private static final IDetectionRule PROTECTED_DATA_PROTECT = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("ProtectedData") + .forMethods("Protect") + .shouldBeDetectedAs(new ValueActionFactory<>("DPAPI_PROTECT")) + .withAnyParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ProtectedData.Unprotect(...) — same overload shapes as Protect, mirrored. + private static final IDetectionRule PROTECTED_DATA_UNPROTECT = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("ProtectedData") + .forMethods("Unprotect") + .shouldBeDetectedAs(new ValueActionFactory<>("DPAPI_UNPROTECT")) + .withAnyParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ProtectedData.TryProtect(ReadOnlySpan, DataProtectionScope, Span, out int, + // ReadOnlySpan) + private static final IDetectionRule PROTECTED_DATA_TRY_PROTECT = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("ProtectedData") + .forMethods("TryProtect") + .shouldBeDetectedAs(new ValueActionFactory<>("DPAPI_PROTECT")) + .withAnyParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ProtectedData.TryUnprotect(...) — mirrors TryProtect. + private static final IDetectionRule PROTECTED_DATA_TRY_UNPROTECT = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("ProtectedData") + .forMethods("TryUnprotect") + .shouldBeDetectedAs(new ValueActionFactory<>("DPAPI_UNPROTECT")) + .withAnyParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // ProtectedMemory — static-only class. No Try* overloads are published for this class. + // ========================================================================= + + // ProtectedMemory.Protect(byte[] userData, MemoryProtectionScope scope) + private static final IDetectionRule PROTECTED_MEMORY_PROTECT = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("ProtectedMemory") + .forMethods("Protect") + .shouldBeDetectedAs(new ValueActionFactory<>("DPAPI_PROTECT")) + .withAnyParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ProtectedMemory.Unprotect(byte[] encryptedData, MemoryProtectionScope scope) + private static final IDetectionRule PROTECTED_MEMORY_UNPROTECT = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("ProtectedMemory") + .forMethods("Unprotect") + .shouldBeDetectedAs(new ValueActionFactory<>("DPAPI_UNPROTECT")) + .withAnyParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // DpapiDataProtector — real tracked instance (new DpapiDataProtector(...)), so this follows + // the AES-family creation/depending-rule pattern instead of the static one-shot pattern above + // (see class javadoc). + // ========================================================================= + + // protector.Protect(byte[] userData) — instance method inherited from DataProtector. + private static final IDetectionRule DPAPI_INSTANCE_PROTECT = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("Protect") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.ENCRYPT)) + .withAnyParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // protector.Unprotect(byte[] encryptedData) — instance method inherited from DataProtector. + private static final IDetectionRule DPAPI_INSTANCE_UNPROTECT = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("Unprotect") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.DECRYPT)) + .withAnyParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + private static final List> DPAPI_INSTANCE_DEPENDING_RULES = + List.of(DPAPI_INSTANCE_PROTECT, DPAPI_INSTANCE_UNPROTECT); + + // new DpapiDataProtector(appName, primaryPurpose, specificPurpose...) — single constructor + // overload, but with a params array (variable arity), so withAnyParameters() is used exactly + // like the AesCng / SP800108HmacCounterKdf precedents for the same reason. + private static final IDetectionRule DPAPI_DATA_PROTECTOR_CTOR = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("DpapiDataProtector") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("DPAPI")) + .withAnyParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(DPAPI_INSTANCE_DEPENDING_RULES); + + @Nonnull + public static List> rules() { + return List.of( + PROTECTED_DATA_PROTECT, + PROTECTED_DATA_UNPROTECT, + PROTECTED_DATA_TRY_PROTECT, + PROTECTED_DATA_TRY_UNPROTECT, + PROTECTED_MEMORY_PROTECT, + PROTECTED_MEMORY_UNPROTECT, + DPAPI_DATA_PROTECTOR_CTOR); + } +} diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetRC2.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetRC2.java index 5c73f7ada..cfb99d81f 100755 --- a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetRC2.java +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetRC2.java @@ -19,23 +19,45 @@ */ package com.ibm.plugin.rules.detection.dotnet; +import com.ibm.engine.detection.MethodMatcher; import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.CipherAction; +import com.ibm.engine.model.Size; import com.ibm.engine.model.context.CipherContext; +import com.ibm.engine.model.factory.BlockSizeFactory; +import com.ibm.engine.model.factory.CipherActionFactory; +import com.ibm.engine.model.factory.KeySizeFactory; +import com.ibm.engine.model.factory.ModeFactory; +import com.ibm.engine.model.factory.PaddingFactory; import com.ibm.engine.model.factory.ValueActionFactory; import com.ibm.engine.rule.IDetectionRule; import com.ibm.engine.rule.builder.DetectionRuleBuilder; import java.util.List; +import java.util.stream.Stream; import javax.annotation.Nonnull; /** * Detection rules for RC2 usage in System.Security.Cryptography. * - *

Detects: + *

Classes covered: * *

    - *
  • {@code RC2.Create()} — abstract factory (weak cipher) - *
  • {@code new RC2CryptoServiceProvider()} — CAPI-backed (weak cipher) + *
  • {@code RC2} — abstract base ({@code RC2.Create()}, {@code RC2.Create(string)}) + *
  • {@code RC2CryptoServiceProvider} — legacy CAPI implementation *
+ * + *

Architecture: all methods inherited from {@code SymmetricAlgorithm} (EncryptCbc, DecryptCbc, + * CreateEncryptor, property setters, etc.) are expressed as depending rules attached to + * each primary creation rule. The detection engine tracks the variable and fires these rules on + * every matching method call, regardless of the concrete RC2 subclass. Like {@code DES}, {@code + * RC2} has no CNG-backed subclass and no AEAD variant, so it mirrors {@code DotNetDES}'s + * depending-rule coverage, with one addition: RC2's own {@code EffectiveKeySize} property. + * + *

Known gap: {@code RC2CryptoServiceProvider.UseSalt} (a CAPI-only boolean flag controlling + * whether an 11-byte zero-value salt is appended when deriving a key) has no corresponding concept + * in the detection model (no boolean/flag value factory exists, and it does not map to a CBOM + * property such as mode, padding or key/block size). It is therefore intentionally left undetected. + * // TODO: RC2CryptoServiceProvider.UseSalt is not detected as a known gap. */ @SuppressWarnings("java:S1192") public final class DotNetRC2 { @@ -44,6 +66,525 @@ private DotNetRC2() { // nothing } + // ========================================================================= + // Property setter rules (synthetic set_X method invocations) + // ========================================================================= + + // rc2.Mode = CipherMode.CBC → synthetic set_Mode(CipherMode.CBC) + private static final IDetectionRule RC2_SET_MODE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_Mode") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new ModeFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // rc2.KeySize = 128 → synthetic set_KeySize(128) + private static final IDetectionRule RC2_SET_KEY_SIZE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_KeySize") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new KeySizeFactory<>(Size.UnitType.BIT)) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // rc2.EffectiveKeySize = 64 → synthetic set_EffectiveKeySize(64) + // RC2-specific property (not present on SymmetricAlgorithm nor on Aes/DES). Semantically it + // constrains the effective cryptographic strength of the key in bits, so it is reused as a + // KeySize detection (same as set_KeySize above) rather than introducing a dedicated factory. + private static final IDetectionRule RC2_SET_EFFECTIVE_KEY_SIZE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_EffectiveKeySize") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new KeySizeFactory<>(Size.UnitType.BIT)) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // rc2.Padding = PaddingMode.PKCS7 → synthetic set_Padding(PaddingMode.PKCS7) + private static final IDetectionRule RC2_SET_PADDING = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_Padding") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // rc2.FeedbackSize = 8 → synthetic set_FeedbackSize(8) + private static final IDetectionRule RC2_SET_FEEDBACK_SIZE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_FeedbackSize") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new BlockSizeFactory<>(Size.UnitType.BIT)) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + private static final List> PROPERTY_SETTER_RULES = + List.of( + RC2_SET_MODE, + RC2_SET_KEY_SIZE, + RC2_SET_EFFECTIVE_KEY_SIZE, + RC2_SET_PADDING, + RC2_SET_FEEDBACK_SIZE); + + // ========================================================================= + // CreateEncryptor / CreateDecryptor rules + // ========================================================================= + + // rc2.CreateEncryptor() + private static final IDetectionRule RC2_CREATE_ENCRYPTOR = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateEncryptor") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.ENCRYPT)) + .withoutParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // rc2.CreateEncryptor(byte[] key, byte[] iv) + private static final IDetectionRule RC2_CREATE_ENCRYPTOR_WITH_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateEncryptor") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.ENCRYPT)) + .withMethodParameter(MethodMatcher.ANY) // key bytes + .withMethodParameter(MethodMatcher.ANY) // iv bytes + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // rc2.CreateDecryptor() + private static final IDetectionRule RC2_CREATE_DECRYPTOR = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateDecryptor") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.DECRYPT)) + .withoutParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // rc2.CreateDecryptor(byte[] key, byte[] iv) + private static final IDetectionRule RC2_CREATE_DECRYPTOR_WITH_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateDecryptor") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.DECRYPT)) + .withMethodParameter(MethodMatcher.ANY) // key bytes + .withMethodParameter(MethodMatcher.ANY) // iv bytes + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // EncryptCbc / DecryptCbc rules + // Mode is constant "CBC" (from method name); padding is detected from last param. + // Two overloads: 3-param and 4-param (with output buffer). + // ========================================================================= + + // EncryptCbc(plaintext, iv, padding) + private static final IDetectionRule RC2_ENCRYPT_CBC_3 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptCbc") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // EncryptCbc(plaintext, iv, destination, padding) [output-buffer overload] + private static final IDetectionRule RC2_ENCRYPT_CBC_4 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptCbc") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptCbc(ciphertext, iv, padding) + private static final IDetectionRule RC2_DECRYPT_CBC_3 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptCbc") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptCbc(ciphertext, iv, destination, padding) + private static final IDetectionRule RC2_DECRYPT_CBC_4 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptCbc") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // EncryptEcb / DecryptEcb rules + // Mode is constant "ECB" (from method name); no IV parameter. + // Two overloads: 2-param and 3-param (with output buffer). + // ========================================================================= + + // EncryptEcb(plaintext, padding) + private static final IDetectionRule RC2_ENCRYPT_ECB_2 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptEcb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // EncryptEcb(plaintext, destination, padding) + private static final IDetectionRule RC2_ENCRYPT_ECB_3 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptEcb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptEcb(ciphertext, padding) + private static final IDetectionRule RC2_DECRYPT_ECB_2 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptEcb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptEcb(ciphertext, destination, padding) + private static final IDetectionRule RC2_DECRYPT_ECB_3 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptEcb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // EncryptCfb / DecryptCfb rules + // Mode is constant "CFB"; padding detected from 3rd param; feedbackSize ignored. + // Two overloads: 4-param and 5-param (with output buffer). + // ========================================================================= + + // EncryptCfb(plaintext, iv, padding, feedbackSize) + private static final IDetectionRule RC2_ENCRYPT_CFB_4 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptCfb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize (int) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // EncryptCfb(plaintext, iv, destination, padding, feedbackSize) + private static final IDetectionRule RC2_ENCRYPT_CFB_5 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptCfb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize (int) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptCfb(ciphertext, iv, padding, feedbackSize) + private static final IDetectionRule RC2_DECRYPT_CFB_4 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptCfb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize (int) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptCfb(ciphertext, iv, destination, padding, feedbackSize) + private static final IDetectionRule RC2_DECRYPT_CFB_5 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptCfb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize (int) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // TryEncrypt* / TryDecrypt* rules + // Signatures: + // TryEncryptCbc(plaintext, iv, destination, out bytesWritten, padding) — 5 params + // TryDecryptCbc(ciphertext, iv, destination, out bytesWritten, padding) — 5 params + // TryEncryptEcb(plaintext, destination, padding, out bytesWritten) — 4 params + // TryDecryptEcb(ciphertext, destination, padding, out bytesWritten) — 4 params + // TryEncryptCfb(plaintext, iv, destination, out bytesWritten, padding, fs) — 6 params + // TryDecryptCfb(ciphertext, iv, destination, out bytesWritten, padding, fs) — 6 params + // ========================================================================= + + // TryEncryptCbc(plaintext, iv, destination, out bytesWritten, padding) + private static final IDetectionRule RC2_TRY_ENCRYPT_CBC = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryEncryptCbc") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryDecryptCbc(ciphertext, iv, destination, out bytesWritten, padding) + private static final IDetectionRule RC2_TRY_DECRYPT_CBC = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryDecryptCbc") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryEncryptEcb(plaintext, destination, padding, out bytesWritten) + private static final IDetectionRule RC2_TRY_ENCRYPT_ECB = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryEncryptEcb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryDecryptEcb(ciphertext, destination, padding, out bytesWritten) + private static final IDetectionRule RC2_TRY_DECRYPT_ECB = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryDecryptEcb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryEncryptCfb(plaintext, iv, destination, out bytesWritten, padding, feedbackSize) + private static final IDetectionRule RC2_TRY_ENCRYPT_CFB = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryEncryptCfb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryDecryptCfb(ciphertext, iv, destination, out bytesWritten, padding, feedbackSize) + private static final IDetectionRule RC2_TRY_DECRYPT_CFB = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryDecryptCfb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // Key / IV generation rules + // ========================================================================= + + // rc2.GenerateKey() — generates a new random key (size determined by KeySize property) + private static final IDetectionRule RC2_GENERATE_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("GenerateKey") + .shouldBeDetectedAs(new ValueActionFactory<>("GenerateKey")) + .withoutParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // rc2.GenerateIV() — generates a new random initialization vector + private static final IDetectionRule RC2_GENERATE_IV = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("GenerateIV") + .shouldBeDetectedAs(new ValueActionFactory<>("GenerateIV")) + .withoutParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // Aggregated depending-rule lists + // ========================================================================= + + /** + * All cipher operation rules that fire on a tracked RC2-family variable. Includes + * CreateEncryptor/CreateDecryptor, direct mode-specific Encrypt/Decrypt methods, Try* variants, + * and key/IV generation. + */ + private static final List> CIPHER_OP_RULES = + List.of( + RC2_CREATE_ENCRYPTOR, + RC2_CREATE_ENCRYPTOR_WITH_KEY, + RC2_CREATE_DECRYPTOR, + RC2_CREATE_DECRYPTOR_WITH_KEY, + RC2_ENCRYPT_CBC_3, + RC2_ENCRYPT_CBC_4, + RC2_DECRYPT_CBC_3, + RC2_DECRYPT_CBC_4, + RC2_ENCRYPT_ECB_2, + RC2_ENCRYPT_ECB_3, + RC2_DECRYPT_ECB_2, + RC2_DECRYPT_ECB_3, + RC2_ENCRYPT_CFB_4, + RC2_ENCRYPT_CFB_5, + RC2_DECRYPT_CFB_4, + RC2_DECRYPT_CFB_5, + RC2_TRY_ENCRYPT_CBC, + RC2_TRY_DECRYPT_CBC, + RC2_TRY_ENCRYPT_ECB, + RC2_TRY_DECRYPT_ECB, + RC2_TRY_ENCRYPT_CFB, + RC2_TRY_DECRYPT_CFB, + RC2_GENERATE_KEY, + RC2_GENERATE_IV); + + /** Full set of depending rules for all RC2-derived classes. */ + private static final List> RC2_DEPENDING_RULES = + Stream.concat(PROPERTY_SETTER_RULES.stream(), CIPHER_OP_RULES.stream()).toList(); + + // ========================================================================= + // Primary creation rules + // ========================================================================= + + // RC2.Create() — abstract factory, no parameters private static final IDetectionRule RC2_CREATE = new DetectionRuleBuilder() .createDetectionRule() @@ -53,8 +594,21 @@ private DotNetRC2() { .withoutParameters() .buildForContext(new CipherContext()) .inBundle(() -> "DotNet") - .withDependingDetectionRules(List.of()); + .withDependingDetectionRules(RC2_DEPENDING_RULES); + + // RC2.Create("RC2") — named factory (obsolete, still detectable) + private static final IDetectionRule RC2_CREATE_NAMED = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("RC2") + .forMethods("Create") + .shouldBeDetectedAs(new ValueActionFactory<>("RC2")) + .withMethodParameter(MethodMatcher.ANY) // algorithm name string + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(RC2_DEPENDING_RULES); + // new RC2CryptoServiceProvider() — legacy CAPI implementation private static final IDetectionRule RC2_CSP = new DetectionRuleBuilder() .createDetectionRule() @@ -64,10 +618,10 @@ private DotNetRC2() { .withoutParameters() .buildForContext(new CipherContext()) .inBundle(() -> "DotNet") - .withDependingDetectionRules(List.of()); + .withDependingDetectionRules(RC2_DEPENDING_RULES); @Nonnull public static List> rules() { - return List.of(RC2_CREATE, RC2_CSP); + return List.of(RC2_CREATE, RC2_CREATE_NAMED, RC2_CSP); } } diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetRSA.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetRSA.java index 43cb3f561..65ef688fe 100755 --- a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetRSA.java +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetRSA.java @@ -19,26 +19,49 @@ */ package com.ibm.plugin.rules.detection.dotnet; +import com.ibm.engine.detection.MethodMatcher; import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.CipherAction; +import com.ibm.engine.model.SignatureAction; +import com.ibm.engine.model.Size; +import com.ibm.engine.model.context.CipherContext; import com.ibm.engine.model.context.KeyContext; +import com.ibm.engine.model.context.SignatureContext; +import com.ibm.engine.model.factory.CipherActionFactory; +import com.ibm.engine.model.factory.KeySizeFactory; +import com.ibm.engine.model.factory.SignatureActionFactory; import com.ibm.engine.model.factory.ValueActionFactory; import com.ibm.engine.rule.IDetectionRule; import com.ibm.engine.rule.builder.DetectionRuleBuilder; import java.util.List; import java.util.Map; +import java.util.stream.Stream; import javax.annotation.Nonnull; /** * Detection rules for RSA usage in System.Security.Cryptography. * - *

Detects: + *

Classes covered: * *

    - *
  • {@code RSA.Create()} — abstract factory, no key size - *
  • {@code RSA.Create(2048)} — factory with key size - *
  • {@code new RSACryptoServiceProvider()} — CAPI-backed, no key size - *
  • {@code new RSACryptoServiceProvider(2048)} — CAPI-backed with key size + *
  • {@code RSA} — abstract base ({@code RSA.Create()}, {@code RSA.Create(int)}, {@code + * RSA.Create(RSAParameters)}, {@code RSA.Create(string)}) + *
  • {@code RSACryptoServiceProvider} — legacy CAPI implementation + *
  • {@code RSACng} — CNG-backed implementation, Windows-only (ephemeral and persisted-key + * constructors) + *
  • {@code RSAOpenSsl} — OpenSSL-backed implementation, non-Windows only *
+ * + *

Architecture: all members inherited from {@code RSA} / {@code AsymmetricAlgorithm} (the {@code + * KeySize} property, {@code Encrypt}/{@code Decrypt}, {@code SignData}/{@code VerifyData}, {@code + * SignHash}/{@code VerifyHash}, and their {@code Try*} variants) are expressed as depending + * rules attached to each primary creation rule. The detection engine tracks the variable and + * fires these rules on every matching method call, regardless of the concrete RSA subclass. Method + * overloads that only differ by array-vs-{@code Span}, offset/length, or output-buffer parameters + * are intentionally collapsed into a single {@code withAnyParameters()} rule per method name: the + * ANTLR4-based C# engine cannot resolve parameter types (see {@code CSharpLanguageTranslation}), so + * distinguishing overloads by parameter type is not possible, and none of the extra parameters + * carry additional cryptographic information worth extracting. */ @SuppressWarnings("java:S1192") public final class DotNetRSA { @@ -47,6 +70,189 @@ private DotNetRSA() { // nothing } + // ========================================================================= + // Property setter rules (synthetic set_X method invocations) + // ========================================================================= + + // rsa.KeySize = 2048 → synthetic set_KeySize(2048) + private static final IDetectionRule RSA_SET_KEY_SIZE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_KeySize") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new KeySizeFactory<>(Size.UnitType.BIT)) + .buildForContext(new KeyContext(Map.of("kind", "RSA"))) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // Encryption / decryption operation rules (RSA used as a public-key cipher) + // Each rule covers every overload of the given method name (arities vary only + // by array-vs-Span / output-buffer parameters, which are not individually + // tracked), mirroring the AES direct Encrypt/Decrypt rules. + // ========================================================================= + + // rsa.Encrypt(data, padding) / rsa.Encrypt(data, destination, padding) + private static final IDetectionRule RSA_ENCRYPT = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("Encrypt") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.ENCRYPT)) + .withAnyParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // rsa.Decrypt(data, padding) / rsa.Decrypt(data, destination, padding) + private static final IDetectionRule RSA_DECRYPT = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("Decrypt") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.DECRYPT)) + .withAnyParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // rsa.TryEncrypt(data, destination, padding, out bytesWritten) + private static final IDetectionRule RSA_TRY_ENCRYPT = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryEncrypt") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.ENCRYPT)) + .withAnyParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // rsa.TryDecrypt(data, destination, padding, out bytesWritten) + private static final IDetectionRule RSA_TRY_DECRYPT = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryDecrypt") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.DECRYPT)) + .withAnyParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + private static final List> CIPHER_OP_RULES = + List.of(RSA_ENCRYPT, RSA_DECRYPT, RSA_TRY_ENCRYPT, RSA_TRY_DECRYPT); + + // ========================================================================= + // Signing / verification operation rules + // Each rule covers every overload of the given method name (arities vary only + // by hash-algorithm / signature-padding / offset-length / output-buffer + // parameters, which are not individually tracked), mirroring the DSA + // SignData()/VerifyData() rules. Note that RSA has no TryVerifyData/ + // TryVerifyHash methods (verification returns a bool directly, so there is no + // output buffer to size). + // ========================================================================= + + // rsa.SignData(data, hashAlgorithm, padding) [+ offset/length or Stream overloads] + private static final IDetectionRule RSA_SIGN_DATA = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("SignData") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.SIGN)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // rsa.TrySignData(data, destination, hashAlgorithm, padding, out bytesWritten) + private static final IDetectionRule RSA_TRY_SIGN_DATA = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TrySignData") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.SIGN)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // rsa.SignHash(hash, hashAlgorithm, padding) + private static final IDetectionRule RSA_SIGN_HASH = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("SignHash") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.SIGN)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // rsa.TrySignHash(hash, destination, hashAlgorithm, padding, out bytesWritten) + private static final IDetectionRule RSA_TRY_SIGN_HASH = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TrySignHash") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.SIGN)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // rsa.VerifyData(data, signature, hashAlgorithm, padding) [+ offset/length or Stream overloads] + private static final IDetectionRule RSA_VERIFY_DATA = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("VerifyData") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.VERIFY)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // rsa.VerifyHash(hash, signature, hashAlgorithm, padding) + private static final IDetectionRule RSA_VERIFY_HASH = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("VerifyHash") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.VERIFY)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + private static final List> SIGNATURE_OP_RULES = + List.of( + RSA_SIGN_DATA, + RSA_TRY_SIGN_DATA, + RSA_SIGN_HASH, + RSA_TRY_SIGN_HASH, + RSA_VERIFY_DATA, + RSA_VERIFY_HASH); + + // ========================================================================= + // Aggregated depending-rule list + // ========================================================================= + + /** Full set of depending rules for all RSA-derived classes. */ + private static final List> RSA_DEPENDING_RULES = + Stream.of( + Stream.of(RSA_SET_KEY_SIZE), + CIPHER_OP_RULES.stream(), + SIGNATURE_OP_RULES.stream()) + .flatMap(i -> i) + .toList(); + + // ========================================================================= + // Primary creation rules + // ========================================================================= + + // RSA.Create() / RSA.Create(int) / RSA.Create(RSAParameters) / RSA.Create(string) private static final IDetectionRule RSA_CREATE = new DetectionRuleBuilder() .createDetectionRule() @@ -56,8 +262,9 @@ private DotNetRSA() { .withAnyParameters() .buildForContext(new KeyContext(Map.of("kind", "RSA"))) .inBundle(() -> "DotNet") - .withDependingDetectionRules(List.of()); + .withDependingDetectionRules(RSA_DEPENDING_RULES); + // new RSACryptoServiceProvider() / (int) / (CspParameters) / (int, CspParameters) private static final IDetectionRule RSA_CRYPTO_SERVICE_PROVIDER = new DetectionRuleBuilder() .createDetectionRule() @@ -67,10 +274,36 @@ private DotNetRSA() { .withAnyParameters() .buildForContext(new KeyContext(Map.of("kind", "RSA"))) .inBundle(() -> "DotNet") - .withDependingDetectionRules(List.of()); + .withDependingDetectionRules(RSA_DEPENDING_RULES); + + // new RSACng() / new RSACng(CngKey) / new RSACng(int) — CNG-backed implementation. + // Uses withAnyParameters() to avoid double-detection that would occur if a separate + // withoutParameters() rule were added alongside this one (see AES_CNG_NAMED). + private static final IDetectionRule RSA_CNG = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("RSACng") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("RSA")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "RSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(RSA_DEPENDING_RULES); + + // new RSAOpenSsl() / (int) / (IntPtr) / (RSAParameters) / (SafeEvpPKeyHandle) + private static final IDetectionRule RSA_OPENSSL = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("RSAOpenSsl") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("RSA")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "RSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(RSA_DEPENDING_RULES); @Nonnull public static List> rules() { - return List.of(RSA_CREATE, RSA_CRYPTO_SERVICE_PROVIDER); + return List.of(RSA_CREATE, RSA_CRYPTO_SERVICE_PROVIDER, RSA_CNG, RSA_OPENSSL); } } diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetRandomNumberGenerator.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetRandomNumberGenerator.java new file mode 100644 index 000000000..784fb4ea1 --- /dev/null +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetRandomNumberGenerator.java @@ -0,0 +1,323 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import com.ibm.engine.detection.MethodMatcher; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.context.PRNGContext; +import com.ibm.engine.model.factory.ValueActionFactory; +import com.ibm.engine.rule.IDetectionRule; +import com.ibm.engine.rule.builder.DetectionRuleBuilder; +import java.util.List; +import javax.annotation.Nonnull; + +/** + * Detection rules for {@code RandomNumberGenerator} and {@code RNGCryptoServiceProvider} in + * System.Security.Cryptography. + * + *

Classes/members covered (per the official API reference, verified via WebFetch of + * learn.microsoft.com — not guessed): + * + *

    + *
  • {@code RandomNumberGenerator.Create()} / {@code Create(string)} — instance factory (the + * {@code Create(string)} overload is marked {@code Obsolete} in recent .NET versions but + * remains valid, detectable legacy source). The returned instance exposes the {@code + * virtual}/{@code abstract} instance methods {@code GetBytes(byte[])}, {@code + * GetBytes(byte[], int, int)}, {@code GetBytes(Span)}, {@code GetNonZeroBytes(byte[])} + * and {@code GetNonZeroBytes(Span)}, tracked as depending rules. Verified directly + * against the .NET 10 compiler (not just the doc prose), because the doc's "When overridden + * in a derived class" phrasing is the only reliable signal distinguishing these from the + * static overloads below — {@code GetBytes(Span)} and {@code + * GetNonZeroBytes(Span)} both fail to compile as {@code + * RandomNumberGenerator.GetBytes(span)}/{@code RandomNumberGenerator.GetNonZeroBytes(span)} + * (CS0120: "An object reference is required for the non-static ... method"), proving they are + * {@code virtual} instance members despite their {@code Span}-based signature superficially + * resembling the genuinely-static {@code Span}-based overloads of {@code Fill}/{@code + * GetHexString}/{@code GetItems}/{@code GetString}. An earlier version of this file + * incorrectly classified both as static-only; that was caught by compiling every {@code .cs} + * test fixture in this module against the real Roslyn compiler (see {@code + * DotNetRandomNumberGeneratorTestFile.cs}). + *
  • {@code RandomNumberGenerator}'s genuinely static-only members (each empirically + * confirmed to compile as a bare {@code RandomNumberGenerator.Method(...)} call with no + * instance): {@code Fill(Span)}, {@code GetBytes(int)}, {@code GetHexString(int, bool)} + * / {@code GetHexString(Span, bool)}, {@code GetInt32(int)} / {@code GetInt32(int, + * int)}, {@code GetItems(ReadOnlySpan, int)} / {@code GetItems(ReadOnlySpan, + * Span)}, {@code GetString(ReadOnlySpan, int)}, and {@code Shuffle(Span)}. + * "Using the static members of this class is the preferred way to generate random values" per + * the official documentation. + *
  • {@code RNGCryptoServiceProvider} — legacy CSP-backed implementation ({@code Obsolete} since + * .NET 6, still valid, detectable legacy source). Four constructor overloads ({@code ()}, + * {@code (byte[])}, {@code (CspParameters)}, {@code (string)}), with instance {@code + * GetBytes}/{@code GetNonZeroBytes} (each with a {@code byte[]} and a {@code Span}-based + * overload) tracked as depending rules. + *
+ * + *

Modeling decision — reused the existing {@code PseudorandomNumberGenerator} /{@code + * PRNGContext} concept instead of inventing a new mapper model class: the mapper module already + * has a generic "randomness source" concept used by both the Java and Go modules for the exact same + * kind of API (a call that produces cryptographically strong random output with no user-selectable + * algorithm identity): {@code java.security.SecureRandom} ({@link + * com.ibm.plugin.rules.detection.random.SecureRandomGetInstance} in the {@code java} module) and + * Go's {@code crypto/rand} ({@link com.ibm.plugin.rules.detection.gocrypto.GoCryptoRand} in the + * {@code go} module) both translate through {@code PRNGContext} to a {@code + * com.ibm.mapper.model.Algorithm} node typed as {@code + * com.ibm.mapper.model.PseudorandomNumberGenerator}. The Go precedent is architecturally the + * closest analogue to .NET's {@code RandomNumberGenerator}: both are "ask the OS/platform for + * cryptographically strong random bytes, no algorithm selection" APIs (unlike Java's {@code + * SecureRandom}, which supports named provider algorithms such as {@code "NativePRNG"} or {@code + * "SHA1PRNG"}). This file follows the Go precedent exactly: every self-contained creation/static + * call is captured with {@code ValueActionFactory<>("NATIVEPRNG")} and dispatched in the new {@code + * CSharpPRNGContextTranslator} to {@code new Algorithm("NATIVEPRNG", + * PseudorandomNumberGenerator.class, detectionLocation)} — no new mapper model class was created + * for this batch. + * + *

Modeling decision — static self-contained calls vs. instance depending-rule operations: + * mirrors the {@code HKDF} vs. {@code SP800108HmacCounterKdf} distinction established in {@link + * DotNetKeyDerivation}. {@code RandomNumberGenerator}'s genuinely static methods ({@code Fill}, + * {@code GetBytes(int)}, {@code GetHexString}, {@code GetInt32}, {@code GetItems}, {@code + * GetString}, {@code Shuffle}) are each a complete, self-contained "the platform CSPRNG was used" + * event with no instance to track — they are top-level rules mapping directly to the {@code + * NATIVEPRNG} algorithm identity, exactly like {@code HKDF.Extract}/{@code Expand}/{@code + * DeriveKey}. Where an actual instance *is* tracked ({@code RandomNumberGenerator.Create()}'s or + * {@code RNGCryptoServiceProvider}'s instance {@code GetBytes}/{@code GetNonZeroBytes}, including + * their {@code Span} overloads — see the "Verified directly against the .NET 10 compiler" + * note above), those calls are depending rules attached to the creation rule, translated to the + * generic {@code Generate} functionality node (mirrors {@code AES_GENERATE_IV} in {@link + * DotNetAES}, which also has no more specific {@code CipherAction} available) as a child of the + * already-identified {@code NATIVEPRNG} algorithm node. + * + *

As with every other file in this rule set, the ANTLR4-based C# engine cannot resolve parameter + * types (see {@code CSharpLanguageTranslation}) or values held in variables (see {@code + * CSharpTreeConverter} — only literals and bare identifiers are read), so all overloads that only + * differ by {@code byte[]} vs. {@code Span}/{@code ReadOnlySpan}, by an optional trailing + * {@code bool}/output-buffer parameter, or by generic type argument are collapsed into a single + * {@code withAnyParameters()} rule per method name. + */ +@SuppressWarnings("java:S1192") +public final class DotNetRandomNumberGenerator { + + private DotNetRandomNumberGenerator() { + // nothing + } + + // ========================================================================= + // Depending rules — instance operations on an already-tracked RNG object, + // shared by RandomNumberGenerator.Create()/Create(string) and + // RNGCryptoServiceProvider's constructors (RNGCryptoServiceProvider derives from + // RandomNumberGenerator and exposes the identical GetBytes/GetNonZeroBytes instance API). + // ========================================================================= + + // rng.GetBytes(data) / rng.GetBytes(data, offset, count) + private static final IDetectionRule RNG_INSTANCE_GET_BYTES = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("GetBytes") + .shouldBeDetectedAs(new ValueActionFactory<>("GetBytes")) + .withAnyParameters() + .buildForContext(new PRNGContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // rng.GetNonZeroBytes(data) + private static final IDetectionRule RNG_INSTANCE_GET_NON_ZERO_BYTES = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("GetNonZeroBytes") + .shouldBeDetectedAs(new ValueActionFactory<>("GetNonZeroBytes")) + .withAnyParameters() + .buildForContext(new PRNGContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + private static final List> RNG_INSTANCE_DEPENDING_RULES = + List.of(RNG_INSTANCE_GET_BYTES, RNG_INSTANCE_GET_NON_ZERO_BYTES); + + // ========================================================================= + // RandomNumberGenerator.Create() / Create(string) — instance factory. + // ========================================================================= + + // RandomNumberGenerator.Create() + private static final IDetectionRule RNG_CREATE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("RandomNumberGenerator") + .forMethods("Create") + .shouldBeDetectedAs(new ValueActionFactory<>("NATIVEPRNG")) + .withoutParameters() + .buildForContext(new PRNGContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(RNG_INSTANCE_DEPENDING_RULES); + + // RandomNumberGenerator.Create(string rngName) — Obsolete in recent .NET, still detectable. + private static final IDetectionRule RNG_CREATE_NAMED = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("RandomNumberGenerator") + .forMethods("Create") + .shouldBeDetectedAs(new ValueActionFactory<>("NATIVEPRNG")) + .withMethodParameter(MethodMatcher.ANY) // algorithm name string + .buildForContext(new PRNGContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(RNG_INSTANCE_DEPENDING_RULES); + + // ========================================================================= + // RandomNumberGenerator static-only methods — each call is both the "creation" and + // the "operation" at once (see class javadoc "Modeling decision"). Overloads distinguished + // only by byte[] vs. Span/ReadOnlySpan, by an optional trailing bool/output-buffer + // parameter, or by generic type argument are collapsed via withAnyParameters(). + // ========================================================================= + + // RandomNumberGenerator.Fill(Span data) + private static final IDetectionRule RNG_FILL = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("RandomNumberGenerator") + .forMethods("Fill") + .shouldBeDetectedAs(new ValueActionFactory<>("NATIVEPRNG")) + .withAnyParameters() + .buildForContext(new PRNGContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // RandomNumberGenerator.GetBytes(int count) — the only genuinely static GetBytes overload + // (GetBytes(byte[]) and GetBytes(Span) are both virtual instance methods, covered by + // RNG_INSTANCE_GET_BYTES above — see class javadoc for the compiler-verified proof). + private static final IDetectionRule RNG_STATIC_GET_BYTES = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("RandomNumberGenerator") + .forMethods("GetBytes") + .shouldBeDetectedAs(new ValueActionFactory<>("NATIVEPRNG")) + .withAnyParameters() + .buildForContext(new PRNGContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // RandomNumberGenerator.GetHexString(int count, bool lowercase = false) + // / GetHexString(Span destination, bool lowercase = false) + private static final IDetectionRule RNG_GET_HEX_STRING = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("RandomNumberGenerator") + .forMethods("GetHexString") + .shouldBeDetectedAs(new ValueActionFactory<>("NATIVEPRNG")) + .withAnyParameters() + .buildForContext(new PRNGContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // RandomNumberGenerator.GetInt32(int toExclusive) / GetInt32(int fromInclusive, int + // toExclusive) + private static final IDetectionRule RNG_GET_INT32 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("RandomNumberGenerator") + .forMethods("GetInt32") + .shouldBeDetectedAs(new ValueActionFactory<>("NATIVEPRNG")) + .withAnyParameters() + .buildForContext(new PRNGContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // RandomNumberGenerator.GetItems(ReadOnlySpan choices, int length) + // / GetItems(ReadOnlySpan choices, Span destination) + private static final IDetectionRule RNG_GET_ITEMS = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("RandomNumberGenerator") + .forMethods("GetItems") + .shouldBeDetectedAs(new ValueActionFactory<>("NATIVEPRNG")) + .withAnyParameters() + .buildForContext(new PRNGContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // NOTE: there is no static GetNonZeroBytes rule here — GetNonZeroBytes(byte[]) *and* + // GetNonZeroBytes(Span) are BOTH virtual instance methods (confirmed by compiling + // `RandomNumberGenerator.GetNonZeroBytes(data)` with the real .NET 10 compiler: CS0120, + // "An object reference is required for the non-static ... method"). Unlike GetBytes, which has + // a genuinely static GetBytes(int) overload alongside its instance overloads, GetNonZeroBytes + // has no static form at all. Both GetNonZeroBytes overloads are already covered by the + // RNG_INSTANCE_GET_NON_ZERO_BYTES depending rule above (withAnyParameters() does not + // distinguish byte[] from Span). + + // RandomNumberGenerator.GetString(ReadOnlySpan choices, int length) + private static final IDetectionRule RNG_GET_STRING = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("RandomNumberGenerator") + .forMethods("GetString") + .shouldBeDetectedAs(new ValueActionFactory<>("NATIVEPRNG")) + .withAnyParameters() + .buildForContext(new PRNGContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // RandomNumberGenerator.Shuffle(Span values) + private static final IDetectionRule RNG_SHUFFLE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("RandomNumberGenerator") + .forMethods("Shuffle") + .shouldBeDetectedAs(new ValueActionFactory<>("NATIVEPRNG")) + .withAnyParameters() + .buildForContext(new PRNGContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // RNGCryptoServiceProvider — legacy CSP-backed implementation (Obsolete since .NET 6, still + // detectable legacy source). Constructor + instance GetBytes/GetNonZeroBytes, reusing the + // same depending-rule pair as RandomNumberGenerator.Create() above since both classes expose + // the identical instance API. + // ========================================================================= + + // new RNGCryptoServiceProvider() / (byte[] rgb) / (CspParameters cspParams) / (string str) + // — 4 constructor overloads, all collapsed via withAnyParameters() (mirrors the AesCng + // precedent in DotNetAES: a single withAnyParameters() rule avoids double-detection that + // would occur if a separate withoutParameters() rule were added alongside it). + private static final IDetectionRule RNG_CSP_CTOR = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("RNGCryptoServiceProvider") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("NATIVEPRNG")) + .withAnyParameters() + .buildForContext(new PRNGContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(RNG_INSTANCE_DEPENDING_RULES); + + @Nonnull + public static List> rules() { + return List.of( + RNG_CREATE, + RNG_CREATE_NAMED, + RNG_FILL, + RNG_STATIC_GET_BYTES, + RNG_GET_HEX_STRING, + RNG_GET_INT32, + RNG_GET_ITEMS, + RNG_GET_STRING, + RNG_SHUFFLE, + RNG_CSP_CTOR); + } +} diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetRfc2898DeriveBytes.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetRfc2898DeriveBytes.java index 0bb0822ed..82ac414f5 100755 --- a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetRfc2898DeriveBytes.java +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetRfc2898DeriveBytes.java @@ -19,6 +19,7 @@ */ package com.ibm.plugin.rules.detection.dotnet; +import com.ibm.engine.detection.MethodMatcher; import com.ibm.engine.language.csharp.tree.CSharpTree; import com.ibm.engine.model.context.KeyContext; import com.ibm.engine.model.factory.ValueActionFactory; @@ -36,7 +37,37 @@ *

    *
  • {@code new Rfc2898DeriveBytes(password, salt, iterations)} — SHA-1 default *
  • {@code new Rfc2898DeriveBytes(password, salt, iterations, hashAlgorithm)} — explicit hash + *
  • {@code Rfc2898DeriveBytes.Pbkdf2(...)} — static one-shot PBKDF2 (added in .NET 8; as of + * net-10.0/net-11.0 this is the recommended replacement for all 8 constructors above, which + * are now marked {@code Obsolete} per the official API reference) + *
  • {@code instance.GetBytes(int)} — pull more derived-key bytes from a constructed instance + *
  • {@code instance.CryptDeriveKey(string, string, int, byte[])} — CAPI-style key derivation + * from a constructed instance (also {@code Obsolete} per the reference, but still valid, + * detectable legacy source, same as the constructors) *
+ * + *

Modeling decision — {@code Pbkdf2(...)} as a top-level rule, not a depending rule: all + * 6 {@code Pbkdf2} overloads (per the official API reference: {@code Pbkdf2(byte[], byte[], int, + * HashAlgorithmName, int)}, {@code Pbkdf2(ReadOnlySpan, ReadOnlySpan, int, + * HashAlgorithmName, int)}, {@code Pbkdf2(ReadOnlySpan, ReadOnlySpan, Span, int, + * HashAlgorithmName)}, {@code Pbkdf2(ReadOnlySpan, ReadOnlySpan, int, + * HashAlgorithmName, int)}, {@code Pbkdf2(ReadOnlySpan, ReadOnlySpan, Span, int, + * HashAlgorithmName)}, {@code Pbkdf2(string, byte[], int, HashAlgorithmName, int)}) are {@code + * static} and self-contained: a single call is both the "creation" and the "operation" at once, + * exactly like {@code HKDF.Extract}/{@code Expand}/{@code DeriveKey} in {@link DotNetKeyDerivation} + * (see that class's javadoc "Modeling decision" for the full rationale). There is no object + * instance to track between a "creation" and a later "operation" step, so the Batch 3 + * creation-rule-with-depending-operations pattern does not apply here; the call is captured + * directly as a {@code ValueActionFactory<>("PBKDF2")} under the same {@code KeyContext} "kind" + * ({@code "KDF"}) as the constructor rule below, so both map to the identical {@code PBKDF2} mapper + * model node. + * + *

All 6 overloads happen to share the same arity (5 parameters each), so — as with every other + * file in this rule set — the ANTLR4-based C# engine's inability to resolve parameter types (see + * {@code CSharpLanguageTranslation}) is moot here even at the arity level: a single {@code + * withAnyParameters()} rule covers every overload without needing to disambiguate by parameter + * count. The {@code HashAlgorithmName} parameter is not decoded into a digest child node, + * consistent with the constructor rule and with {@code DotNetKeyDerivation}'s HKDF rules. */ @SuppressWarnings("java:S1192") public final class DotNetRfc2898DeriveBytes { @@ -45,6 +76,44 @@ private DotNetRfc2898DeriveBytes() { // nothing } + // ========================================================================= + // Instance operation depending rules, reusing the Batch 3 KeyDerivation pattern + // (see DotNetKeyDerivation's PasswordDeriveBytes section, which models the exact + // same two operation shapes for its sibling legacy KDF class). + // ========================================================================= + + // instance.GetBytes(cb) — pull more pseudo-random derived-key bytes + private static final IDetectionRule RFC2898_GET_BYTES = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("GetBytes") + .shouldBeDetectedAs(new ValueActionFactory<>("GetBytes")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "KDF_RFC2898_GET_BYTES"))) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // instance.CryptDeriveKey(algName, algHashName, keySize, rgbIV) + private static final IDetectionRule RFC2898_CRYPT_DERIVE_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CryptDeriveKey") + .shouldBeDetectedAs(new ValueActionFactory<>("CryptDeriveKey")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "KDF_RFC2898_CRYPT_DERIVE_KEY"))) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + private static final List> RFC2898_DEPENDING_RULES = + List.of(RFC2898_GET_BYTES, RFC2898_CRYPT_DERIVE_KEY); + + // ========================================================================= + // Primary rules + // ========================================================================= + + // new Rfc2898DeriveBytes(...) — all 8 constructor overloads, collapsed via withAnyParameters() private static final IDetectionRule RFC2898 = new DetectionRuleBuilder() .createDetectionRule() @@ -54,10 +123,23 @@ private DotNetRfc2898DeriveBytes() { .withAnyParameters() .buildForContext(new KeyContext(Map.of("kind", "KDF"))) .inBundle(() -> "DotNet") - .withDependingDetectionRules(List.of()); + .withDependingDetectionRules(RFC2898_DEPENDING_RULES); + + // Rfc2898DeriveBytes.Pbkdf2(...) — static one-shot, all 6 overloads (see class javadoc + // "Modeling decision"). + private static final IDetectionRule RFC2898_PBKDF2_STATIC = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("Rfc2898DeriveBytes") + .forMethods("Pbkdf2") + .shouldBeDetectedAs(new ValueActionFactory<>("PBKDF2")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "KDF"))) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); @Nonnull public static List> rules() { - return List.of(RFC2898); + return List.of(RFC2898, RFC2898_PBKDF2_STATIC); } } diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetSHA.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetSHA.java index 5911f9c64..c07eccd62 100755 --- a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetSHA.java +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetSHA.java @@ -19,6 +19,7 @@ */ package com.ibm.plugin.rules.detection.dotnet; +import com.ibm.engine.detection.MethodMatcher; import com.ibm.engine.language.csharp.tree.CSharpTree; import com.ibm.engine.model.context.DigestContext; import com.ibm.engine.model.factory.ValueActionFactory; @@ -28,10 +29,37 @@ import javax.annotation.Nonnull; /** - * Detection rules for SHA hash algorithms in System.Security.Cryptography. + * Detection rules for the SHA/MD5 hash algorithm family in System.Security.Cryptography. * - *

Detects factory methods and concrete class constructors for SHA-1, SHA-256, SHA-384, and - * SHA-512 (and their {@code Managed} concrete variants). + *

Classes covered: + * + *

    + *
  • {@code MD5} — abstract base ({@code MD5.Create()}, {@code MD5.Create(string)}), {@code + * MD5Cng}, {@code MD5CryptoServiceProvider} + *
  • {@code SHA1} — abstract base ({@code SHA1.Create()}, {@code SHA1.Create(string)}), {@code + * SHA1Managed}, {@code SHA1Cng}, {@code SHA1CryptoServiceProvider} + *
  • {@code SHA256} — abstract base ({@code SHA256.Create()}, {@code SHA256.Create(string)}), + * {@code SHA256Managed}, {@code SHA256Cng}, {@code SHA256CryptoServiceProvider} + *
  • {@code SHA384} — abstract base ({@code SHA384.Create()}, {@code SHA384.Create(string)}), + * {@code SHA384Managed}, {@code SHA384Cng}, {@code SHA384CryptoServiceProvider} + *
  • {@code SHA512} — abstract base ({@code SHA512.Create()}, {@code SHA512.Create(string)}), + * {@code SHA512Managed}, {@code SHA512Cng}, {@code SHA512CryptoServiceProvider} + *
  • {@code RIPEMD160} — abstract base ({@code RIPEMD160.Create()}, {@code + * RIPEMD160.Create(string)}), {@code RIPEMD160Managed}. Note: RIPEMD160 only ever existed in + * .NET Framework (documented up to net framework 4.8.1); it was never ported to .NET Core/5+. + * It is detected here for legacy/.NET Framework source code. + *
+ * + *

Architecture: unlike cipher/signature algorithms, a hash algorithm's complete + * cryptographically-relevant information (which digest algorithm, which digest length) is already + * fully captured by the creation rule itself (e.g. {@code new SHA256Managed()} already says + * everything necessary — SHA-256, 256 bits). Operation methods inherited from {@code HashAlgorithm} + * ({@code ComputeHash}, {@code ComputeHash(byte[])}, {@code ComputeHash(Stream)}, {@code + * TransformBlock}, {@code TransformFinalBlock}, {@code TryComputeHash}, the static {@code + * HashData}/{@code TryHashData} helpers) do not add cryptographically relevant information to the + * CBOM model (unlike Encrypt vs. Decrypt for ciphers, or Sign vs. Verify for signatures). + * Consistent with the pre-existing rules in this file (which never attached depending rules for + * these operations), no depending rules are added here either. */ @SuppressWarnings("java:S1192") public final class DotNetSHA { @@ -40,7 +68,63 @@ private DotNetSHA() { // nothing } + // ========================================================================= + // MD5 + // ========================================================================= + + // MD5.Create() — abstract factory, no parameters + private static final IDetectionRule MD5_CREATE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("MD5") + .forMethods("Create") + .shouldBeDetectedAs(new ValueActionFactory<>("MD5")) + .withoutParameters() + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of()); + + // MD5.Create("MD5") — named factory (obsolete in .NET 7+, still detectable) + private static final IDetectionRule MD5_CREATE_NAMED = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("MD5") + .forMethods("Create") + .shouldBeDetectedAs(new ValueActionFactory<>("MD5")) + .withMethodParameter(MethodMatcher.ANY) // algorithm name string + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of()); + + // new MD5CryptoServiceProvider() — legacy CAPI implementation + private static final IDetectionRule MD5_CSP = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("MD5CryptoServiceProvider") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("MD5")) + .withoutParameters() + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of()); + + // new MD5Cng() — CNG-backed implementation + private static final IDetectionRule MD5_CNG = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("MD5Cng") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("MD5")) + .withoutParameters() + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of()); + + // ========================================================================= // SHA1 + // ========================================================================= + + // SHA1.Create() — abstract factory, no parameters private static final IDetectionRule SHA1_CREATE = new DetectionRuleBuilder() .createDetectionRule() @@ -52,6 +136,19 @@ private DotNetSHA() { .inBundle(() -> "DotNet") .withDependingDetectionRules(List.of()); + // SHA1.Create("SHA1") — named factory (obsolete in .NET 7+, still detectable) + private static final IDetectionRule SHA1_CREATE_NAMED = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("SHA1") + .forMethods("Create") + .shouldBeDetectedAs(new ValueActionFactory<>("SHA1")) + .withMethodParameter(MethodMatcher.ANY) // algorithm name string + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of()); + + // new SHA1Managed() — pure-managed implementation private static final IDetectionRule SHA1_MANAGED = new DetectionRuleBuilder() .createDetectionRule() @@ -63,7 +160,35 @@ private DotNetSHA() { .inBundle(() -> "DotNet") .withDependingDetectionRules(List.of()); + // new SHA1Cng() — CNG-backed implementation + private static final IDetectionRule SHA1_CNG = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("SHA1Cng") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("SHA1")) + .withoutParameters() + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of()); + + // new SHA1CryptoServiceProvider() — legacy CAPI implementation + private static final IDetectionRule SHA1_CSP = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("SHA1CryptoServiceProvider") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("SHA1")) + .withoutParameters() + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of()); + + // ========================================================================= // SHA256 + // ========================================================================= + + // SHA256.Create() — abstract factory, no parameters private static final IDetectionRule SHA256_CREATE = new DetectionRuleBuilder() .createDetectionRule() @@ -75,6 +200,19 @@ private DotNetSHA() { .inBundle(() -> "DotNet") .withDependingDetectionRules(List.of()); + // SHA256.Create("SHA256") — named factory (obsolete in .NET 7+, still detectable) + private static final IDetectionRule SHA256_CREATE_NAMED = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("SHA256") + .forMethods("Create") + .shouldBeDetectedAs(new ValueActionFactory<>("SHA256")) + .withMethodParameter(MethodMatcher.ANY) // algorithm name string + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of()); + + // new SHA256Managed() — pure-managed implementation private static final IDetectionRule SHA256_MANAGED = new DetectionRuleBuilder() .createDetectionRule() @@ -86,7 +224,35 @@ private DotNetSHA() { .inBundle(() -> "DotNet") .withDependingDetectionRules(List.of()); + // new SHA256Cng() — CNG-backed implementation + private static final IDetectionRule SHA256_CNG = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("SHA256Cng") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("SHA256")) + .withoutParameters() + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of()); + + // new SHA256CryptoServiceProvider() — legacy CAPI implementation + private static final IDetectionRule SHA256_CSP = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("SHA256CryptoServiceProvider") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("SHA256")) + .withoutParameters() + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of()); + + // ========================================================================= // SHA384 + // ========================================================================= + + // SHA384.Create() — abstract factory, no parameters private static final IDetectionRule SHA384_CREATE = new DetectionRuleBuilder() .createDetectionRule() @@ -98,6 +264,19 @@ private DotNetSHA() { .inBundle(() -> "DotNet") .withDependingDetectionRules(List.of()); + // SHA384.Create("SHA384") — named factory (obsolete in .NET 7+, still detectable) + private static final IDetectionRule SHA384_CREATE_NAMED = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("SHA384") + .forMethods("Create") + .shouldBeDetectedAs(new ValueActionFactory<>("SHA384")) + .withMethodParameter(MethodMatcher.ANY) // algorithm name string + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of()); + + // new SHA384Managed() — pure-managed implementation private static final IDetectionRule SHA384_MANAGED = new DetectionRuleBuilder() .createDetectionRule() @@ -109,7 +288,35 @@ private DotNetSHA() { .inBundle(() -> "DotNet") .withDependingDetectionRules(List.of()); + // new SHA384Cng() — CNG-backed implementation + private static final IDetectionRule SHA384_CNG = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("SHA384Cng") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("SHA384")) + .withoutParameters() + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of()); + + // new SHA384CryptoServiceProvider() — legacy CAPI implementation + private static final IDetectionRule SHA384_CSP = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("SHA384CryptoServiceProvider") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("SHA384")) + .withoutParameters() + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of()); + + // ========================================================================= // SHA512 + // ========================================================================= + + // SHA512.Create() — abstract factory, no parameters private static final IDetectionRule SHA512_CREATE = new DetectionRuleBuilder() .createDetectionRule() @@ -121,6 +328,19 @@ private DotNetSHA() { .inBundle(() -> "DotNet") .withDependingDetectionRules(List.of()); + // SHA512.Create("SHA512") — named factory (obsolete in .NET 7+, still detectable) + private static final IDetectionRule SHA512_CREATE_NAMED = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("SHA512") + .forMethods("Create") + .shouldBeDetectedAs(new ValueActionFactory<>("SHA512")) + .withMethodParameter(MethodMatcher.ANY) // algorithm name string + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of()); + + // new SHA512Managed() — pure-managed implementation private static final IDetectionRule SHA512_MANAGED = new DetectionRuleBuilder() .createDetectionRule() @@ -132,24 +352,68 @@ private DotNetSHA() { .inBundle(() -> "DotNet") .withDependingDetectionRules(List.of()); - // MD5 - private static final IDetectionRule MD5_CREATE = + // new SHA512Cng() — CNG-backed implementation + private static final IDetectionRule SHA512_CNG = new DetectionRuleBuilder() .createDetectionRule() - .forObjectTypes("MD5") + .forObjectTypes("SHA512Cng") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("SHA512")) + .withoutParameters() + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of()); + + // new SHA512CryptoServiceProvider() — legacy CAPI implementation + private static final IDetectionRule SHA512_CSP = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("SHA512CryptoServiceProvider") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("SHA512")) + .withoutParameters() + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of()); + + // ========================================================================= + // RIPEMD160 + // Note: RIPEMD160/RIPEMD160Managed only ever existed in .NET Framework (documentation + // covers up to netframework-4.8.1 only; no netcoreapp/net5+ monikers). It was never ported + // to .NET Core / modern .NET, so it is only relevant for legacy .NET Framework source code. + // ========================================================================= + + // RIPEMD160.Create() — abstract factory, no parameters + private static final IDetectionRule RIPEMD160_CREATE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("RIPEMD160") .forMethods("Create") - .shouldBeDetectedAs(new ValueActionFactory<>("MD5")) + .shouldBeDetectedAs(new ValueActionFactory<>("RIPEMD160")) .withoutParameters() .buildForContext(new DigestContext()) .inBundle(() -> "DotNet") .withDependingDetectionRules(List.of()); - private static final IDetectionRule MD5_CSP = + // RIPEMD160.Create("System.Security.Cryptography.RIPEMD160") — named factory + private static final IDetectionRule RIPEMD160_CREATE_NAMED = new DetectionRuleBuilder() .createDetectionRule() - .forObjectTypes("MD5CryptoServiceProvider") + .forObjectTypes("RIPEMD160") + .forMethods("Create") + .shouldBeDetectedAs(new ValueActionFactory<>("RIPEMD160")) + .withMethodParameter(MethodMatcher.ANY) // algorithm name string + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of()); + + // new RIPEMD160Managed() — pure-managed implementation + private static final IDetectionRule RIPEMD160_MANAGED = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("RIPEMD160Managed") .forMethods("") - .shouldBeDetectedAs(new ValueActionFactory<>("MD5")) + .shouldBeDetectedAs(new ValueActionFactory<>("RIPEMD160")) .withoutParameters() .buildForContext(new DigestContext()) .inBundle(() -> "DotNet") @@ -158,15 +422,32 @@ private DotNetSHA() { @Nonnull public static List> rules() { return List.of( + MD5_CREATE, + MD5_CREATE_NAMED, + MD5_CSP, + MD5_CNG, SHA1_CREATE, + SHA1_CREATE_NAMED, SHA1_MANAGED, + SHA1_CNG, + SHA1_CSP, SHA256_CREATE, + SHA256_CREATE_NAMED, SHA256_MANAGED, + SHA256_CNG, + SHA256_CSP, SHA384_CREATE, + SHA384_CREATE_NAMED, SHA384_MANAGED, + SHA384_CNG, + SHA384_CSP, SHA512_CREATE, + SHA512_CREATE_NAMED, SHA512_MANAGED, - MD5_CREATE, - MD5_CSP); + SHA512_CNG, + SHA512_CSP, + RIPEMD160_CREATE, + RIPEMD160_CREATE_NAMED, + RIPEMD160_MANAGED); } } diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetSHA3.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetSHA3.java new file mode 100644 index 000000000..c9244d052 --- /dev/null +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetSHA3.java @@ -0,0 +1,157 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.context.DigestContext; +import com.ibm.engine.model.factory.ValueActionFactory; +import com.ibm.engine.rule.IDetectionRule; +import com.ibm.engine.rule.builder.DetectionRuleBuilder; +import java.util.List; +import javax.annotation.Nonnull; + +/** + * Detection rules for the SHA-3/SHAKE hash algorithm family in System.Security.Cryptography. + * + *

Classes covered: + * + *

    + *
  • {@code SHA3_256} — abstract base, {@code SHA3_256.Create()} only + *
  • {@code SHA3_384} — abstract base, {@code SHA3_384.Create()} only + *
  • {@code SHA3_512} — abstract base, {@code SHA3_512.Create()} only + *
  • {@code Shake128} — sealed class, directly constructible via {@code new Shake128()} + *
  • {@code Shake256} — sealed class, directly constructible via {@code new Shake256()} + *
+ * + *

Unlike the SHA-1/SHA-2/MD5/RIPEMD160 family in {@link DotNetSHA}, none of these five classes + * has a {@code Create(string)} named-factory overload (verified against the official API reference + * for .NET 8/9/10/11 — the {@code Create()} method table for {@code SHA3_256}/{@code SHA3_384}/ + * {@code SHA3_512} lists only the parameterless overload), and none has legacy {@code *Managed}, + * {@code *Cng} or {@code *CryptoServiceProvider} implementations — these algorithms were introduced + * directly with .NET 8 and are platform-dependent (see {@code IsSupported}, which is a pure + * availability check and intentionally not modeled here, consistent with how other {@code + * IsSupported} properties are ignored across this rule set). + * + *

{@code Shake128}/{@code Shake256} are sealed classes (not abstract), have a public + * parameterless constructor, and have no {@code Create()} factory at all — they are + * instantiated directly with {@code new Shake128()} / {@code new Shake256()}, which is why their + * detection rule matches the constructor ({@code }) rather than a static factory method, the + * same pattern used for {@code AesManaged} in {@link DotNetAES}. + * + *

Why SHAKE's output-length parameter is not modeled as a depending rule: SHAKE128/256 + * are extendable-output functions (XOF) whose output length is variable and specified per call + * (e.g. {@code Shake128.HashData(data, outputLength)}, {@code shake.GetHashAndReset(outputLength)}, + * {@code shake.Read(outputLength)}). Per the official remarks: "The size of the XOF indicates the + * security strength of the algorithm, not the output size" — i.e. the "128"/"256" in the class name + * is a security-strength parameter (captured by the creation rule itself, analogous to AES-128 vs. + * AES-256), while the length passed to {@code HashData}/{@code GetHashAndReset}/{@code + * GetCurrentHash}/{@code Read} is an arbitrary output-length request that carries no additional + * cryptographically relevant information about the algorithm. This mirrors two independent existing + * conventions: (1) the pre-established rule in {@link DotNetSHA} that {@code ComputeHash} and + * friends add nothing beyond what the creation rule already captures, and (2) the Go module's + * {@code golang.org/x/crypto/sha3} rules ({@code GoCryptoSHA3}), which likewise detect only {@code + * NewShake128()}/{@code NewShake256()} and attach no depending rules for any subsequent read/output + * operation. No depending rules are added here either. + * + *

Both {@code SHA3_256}/{@code SHA3_384}/{@code SHA3_512} (fixed-length digests) and {@code + * Shake128}/{@code Shake256} (XOF) inherit the same set of hash-computation operations ({@code + * HashData}, {@code AppendData}, {@code TryHashData}, etc.) that, following the established + * convention above, are not modeled as depending rules for the fixed-digest classes either. + */ +@SuppressWarnings("java:S1192") +public final class DotNetSHA3 { + + private DotNetSHA3() { + // nothing + } + + // ========================================================================= + // SHA3-256 / SHA3-384 / SHA3-512 + // ========================================================================= + + // SHA3_256.Create() — abstract factory, no parameters, no named-factory overload + private static final IDetectionRule SHA3_256_CREATE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("SHA3_256") + .forMethods("Create") + .shouldBeDetectedAs(new ValueActionFactory<>("SHA3_256")) + .withoutParameters() + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of()); + + // SHA3_384.Create() — abstract factory, no parameters, no named-factory overload + private static final IDetectionRule SHA3_384_CREATE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("SHA3_384") + .forMethods("Create") + .shouldBeDetectedAs(new ValueActionFactory<>("SHA3_384")) + .withoutParameters() + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of()); + + // SHA3_512.Create() — abstract factory, no parameters, no named-factory overload + private static final IDetectionRule SHA3_512_CREATE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("SHA3_512") + .forMethods("Create") + .shouldBeDetectedAs(new ValueActionFactory<>("SHA3_512")) + .withoutParameters() + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of()); + + // ========================================================================= + // Shake128 / Shake256 (extendable-output functions, XOF) + // ========================================================================= + + // new Shake128() — sealed class, no Create() factory, only a public constructor + private static final IDetectionRule SHAKE_128 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("Shake128") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("Shake128")) + .withoutParameters() + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of()); + + // new Shake256() — sealed class, no Create() factory, only a public constructor + private static final IDetectionRule SHAKE_256 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("Shake256") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("Shake256")) + .withoutParameters() + .buildForContext(new DigestContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(List.of()); + + @Nonnull + public static List> rules() { + return List.of(SHA3_256_CREATE, SHA3_384_CREATE, SHA3_512_CREATE, SHAKE_128, SHAKE_256); + } +} diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetSlhDsa.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetSlhDsa.java new file mode 100644 index 000000000..53406ecf2 --- /dev/null +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetSlhDsa.java @@ -0,0 +1,370 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import com.ibm.engine.detection.MethodMatcher; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.SignatureAction; +import com.ibm.engine.model.context.KeyContext; +import com.ibm.engine.model.context.SignatureContext; +import com.ibm.engine.model.factory.ParameterIdentifierFactory; +import com.ibm.engine.model.factory.SignatureActionFactory; +import com.ibm.engine.model.factory.ValueActionFactory; +import com.ibm.engine.rule.IDetectionRule; +import com.ibm.engine.rule.builder.DetectionRuleBuilder; +import java.util.List; +import java.util.Map; +import javax.annotation.Nonnull; + +/** + * Detection rules for SLH-DSA (Stateless Hash-Based Digital Signature Algorithm, FIPS 205, formerly + * known as SPHINCS+) usage in {@code System.Security.Cryptography}. + * + *

API availability (verified against the official Microsoft Learn API reference, not + * assumed): {@code SlhDsa}, {@code SlhDsaCng}, {@code SlhDsaOpenSsl} and {@code + * SlhDsaAlgorithm} are documented for the {@code net-10.0} and {@code net-11.0} monikers (the + * {@code net-11.0} page redirects/renders identically to {@code net-10.0} content — {@code + * defaultMoniker: net-10.0} — both listing "Assembly: System.Security.Cryptography.dll" as in-box, + * plus "Assembly: Microsoft.Bcl.Cryptography.dll" — package {@code Microsoft.Bcl.Cryptography + * v11.0.0-preview.7.26381.103} — as a back-compat NuGet shim for {@code SlhDsa}/{@code + * SlhDsaCng}/{@code SlhDsaAlgorithm} on older TFMs; {@code SlhDsaOpenSsl} lists only the in-box + * assembly, mirroring {@code MLKemOpenSsl}/{@code MLDsaOpenSsl}). Unlike plain {@code MLDsa}/{@code + * MLKem}, the whole {@code SlhDsa} surface (base class, both derived classes, and {@code + * SlhDsaAlgorithm}) carries a class-level {@code + * [System.Diagnostics.CodeAnalysis.Experimental("SYSLIB5006")]} attribute — SLH-DSA is still a + * preview API as of .NET 10/11, unlike the now-stable plain ML-DSA/ML-KEM surface. + * + *

Classes covered: + * + *

    + *
  • {@code SlhDsa} — abstract base. All of its members are {@code static} factory methods (the + * only listed constructor, {@code SlhDsa(SlhDsaAlgorithm)}, is for derived classes, not + * called by ordinary consumer code): {@code GenerateKey(SlhDsaAlgorithm)}, {@code + * ImportSlhDsaPrivateKey(SlhDsaAlgorithm, byte[]/ReadOnlySpan<byte>)}, {@code + * ImportSlhDsaPublicKey(SlhDsaAlgorithm, byte[]/ReadOnlySpan<byte>)}, {@code + * ImportPkcs8PrivateKey(...)}, {@code ImportSubjectPublicKeyInfo(...)}, {@code + * ImportFromPem(...)}, {@code ImportEncryptedPkcs8PrivateKey(...)}, {@code + * ImportFromEncryptedPem(...)}. + *
  • {@code SlhDsaCng} — CNG-backed implementation ({@code SlhDsaCng(CngKey)} constructor). + *
  • {@code SlhDsaOpenSsl} — OpenSSL-backed implementation ({@code + * SlhDsaOpenSsl(SafeEvpPKeyHandle)} constructor). + *
+ * + *

Differences from ML-DSA, verified from the official reference rather than assumed by + * analogy (SLH-DSA is a hash-based signature scheme per FIPS 205, structurally unrelated to the + * lattice-based FIPS 204 ML-DSA, despite both exposing a similar C# surface): + * + *

    + *
  • No private-seed import. {@code SlhDsa} has no {@code ImportSlhDsaPrivateSeed} + * equivalent to {@code MLDsa.ImportMLDsaPrivateSeed} — confirmed absent from the verified + * method table. This makes sense: ML-DSA's seed-expansion key derivation is a + * lattice-specific construction, not part of the FIPS 205 SLH-DSA private key format. + *
  • No {@code SignMu}/{@code VerifyMu}. ML-DSA's {@code SignMu}/{@code VerifyMu} + * (signing a pre-computed FIPS 204 "mu" digest) are absent from {@code SlhDsa} — confirmed + * absent from the verified method table. This is expected: "mu" is a + * ML-DSA/Dilithium-specific message-representative construction with no FIPS 205 equivalent. + *
  • {@code SignPreHash}/{@code VerifyPreHash} do exist (the FIPS 205 pre-hash signing + * variant, {@code SignPreHash(byte[]/ReadOnlySpan<byte>, ..., string hashAlgorithmOid, + * ...)}) — confirmed present in the verified method table, with the exact same shape as + * ML-DSA's pre-hash methods. + *
  • No Composite variant. Unlike {@code CompositeMLDsa}, no {@code CompositeSlhDsa} + * class exists in the official reference (the {@code SlhDsa} page's "Derived" list contains + * only {@code SlhDsaCng} and {@code SlhDsaOpenSsl}), so no composite/hybrid detection or + * modeling compromise is needed here. + *
  • Parameter set names are completely different. {@code SlhDsaAlgorithm} exposes twelve + * static properties, verified from the official {@code SlhDsaAlgorithm} reference page: + * {@code SlhDsaSha2_128s}, {@code SlhDsaSha2_128f}, {@code SlhDsaSha2_192s}, {@code + * SlhDsaSha2_192f}, {@code SlhDsaSha2_256s}, {@code SlhDsaSha2_256f}, {@code + * SlhDsaShake128s}, {@code SlhDsaShake128f}, {@code SlhDsaShake192s}, {@code + * SlhDsaShake192f}, {@code SlhDsaShake256s}, {@code SlhDsaShake256f} — a hash-family + * (SHA2/SHAKE) x security-level (128/192/256) x speed-tradeoff (s=small signature/slower, + * f=fast/larger signature) matrix, nothing like ML-DSA's simple {@code MLDsa44}/{@code + * MLDsa65}/{@code MLDsa87} three-value set. + *
+ * + *

Architecture — {@code Import*} as primary creation rules, not skipped: exactly as + * established for ML-KEM/ML-DSA (see {@code DotNetMLKem}'s javadoc for the full rationale), every + * {@code Import*} method verified above on {@code SlhDsa} is a {@code static} factory that is the + * only way (besides {@code GenerateKey} or the {@code Cng}/{@code OpenSsl} native-interop + * constructors) to obtain an instance in the first place. They are therefore treated as primary + * creation rules here. + * + *

Two creation-rule shapes result from this: + * + *

    + *
  • Algorithm-parameterized ({@code GenerateKey}, {@code ImportSlhDsaPrivateKey}, {@code + * ImportSlhDsaPublicKey}): take an {@code SlhDsaAlgorithm} argument (e.g. {@code + * SlhDsaAlgorithm.SlhDsaSha2_128s}), a member-access expression the engine resolves to the + * bare identifier {@code "SlhDsaSha2_128s"} (see {@code CSharpLanguageTranslation}, the same + * mechanism {@code DotNetMLDsa}/{@code DotNetMLKem}'s rules rely on). Captured with {@code + * ParameterIdentifierFactory<>()} as a child of the top-level detection. + *
  • Structural imports ({@code ImportPkcs8PrivateKey}, {@code + * ImportSubjectPublicKeyInfo}, {@code ImportFromPem}, {@code ImportEncryptedPkcs8PrivateKey}, + * {@code ImportFromEncryptedPem}): the parameter set is embedded inside the encoded key + * material / PEM text, not present as a separate literal argument, so — exactly as for + * ML-KEM/ML-DSA — it cannot be recovered by this engine. These translate to a generic node + * with no {@code ParameterSetIdentifier} child, a known, inherent precision gap, not a bug. + *
+ * + *

{@code SlhDsaCng(CngKey)} and {@code SlhDsaOpenSsl(SafeEvpPKeyHandle)} wrap an + * already-existing native key handle and never receive an algorithm argument at all, so they always + * translate to the generic node, mirroring {@code MLDsaCng(CngKey)}/{@code + * MLDsaOpenSsl(SafeEvpPKeyHandle)}. + * + *

Sign/Verify modeling: {@code SlhDsa} is an ordinary signature primitive, so its + * operations are modeled with {@code SignatureActionFactory} under a {@code SignatureContext} — the + * exact same shape as {@code DotNetDSA}/{@code DotNetECDsa}/{@code DotNetMLDsa}'s {@code + * SignData}/{@code VerifyData} rules, reusing the existing generic SIGN/VERIFY dispatch in {@code + * CSharpSignatureContextTranslator} (no changes needed there: it already maps {@code + * SignatureAction.Action.SIGN}/{@code VERIFY} to {@code Sign}/{@code Verify} functionality nodes + * regardless of which algorithm produced the action, so this rule set is purely additive from its + * perspective). {@code SignPreHash}/{@code VerifyPreHash} (the FIPS 205 pre-hash variant) are + * modeled the same way as {@code SignData}/{@code VerifyData}, since the engine cannot meaningfully + * distinguish "what was hashed before signing" from "what was signed" without value tracking — no + * {@code SignMu}/{@code VerifyMu} rules exist here, since (per the verified reference) {@code + * SlhDsa} has no such methods. The {@code *Core} overrides ({@code SignDataCore}, {@code + * SignPreHashCore}, {@code VerifyDataCore}, {@code VerifyPreHashCore}) are protected extensibility + * hooks for subclassing, not called by ordinary consumer code, and are intentionally not modeled — + * consistent with not modeling {@code SignMuCore}/{@code VerifyMuCore} in {@code DotNetMLDsa}. + * + *

Mapper model reuse — no new model class needed: a repository-wide search ({@code grep + * -rl "SlhDsa\|SLHDSA\|SLH-DSA\|SPHINCS" mapper/}) found that {@code + * com.ibm.mapper.model.algorithms.SPHINCSPlus} (implementing {@code Signature}, {@code NAME = + * "SLH-DSA"}, documented as "Other Names and Related Standards: SPHINCS+") already existed before + * this batch — used by the BouncyCastle {@code SPHINCSPlusSigner} translation ({@code + * BcMessageSignerMapper}) — but its {@code asString()} only appended a {@code MessageDigest} child + * (never actually exercised — {@code SPHINCSPlusSigner} maps to a bare {@code new + * SPHINCSPlus(detectionLocation)} with no children, confirmed by grep — no existing test asserts + * {@code asString()} behavior with a child attached, so extending it is safe). This batch adds one + * new, purely additive {@code SPHINCSPlus(String parameterSetIdentifier, DetectionLocation)} + * constructor and extends {@code asString()} to also check for a {@code ParameterSetIdentifier} + * child (prepended with "-", independent of and before the pre-existing {@code MessageDigest} + * check, whose no-args/no-children call path is completely unchanged), so {@code asString()} now + * yields e.g. {@code "SLH-DSA-SHA2-128s"} — matching the exact FIPS 205 parameter set naming + * convention (hash family + security level + speed tradeoff) referenced by the class's own + * "cyclonedx.org/schema/cryptography-defs.json (algorithmName: SLH-DSA)" specification link. No new + * mapper model class was introduced for SLH-DSA — an existing, if previously underused, class was + * completed instead. + * + *

Not covered (deliberately, consistent with the rest of this rule set): the {@code + * Algorithm}/{@code IsSupported} getters (state reads, not configuration — no property setters + * exist on {@code SlhDsa} at all), {@code SlhDsaCng.GetKey()}/{@code + * SlhDsaOpenSsl.DuplicateKeyHandle()} (native-handle export, the {@code SlhDsa}-specific equivalent + * of the {@code Export*} methods skipped for RSA/ECDsa/ECDiffieHellman/MLDsa), {@code Dispose()}, + * and all {@code ExportXxx}/{@code TryExportXxx}/{@code ExportXxxPem} instance methods (standard + * PKCS#8/SPKI/PEM export — same convention as RSA/ECDsa/ECDiffieHellman/MLKem/MLDsa). + */ +@SuppressWarnings("java:S1192") +public final class DotNetSlhDsa { + + private DotNetSlhDsa() { + // nothing + } + + // ========================================================================= + // Sign / Verify operation rules (depending rules on any tracked SlhDsa-family variable, + // mirroring DotNetMLDsa.java's SignData/VerifyData/SignPreHash/VerifyPreHash rules; no + // SignMu/VerifyMu — confirmed absent from the official SlhDsa reference, see class javadoc) + // ========================================================================= + + // slhDsa.SignData(data, context) — 2 overloads (array-based and Span-based) + private static final IDetectionRule SLHDSA_SIGN_DATA = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("SignData") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.SIGN)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // slhDsa.VerifyData(data, signature, context) — 2 overloads + private static final IDetectionRule SLHDSA_VERIFY_DATA = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("VerifyData") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.VERIFY)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // slhDsa.SignPreHash(hash, hashAlgorithmOid, context) — 2 overloads + private static final IDetectionRule SLHDSA_SIGN_PRE_HASH = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("SignPreHash") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.SIGN)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // slhDsa.VerifyPreHash(hash, signature, hashAlgorithmOid, context) — 2 overloads + private static final IDetectionRule SLHDSA_VERIFY_PRE_HASH = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("VerifyPreHash") + .shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.VERIFY)) + .withAnyParameters() + .buildForContext(new SignatureContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + /** Full set of depending rules for {@code SlhDsa}/{@code SlhDsaCng}/{@code SlhDsaOpenSsl}. */ + private static final List> SLHDSA_DEPENDING_RULES = + List.of( + SLHDSA_SIGN_DATA, + SLHDSA_VERIFY_DATA, + SLHDSA_SIGN_PRE_HASH, + SLHDSA_VERIFY_PRE_HASH); + + // ========================================================================= + // SlhDsa — primary creation rules, algorithm-parameterized (SlhDsaAlgorithm argument captured + // as the parameter set, see class javadoc) + // ========================================================================= + + // SlhDsa.GenerateKey(SlhDsaAlgorithm.SlhDsaSha2_128s) + private static final IDetectionRule SLHDSA_GENERATE_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("SlhDsa") + .forMethods("GenerateKey") + .shouldBeDetectedAs(new ValueActionFactory<>("SLH-DSA")) + .withMethodParameter(MethodMatcher.ANY) // SlhDsaAlgorithm + .shouldBeDetectedAs(new ParameterIdentifierFactory<>()) + .asChildOfParameterWithId(-1) + .buildForContext(new KeyContext(Map.of("kind", "SLHDSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(SLHDSA_DEPENDING_RULES); + + // SlhDsa.ImportSlhDsaPrivateKey(SlhDsaAlgorithm.SlhDsaSha2_128s, privateKeyBytes) + private static final IDetectionRule SLHDSA_IMPORT_PRIVATE_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("SlhDsa") + .forMethods("ImportSlhDsaPrivateKey") + .shouldBeDetectedAs(new ValueActionFactory<>("SLH-DSA")) + .withMethodParameter(MethodMatcher.ANY) // SlhDsaAlgorithm + .shouldBeDetectedAs(new ParameterIdentifierFactory<>()) + .asChildOfParameterWithId(-1) + .withMethodParameter(MethodMatcher.ANY) // private key bytes + .buildForContext(new KeyContext(Map.of("kind", "SLHDSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(SLHDSA_DEPENDING_RULES); + + // SlhDsa.ImportSlhDsaPublicKey(SlhDsaAlgorithm.SlhDsaSha2_128s, publicKeyBytes) + private static final IDetectionRule SLHDSA_IMPORT_PUBLIC_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("SlhDsa") + .forMethods("ImportSlhDsaPublicKey") + .shouldBeDetectedAs(new ValueActionFactory<>("SLH-DSA")) + .withMethodParameter(MethodMatcher.ANY) // SlhDsaAlgorithm + .shouldBeDetectedAs(new ParameterIdentifierFactory<>()) + .asChildOfParameterWithId(-1) + .withMethodParameter(MethodMatcher.ANY) // public key bytes + .buildForContext(new KeyContext(Map.of("kind", "SLHDSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(SLHDSA_DEPENDING_RULES); + + // ========================================================================= + // SlhDsa — primary creation rules, structural imports (no SlhDsaAlgorithm argument; see class + // javadoc for why the parameter set cannot be captured for these) + // ========================================================================= + + private static IDetectionRule structuralImportRule(@Nonnull String methodName) { + return new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("SlhDsa") + .forMethods(methodName) + .shouldBeDetectedAs(new ValueActionFactory<>("SLH-DSA")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "SLHDSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(SLHDSA_DEPENDING_RULES); + } + + // SlhDsa.ImportPkcs8PrivateKey(source) / SlhDsa.ImportSubjectPublicKeyInfo(source) / + // SlhDsa.ImportFromPem(pem) / SlhDsa.ImportEncryptedPkcs8PrivateKey(...) / + // SlhDsa.ImportFromEncryptedPem(...) + private static final IDetectionRule SLHDSA_IMPORT_PKCS8_PRIVATE_KEY = + structuralImportRule("ImportPkcs8PrivateKey"); + + private static final IDetectionRule SLHDSA_IMPORT_SUBJECT_PUBLIC_KEY_INFO = + structuralImportRule("ImportSubjectPublicKeyInfo"); + + private static final IDetectionRule SLHDSA_IMPORT_FROM_PEM = + structuralImportRule("ImportFromPem"); + + private static final IDetectionRule SLHDSA_IMPORT_ENCRYPTED_PKCS8_PRIVATE_KEY = + structuralImportRule("ImportEncryptedPkcs8PrivateKey"); + + private static final IDetectionRule SLHDSA_IMPORT_FROM_ENCRYPTED_PEM = + structuralImportRule("ImportFromEncryptedPem"); + + // ========================================================================= + // SlhDsa — primary creation rules, native-interop constructors (no SlhDsaAlgorithm argument; + // wrap an already-existing native key handle, mirroring MLDsaCng/MLDsaOpenSsl in DotNetMLDsa) + // ========================================================================= + + // new SlhDsaCng(cngKey) + private static final IDetectionRule SLHDSA_CNG = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("SlhDsaCng") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("SLH-DSA")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "SLHDSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(SLHDSA_DEPENDING_RULES); + + // new SlhDsaOpenSsl(safeEvpPKeyHandle) + private static final IDetectionRule SLHDSA_OPENSSL = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("SlhDsaOpenSsl") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("SLH-DSA")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "SLHDSA"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(SLHDSA_DEPENDING_RULES); + + @Nonnull + public static List> rules() { + return List.of( + SLHDSA_GENERATE_KEY, + SLHDSA_IMPORT_PRIVATE_KEY, + SLHDSA_IMPORT_PUBLIC_KEY, + SLHDSA_IMPORT_PKCS8_PRIVATE_KEY, + SLHDSA_IMPORT_SUBJECT_PUBLIC_KEY_INFO, + SLHDSA_IMPORT_FROM_PEM, + SLHDSA_IMPORT_ENCRYPTED_PKCS8_PRIVATE_KEY, + SLHDSA_IMPORT_FROM_ENCRYPTED_PEM, + SLHDSA_CNG, + SLHDSA_OPENSSL); + } +} diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetTripleDES.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetTripleDES.java index 2f1892e4d..bb9ebff72 100755 --- a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetTripleDES.java +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetTripleDES.java @@ -19,23 +19,43 @@ */ package com.ibm.plugin.rules.detection.dotnet; +import com.ibm.engine.detection.MethodMatcher; import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.CipherAction; +import com.ibm.engine.model.Size; import com.ibm.engine.model.context.CipherContext; +import com.ibm.engine.model.factory.BlockSizeFactory; +import com.ibm.engine.model.factory.CipherActionFactory; +import com.ibm.engine.model.factory.KeySizeFactory; +import com.ibm.engine.model.factory.ModeFactory; +import com.ibm.engine.model.factory.PaddingFactory; import com.ibm.engine.model.factory.ValueActionFactory; import com.ibm.engine.rule.IDetectionRule; import com.ibm.engine.rule.builder.DetectionRuleBuilder; import java.util.List; +import java.util.stream.Stream; import javax.annotation.Nonnull; /** - * Detection rules for Triple DES usage in System.Security.Cryptography. + * Detection rules for the Triple DES (3DES) family in System.Security.Cryptography. * - *

Detects: + *

Classes covered: * *

    - *
  • {@code TripleDES.Create()} — abstract factory - *
  • {@code new TripleDESCryptoServiceProvider()} — CAPI-backed (deprecated) + *
  • {@code TripleDES} — abstract base ({@code TripleDES.Create()}, {@code + * TripleDES.Create(string)}) + *
  • {@code TripleDESCryptoServiceProvider} — legacy CAPI implementation + *
  • {@code TripleDESCng} — CNG-backed implementation (ephemeral and persisted-key constructors) *
+ * + *

Architecture: all methods inherited from {@code SymmetricAlgorithm} (EncryptCbc, DecryptCbc, + * CreateEncryptor, property setters, etc.) are expressed as depending rules attached to + * each primary creation rule. The detection engine tracks the variable and fires these rules on + * every matching method call, regardless of the concrete TripleDES subclass. Like {@code Aes}, + * {@code TripleDES} has a CNG-backed subclass ({@code TripleDESCng}), but unlike {@code Aes} it has + * no AEAD variant, so depending-rule coverage mirrors {@code DotNetDES} and {@code DotNetRC2} + * exactly (no TripleDES-specific properties exist beyond those inherited from {@code + * SymmetricAlgorithm}). */ @SuppressWarnings("java:S1192") public final class DotNetTripleDES { @@ -44,6 +64,509 @@ private DotNetTripleDES() { // nothing } + // ========================================================================= + // Property setter rules (synthetic set_X method invocations) + // ========================================================================= + + // tdes.Mode = CipherMode.CBC → synthetic set_Mode(CipherMode.CBC) + private static final IDetectionRule TRIPLE_DES_SET_MODE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_Mode") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new ModeFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // tdes.KeySize = 192 → synthetic set_KeySize(192) + private static final IDetectionRule TRIPLE_DES_SET_KEY_SIZE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_KeySize") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new KeySizeFactory<>(Size.UnitType.BIT)) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // tdes.Padding = PaddingMode.PKCS7 → synthetic set_Padding(PaddingMode.PKCS7) + private static final IDetectionRule TRIPLE_DES_SET_PADDING = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_Padding") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // tdes.FeedbackSize = 8 → synthetic set_FeedbackSize(8) + private static final IDetectionRule TRIPLE_DES_SET_FEEDBACK_SIZE = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("set_FeedbackSize") + .withMethodParameter(MethodMatcher.ANY) + .shouldBeDetectedAs(new BlockSizeFactory<>(Size.UnitType.BIT)) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + private static final List> PROPERTY_SETTER_RULES = + List.of( + TRIPLE_DES_SET_MODE, + TRIPLE_DES_SET_KEY_SIZE, + TRIPLE_DES_SET_PADDING, + TRIPLE_DES_SET_FEEDBACK_SIZE); + + // ========================================================================= + // CreateEncryptor / CreateDecryptor rules + // ========================================================================= + + // tdes.CreateEncryptor() + private static final IDetectionRule TRIPLE_DES_CREATE_ENCRYPTOR = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateEncryptor") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.ENCRYPT)) + .withoutParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // tdes.CreateEncryptor(byte[] key, byte[] iv) + private static final IDetectionRule TRIPLE_DES_CREATE_ENCRYPTOR_WITH_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateEncryptor") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.ENCRYPT)) + .withMethodParameter(MethodMatcher.ANY) // key bytes + .withMethodParameter(MethodMatcher.ANY) // iv bytes + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // tdes.CreateDecryptor() + private static final IDetectionRule TRIPLE_DES_CREATE_DECRYPTOR = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateDecryptor") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.DECRYPT)) + .withoutParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // tdes.CreateDecryptor(byte[] key, byte[] iv) + private static final IDetectionRule TRIPLE_DES_CREATE_DECRYPTOR_WITH_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("CreateDecryptor") + .shouldBeDetectedAs(new CipherActionFactory<>(CipherAction.Action.DECRYPT)) + .withMethodParameter(MethodMatcher.ANY) // key bytes + .withMethodParameter(MethodMatcher.ANY) // iv bytes + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // EncryptCbc / DecryptCbc rules + // Mode is constant "CBC" (from method name); padding is detected from last param. + // Two overloads: 3-param and 4-param (with output buffer). + // ========================================================================= + + // EncryptCbc(plaintext, iv, padding) + private static final IDetectionRule TRIPLE_DES_ENCRYPT_CBC_3 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptCbc") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // EncryptCbc(plaintext, iv, destination, padding) [output-buffer overload] + private static final IDetectionRule TRIPLE_DES_ENCRYPT_CBC_4 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptCbc") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptCbc(ciphertext, iv, padding) + private static final IDetectionRule TRIPLE_DES_DECRYPT_CBC_3 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptCbc") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptCbc(ciphertext, iv, destination, padding) + private static final IDetectionRule TRIPLE_DES_DECRYPT_CBC_4 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptCbc") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // EncryptEcb / DecryptEcb rules + // Mode is constant "ECB" (from method name); no IV parameter. + // Two overloads: 2-param and 3-param (with output buffer). + // ========================================================================= + + // EncryptEcb(plaintext, padding) + private static final IDetectionRule TRIPLE_DES_ENCRYPT_ECB_2 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptEcb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // EncryptEcb(plaintext, destination, padding) + private static final IDetectionRule TRIPLE_DES_ENCRYPT_ECB_3 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptEcb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptEcb(ciphertext, padding) + private static final IDetectionRule TRIPLE_DES_DECRYPT_ECB_2 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptEcb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptEcb(ciphertext, destination, padding) + private static final IDetectionRule TRIPLE_DES_DECRYPT_ECB_3 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptEcb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // EncryptCfb / DecryptCfb rules + // Mode is constant "CFB"; padding detected from 3rd param; feedbackSize ignored. + // Two overloads: 4-param and 5-param (with output buffer). + // ========================================================================= + + // EncryptCfb(plaintext, iv, padding, feedbackSize) + private static final IDetectionRule TRIPLE_DES_ENCRYPT_CFB_4 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptCfb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize (int) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // EncryptCfb(plaintext, iv, destination, padding, feedbackSize) + private static final IDetectionRule TRIPLE_DES_ENCRYPT_CFB_5 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("EncryptCfb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize (int) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptCfb(ciphertext, iv, padding, feedbackSize) + private static final IDetectionRule TRIPLE_DES_DECRYPT_CFB_4 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptCfb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize (int) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // DecryptCfb(ciphertext, iv, destination, padding, feedbackSize) + private static final IDetectionRule TRIPLE_DES_DECRYPT_CFB_5 = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DecryptCfb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // destination buffer + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize (int) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // TryEncrypt* / TryDecrypt* rules + // Signatures: + // TryEncryptCbc(plaintext, iv, destination, out bytesWritten, padding) — 5 params + // TryDecryptCbc(ciphertext, iv, destination, out bytesWritten, padding) — 5 params + // TryEncryptEcb(plaintext, destination, padding, out bytesWritten) — 4 params + // TryDecryptEcb(ciphertext, destination, padding, out bytesWritten) — 4 params + // TryEncryptCfb(plaintext, iv, destination, out bytesWritten, padding, fs) — 6 params + // TryDecryptCfb(ciphertext, iv, destination, out bytesWritten, padding, fs) — 6 params + // ========================================================================= + + // TryEncryptCbc(plaintext, iv, destination, out bytesWritten, padding) + private static final IDetectionRule TRIPLE_DES_TRY_ENCRYPT_CBC = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryEncryptCbc") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryDecryptCbc(ciphertext, iv, destination, out bytesWritten, padding) + private static final IDetectionRule TRIPLE_DES_TRY_DECRYPT_CBC = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryDecryptCbc") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CBC")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryEncryptEcb(plaintext, destination, padding, out bytesWritten) + private static final IDetectionRule TRIPLE_DES_TRY_ENCRYPT_ECB = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryEncryptEcb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryDecryptEcb(ciphertext, destination, padding, out bytesWritten) + private static final IDetectionRule TRIPLE_DES_TRY_DECRYPT_ECB = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryDecryptEcb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .shouldBeDetectedAs(new ModeFactory<>("ECB")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryEncryptCfb(plaintext, iv, destination, out bytesWritten, padding, feedbackSize) + private static final IDetectionRule TRIPLE_DES_TRY_ENCRYPT_CFB = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryEncryptCfb") + .withMethodParameter(MethodMatcher.ANY) // plaintext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // TryDecryptCfb(ciphertext, iv, destination, out bytesWritten, padding, feedbackSize) + private static final IDetectionRule TRIPLE_DES_TRY_DECRYPT_CFB = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("TryDecryptCfb") + .withMethodParameter(MethodMatcher.ANY) // ciphertext + .withMethodParameter(MethodMatcher.ANY) // iv + .shouldBeDetectedAs(new ModeFactory<>("CFB")) + .withMethodParameter(MethodMatcher.ANY) // destination + .withMethodParameter(MethodMatcher.ANY) // out bytesWritten + .withMethodParameter(MethodMatcher.ANY) // padding + .shouldBeDetectedAs(new PaddingFactory<>()) + .withMethodParameter(MethodMatcher.ANY) // feedbackSize + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // Key / IV generation rules + // ========================================================================= + + // tdes.GenerateKey() — generates a new random key (size determined by KeySize property) + private static final IDetectionRule TRIPLE_DES_GENERATE_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("GenerateKey") + .shouldBeDetectedAs(new ValueActionFactory<>("GenerateKey")) + .withoutParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // tdes.GenerateIV() — generates a new random initialization vector + private static final IDetectionRule TRIPLE_DES_GENERATE_IV = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("GenerateIV") + .shouldBeDetectedAs(new ValueActionFactory<>("GenerateIV")) + .withoutParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // Aggregated depending-rule lists + // ========================================================================= + + /** + * All cipher operation rules that fire on a tracked TripleDES-family variable. Includes + * CreateEncryptor/CreateDecryptor, direct mode-specific Encrypt/Decrypt methods, Try* variants, + * and key/IV generation. + */ + private static final List> CIPHER_OP_RULES = + List.of( + TRIPLE_DES_CREATE_ENCRYPTOR, + TRIPLE_DES_CREATE_ENCRYPTOR_WITH_KEY, + TRIPLE_DES_CREATE_DECRYPTOR, + TRIPLE_DES_CREATE_DECRYPTOR_WITH_KEY, + TRIPLE_DES_ENCRYPT_CBC_3, + TRIPLE_DES_ENCRYPT_CBC_4, + TRIPLE_DES_DECRYPT_CBC_3, + TRIPLE_DES_DECRYPT_CBC_4, + TRIPLE_DES_ENCRYPT_ECB_2, + TRIPLE_DES_ENCRYPT_ECB_3, + TRIPLE_DES_DECRYPT_ECB_2, + TRIPLE_DES_DECRYPT_ECB_3, + TRIPLE_DES_ENCRYPT_CFB_4, + TRIPLE_DES_ENCRYPT_CFB_5, + TRIPLE_DES_DECRYPT_CFB_4, + TRIPLE_DES_DECRYPT_CFB_5, + TRIPLE_DES_TRY_ENCRYPT_CBC, + TRIPLE_DES_TRY_DECRYPT_CBC, + TRIPLE_DES_TRY_ENCRYPT_ECB, + TRIPLE_DES_TRY_DECRYPT_ECB, + TRIPLE_DES_TRY_ENCRYPT_CFB, + TRIPLE_DES_TRY_DECRYPT_CFB, + TRIPLE_DES_GENERATE_KEY, + TRIPLE_DES_GENERATE_IV); + + /** Full set of depending rules for all TripleDES-derived classes. */ + private static final List> TRIPLE_DES_DEPENDING_RULES = + Stream.concat(PROPERTY_SETTER_RULES.stream(), CIPHER_OP_RULES.stream()).toList(); + + // ========================================================================= + // Primary creation rules + // ========================================================================= + + // TripleDES.Create() — abstract factory, no parameters private static final IDetectionRule TRIPLE_DES_CREATE = new DetectionRuleBuilder() .createDetectionRule() @@ -53,8 +576,21 @@ private DotNetTripleDES() { .withoutParameters() .buildForContext(new CipherContext()) .inBundle(() -> "DotNet") - .withDependingDetectionRules(List.of()); + .withDependingDetectionRules(TRIPLE_DES_DEPENDING_RULES); + // TripleDES.Create("TripleDES") — named factory (obsolete in .NET 7+, still detectable) + private static final IDetectionRule TRIPLE_DES_CREATE_NAMED = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("TripleDES") + .forMethods("Create") + .shouldBeDetectedAs(new ValueActionFactory<>("TRIPLEDES")) + .withMethodParameter(MethodMatcher.ANY) // algorithm name string + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(TRIPLE_DES_DEPENDING_RULES); + + // new TripleDESCryptoServiceProvider() — legacy CAPI implementation private static final IDetectionRule TRIPLE_DES_CSP = new DetectionRuleBuilder() .createDetectionRule() @@ -64,10 +600,26 @@ private DotNetTripleDES() { .withoutParameters() .buildForContext(new CipherContext()) .inBundle(() -> "DotNet") - .withDependingDetectionRules(List.of()); + .withDependingDetectionRules(TRIPLE_DES_DEPENDING_RULES); + + // new TripleDESCng() / new TripleDESCng("keyName") / new TripleDESCng("keyName", provider) / + // ... + // Matches all TripleDESCng constructors (ephemeral 0-param and persisted 1-3 params). + // Uses withAnyParameters() to avoid double-detection that would occur if a separate + // withoutParameters() rule were added alongside this one. + private static final IDetectionRule TRIPLE_DES_CNG = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("TripleDESCng") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("TRIPLEDES")) + .withAnyParameters() + .buildForContext(new CipherContext()) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(TRIPLE_DES_DEPENDING_RULES); @Nonnull public static List> rules() { - return List.of(TRIPLE_DES_CREATE, TRIPLE_DES_CSP); + return List.of(TRIPLE_DES_CREATE, TRIPLE_DES_CREATE_NAMED, TRIPLE_DES_CSP, TRIPLE_DES_CNG); } } diff --git a/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetX25519DiffieHellman.java b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetX25519DiffieHellman.java new file mode 100644 index 000000000..feb35f5f6 --- /dev/null +++ b/csharp/src/main/java/com/ibm/plugin/rules/detection/dotnet/DotNetX25519DiffieHellman.java @@ -0,0 +1,248 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import com.ibm.engine.detection.MethodMatcher; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.context.KeyContext; +import com.ibm.engine.model.factory.ValueActionFactory; +import com.ibm.engine.rule.IDetectionRule; +import com.ibm.engine.rule.builder.DetectionRuleBuilder; +import java.util.List; +import java.util.Map; +import javax.annotation.Nonnull; + +/** + * Detection rules for X25519 (Curve25519-based Diffie-Hellman key agreement) usage in {@code + * System.Security.Cryptography}. + * + *

API verification (Microsoft Learn, 2026-08-20): {@code X25519DiffieHellman}, {@code + * X25519DiffieHellmanCng} and {@code X25519DiffieHellmanOpenSsl} were confirmed to exist under + * these exact names via {@code learn.microsoft.com/en-us/dotnet/api/...?view=net-11.0}. This is a + * brand-new, still-in-preview API: the doc pages show {@code Package: + * Microsoft.Bcl.Cryptography v11.0.0-preview.7.26381.103} and {@code ms.date: 2025-07-01} — i.e. it + * ships as a preview NuGet package layered on top of {@code System.Security.Cryptography.dll} (also + * backportable to older TFMs per the {@code netframework-4.6.2-pp}…{@code netframework-4.8.1-pp} + * "preview package" monikers listed on the class pages), not yet a stable in-box .NET API. + * Detection rules are added regardless, mirroring how the rest of this plugin detects APIs across + * their full supported version range. + * + *

Classes covered: + * + *

    + *
  • {@code X25519DiffieHellman} — abstract base. Verified to have no {@code Create()} + * factory (unlike {@code ECDiffieHellman}); fresh instances are obtained only through the + * static factories {@code X25519DiffieHellman.GenerateKey()} (confirmed via the dedicated + * method-reference page: {@code public static X25519DiffieHellman GenerateKey()}), {@code + * ImportPrivateKey(byte[]/ReadOnlySpan)}, or {@code + * ImportPublicKey(byte[]/ReadOnlySpan)} (both confirmed via their own dedicated + * method-reference pages, each declared directly on {@code X25519DiffieHellman} itself, each + * returning a new {@code X25519DiffieHellman}). Its constructor is {@code protected + * X25519DiffieHellman()} (confirmed via the dedicated constructor page), so the class cannot + * be instantiated directly — only through one of those static factories or one of the two + * concrete subclasses below. + *
  • {@code X25519DiffieHellmanCng} — CNG-backed implementation. Single constructor {@code + * X25519DiffieHellmanCng(CngKey)} (wraps an existing key). + *
  • {@code X25519DiffieHellmanOpenSsl} — OpenSSL-backed implementation. Single constructor + * {@code X25519DiffieHellmanOpenSsl(SafeEvpPKeyHandle)} (wraps an existing key handle). + *
+ * + *

Architecture: mirrors {@code DotNetECDiffieHellman.java} (Batch 3) as closely as the actual, + * verified API shape allows. The overload-collapsing rationale documented there (the ANTLR4-based + * C# engine cannot resolve parameter types — see {@code CSharpLanguageTranslation}) applies here + * too: all four {@code DeriveRawSecretAgreement} overloads (array-returning, {@code + * Span}-returning, and both taking either a raw public-key byte array or another {@code + * X25519DiffieHellman} instance as the peer) are collapsed into a single {@code + * withAnyParameters()} rule per method name. + * + *

Deliberately NOT ported from {@code DotNetECDiffieHellman.java}: + * + *

    + *
  • {@code DeriveKeyMaterial}, {@code DeriveKeyFromHash}, {@code DeriveKeyFromHmac}, {@code + * DeriveKeyTls} — these methods do not exist on {@code X25519DiffieHellman}. Verified + * against the full, official method table on the class's Microsoft Learn reference page: the + * only key-agreement-derivation method is {@code DeriveRawSecretAgreement} (4 overloads). + * X25519 in .NET is a strictly lower-level, "raw shared secret only" API compared to {@code + * ECDiffieHellman} — callers are expected to apply their own KDF. + *
  • a settable {@code KeySize} property — {@code X25519DiffieHellman} exposes {@code + * PrivateKeySizeInBytes}, {@code PublicKeySizeInBytes} and {@code SecretAgreementSizeInBytes} + * only as read-only {@code const int} fields (all fixed at 32 for Curve25519), not + * as a settable property — so there is no {@code set_KeySize}-style assignment to detect, + * unlike the configurable-curve {@code ECDiffieHellman.KeySize}. + *
+ * + *

Following the same modeling convention as {@code DotNetECDiffieHellman.java}: {@code + * DeriveRawSecretAgreement} returns the raw shared secret with no KDF post-processing, so it is + * translated to the generic {@code Generate} functionality node (not {@code KeyDerivation}) in + * {@code CSharpKeyContextTranslator}. + * + *

Mapper model: a dedicated {@link com.ibm.mapper.model.algorithms.X25519} algorithm + * model class already exists (verified via {@code grep -rl "X25519" mapper/} — it is already used + * by the JCA {@code XDH}/{@code X25519} key-agreement translation and by the Go {@code crypto/ecdh} + * curve translation), so it is reused as-is here; no new mapper model class was needed. + * + *

{@code ImportPrivateKey}/{@code ImportPublicKey} — modeled as primary creation rules, not + * skipped: confirmed via the dedicated method-reference pages ({@code + * X25519DiffieHellman.ImportPrivateKey} / {@code X25519DiffieHellman.ImportPublicKey}, {@code + * learn.microsoft.com}, checked 2026-08-21) that both are declared directly on {@code + * X25519DiffieHellman} itself (not on a subclass) as {@code public static X25519DiffieHellman + * ImportPrivateKey(byte[])} / {@code ImportPrivateKey(ReadOnlySpan)}, and {@code public + * static X25519DiffieHellman ImportPublicKey(byte[])} / {@code ImportPublicKey(ReadOnlySpan)} + * — two overloads each, both returning a brand-new {@code X25519DiffieHellman} instance built + * directly from raw 32-byte key material. This is structurally identical to {@code + * MLKem.ImportDecapsulationKey}/{@code ImportEncapsulationKey} in {@code DotNetMLKem.java} (both + * static factories that are themselves the only way, besides {@code GenerateKey}, to obtain an + * instance) — the two overloads per method are collapsed with a single {@code withAnyParameters()} + * rule per method name, following the same ANTLR4-cannot-resolve-parameter-types rationale used + * throughout this file. + * + *

Known gaps (same reasoning as {@code DotNetECDiffieHellman.java}): + * + *

    + *
  • Reading the public key (e.g. {@code x25519.ExportPublicKey()} to send to a peer) is not + * modeled as a depending rule, mirroring the {@code PublicKey} property gap documented in + * {@code DotNetECDiffieHellman.java} for the same underlying reason: these calls carry no + * additional cryptographic information beyond "an X25519 key exists" (already captured by the + * primary creation rule), and speculatively wiring up the remaining family of {@code Export} + * / {@code TryExport} methods (PKCS8, SPKI, PEM, encrypted-PKCS8) would add many rules + * without adding any new detectable cryptographic fact. // TODO: revisit if a future need + * arises to track key material export as its own finding. + *
  • {@code X25519DiffieHellmanCng.GetKey()} and {@code + * X25519DiffieHellmanOpenSsl.DuplicateKeyHandle()} — these return the underlying platform key + * handle ({@code CngKey} / {@code SafeEvpPKeyHandle}), not a new cryptographic fact; skipped + * for the same reason as the export methods above. + *
+ */ +@SuppressWarnings("java:S1192") +public final class DotNetX25519DiffieHellman { + + private DotNetX25519DiffieHellman() { + // nothing + } + + // ========================================================================= + // Key-derivation / secret-agreement operation rule + // ========================================================================= + + // x25519.DeriveRawSecretAgreement(otherPartyPublicKey) — raw shared secret, no KDF applied. + // Collapses all 4 overloads (byte[]/Span-returning, byte[]-vs-X25519DiffieHellman peer param). + private static final IDetectionRule X25519_DERIVE_RAW_SECRET_AGREEMENT = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes(MethodMatcher.ANY) + .forMethods("DeriveRawSecretAgreement") + .shouldBeDetectedAs(new ValueActionFactory<>("DeriveRawSecretAgreement")) + .withAnyParameters() + .buildForContext( + new KeyContext(Map.of("kind", "X25519_DERIVE_RAW_SECRET_AGREEMENT"))) + .inBundle(() -> "DotNet") + .withoutDependingDetectionRules(); + + // ========================================================================= + // Aggregated depending-rule list + // ========================================================================= + + /** Full set of depending rules for all X25519DiffieHellman-derived classes. */ + private static final List> X25519_DEPENDING_RULES = + List.of(X25519_DERIVE_RAW_SECRET_AGREEMENT); + + // ========================================================================= + // Primary creation rules + // ========================================================================= + + // X25519DiffieHellman.GenerateKey() — static factory, no parameters (no Create() exists). + private static final IDetectionRule X25519_GENERATE_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("X25519DiffieHellman") + .forMethods("GenerateKey") + .shouldBeDetectedAs(new ValueActionFactory<>("X25519")) + .withoutParameters() + .buildForContext(new KeyContext(Map.of("kind", "X25519"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(X25519_DEPENDING_RULES); + + // X25519DiffieHellman.ImportPrivateKey(source) — static factory, builds a new instance + // directly from raw 32-byte private key material. Collapses both overloads (byte[] / + // ReadOnlySpan) with withAnyParameters(), mirroring MLKEM_IMPORT_DECAPSULATION_KEY in + // DotNetMLKem.java. + private static final IDetectionRule X25519_IMPORT_PRIVATE_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("X25519DiffieHellman") + .forMethods("ImportPrivateKey") + .shouldBeDetectedAs(new ValueActionFactory<>("X25519")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "X25519"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(X25519_DEPENDING_RULES); + + // X25519DiffieHellman.ImportPublicKey(source) — static factory, builds a new instance + // directly from raw 32-byte public key material. Collapses both overloads (byte[] / + // ReadOnlySpan) with withAnyParameters(), mirroring MLKEM_IMPORT_ENCAPSULATION_KEY in + // DotNetMLKem.java. + private static final IDetectionRule X25519_IMPORT_PUBLIC_KEY = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("X25519DiffieHellman") + .forMethods("ImportPublicKey") + .shouldBeDetectedAs(new ValueActionFactory<>("X25519")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "X25519"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(X25519_DEPENDING_RULES); + + // new X25519DiffieHellmanCng(CngKey) — CNG-backed implementation, wraps an existing key. + // Uses withAnyParameters() for consistency with ECDH_CNG / AES_CNG_NAMED even though only one + // constructor overload exists, to avoid a brittle single-parameter-type assumption. + private static final IDetectionRule X25519_CNG = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("X25519DiffieHellmanCng") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("X25519")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "X25519"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(X25519_DEPENDING_RULES); + + // new X25519DiffieHellmanOpenSsl(SafeEvpPKeyHandle) — OpenSSL-backed implementation, wraps an + // existing key handle. Uses withAnyParameters() for the same reason as X25519_CNG. + private static final IDetectionRule X25519_OPENSSL = + new DetectionRuleBuilder() + .createDetectionRule() + .forObjectTypes("X25519DiffieHellmanOpenSsl") + .forMethods("") + .shouldBeDetectedAs(new ValueActionFactory<>("X25519")) + .withAnyParameters() + .buildForContext(new KeyContext(Map.of("kind", "X25519"))) + .inBundle(() -> "DotNet") + .withDependingDetectionRules(X25519_DEPENDING_RULES); + + @Nonnull + public static List> rules() { + return List.of( + X25519_GENERATE_KEY, + X25519_IMPORT_PRIVATE_KEY, + X25519_IMPORT_PUBLIC_KEY, + X25519_CNG, + X25519_OPENSSL); + } +} diff --git a/csharp/src/main/java/com/ibm/plugin/translation/translator/CSharpTranslator.java b/csharp/src/main/java/com/ibm/plugin/translation/translator/CSharpTranslator.java index 793796b3f..449367b14 100755 --- a/csharp/src/main/java/com/ibm/plugin/translation/translator/CSharpTranslator.java +++ b/csharp/src/main/java/com/ibm/plugin/translation/translator/CSharpTranslator.java @@ -30,6 +30,7 @@ import com.ibm.engine.model.context.DigestContext; import com.ibm.engine.model.context.IDetectionContext; import com.ibm.engine.model.context.KeyContext; +import com.ibm.engine.model.context.KeyDerivationFunctionContext; import com.ibm.engine.model.context.MacContext; import com.ibm.engine.model.context.PRNGContext; import com.ibm.engine.model.context.ProtocolContext; @@ -42,6 +43,8 @@ import com.ibm.plugin.translation.translator.contexts.CSharpDigestContextTranslator; import com.ibm.plugin.translation.translator.contexts.CSharpKeyContextTranslator; import com.ibm.plugin.translation.translator.contexts.CSharpMacContextTranslator; +import com.ibm.plugin.translation.translator.contexts.CSharpPRNGContextTranslator; +import com.ibm.plugin.translation.translator.contexts.CSharpSignatureContextTranslator; import java.util.List; import java.util.Optional; import javax.annotation.Nonnull; @@ -88,9 +91,34 @@ public Optional translate( .translate(bundleIdentifier, value, detectionValueContext, detectionLocation); } - if (detectionValueContext.is(PRNGContext.class) - || detectionValueContext.is(SignatureContext.class) - || detectionValueContext.is(ProtocolContext.class)) { + if (detectionValueContext.is(SignatureContext.class)) { + return new CSharpSignatureContextTranslator() + .translate(bundleIdentifier, value, detectionValueContext, detectionLocation); + } + + if (detectionValueContext.is(PRNGContext.class)) { + return new CSharpPRNGContextTranslator() + .translate(bundleIdentifier, value, detectionValueContext, detectionLocation); + } + + if (detectionValueContext.is(ProtocolContext.class)) { + return Optional.empty(); + } + + if (detectionValueContext.is(KeyDerivationFunctionContext.class)) { + // No C# rule currently builds a KeyDerivationFunctionContext: unlike the Python + // module (see PycaKDF/PycaKeyDerivationContextTranslator), every KDF rule in this + // module (DotNetKeyDerivation, DotNetRfc2898DeriveBytes; also mirrored by the Java + // BouncyCastle rules in BcDerivationFunction) reuses the generic KeyContext with a + // "kind" discriminator (e.g. "KDF_HKDF", "KDF_SP800108") instead, dispatched in + // CSharpKeyContextTranslator. That already produces the correct KDF-specific model + // nodes (HKDF/PBKDF1/PBKDF2/KDFCounter, which implement KeyDerivationFunction / + // PasswordBasedKeyDerivationFunction in the mapper model), so no information is lost + // by not using this context. This branch is a deliberate, documented no-op — it + // behaves the same as the default fallthrough below, but exists so this gap reads as + // an intentional choice rather than a missed dispatch case if a future C# rule ever + // reaches for KeyDerivationFunctionContext (at which point a real translator, mirroring + // CSharpKeyContextTranslator's "KDF_*" cases, would need to be wired in here). return Optional.empty(); } diff --git a/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpCipherContextTranslator.java b/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpCipherContextTranslator.java index 4fa746e89..ed6f17517 100755 --- a/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpCipherContextTranslator.java +++ b/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpCipherContextTranslator.java @@ -19,7 +19,9 @@ */ package com.ibm.plugin.translation.translator.contexts; +import com.ibm.engine.model.Algorithm; import com.ibm.engine.model.BlockSize; +import com.ibm.engine.model.CipherAction; import com.ibm.engine.model.IValue; import com.ibm.engine.model.KeySize; import com.ibm.engine.model.Mode; @@ -32,6 +34,7 @@ import com.ibm.mapper.mapper.jca.JcaCipherOperationModeMapper; import com.ibm.mapper.mapper.jca.JcaModeMapper; import com.ibm.mapper.mapper.jca.JcaPaddingMapper; +import com.ibm.mapper.model.Cipher; import com.ibm.mapper.model.INode; import com.ibm.mapper.model.KeyLength; import com.ibm.mapper.model.algorithms.AES; @@ -39,6 +42,10 @@ import com.ibm.mapper.model.algorithms.DESede; import com.ibm.mapper.model.algorithms.RC2; import com.ibm.mapper.model.algorithms.RSA; +import com.ibm.mapper.model.functionality.Decrypt; +import com.ibm.mapper.model.functionality.Encrypt; +import com.ibm.mapper.model.functionality.Generate; +import com.ibm.mapper.model.functionality.KeyGeneration; import com.ibm.mapper.utils.DetectionLocation; import java.util.Optional; import javax.annotation.Nonnull; @@ -68,6 +75,34 @@ public final class CSharpCipherContextTranslator Optional.of(new DESede(detectionLocation)); case "RSA" -> Optional.of(new RSA(detectionLocation)); case "RC2" -> Optional.of(new RC2(detectionLocation)); + case "GENERATEKEY" -> Optional.of(new KeyGeneration(detectionLocation)); + case "GENERATEIV" -> Optional.of(new Generate(detectionLocation)); + // DPAPI (System.Security.Cryptography.ProtectedData / ProtectedMemory / + // DpapiDataProtector — see DotNetProtectedData): the concrete underlying + // algorithm is not exposed by the API, so it is captured as a generic + // Cipher-kind Algorithm rather than an invented concrete algorithm name + // (mirrors the NATIVEPRNG precedent in CSharpPRNGContextTranslator). + case "DPAPI" -> + Optional.of( + new com.ibm.mapper.model.Algorithm( + "DPAPI", Cipher.class, detectionLocation)); + // Static one-shot ProtectedData/ProtectedMemory calls: identity + action + // are captured together (see DotNetProtectedData class javadoc "Modeling + // decision — static one-shot calls"). + case "DPAPI_PROTECT" -> { + com.ibm.mapper.model.Algorithm dpapi = + new com.ibm.mapper.model.Algorithm( + "DPAPI", Cipher.class, detectionLocation); + dpapi.put(new Encrypt(detectionLocation)); + yield Optional.of(dpapi); + } + case "DPAPI_UNPROTECT" -> { + com.ibm.mapper.model.Algorithm dpapi = + new com.ibm.mapper.model.Algorithm( + "DPAPI", Cipher.class, detectionLocation); + dpapi.put(new Decrypt(detectionLocation)); + yield Optional.of(dpapi); + } default -> Optional.empty(); }; if (result.isPresent()) { @@ -76,6 +111,12 @@ public final class CSharpCipherContextTranslator // Try operation mode JcaCipherOperationModeMapper modeMapper = new JcaCipherOperationModeMapper(); return modeMapper.parse(valueStr, detectionLocation).map(mode -> mode); + } else if (value instanceof CipherAction cipherAction) { + return switch (cipherAction.getAction()) { + case ENCRYPT -> Optional.of(new Encrypt(detectionLocation)); + case DECRYPT -> Optional.of(new Decrypt(detectionLocation)); + default -> Optional.empty(); + }; } else if (value instanceof BlockSize blockSize) { return Optional.of( new com.ibm.mapper.model.BlockSize(blockSize.getValue(), detectionLocation)); @@ -94,6 +135,19 @@ public final class CSharpCipherContextTranslator // From set_Padding property setter: PaddingMode.PKCS7 → "PKCS7" JcaPaddingMapper paddingMapper = new JcaPaddingMapper(); return paddingMapper.parse(padding.asString(), detectionLocation).map(p -> p); + } else if (value instanceof Algorithm) { + // SymmetricAlgorithm.Create(string) — unlike its four sibling Create(string) overloads + // (HashAlgorithm/KeyedHashAlgorithm/HMAC/AsymmetricAlgorithm), the official API + // reference for this one does not publish an explicit value table (see + // DotNetAlgorithmFactory javadoc), so this reuses the same well-known names already + // accepted in the ValueAction branch above for this codebase's .NET support. + return switch (value.asString().toUpperCase().trim()) { + case "AES" -> Optional.of(new AES(detectionLocation)); + case "DES" -> Optional.of(new DES(detectionLocation)); + case "3DES", "DESEDE", "TRIPLEDES" -> Optional.of(new DESede(detectionLocation)); + case "RC2" -> Optional.of(new RC2(detectionLocation)); + default -> Optional.empty(); + }; } return Optional.empty(); diff --git a/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpDigestContextTranslator.java b/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpDigestContextTranslator.java index 121acad2b..c5e30d716 100755 --- a/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpDigestContextTranslator.java +++ b/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpDigestContextTranslator.java @@ -20,6 +20,7 @@ package com.ibm.plugin.translation.translator.contexts; import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.Algorithm; import com.ibm.engine.model.IValue; import com.ibm.engine.model.ValueAction; import com.ibm.engine.model.context.IDetectionContext; @@ -27,8 +28,11 @@ import com.ibm.mapper.IContextTranslation; import com.ibm.mapper.model.INode; import com.ibm.mapper.model.algorithms.MD5; +import com.ibm.mapper.model.algorithms.RIPEMD; import com.ibm.mapper.model.algorithms.SHA; import com.ibm.mapper.model.algorithms.SHA2; +import com.ibm.mapper.model.algorithms.SHA3; +import com.ibm.mapper.model.algorithms.shake.SHAKE; import com.ibm.mapper.utils.DetectionLocation; import java.util.Optional; import javax.annotation.Nonnull; @@ -50,6 +54,33 @@ public final class CSharpDigestContextTranslator implements IContextTranslation< case "SHA384" -> Optional.of(new SHA2(384, detectionLocation)); case "SHA512" -> Optional.of(new SHA2(512, detectionLocation)); case "MD5" -> Optional.of(new MD5(detectionLocation)); + case "RIPEMD160" -> Optional.of(new RIPEMD(160, detectionLocation)); + case "SHA3_256" -> Optional.of(new SHA3(256, detectionLocation)); + case "SHA3_384" -> Optional.of(new SHA3(384, detectionLocation)); + case "SHA3_512" -> Optional.of(new SHA3(512, detectionLocation)); + case "SHAKE128" -> Optional.of(new SHAKE(128, detectionLocation)); + case "SHAKE256" -> Optional.of(new SHAKE(256, detectionLocation)); + default -> Optional.empty(); + }; + } else if (value instanceof Algorithm) { + // HashAlgorithm.Create(string) — string table verified against the official API + // reference (learn.microsoft.com), see DotNetAlgorithmFactory javadoc. Unlike the + // ValueAction branch above (fixed per-class identity), the concrete digest here is + // resolved purely from the captured runtime string. + return switch (value.asString().toUpperCase().trim()) { + case "SHA", + "SHA1", + "SYSTEM.SECURITY.CRYPTOGRAPHY.SHA1", + "SYSTEM.SECURITY.CRYPTOGRAPHY.HASHALGORITHM" -> + Optional.of(new SHA(detectionLocation)); + case "MD5", "SYSTEM.SECURITY.CRYPTOGRAPHY.MD5" -> + Optional.of(new MD5(detectionLocation)); + case "SHA256", "SHA-256", "SYSTEM.SECURITY.CRYPTOGRAPHY.SHA256" -> + Optional.of(new SHA2(256, detectionLocation)); + case "SHA384", "SHA-384", "SYSTEM.SECURITY.CRYPTOGRAPHY.SHA384" -> + Optional.of(new SHA2(384, detectionLocation)); + case "SHA512", "SHA-512", "SYSTEM.SECURITY.CRYPTOGRAPHY.SHA512" -> + Optional.of(new SHA2(512, detectionLocation)); default -> Optional.empty(); }; } diff --git a/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpKeyContextTranslator.java b/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpKeyContextTranslator.java index 6b50ce83c..ab7e46076 100755 --- a/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpKeyContextTranslator.java +++ b/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpKeyContextTranslator.java @@ -20,8 +20,12 @@ package com.ibm.plugin.translation.translator.contexts; import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.Algorithm; import com.ibm.engine.model.IValue; +import com.ibm.engine.model.IterationCount; +import com.ibm.engine.model.KeyAction; import com.ibm.engine.model.KeySize; +import com.ibm.engine.model.ParameterIdentifier; import com.ibm.engine.model.ValueAction; import com.ibm.engine.model.context.DetectionContext; import com.ibm.engine.model.context.IDetectionContext; @@ -29,11 +33,25 @@ import com.ibm.mapper.IContextTranslation; import com.ibm.mapper.model.INode; import com.ibm.mapper.model.KeyLength; +import com.ibm.mapper.model.NumberOfIterations; +import com.ibm.mapper.model.ParameterSetIdentifier; import com.ibm.mapper.model.algorithms.DSA; import com.ibm.mapper.model.algorithms.ECDH; import com.ibm.mapper.model.algorithms.ECDSA; +import com.ibm.mapper.model.algorithms.HKDF; +import com.ibm.mapper.model.algorithms.KDFCounter; +import com.ibm.mapper.model.algorithms.MGF1; +import com.ibm.mapper.model.algorithms.MLDSA; +import com.ibm.mapper.model.algorithms.MLKEM; +import com.ibm.mapper.model.algorithms.PBKDF1; import com.ibm.mapper.model.algorithms.PBKDF2; import com.ibm.mapper.model.algorithms.RSA; +import com.ibm.mapper.model.algorithms.SPHINCSPlus; +import com.ibm.mapper.model.algorithms.X25519; +import com.ibm.mapper.model.functionality.Decapsulate; +import com.ibm.mapper.model.functionality.Encapsulate; +import com.ibm.mapper.model.functionality.Generate; +import com.ibm.mapper.model.functionality.KeyDerivation; import com.ibm.mapper.utils.DetectionLocation; import java.util.Optional; import javax.annotation.Nonnull; @@ -55,12 +73,209 @@ public final class CSharpKeyContextTranslator implements IContextTranslation Optional.of(new RSA(detectionLocation)); case "ECDSA" -> Optional.of(new ECDSA(detectionLocation)); case "ECDH" -> Optional.of(new ECDH(detectionLocation)); + // X25519 (DotNetX25519DiffieHellman.java): reuses the existing X25519 mapper + // model class (already used by the JCA XDH/X25519 and Go crypto/ecdh + // translations) — no new mapper model class was needed. See that rule class's + // javadoc for why X25519DiffieHellman's API shape (no Create(), no + // DeriveKeyMaterial/FromHash/FromHmac/Tls, no settable KeySize) differs from + // ECDiffieHellman's. + case "X25519" -> Optional.of(new X25519(detectionLocation)); case "DSA" -> Optional.of(new DSA(detectionLocation)); + // MGF1 (DotNetLegacyFormatters.java, PKCS1MaskGenerationMethod): reuses the + // existing MGF1 mapper model class (already used by JcaMGFMapper/ + // JcaOAEPPaddingMapper for JCA's OAEP padding translation) — no new mapper model + // class was needed. KeyContext is reused here purely as the generic "identify + // which standalone algorithm was just constructed" context already used for the + // other cases in this switch, not because MGF1 is a "key" algorithm per se — see + // that rule class's javadoc for the full rationale. + case "MGF1" -> Optional.of(new MGF1(detectionLocation)); + // ML-KEM (DotNetMLKem.java): generic top-level node. The parameter set (when + // captured — see the ParameterIdentifier branch below) is attached as a child by + // the tree-shape-preserving translation process, yielding "ML-KEM-768" etc. via + // MLKEM#asString(). Reuses the mapper model class already used by the JCA, + // BouncyCastle and Go crypto/mlkem translations — no new model class needed. + case "KEM" -> Optional.of(new MLKEM(detectionLocation)); + // ML-DSA (DotNetMLDsa.java): generic top-level node, same shape as the "KEM" case + // above. The parameter set (when captured — see the ParameterIdentifier branch + // below) is attached as a child, yielding "ML-DSA-44"/"ML-DSA-65"/"ML-DSA-87" via + // MLDSA#asString(). Reuses the MLDSA mapper model class that already existed + // (added alongside MLKEM) — no new model class needed for plain ML-DSA. + // "MLDSA_COMPOSITE" (CompositeMLDsa/CompositeMLDsaCng) intentionally maps to the + // very same MLDSA node: no composite/hybrid algorithm concept exists yet in the + // mapper model (verified: grep -rl "Composite\|Hybrid" mapper/ found nothing), so + // rather than fabricate one or drop the detection, the full CompositeMLDsaAlgorithm + // name (e.g. "MLDsa44WithECDsaP256") is captured verbatim as the + // ParameterSetIdentifier below — see DotNetMLDsa's class javadoc for the full + // rationale and the recommendation for a future proper composite/hybrid model. + case "MLDSA", "MLDSA_COMPOSITE" -> Optional.of(new MLDSA(detectionLocation)); + // SLH-DSA (DotNetSlhDsa.java): generic top-level node, same shape as the "MLDSA" + // case above. The parameter set (when captured — see the ParameterIdentifier + // branch below) is attached as a child, yielding e.g. "SLH-DSA-SHA2-128s" via + // SPHINCSPlus#asString(). Reuses the SPHINCSPlus mapper model class that already + // existed (used by the BouncyCastle SPHINCSPlusSigner translation) — no new model + // class needed, per the class's "SLH-DSA .. Other Names: SPHINCS+" documentation. + case "SLHDSA" -> Optional.of(new SPHINCSPlus(detectionLocation)); case "KDF" -> Optional.of(new PBKDF2(detectionLocation)); + // Key derivation functions (DotNetKeyDerivation.java): HKDF and + // SP800108HmacCounterKdf's static one-shot calls, and PasswordDeriveBytes's + // constructor, map directly to their KDF algorithm model node — the class itself + // *is* the KDF (mirrors the "KDF" -> PBKDF2 case above for Rfc2898DeriveBytes), + // unlike the ECDiffieHellman derive operations below (see that case's comment). + case "KDF_HKDF" -> Optional.of(new HKDF(detectionLocation)); + case "KDF_SP800108" -> Optional.of(new KDFCounter(detectionLocation)); + case "KDF_PASSWORD_DERIVE_BYTES" -> Optional.of(new PBKDF1(detectionLocation)); + // Instance derive-operations on an already-identified KDF object + // (SP800108HmacCounterKdf.DeriveKey, PasswordDeriveBytes.GetBytes/ + // CryptDeriveKey, and Rfc2898DeriveBytes.GetBytes/CryptDeriveKey — see + // DotNetRfc2898DeriveBytes.java): reuses the same generic KeyDerivation + // functionality node as the ECDiffieHellman derive operations below (Batch 3 + // pattern), captured as a child of the already-typed KDF algorithm node rather + // than folded into it. + case "KDF_SP800108_DERIVE_KEY", + "KDF_PDB_GET_BYTES", + "KDF_PDB_CRYPT_DERIVE_KEY", + "KDF_RFC2898_GET_BYTES", + "KDF_RFC2898_CRYPT_DERIVE_KEY" -> + Optional.of(new KeyDerivation(detectionLocation)); + // ECDiffieHellman key-derivation operations (DotNetECDiffieHellman.java): + // no typed CipherAction.Action fits "derive a key", so each operation is + // captured with a generic ValueActionFactory under its own "kind" and + // dispatched here (see that class's javadoc for the full rationale). + case "ECDH_DERIVE_KEY_MATERIAL", + "ECDH_DERIVE_KEY_FROM_HASH", + "ECDH_DERIVE_KEY_FROM_HMAC", + "ECDH_DERIVE_KEY_TLS" -> + Optional.of(new KeyDerivation(detectionLocation)); + // DeriveRawSecretAgreement returns the raw shared secret with no KDF + // post-processing, so it maps to the generic Generate functionality instead + // of KeyDerivation (mirroring how GenerateIV is translated in + // CSharpCipherContextTranslator). + case "ECDH_DERIVE_RAW_SECRET_AGREEMENT" -> + Optional.of(new Generate(detectionLocation)); + // X25519 DeriveRawSecretAgreement (DotNetX25519DiffieHellman.java): same + // reasoning as the ECDH case immediately above — raw shared secret, no KDF + // post-processing, so it maps to Generate rather than KeyDerivation. + case "X25519_DERIVE_RAW_SECRET_AGREEMENT" -> + Optional.of(new Generate(detectionLocation)); default -> Optional.empty(); }; } else if (value instanceof KeySize keySize) { return Optional.of(new KeyLength(keySize.getValue(), detectionLocation)); + } else if (value instanceof IterationCount iterationCount) { + // From set_IterationCount property setter (PasswordDeriveBytes.IterationCount — + // DotNetKeyDerivation.java): mirrors the Go module's identical IterationCount -> + // NumberOfIterations mapping (see GoKeyContextTranslator), the only other place in + // this codebase that produces an IterationCount value under a KeyContext today. + return Optional.of( + new NumberOfIterations(iterationCount.getValue(), detectionLocation)); + } else if (value instanceof ParameterIdentifier parameterIdentifier + && detectionContext instanceof DetectionContext context + && "KEM".equals(context.get("kind").orElse(""))) { + // ML-KEM parameter set (DotNetMLKem.java): captured from the MLKemAlgorithm argument + // (e.g. MLKemAlgorithm.MLKem768, resolved to the bare enum member name "MLKem768" — see + // that class's javadoc). Mapped to the numeric suffix expected by + // MLKEM(int, DetectionLocation) / ParameterSetIdentifier, matching the + // "768"/"512"/"1024" + // values already produced by JcaKemMapper/GoCryptoKEMMapper for the same algorithm. + return switch (parameterIdentifier.asString()) { + case "MLKem512" -> + Optional.of(new ParameterSetIdentifier("512", detectionLocation)); + case "MLKem768" -> + Optional.of(new ParameterSetIdentifier("768", detectionLocation)); + case "MLKem1024" -> + Optional.of(new ParameterSetIdentifier("1024", detectionLocation)); + default -> Optional.empty(); + }; + } else if (value instanceof ParameterIdentifier parameterIdentifier + && detectionContext instanceof DetectionContext context + && "MLDSA".equals(context.get("kind").orElse(""))) { + // ML-DSA parameter set (DotNetMLDsa.java): captured from the MLDsaAlgorithm argument + // (e.g. MLDsaAlgorithm.MLDsa65, resolved to the bare enum member name "MLDsa65"), + // mapped to the numeric suffix expected by MLDSA(int, DetectionLocation) / + // ParameterSetIdentifier — mirrors the "KEM" branch above for MLKemAlgorithm. + return switch (parameterIdentifier.asString()) { + case "MLDsa44" -> Optional.of(new ParameterSetIdentifier("44", detectionLocation)); + case "MLDsa65" -> Optional.of(new ParameterSetIdentifier("65", detectionLocation)); + case "MLDsa87" -> Optional.of(new ParameterSetIdentifier("87", detectionLocation)); + default -> Optional.empty(); + }; + } else if (value instanceof ParameterIdentifier parameterIdentifier + && detectionContext instanceof DetectionContext context + && "MLDSA_COMPOSITE".equals(context.get("kind").orElse(""))) { + // Composite ML-DSA parameter set (DotNetMLDsa.java): the CompositeMLDsaAlgorithm + // member name (e.g. "MLDsa44WithECDsaP256") is a compound identifier, not a simple + // numeric parameter set, so it is captured verbatim rather than parsed — see + // DotNetMLDsa's class javadoc for why no structured composite/hybrid model is used. + return Optional.of( + new ParameterSetIdentifier(parameterIdentifier.asString(), detectionLocation)); + } else if (value instanceof ParameterIdentifier parameterIdentifier + && detectionContext instanceof DetectionContext context + && "SLHDSA".equals(context.get("kind").orElse(""))) { + // SLH-DSA parameter set (DotNetSlhDsa.java): captured from the SlhDsaAlgorithm + // argument (e.g. SlhDsaAlgorithm.SlhDsaSha2_128s, resolved to the bare enum member + // name "SlhDsaSha2_128s"), mapped to the FIPS 205 parameter set suffix (e.g. + // "SHA2-128s") expected by SPHINCSPlus's String constructor / ParameterSetIdentifier — + // mirrors the "MLDSA" branch above for MLDsaAlgorithm. All 12 values verified against + // the official SlhDsaAlgorithm reference page (SHA2 and SHAKE families, each with + // 128/192/256-bit security levels and s(mall)/f(ast) speed tradeoffs). + return switch (parameterIdentifier.asString()) { + case "SlhDsaSha2_128s" -> + Optional.of(new ParameterSetIdentifier("SHA2-128s", detectionLocation)); + case "SlhDsaSha2_128f" -> + Optional.of(new ParameterSetIdentifier("SHA2-128f", detectionLocation)); + case "SlhDsaSha2_192s" -> + Optional.of(new ParameterSetIdentifier("SHA2-192s", detectionLocation)); + case "SlhDsaSha2_192f" -> + Optional.of(new ParameterSetIdentifier("SHA2-192f", detectionLocation)); + case "SlhDsaSha2_256s" -> + Optional.of(new ParameterSetIdentifier("SHA2-256s", detectionLocation)); + case "SlhDsaSha2_256f" -> + Optional.of(new ParameterSetIdentifier("SHA2-256f", detectionLocation)); + case "SlhDsaShake128s" -> + Optional.of(new ParameterSetIdentifier("SHAKE-128s", detectionLocation)); + case "SlhDsaShake128f" -> + Optional.of(new ParameterSetIdentifier("SHAKE-128f", detectionLocation)); + case "SlhDsaShake192s" -> + Optional.of(new ParameterSetIdentifier("SHAKE-192s", detectionLocation)); + case "SlhDsaShake192f" -> + Optional.of(new ParameterSetIdentifier("SHAKE-192f", detectionLocation)); + case "SlhDsaShake256s" -> + Optional.of(new ParameterSetIdentifier("SHAKE-256s", detectionLocation)); + case "SlhDsaShake256f" -> + Optional.of(new ParameterSetIdentifier("SHAKE-256f", detectionLocation)); + default -> Optional.empty(); + }; + } else if (value instanceof KeyAction keyAction) { + // ML-KEM encapsulate/decapsulate (DotNetMLKem.java): KeyAction.Action already defines + // ENCAPSULATION/DECAPSULATION (used identically by the Go crypto/mlkem translation in + // GoKeyContextTranslator), so no new engine or mapper enum value was needed. + return switch (keyAction.getAction()) { + case ENCAPSULATION -> Optional.of(new Encapsulate(detectionLocation)); + case DECAPSULATION -> Optional.of(new Decapsulate(detectionLocation)); + default -> Optional.empty(); + }; + } else if (value instanceof Algorithm) { + // AsymmetricAlgorithm.Create(string) — string table verified against the official API + // reference (learn.microsoft.com), see DotNetAlgorithmFactory javadoc. Unlike the + // ValueAction branch above (dispatched by a fixed per-rule "kind" property), a single + // AsymmetricAlgorithm.Create(string) call site can produce any of RSA/DSA/ECDSA/ECDH, + // so the concrete algorithm here is resolved purely from the captured runtime string. + return switch (value.asString().toUpperCase().trim()) { + case "RSA", "SYSTEM.SECURITY.CRYPTOGRAPHY.RSA" -> + Optional.of(new RSA(detectionLocation)); + case "DSA", "SYSTEM.SECURITY.CRYPTOGRAPHY.DSA" -> + Optional.of(new DSA(detectionLocation)); + case "ECDSA", "ECDSACNG", "SYSTEM.SECURITY.CRYPTOGRAPHY.ECDSACNG" -> + Optional.of(new ECDSA(detectionLocation)); + case "ECDH", + "ECDIFFIEHELLMAN", + "ECDIFFIEHELLMANCNG", + "SYSTEM.SECURITY.CRYPTOGRAPHY.ECDIFFIEHELLMANCNG" -> + Optional.of(new ECDH(detectionLocation)); + // System.Security.Cryptography.AsymmetricAlgorithm has no concrete algorithm of + // its own per the official reference table — intentionally left unresolved. + default -> Optional.empty(); + }; } return Optional.empty(); diff --git a/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpMacContextTranslator.java b/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpMacContextTranslator.java index c0e0ddd68..9736892fd 100755 --- a/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpMacContextTranslator.java +++ b/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpMacContextTranslator.java @@ -20,16 +20,22 @@ package com.ibm.plugin.translation.translator.contexts; import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.Algorithm; import com.ibm.engine.model.IValue; import com.ibm.engine.model.ValueAction; import com.ibm.engine.model.context.IDetectionContext; import com.ibm.engine.rule.IBundle; import com.ibm.mapper.IContextTranslation; import com.ibm.mapper.model.INode; +import com.ibm.mapper.model.Mac; +import com.ibm.mapper.model.algorithms.DESede; import com.ibm.mapper.model.algorithms.HMAC; +import com.ibm.mapper.model.algorithms.KMAC; import com.ibm.mapper.model.algorithms.MD5; +import com.ibm.mapper.model.algorithms.RIPEMD; import com.ibm.mapper.model.algorithms.SHA; import com.ibm.mapper.model.algorithms.SHA2; +import com.ibm.mapper.model.algorithms.SHA3; import com.ibm.mapper.utils.DetectionLocation; import java.util.Optional; import javax.annotation.Nonnull; @@ -51,6 +57,50 @@ public final class CSharpMacContextTranslator implements IContextTranslation Optional.of(new HMAC(new SHA2(384, detectionLocation))); case "HMACSHA512" -> Optional.of(new HMAC(new SHA2(512, detectionLocation))); case "HMACMD5" -> Optional.of(new HMAC(new MD5(detectionLocation))); + case "HMACRIPEMD160" -> Optional.of(new HMAC(new RIPEMD(160, detectionLocation))); + case "HMACSHA3_256" -> Optional.of(new HMAC(new SHA3(256, detectionLocation))); + case "HMACSHA3_384" -> Optional.of(new HMAC(new SHA3(384, detectionLocation))); + case "HMACSHA3_512" -> Optional.of(new HMAC(new SHA3(512, detectionLocation))); + // Kmac128/KmacXof128 and Kmac256/KmacXof256 both reuse the existing KMAC mapper + // model (already used by the BouncyCastle KMAC mapper). The model has no concept + // distinguishing the fixed-output KMAC construction from the true + // extendable-output KMACXOF construction, so both collapse to the identical + // translated node per security-strength pair — see DotNetKMAC javadoc "Known + // modeling gap" section for the full rationale. + case "KMAC128", "KMACXOF128" -> Optional.of(new KMAC(128, detectionLocation)); + case "KMAC256", "KMACXOF256" -> Optional.of(new KMAC(256, detectionLocation)); + // MACTripleDES is not HMAC-based (see DotNetHMAC javadoc); reuse the DESede + // algorithm model "as" a Mac kind, the same idiom used elsewhere in this codebase + // to reinterpret DESede as a KeyWrap (JcaCipherMapper / BcWrapperMapper). + case "MACTRIPLEDES" -> + Optional.of(new DESede(Mac.class, new DESede(detectionLocation))); + default -> Optional.empty(); + }; + } else if (value instanceof Algorithm) { + // KeyedHashAlgorithm.Create(string) / HMAC.Create(string) — both methods document the + // identical string table, verified against the official API reference + // (learn.microsoft.com), see DotNetAlgorithmFactory javadoc. Unlike the ValueAction + // branch above (fixed per-class identity), the concrete MAC here is resolved purely + // from the captured runtime string. + return switch (value.asString().toUpperCase().trim()) { + case "HMACSHA1", + "SYSTEM.SECURITY.CRYPTOGRAPHY.HMACSHA1", + "SYSTEM.SECURITY.CRYPTOGRAPHY.HMAC", + "SYSTEM.SECURITY.CRYPTOGRAPHY.KEYEDHASHALGORITHM" -> + Optional.of(new HMAC(new SHA(detectionLocation))); + case "HMACSHA256", "SYSTEM.SECURITY.CRYPTOGRAPHY.HMACSHA256" -> + Optional.of(new HMAC(new SHA2(256, detectionLocation))); + case "HMACSHA384", "SYSTEM.SECURITY.CRYPTOGRAPHY.HMACSHA384" -> + Optional.of(new HMAC(new SHA2(384, detectionLocation))); + case "HMACSHA512", "SYSTEM.SECURITY.CRYPTOGRAPHY.HMACSHA512" -> + Optional.of(new HMAC(new SHA2(512, detectionLocation))); + case "HMACMD5", "SYSTEM.SECURITY.CRYPTOGRAPHY.HMACMD5" -> + Optional.of(new HMAC(new MD5(detectionLocation))); + case "HMACRIPEMD160", "SYSTEM.SECURITY.CRYPTOGRAPHY.HMACRIPEMD160" -> + Optional.of(new HMAC(new RIPEMD(160, detectionLocation))); + // Same DESede-as-Mac idiom as the ValueAction branch above. + case "MACTRIPLEDES", "SYSTEM.SECURITY.CRYPTOGRAPHY.MACTRIPLEDES" -> + Optional.of(new DESede(Mac.class, new DESede(detectionLocation))); default -> Optional.empty(); }; } diff --git a/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpPRNGContextTranslator.java b/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpPRNGContextTranslator.java new file mode 100644 index 000000000..f243d05fa --- /dev/null +++ b/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpPRNGContextTranslator.java @@ -0,0 +1,81 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.translation.translator.contexts; + +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.IValue; +import com.ibm.engine.model.ValueAction; +import com.ibm.engine.model.context.IDetectionContext; +import com.ibm.engine.rule.IBundle; +import com.ibm.mapper.IContextTranslation; +import com.ibm.mapper.model.Algorithm; +import com.ibm.mapper.model.INode; +import com.ibm.mapper.model.PseudorandomNumberGenerator; +import com.ibm.mapper.model.functionality.Generate; +import com.ibm.mapper.utils.DetectionLocation; +import java.util.Optional; +import javax.annotation.Nonnull; + +/** + * Translates {@link com.ibm.engine.model.context.PRNGContext} detections for .NET APIs ({@code + * RandomNumberGenerator}, {@code RNGCryptoServiceProvider} — see {@link + * com.ibm.plugin.rules.detection.dotnet.DotNetRandomNumberGenerator}). + * + *

Mirrors {@code GoPRNGContextTranslator}: .NET's {@code RandomNumberGenerator} is + * architecturally the same shape as Go's {@code crypto/rand} (ask the platform for + * cryptographically strong random output, no user-selectable algorithm), so the same {@code + * "NATIVEPRNG"} {@link Algorithm} identity is reused instead of introducing a new mapper model + * class. + */ +public final class CSharpPRNGContextTranslator implements IContextTranslation { + + @Override + public @Nonnull Optional translate( + @Nonnull IBundle bundleIdentifier, + @Nonnull IValue value, + @Nonnull IDetectionContext detectionContext, + @Nonnull DetectionLocation detectionLocation) { + + if (value instanceof ValueAction) { + return switch (value.asString().toUpperCase().trim()) { + // RandomNumberGenerator.Create()/Create(string), every static self-contained + // method (Fill/GetBytes/GetHexString/GetInt32/GetItems/GetNonZeroBytes/ + // GetString/Shuffle), and the RNGCryptoServiceProvider constructor all identify + // the same underlying platform CSPRNG. + case "NATIVEPRNG" -> + Optional.of( + new Algorithm( + "NATIVEPRNG", + PseudorandomNumberGenerator.class, + detectionLocation)); + // Instance operations on an already-identified RNG object + // (RandomNumberGenerator.Create()'s or RNGCryptoServiceProvider's + // GetBytes/GetNonZeroBytes) — the RNG identity is already captured by the parent + // node, so these depending calls only need to record that a generate operation + // happened (mirrors GenerateIV in CSharpCipherContextTranslator, which has no + // more specific CipherAction available either). + case "GETBYTES", "GETNONZEROBYTES" -> Optional.of(new Generate(detectionLocation)); + default -> Optional.empty(); + }; + } + + return Optional.empty(); + } +} diff --git a/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpSignatureContextTranslator.java b/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpSignatureContextTranslator.java new file mode 100644 index 000000000..5b3f77f91 --- /dev/null +++ b/csharp/src/main/java/com/ibm/plugin/translation/translator/contexts/CSharpSignatureContextTranslator.java @@ -0,0 +1,54 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.translation.translator.contexts; + +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.IValue; +import com.ibm.engine.model.SignatureAction; +import com.ibm.engine.model.context.IDetectionContext; +import com.ibm.engine.rule.IBundle; +import com.ibm.mapper.IContextTranslation; +import com.ibm.mapper.model.INode; +import com.ibm.mapper.model.functionality.Sign; +import com.ibm.mapper.model.functionality.Verify; +import com.ibm.mapper.utils.DetectionLocation; +import java.util.Optional; +import javax.annotation.Nonnull; + +/** Translates {@link com.ibm.engine.model.context.SignatureContext} detections for .NET APIs. */ +public final class CSharpSignatureContextTranslator implements IContextTranslation { + + @Override + public @Nonnull Optional translate( + @Nonnull IBundle bundleIdentifier, + @Nonnull IValue value, + @Nonnull IDetectionContext detectionContext, + @Nonnull DetectionLocation detectionLocation) { + + if (value instanceof SignatureAction signatureAction) { + return switch (signatureAction.getAction()) { + case SIGN -> Optional.of(new Sign(detectionLocation)); + case VERIFY -> Optional.of(new Verify(detectionLocation)); + }; + } + + return Optional.empty(); + } +} diff --git a/csharp/src/test/files/rules/detection/dotnet/DotNetAESComprehensiveTestFile.cs b/csharp/src/test/files/rules/detection/dotnet/DotNetAESComprehensiveTestFile.cs new file mode 100644 index 000000000..4ef62b151 --- /dev/null +++ b/csharp/src/test/files/rules/detection/dotnet/DotNetAESComprehensiveTestFile.cs @@ -0,0 +1,501 @@ +/* + * Comprehensive test file for System.Security.Cryptography AES detection rules. + * + * Covers all five AES-related classes and their complete API surface: + * - Aes (abstract base) + * - AesManaged, AesCng, AesCryptoServiceProvider (derived from Aes) + * - AesGcm, AesCcm (AEAD, separate class hierarchy, same namespace) + * + * Architecture note: all methods inherited from SymmetricAlgorithm (EncryptCbc, + * CreateEncryptor, etc.) are covered once here. The detection engine tracks the + * variable and fires the same depending rules for every concrete Aes subclass. + */ + +using System.Security.Cryptography; + +public class DotNetAESComprehensiveTest +{ + // ------------------------------------------------------------------------- + // Section 1: Factory methods / constructors + // ------------------------------------------------------------------------- + + public void TestAesCreate() + { + var aes = Aes.Create(); + } + + public void TestAesCreateNamed() + { + var aes = Aes.Create("AES"); + } + + public void TestAesManaged() + { + var aes = new AesManaged(); + } + + public void TestAesCng() + { + var aes = new AesCng(); + } + + public void TestAesCngNamed() + { + var aes = new AesCng("myKey"); + } + + public void TestAesCsp() + { + var aes = new AesCryptoServiceProvider(); + } + + public void TestAesGcm() + { + byte[] key = new byte[32]; + var aesGcm = new AesGcm(key); + } + + public void TestAesCcm() + { + byte[] key = new byte[32]; + var aesCcm = new AesCcm(key); + } + + // ------------------------------------------------------------------------- + // Section 2: Property setters (via assignment → synthetic set_X invocations) + // ------------------------------------------------------------------------- + + public void TestPropertyModeCBC() + { + var aes = Aes.Create(); + aes.Mode = CipherMode.CBC; + } + + public void TestPropertyModeECB() + { + var aes = Aes.Create(); + aes.Mode = CipherMode.ECB; + } + + public void TestPropertyModeCFB() + { + var aes = Aes.Create(); + aes.Mode = CipherMode.CFB; + } + + public void TestPropertyModeOFB() + { + var aes = Aes.Create(); + aes.Mode = CipherMode.OFB; + } + + public void TestPropertyModeCTS() + { + var aes = Aes.Create(); + aes.Mode = CipherMode.CTS; + } + + public void TestPropertyKeySize128() + { + var aes = Aes.Create(); + aes.KeySize = 128; + } + + public void TestPropertyKeySize192() + { + var aes = Aes.Create(); + aes.KeySize = 192; + } + + public void TestPropertyKeySize256() + { + var aes = Aes.Create(); + aes.KeySize = 256; + } + + public void TestPropertyPaddingPKCS7() + { + var aes = Aes.Create(); + aes.Padding = PaddingMode.PKCS7; + } + + public void TestPropertyPaddingNone() + { + var aes = Aes.Create(); + aes.Padding = PaddingMode.None; + } + + public void TestPropertyPaddingZeros() + { + var aes = Aes.Create(); + aes.Padding = PaddingMode.Zeros; + } + + public void TestPropertyPaddingANSIX923() + { + var aes = Aes.Create(); + aes.Padding = PaddingMode.ANSIX923; + } + + public void TestPropertyFeedbackSize() + { + var aes = Aes.Create(); + aes.FeedbackSize = 128; + } + + public void TestPropertyIV() + { + var aes = Aes.Create(); + aes.IV = new byte[16]; + } + + public void TestPropertyKey() + { + var aes = Aes.Create(); + aes.Key = new byte[32]; + } + + // ------------------------------------------------------------------------- + // Section 3: CreateEncryptor / CreateDecryptor + // ------------------------------------------------------------------------- + + public void TestCreateEncryptorNoArgs() + { + var aes = Aes.Create(); + var encryptor = aes.CreateEncryptor(); + } + + public void TestCreateEncryptorWithArgs() + { + var aes = Aes.Create(); + byte[] key = new byte[32]; + byte[] iv = new byte[16]; + var encryptor = aes.CreateEncryptor(key, iv); + } + + public void TestCreateDecryptorNoArgs() + { + var aes = Aes.Create(); + var decryptor = aes.CreateDecryptor(); + } + + public void TestCreateDecryptorWithArgs() + { + var aes = Aes.Create(); + byte[] key = new byte[32]; + byte[] iv = new byte[16]; + var decryptor = aes.CreateDecryptor(key, iv); + } + + // ------------------------------------------------------------------------- + // Section 4: Direct mode-specific encrypt methods + // ------------------------------------------------------------------------- + + public void TestEncryptCbc() + { + var aes = Aes.Create(); + byte[] plaintext = new byte[32]; + byte[] iv = new byte[16]; + byte[] ciphertext = aes.EncryptCbc(plaintext, iv, PaddingMode.PKCS7); + } + + public void TestEncryptEcb() + { + var aes = Aes.Create(); + byte[] plaintext = new byte[32]; + byte[] ciphertext = aes.EncryptEcb(plaintext, PaddingMode.None); + } + + public void TestEncryptCfb() + { + var aes = Aes.Create(); + byte[] plaintext = new byte[32]; + byte[] iv = new byte[16]; + byte[] ciphertext = aes.EncryptCfb(plaintext, iv, PaddingMode.None, 128); + } + + // ------------------------------------------------------------------------- + // Section 5: Direct mode-specific decrypt methods + // ------------------------------------------------------------------------- + + public void TestDecryptCbc() + { + var aes = Aes.Create(); + byte[] ciphertext = new byte[32]; + byte[] iv = new byte[16]; + byte[] plaintext = aes.DecryptCbc(ciphertext, iv, PaddingMode.PKCS7); + } + + public void TestDecryptEcb() + { + var aes = Aes.Create(); + byte[] ciphertext = new byte[32]; + byte[] plaintext = aes.DecryptEcb(ciphertext, PaddingMode.None); + } + + public void TestDecryptCfb() + { + var aes = Aes.Create(); + byte[] ciphertext = new byte[32]; + byte[] iv = new byte[16]; + byte[] plaintext = aes.DecryptCfb(ciphertext, iv, PaddingMode.None, 128); + } + + // ------------------------------------------------------------------------- + // Section 6: Try* variants + // ------------------------------------------------------------------------- + + public void TestTryEncryptCbc() + { + var aes = Aes.Create(); + byte[] plaintext = new byte[32]; + byte[] iv = new byte[16]; + byte[] destination = new byte[48]; + int bytesWritten; + aes.TryEncryptCbc(plaintext, iv, destination, out bytesWritten, PaddingMode.PKCS7); + } + + public void TestTryDecryptCbc() + { + var aes = Aes.Create(); + byte[] ciphertext = new byte[32]; + byte[] iv = new byte[16]; + byte[] destination = new byte[32]; + int bytesWritten; + aes.TryDecryptCbc(ciphertext, iv, destination, out bytesWritten, PaddingMode.PKCS7); + } + + public void TestTryEncryptEcb() + { + var aes = Aes.Create(); + byte[] plaintext = new byte[32]; + byte[] destination = new byte[48]; + int bytesWritten; + aes.TryEncryptEcb(plaintext, destination, PaddingMode.None, out bytesWritten); + } + + public void TestTryDecryptEcb() + { + var aes = Aes.Create(); + byte[] ciphertext = new byte[32]; + byte[] destination = new byte[32]; + int bytesWritten; + aes.TryDecryptEcb(ciphertext, destination, PaddingMode.None, out bytesWritten); + } + + public void TestTryEncryptCfb() + { + var aes = Aes.Create(); + byte[] plaintext = new byte[32]; + byte[] iv = new byte[16]; + byte[] destination = new byte[48]; + int bytesWritten; + aes.TryEncryptCfb(plaintext, iv, destination, out bytesWritten, PaddingMode.None, 128); + } + + public void TestTryDecryptCfb() + { + var aes = Aes.Create(); + byte[] ciphertext = new byte[32]; + byte[] iv = new byte[16]; + byte[] destination = new byte[32]; + int bytesWritten; + aes.TryDecryptCfb(ciphertext, iv, destination, out bytesWritten, PaddingMode.None, 128); + } + + // ------------------------------------------------------------------------- + // Section 7: Key/IV generation + // ------------------------------------------------------------------------- + + public void TestGenerateKey() + { + var aes = Aes.Create(); + aes.GenerateKey(); + } + + public void TestGenerateIV() + { + var aes = Aes.Create(); + aes.GenerateIV(); + } + + // ------------------------------------------------------------------------- + // Section 8: AesGcm AEAD operations + // ------------------------------------------------------------------------- + + public void TestAesGcmEncrypt() + { + byte[] key = new byte[32]; + var aesGcm = new AesGcm(key); + byte[] nonce = new byte[12]; + byte[] plaintext = new byte[32]; + byte[] ciphertext = new byte[32]; + byte[] tag = new byte[16]; + byte[] aad = new byte[8]; + aesGcm.Encrypt(nonce, plaintext, ciphertext, tag, aad); + } + + public void TestAesGcmDecrypt() + { + byte[] key = new byte[32]; + var aesGcm = new AesGcm(key); + byte[] nonce = new byte[12]; + byte[] ciphertext = new byte[32]; + byte[] tag = new byte[16]; + byte[] plaintext = new byte[32]; + byte[] aad = new byte[8]; + aesGcm.Decrypt(nonce, ciphertext, tag, plaintext, aad); + } + + // ------------------------------------------------------------------------- + // Section 9: AesCcm AEAD operations + // ------------------------------------------------------------------------- + + public void TestAesCcmEncrypt() + { + byte[] key = new byte[32]; + var aesCcm = new AesCcm(key); + byte[] nonce = new byte[12]; + byte[] plaintext = new byte[32]; + byte[] ciphertext = new byte[32]; + byte[] tag = new byte[16]; + byte[] aad = new byte[8]; + aesCcm.Encrypt(nonce, plaintext, ciphertext, tag, aad); + } + + public void TestAesCcmDecrypt() + { + byte[] key = new byte[32]; + var aesCcm = new AesCcm(key); + byte[] nonce = new byte[12]; + byte[] ciphertext = new byte[32]; + byte[] tag = new byte[16]; + byte[] plaintext = new byte[32]; + byte[] aad = new byte[8]; + aesCcm.Decrypt(nonce, ciphertext, tag, plaintext, aad); + } + + // ------------------------------------------------------------------------- + // Section 10: Combined usage patterns (real-world scenarios) + // Demonstrates that depending rules fire correctly for ALL derived classes. + // ------------------------------------------------------------------------- + + public void TestAesCbcFullFlow() + { + var aes = Aes.Create(); + aes.Mode = CipherMode.CBC; + aes.KeySize = 256; + aes.Padding = PaddingMode.PKCS7; + var encryptor = aes.CreateEncryptor(); + } + + public void TestAesCngEncryptCbc() + { + var aes = new AesCng(); + aes.Mode = CipherMode.CBC; + byte[] plaintext = new byte[32]; + byte[] iv = new byte[16]; + byte[] ciphertext = aes.EncryptCbc(plaintext, iv, PaddingMode.PKCS7); + } + + public void TestAesCspDecryptCbc() + { + var aes = new AesCryptoServiceProvider(); + byte[] ciphertext = new byte[32]; + byte[] iv = new byte[16]; + byte[] plaintext = aes.DecryptCbc(ciphertext, iv, PaddingMode.PKCS7); + } + + public void TestAesManagedCfbFeedback() + { + var aes = new AesManaged(); + aes.Mode = CipherMode.CFB; + aes.FeedbackSize = 128; + byte[] plaintext = new byte[32]; + byte[] iv = new byte[16]; + byte[] ciphertext = aes.EncryptCfb(plaintext, iv, PaddingMode.None, 128); + } + + public void TestAesCbcWithEncryptorOverload() + { + var aes = Aes.Create(); + byte[] key = new byte[32]; + byte[] iv = new byte[16]; + var encryptor = aes.CreateEncryptor(key, iv); + } + + public void TestAesEcbEncrypt() + { + var aes = new AesCng(); + byte[] plaintext = new byte[32]; + byte[] ciphertext = aes.EncryptEcb(plaintext, PaddingMode.None); + } + + public void TestAesGcmFullFlow() + { + byte[] key = new byte[32]; + var aesGcm = new AesGcm(key); + byte[] nonce = new byte[12]; + byte[] plaintext = new byte[32]; + byte[] ciphertext = new byte[32]; + byte[] tag = new byte[16]; + byte[] aad = new byte[8]; + aesGcm.Encrypt(nonce, plaintext, ciphertext, tag, aad); + } + + public void TestAesCcmFullFlow() + { + byte[] key = new byte[32]; + var aesCcm = new AesCcm(key); + byte[] nonce = new byte[12]; + byte[] plaintext = new byte[32]; + byte[] ciphertext = new byte[32]; + byte[] tag = new byte[16]; + byte[] aad = new byte[8]; + aesCcm.Encrypt(nonce, plaintext, ciphertext, tag, aad); + } + + // ------------------------------------------------------------------------- + // Section 11: EncryptKeyWrapPadded / DecryptKeyWrapPadded / + // TryDecryptKeyWrapPadded (RFC 5649 AES Key Wrap with Padding, .NET 10.0+) + // ------------------------------------------------------------------------- + + public void TestEncryptKeyWrapPadded1() + { + var aes = Aes.Create(); + byte[] plaintext = new byte[32]; + byte[] wrapped = aes.EncryptKeyWrapPadded(plaintext); + } + + public void TestEncryptKeyWrapPadded2() + { + var aes = Aes.Create(); + byte[] plaintext = new byte[32]; + byte[] destination = new byte[40]; + aes.EncryptKeyWrapPadded(plaintext, destination); + } + + public void TestDecryptKeyWrapPadded1() + { + var aes = Aes.Create(); + byte[] ciphertext = new byte[40]; + byte[] unwrapped = aes.DecryptKeyWrapPadded(ciphertext); + } + + public void TestDecryptKeyWrapPadded2() + { + var aes = Aes.Create(); + byte[] ciphertext = new byte[40]; + byte[] destination = new byte[32]; + int written = aes.DecryptKeyWrapPadded(ciphertext, destination); + } + + public void TestTryDecryptKeyWrapPadded() + { + var aes = Aes.Create(); + byte[] ciphertext = new byte[40]; + byte[] destination = new byte[32]; + int bytesWritten; + aes.TryDecryptKeyWrapPadded(ciphertext, destination, out bytesWritten); + } +} diff --git a/csharp/src/test/files/rules/detection/dotnet/DotNetAlgorithmFactoryTestFile.cs b/csharp/src/test/files/rules/detection/dotnet/DotNetAlgorithmFactoryTestFile.cs new file mode 100644 index 000000000..eeca66d06 --- /dev/null +++ b/csharp/src/test/files/rules/detection/dotnet/DotNetAlgorithmFactoryTestFile.cs @@ -0,0 +1,126 @@ +/* + * Test file for the generic, string-based Create(string) factory methods declared directly on + * the abstract base classes of System.Security.Cryptography (DotNetAlgorithmFactory.java): + * - SymmetricAlgorithm.Create(string) + * - HashAlgorithm.Create(string) + * - KeyedHashAlgorithm.Create(string) + * - HMAC.Create(string) + * - AsymmetricAlgorithm.Create(string) + */ + +using System.Security.Cryptography; + +public class DotNetAlgorithmFactoryTest +{ + // ------------------------------------------------------------------------- + // SymmetricAlgorithm.Create(string algName) + // ------------------------------------------------------------------------- + + public void TestSymmetricAlgorithmCreateAes() + { + SymmetricAlgorithm alg = SymmetricAlgorithm.Create("AES"); + } + + public void TestSymmetricAlgorithmCreateRc2() + { + SymmetricAlgorithm alg = SymmetricAlgorithm.Create("RC2"); + } + + public void TestSymmetricAlgorithmCreateTripleDes() + { + SymmetricAlgorithm alg = SymmetricAlgorithm.Create("3DES"); + } + + public void TestSymmetricAlgorithmCreateDes() + { + SymmetricAlgorithm alg = SymmetricAlgorithm.Create("DES"); + } + + // SymmetricAlgorithm.Create(string) followed by CreateEncryptor/CreateDecryptor: verifies that + // the generic, algorithm-independent depending rules attached to SYMMETRIC_ALGORITHM_CREATE + // (mirroring DotNetAES/DotNetRC2's CIPHER_OP_RULES) fire on the tracked variable. + public void TestSymmetricAlgorithmCreateAesWithEncryptorAndDecryptor() + { + var alg = SymmetricAlgorithm.Create("AES"); + byte[] key = new byte[32]; + byte[] iv = new byte[16]; + var enc = alg.CreateEncryptor(key, iv); + var dec = alg.CreateDecryptor(key, iv); + } + + // ------------------------------------------------------------------------- + // HashAlgorithm.Create(string hashName) + // ------------------------------------------------------------------------- + + public void TestHashAlgorithmCreateSha256() + { + HashAlgorithm alg = HashAlgorithm.Create("SHA256"); + } + + public void TestHashAlgorithmCreateShaHyphenated() + { + HashAlgorithm alg = HashAlgorithm.Create("SHA-512"); + } + + public void TestHashAlgorithmCreateMd5() + { + HashAlgorithm alg = HashAlgorithm.Create("MD5"); + } + + public void TestHashAlgorithmCreateShaBare() + { + HashAlgorithm alg = HashAlgorithm.Create("SHA"); + } + + // ------------------------------------------------------------------------- + // KeyedHashAlgorithm.Create(string algName) + // ------------------------------------------------------------------------- + + public void TestKeyedHashAlgorithmCreateHmacSha256() + { + KeyedHashAlgorithm alg = KeyedHashAlgorithm.Create("HMACSHA256"); + } + + public void TestKeyedHashAlgorithmCreateMacTripleDes() + { + KeyedHashAlgorithm alg = KeyedHashAlgorithm.Create("MACTripleDES"); + } + + // ------------------------------------------------------------------------- + // HMAC.Create(string algorithmName) + // ------------------------------------------------------------------------- + + public void TestHmacCreateHmacSha1() + { + HMAC alg = HMAC.Create("HMACSHA1"); + } + + public void TestHmacCreateFullyQualified() + { + HMAC alg = HMAC.Create("System.Security.Cryptography.HMACSHA256"); + } + + // ------------------------------------------------------------------------- + // AsymmetricAlgorithm.Create(string algName) + // ------------------------------------------------------------------------- + + public void TestAsymmetricAlgorithmCreateRsa() + { + AsymmetricAlgorithm alg = AsymmetricAlgorithm.Create("RSA"); + } + + public void TestAsymmetricAlgorithmCreateDsa() + { + AsymmetricAlgorithm alg = AsymmetricAlgorithm.Create("DSA"); + } + + public void TestAsymmetricAlgorithmCreateEcdsa() + { + AsymmetricAlgorithm alg = AsymmetricAlgorithm.Create("ECDsa"); + } + + public void TestAsymmetricAlgorithmCreateEcdh() + { + AsymmetricAlgorithm alg = AsymmetricAlgorithm.Create("ECDH"); + } +} diff --git a/csharp/src/test/files/rules/detection/dotnet/DotNetDESComprehensiveTestFile.cs b/csharp/src/test/files/rules/detection/dotnet/DotNetDESComprehensiveTestFile.cs new file mode 100644 index 000000000..4dc4ebdfa --- /dev/null +++ b/csharp/src/test/files/rules/detection/dotnet/DotNetDESComprehensiveTestFile.cs @@ -0,0 +1,339 @@ +/* + * Comprehensive test file for System.Security.Cryptography DES detection rules. + * + * Covers both DES-related classes and their complete API surface: + * - DES (abstract base) + * - DESCryptoServiceProvider (derived from DES) + * + * Unlike Aes, DES has no CNG-backed subclass and no AEAD variant, so there are + * fewer constructor scenarios in Section 1, and no AEAD sections. + * + * Architecture note: all methods inherited from SymmetricAlgorithm (EncryptCbc, + * CreateEncryptor, etc.) are covered once here. The detection engine tracks the + * variable and fires the same depending rules for every concrete DES subclass. + */ + +using System.Security.Cryptography; + +public class DotNetDESComprehensiveTest +{ + // ------------------------------------------------------------------------- + // Section 1: Factory methods / constructors + // ------------------------------------------------------------------------- + + public void TestDesCreate() + { + var des = DES.Create(); + } + + public void TestDesCreateNamed() + { + var des = DES.Create("DES"); + } + + public void TestDesCsp() + { + var des = new DESCryptoServiceProvider(); + } + + // ------------------------------------------------------------------------- + // Section 2: Property setters (via assignment → synthetic set_X invocations) + // ------------------------------------------------------------------------- + + public void TestPropertyModeCBC() + { + var des = DES.Create(); + des.Mode = CipherMode.CBC; + } + + public void TestPropertyModeECB() + { + var des = DES.Create(); + des.Mode = CipherMode.ECB; + } + + public void TestPropertyModeCFB() + { + var des = DES.Create(); + des.Mode = CipherMode.CFB; + } + + public void TestPropertyModeOFB() + { + var des = DES.Create(); + des.Mode = CipherMode.OFB; + } + + public void TestPropertyModeCTS() + { + var des = DES.Create(); + des.Mode = CipherMode.CTS; + } + + public void TestPropertyKeySize() + { + var des = DES.Create(); + des.KeySize = 64; + } + + public void TestPropertyPaddingPKCS7() + { + var des = DES.Create(); + des.Padding = PaddingMode.PKCS7; + } + + public void TestPropertyPaddingNone() + { + var des = DES.Create(); + des.Padding = PaddingMode.None; + } + + public void TestPropertyPaddingZeros() + { + var des = DES.Create(); + des.Padding = PaddingMode.Zeros; + } + + public void TestPropertyPaddingANSIX923() + { + var des = DES.Create(); + des.Padding = PaddingMode.ANSIX923; + } + + public void TestPropertyFeedbackSize() + { + var des = DES.Create(); + des.FeedbackSize = 8; + } + + public void TestPropertyIV() + { + var des = DES.Create(); + des.IV = new byte[8]; + } + + public void TestPropertyKey() + { + var des = DES.Create(); + des.Key = new byte[8]; + } + + // ------------------------------------------------------------------------- + // Section 3: CreateEncryptor / CreateDecryptor + // ------------------------------------------------------------------------- + + public void TestCreateEncryptorNoArgs() + { + var des = DES.Create(); + var encryptor = des.CreateEncryptor(); + } + + public void TestCreateEncryptorWithArgs() + { + var des = DES.Create(); + byte[] key = new byte[8]; + byte[] iv = new byte[8]; + var encryptor = des.CreateEncryptor(key, iv); + } + + public void TestCreateDecryptorNoArgs() + { + var des = DES.Create(); + var decryptor = des.CreateDecryptor(); + } + + public void TestCreateDecryptorWithArgs() + { + var des = DES.Create(); + byte[] key = new byte[8]; + byte[] iv = new byte[8]; + var decryptor = des.CreateDecryptor(key, iv); + } + + // ------------------------------------------------------------------------- + // Section 4: Direct mode-specific encrypt methods + // ------------------------------------------------------------------------- + + public void TestEncryptCbc() + { + var des = DES.Create(); + byte[] plaintext = new byte[16]; + byte[] iv = new byte[8]; + byte[] ciphertext = des.EncryptCbc(plaintext, iv, PaddingMode.PKCS7); + } + + public void TestEncryptEcb() + { + var des = DES.Create(); + byte[] plaintext = new byte[16]; + byte[] ciphertext = des.EncryptEcb(plaintext, PaddingMode.None); + } + + public void TestEncryptCfb() + { + var des = DES.Create(); + byte[] plaintext = new byte[16]; + byte[] iv = new byte[8]; + byte[] ciphertext = des.EncryptCfb(plaintext, iv, PaddingMode.None, 8); + } + + // ------------------------------------------------------------------------- + // Section 5: Direct mode-specific decrypt methods + // ------------------------------------------------------------------------- + + public void TestDecryptCbc() + { + var des = DES.Create(); + byte[] ciphertext = new byte[16]; + byte[] iv = new byte[8]; + byte[] plaintext = des.DecryptCbc(ciphertext, iv, PaddingMode.PKCS7); + } + + public void TestDecryptEcb() + { + var des = DES.Create(); + byte[] ciphertext = new byte[16]; + byte[] plaintext = des.DecryptEcb(ciphertext, PaddingMode.None); + } + + public void TestDecryptCfb() + { + var des = DES.Create(); + byte[] ciphertext = new byte[16]; + byte[] iv = new byte[8]; + byte[] plaintext = des.DecryptCfb(ciphertext, iv, PaddingMode.None, 8); + } + + // ------------------------------------------------------------------------- + // Section 6: Try* variants + // ------------------------------------------------------------------------- + + public void TestTryEncryptCbc() + { + var des = DES.Create(); + byte[] plaintext = new byte[16]; + byte[] iv = new byte[8]; + byte[] destination = new byte[24]; + int bytesWritten; + des.TryEncryptCbc(plaintext, iv, destination, out bytesWritten, PaddingMode.PKCS7); + } + + public void TestTryDecryptCbc() + { + var des = DES.Create(); + byte[] ciphertext = new byte[16]; + byte[] iv = new byte[8]; + byte[] destination = new byte[16]; + int bytesWritten; + des.TryDecryptCbc(ciphertext, iv, destination, out bytesWritten, PaddingMode.PKCS7); + } + + public void TestTryEncryptEcb() + { + var des = DES.Create(); + byte[] plaintext = new byte[16]; + byte[] destination = new byte[24]; + int bytesWritten; + des.TryEncryptEcb(plaintext, destination, PaddingMode.None, out bytesWritten); + } + + public void TestTryDecryptEcb() + { + var des = DES.Create(); + byte[] ciphertext = new byte[16]; + byte[] destination = new byte[16]; + int bytesWritten; + des.TryDecryptEcb(ciphertext, destination, PaddingMode.None, out bytesWritten); + } + + public void TestTryEncryptCfb() + { + var des = DES.Create(); + byte[] plaintext = new byte[16]; + byte[] iv = new byte[8]; + byte[] destination = new byte[24]; + int bytesWritten; + des.TryEncryptCfb(plaintext, iv, destination, out bytesWritten, PaddingMode.None, 8); + } + + public void TestTryDecryptCfb() + { + var des = DES.Create(); + byte[] ciphertext = new byte[16]; + byte[] iv = new byte[8]; + byte[] destination = new byte[16]; + int bytesWritten; + des.TryDecryptCfb(ciphertext, iv, destination, out bytesWritten, PaddingMode.None, 8); + } + + // ------------------------------------------------------------------------- + // Section 7: Key/IV generation + // ------------------------------------------------------------------------- + + public void TestGenerateKey() + { + var des = DES.Create(); + des.GenerateKey(); + } + + public void TestGenerateIV() + { + var des = DES.Create(); + des.GenerateIV(); + } + + // ------------------------------------------------------------------------- + // Section 8: Combined usage patterns (real-world scenarios) + // Demonstrates that depending rules fire correctly for both derived classes. + // ------------------------------------------------------------------------- + + public void TestDesCbcFullFlow() + { + var des = DES.Create(); + des.Mode = CipherMode.CBC; + des.Padding = PaddingMode.PKCS7; + var encryptor = des.CreateEncryptor(); + } + + public void TestDesCspEncryptCbc() + { + var des = new DESCryptoServiceProvider(); + des.Mode = CipherMode.CBC; + byte[] plaintext = new byte[16]; + byte[] iv = new byte[8]; + byte[] ciphertext = des.EncryptCbc(plaintext, iv, PaddingMode.PKCS7); + } + + public void TestDesCspDecryptCbc() + { + var des = new DESCryptoServiceProvider(); + byte[] ciphertext = new byte[16]; + byte[] iv = new byte[8]; + byte[] plaintext = des.DecryptCbc(ciphertext, iv, PaddingMode.PKCS7); + } + + public void TestDesCfbFeedback() + { + var des = DES.Create(); + des.Mode = CipherMode.CFB; + des.FeedbackSize = 8; + byte[] plaintext = new byte[16]; + byte[] iv = new byte[8]; + byte[] ciphertext = des.EncryptCfb(plaintext, iv, PaddingMode.None, 8); + } + + public void TestDesCbcWithEncryptorOverload() + { + var des = DES.Create(); + byte[] key = new byte[8]; + byte[] iv = new byte[8]; + var encryptor = des.CreateEncryptor(key, iv); + } + + public void TestDesEcbEncrypt() + { + var des = new DESCryptoServiceProvider(); + byte[] plaintext = new byte[16]; + byte[] ciphertext = des.EncryptEcb(plaintext, PaddingMode.None); + } +} diff --git a/csharp/src/test/files/rules/detection/dotnet/DotNetDSAComprehensiveTestFile.cs b/csharp/src/test/files/rules/detection/dotnet/DotNetDSAComprehensiveTestFile.cs new file mode 100644 index 000000000..8a870a576 --- /dev/null +++ b/csharp/src/test/files/rules/detection/dotnet/DotNetDSAComprehensiveTestFile.cs @@ -0,0 +1,200 @@ +/* + * Comprehensive test file for System.Security.Cryptography DSA detection rules. + * + * Covers all four DSA-related classes and their complete operational API surface: + * - DSA (abstract base) + * - DSACng, DSACryptoServiceProvider, DSAOpenSsl (derived from DSA) + * + * Architecture note: all methods inherited from DSA / AsymmetricAlgorithm (KeySize, + * CreateSignature, VerifySignature, SignData, VerifyData, Try* variants, etc.) are + * covered once here. The detection engine tracks the variable and fires the same + * depending rules for every concrete DSA subclass. + */ + +using System.Security.Cryptography; + +public class DotNetDSAComprehensiveTest +{ + // ------------------------------------------------------------------------- + // Section 1: Factory methods / constructors + // ------------------------------------------------------------------------- + + public void TestDsaCreate() + { + var dsa = DSA.Create(); + } + + public void TestDsaCreateWithKeySize() + { + var dsa = DSA.Create(2048); + } + + public void TestDsaCng() + { + var dsa = new DSACng(); + } + + public void TestDsaCngWithKeySize() + { + var dsa = new DSACng(2048); + } + + public void TestDsaCsp() + { + var dsa = new DSACryptoServiceProvider(); + } + + public void TestDsaOpenSsl() + { + var dsa = new DSAOpenSsl(); + } + + // ------------------------------------------------------------------------- + // Section 2: Property setters (via assignment → synthetic set_X invocations) + // ------------------------------------------------------------------------- + + public void TestPropertyKeySize1024() + { + var dsa = DSA.Create(); + dsa.KeySize = 1024; + } + + public void TestPropertyKeySize2048() + { + var dsa = DSA.Create(); + dsa.KeySize = 2048; + } + + public void TestPropertyKeySize3072() + { + var dsa = DSA.Create(); + dsa.KeySize = 3072; + } + + // ------------------------------------------------------------------------- + // Section 3: CreateSignature / TryCreateSignature + // ------------------------------------------------------------------------- + + public void TestCreateSignature() + { + var dsa = DSA.Create(); + byte[] hash = new byte[20]; + byte[] signature = dsa.CreateSignature(hash); + } + + public void TestCreateSignatureWithFormat() + { + var dsa = DSA.Create(); + byte[] hash = new byte[20]; + byte[] signature = dsa.CreateSignature(hash, DSASignatureFormat.IeeeP1363FixedFieldConcatenation); + } + + public void TestTryCreateSignature() + { + var dsa = DSA.Create(); + byte[] hash = new byte[20]; + byte[] destination = new byte[64]; + int bytesWritten; + dsa.TryCreateSignature(hash, destination, out bytesWritten); + } + + // ------------------------------------------------------------------------- + // Section 4: VerifySignature + // ------------------------------------------------------------------------- + + public void TestVerifySignature() + { + var dsa = DSA.Create(); + byte[] hash = new byte[20]; + byte[] signature = new byte[64]; + bool valid = dsa.VerifySignature(hash, signature); + } + + public void TestVerifySignatureWithFormat() + { + var dsa = DSA.Create(); + byte[] hash = new byte[20]; + byte[] signature = new byte[64]; + bool valid = dsa.VerifySignature(hash, signature, DSASignatureFormat.IeeeP1363FixedFieldConcatenation); + } + + // ------------------------------------------------------------------------- + // Section 5: SignData / TrySignData + // ------------------------------------------------------------------------- + + public void TestSignData() + { + var dsa = DSA.Create(); + byte[] data = new byte[64]; + byte[] signature = dsa.SignData(data, HashAlgorithmName.SHA256); + } + + public void TestTrySignData() + { + var dsa = DSA.Create(); + byte[] data = new byte[64]; + byte[] destination = new byte[64]; + int bytesWritten; + dsa.TrySignData(data, destination, HashAlgorithmName.SHA256, out bytesWritten); + } + + // ------------------------------------------------------------------------- + // Section 6: VerifyData + // ------------------------------------------------------------------------- + + public void TestVerifyData() + { + var dsa = DSA.Create(); + byte[] data = new byte[64]; + byte[] signature = new byte[64]; + bool valid = dsa.VerifyData(data, signature, HashAlgorithmName.SHA256); + } + + // ------------------------------------------------------------------------- + // Section 7: SignHash / VerifyHash (legacy CSP-era methods; only real on + // DSACryptoServiceProvider, not on the abstract DSA base class) + // ------------------------------------------------------------------------- + + public void TestSignHash() + { + var dsa = new DSACryptoServiceProvider(); + byte[] hash = new byte[20]; + byte[] signature = dsa.SignHash(hash, "SHA1"); + } + + public void TestVerifyHash() + { + var dsa = new DSACryptoServiceProvider(); + byte[] hash = new byte[20]; + byte[] signature = new byte[40]; + bool valid = dsa.VerifyHash(hash, "SHA1", signature); + } + + // ------------------------------------------------------------------------- + // Section 8: Combined usage patterns (real-world scenarios) + // Demonstrates that depending rules fire correctly for ALL derived classes. + // ------------------------------------------------------------------------- + + public void TestDsaCngFullFlow() + { + var dsa = new DSACng(); + dsa.KeySize = 2048; + byte[] data = new byte[64]; + byte[] signature = dsa.SignData(data, HashAlgorithmName.SHA256); + } + + public void TestDsaCspVerifyFlow() + { + var dsa = new DSACryptoServiceProvider(); + byte[] data = new byte[64]; + byte[] signature = new byte[64]; + bool valid = dsa.VerifyData(data, signature, HashAlgorithmName.SHA1); + } + + public void TestDsaOpenSslSignFlow() + { + var dsa = new DSAOpenSsl(); + byte[] hash = new byte[20]; + byte[] signature = dsa.CreateSignature(hash); + } +} diff --git a/csharp/src/test/files/rules/detection/dotnet/DotNetECDiffieHellmanTestFile.cs b/csharp/src/test/files/rules/detection/dotnet/DotNetECDiffieHellmanTestFile.cs index 2c918535e..700cfca9c 100755 --- a/csharp/src/test/files/rules/detection/dotnet/DotNetECDiffieHellmanTestFile.cs +++ b/csharp/src/test/files/rules/detection/dotnet/DotNetECDiffieHellmanTestFile.cs @@ -1,4 +1,123 @@ +/* + * Comprehensive test file for System.Security.Cryptography ECDiffieHellman (ECDH) + * detection rules. + * + * Covers all three ECDiffieHellman-related classes and their complete operational API surface: + * - ECDiffieHellman (abstract base) + * - ECDiffieHellmanCng, ECDiffieHellmanOpenSsl (derived from ECDiffieHellman) + * + * Architecture note: all methods inherited from ECDiffieHellman / AsymmetricAlgorithm + * (KeySize, DeriveKeyMaterial, DeriveKeyFromHash, DeriveKeyFromHmac, DeriveKeyTls, + * DeriveRawSecretAgreement) are covered once here. The detection engine tracks the variable + * and fires the same depending rules for every concrete ECDiffieHellman subclass. + * + * Known gap: the PublicKey property cannot be detected at all (see DotNetECDiffieHellman.java + * class javadoc for the full, verified explanation) — no test attempts it. + */ + using System.Security.Cryptography; -public class DotNetECDiffieHellmanTest { - public void TestECDHCreate() { var ecdh = ECDiffieHellman.Create(); } // Noncompliant + +public class DotNetECDiffieHellmanTest +{ + // ------------------------------------------------------------------------- + // Section 1: Factory methods / constructors + // ------------------------------------------------------------------------- + + public void TestECDHCreate() + { + var ecdh = ECDiffieHellman.Create(); + } + + public void TestECDHCreateWithCurve() + { + var ecdh = ECDiffieHellman.Create(ECCurve.NamedCurves.nistP256); + } + + public void TestECDHCng() + { + var ecdh = new ECDiffieHellmanCng(); + } + + public void TestECDHOpenSsl() + { + var ecdh = new ECDiffieHellmanOpenSsl(); + } + + // ------------------------------------------------------------------------- + // Section 2: Property setters (via assignment → synthetic set_X invocations) + // ------------------------------------------------------------------------- + + public void TestPropertyKeySize256() + { + var ecdh = ECDiffieHellman.Create(); + ecdh.KeySize = 256; + } + + public void TestPropertyKeySize384() + { + var ecdh = ECDiffieHellman.Create(); + ecdh.KeySize = 384; + } + + // ------------------------------------------------------------------------- + // Section 3: Key-derivation operations + // ------------------------------------------------------------------------- + + public void TestDeriveKeyMaterial() + { + var ecdh = ECDiffieHellman.Create(); + ECDiffieHellmanPublicKey otherPartyKey = null; + byte[] keyMaterial = ecdh.DeriveKeyMaterial(otherPartyKey); + } + + public void TestDeriveKeyFromHash() + { + var ecdh = ECDiffieHellman.Create(); + ECDiffieHellmanPublicKey otherPartyKey = null; + byte[] keyMaterial = ecdh.DeriveKeyFromHash(otherPartyKey, HashAlgorithmName.SHA256); + } + + public void TestDeriveKeyFromHmac() + { + var ecdh = ECDiffieHellman.Create(); + ECDiffieHellmanPublicKey otherPartyKey = null; + byte[] hmacKey = new byte[32]; + byte[] keyMaterial = ecdh.DeriveKeyFromHmac(otherPartyKey, HashAlgorithmName.SHA256, hmacKey); + } + + public void TestDeriveKeyTls() + { + var ecdh = ECDiffieHellman.Create(); + ECDiffieHellmanPublicKey otherPartyKey = null; + byte[] prfLabel = new byte[16]; + byte[] prfSeed = new byte[32]; + byte[] keyMaterial = ecdh.DeriveKeyTls(otherPartyKey, prfLabel, prfSeed); + } + + public void TestDeriveRawSecretAgreement() + { + var ecdh = ECDiffieHellman.Create(); + ECDiffieHellmanPublicKey otherPartyKey = null; + byte[] secret = ecdh.DeriveRawSecretAgreement(otherPartyKey); + } + + // ------------------------------------------------------------------------- + // Section 4: Combined usage patterns (real-world scenarios) + // Demonstrates that depending rules fire correctly for ALL derived classes. + // ------------------------------------------------------------------------- + + public void TestECDHCngFullFlow() + { + var ecdh = new ECDiffieHellmanCng(); + ecdh.KeySize = 384; + ECDiffieHellmanPublicKey otherPartyKey = null; + byte[] keyMaterial = ecdh.DeriveKeyMaterial(otherPartyKey); + } + + public void TestECDHOpenSslDeriveFlow() + { + var ecdh = new ECDiffieHellmanOpenSsl(); + ECDiffieHellmanPublicKey otherPartyKey = null; + byte[] keyMaterial = ecdh.DeriveKeyFromHash(otherPartyKey, HashAlgorithmName.SHA256); + } } diff --git a/csharp/src/test/files/rules/detection/dotnet/DotNetECDsaTestFile.cs b/csharp/src/test/files/rules/detection/dotnet/DotNetECDsaTestFile.cs index addadc0c4..44cdf2734 100755 --- a/csharp/src/test/files/rules/detection/dotnet/DotNetECDsaTestFile.cs +++ b/csharp/src/test/files/rules/detection/dotnet/DotNetECDsaTestFile.cs @@ -1,4 +1,153 @@ +/* + * Comprehensive test file for System.Security.Cryptography ECDsa detection rules. + * + * Covers all three ECDsa-related classes and their complete operational API surface: + * - ECDsa (abstract base) + * - ECDsaCng, ECDsaOpenSsl (derived from ECDsa) + * + * Architecture note: all methods inherited from ECDsa / AsymmetricAlgorithm (KeySize, + * SignData, VerifyData, SignHash, VerifyHash, Try* variants, etc.) are covered once here. + * The detection engine tracks the variable and fires the same depending rules for every + * concrete ECDsa subclass. Unlike RSA, ECDSA has no Encrypt/Decrypt operations. + * + * The ECDsaCng constructor rule uses withAnyParameters() and therefore matches all four + * constructor overloads: ECDsaCng(), ECDsaCng(CngKey), ECDsaCng(ECCurve), ECDsaCng(int). + */ + using System.Security.Cryptography; -public class DotNetECDsaTest { - public void TestECDsaCreate() { var ecdsa = ECDsa.Create(); } // Noncompliant + +public class DotNetECDsaTest +{ + // ------------------------------------------------------------------------- + // Section 1: Factory methods / constructors + // ------------------------------------------------------------------------- + + public void TestECDsaCreate() + { + var ecdsa = ECDsa.Create(); // Noncompliant + } + + public void TestECDsaCreateWithCurve() + { + var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256); + } + + public void TestECDsaCng() + { + var ecdsa = new ECDsaCng(); + } + + public void TestECDsaOpenSsl() + { + var ecdsa = new ECDsaOpenSsl(); + } + + public void TestECDsaCngWithKey() + { + CngKey cngKey = null; + var ecdsa = new ECDsaCng(cngKey); + } + + public void TestECDsaCngWithCurve() + { + var ecdsa = new ECDsaCng(ECCurve.NamedCurves.nistP521); + } + + public void TestECDsaCngWithKeySize() + { + var ecdsa = new ECDsaCng(521); + } + + // ------------------------------------------------------------------------- + // Section 2: Property setters (via assignment → synthetic set_X invocations) + // ------------------------------------------------------------------------- + + public void TestPropertyKeySize256() + { + var ecdsa = ECDsa.Create(); + ecdsa.KeySize = 256; + } + + public void TestPropertyKeySize384() + { + var ecdsa = ECDsa.Create(); + ecdsa.KeySize = 384; + } + + // ------------------------------------------------------------------------- + // Section 3: SignData / TrySignData / VerifyData + // ------------------------------------------------------------------------- + + public void TestSignData() + { + var ecdsa = ECDsa.Create(); + byte[] data = new byte[64]; + byte[] signature = ecdsa.SignData(data, HashAlgorithmName.SHA256); + } + + public void TestTrySignData() + { + var ecdsa = ECDsa.Create(); + byte[] data = new byte[64]; + byte[] destination = new byte[256]; + int bytesWritten; + ecdsa.TrySignData(data, destination, HashAlgorithmName.SHA256, out bytesWritten); + } + + public void TestVerifyData() + { + var ecdsa = ECDsa.Create(); + byte[] data = new byte[64]; + byte[] signature = new byte[256]; + bool valid = ecdsa.VerifyData(data, signature, HashAlgorithmName.SHA256); + } + + // ------------------------------------------------------------------------- + // Section 4: SignHash / TrySignHash / VerifyHash + // ------------------------------------------------------------------------- + + public void TestSignHash() + { + var ecdsa = ECDsa.Create(); + byte[] hash = new byte[32]; + byte[] signature = ecdsa.SignHash(hash); + } + + public void TestTrySignHash() + { + var ecdsa = ECDsa.Create(); + byte[] hash = new byte[32]; + byte[] destination = new byte[256]; + int bytesWritten; + ecdsa.TrySignHash(hash, destination, out bytesWritten); + } + + public void TestVerifyHash() + { + var ecdsa = ECDsa.Create(); + byte[] hash = new byte[32]; + byte[] signature = new byte[256]; + bool valid = ecdsa.VerifyHash(hash, signature); + } + + // ------------------------------------------------------------------------- + // Section 5: Combined usage patterns (real-world scenarios) + // Demonstrates that depending rules fire correctly for ALL derived classes. + // ------------------------------------------------------------------------- + + public void TestECDsaCngFullFlow() + { + var ecdsa = new ECDsaCng(); + ecdsa.KeySize = 384; + byte[] data = new byte[64]; + byte[] signature = ecdsa.SignData(data, HashAlgorithmName.SHA384); + } + + public void TestECDsaOpenSslVerifyFlow() + { + var ecdsa = new ECDsaOpenSsl(); + byte[] data = new byte[64]; + byte[] signature = new byte[256]; + bool valid = ecdsa.VerifyData(data, signature, HashAlgorithmName.SHA256); + } } diff --git a/csharp/src/test/files/rules/detection/dotnet/DotNetHMACTestFile.cs b/csharp/src/test/files/rules/detection/dotnet/DotNetHMACTestFile.cs index b652359c4..89d54ca7f 100755 --- a/csharp/src/test/files/rules/detection/dotnet/DotNetHMACTestFile.cs +++ b/csharp/src/test/files/rules/detection/dotnet/DotNetHMACTestFile.cs @@ -5,4 +5,9 @@ public class DotNetHMACTest { public void TestHmacSha384() { var h = new HMACSHA384(); } // Noncompliant public void TestHmacSha512() { var h = new HMACSHA512(); } // Noncompliant public void TestHmacMd5() { var h = new HMACMD5(); } // Noncompliant + public void TestHmacRipemd160() { var h = new HMACRIPEMD160(); } // Noncompliant + public void TestHmacSha3_256() { var h = new HMACSHA3_256(); } // Noncompliant + public void TestHmacSha3_384() { var h = new HMACSHA3_384(); } // Noncompliant + public void TestHmacSha3_512() { var h = new HMACSHA3_512(); } // Noncompliant + public void TestMacTripleDes() { var h = new MACTripleDES(); } // Noncompliant } diff --git a/csharp/src/test/files/rules/detection/dotnet/DotNetKMACTestFile.cs b/csharp/src/test/files/rules/detection/dotnet/DotNetKMACTestFile.cs new file mode 100644 index 000000000..8442d3180 --- /dev/null +++ b/csharp/src/test/files/rules/detection/dotnet/DotNetKMACTestFile.cs @@ -0,0 +1,7 @@ +using System.Security.Cryptography; +public class DotNetKMACTest { + public void TestKmac128() { byte[] key = new byte[16]; var m = new Kmac128(key); } // Noncompliant + public void TestKmac256() { byte[] key = new byte[32]; var m = new Kmac256(key); } // Noncompliant + public void TestKmacXof128() { byte[] key = new byte[16]; var m = new KmacXof128(key); } // Noncompliant + public void TestKmacXof256() { byte[] key = new byte[32]; var m = new KmacXof256(key); } // Noncompliant +} diff --git a/csharp/src/test/files/rules/detection/dotnet/DotNetKeyDerivationTestFile.cs b/csharp/src/test/files/rules/detection/dotnet/DotNetKeyDerivationTestFile.cs new file mode 100644 index 000000000..200c555d9 --- /dev/null +++ b/csharp/src/test/files/rules/detection/dotnet/DotNetKeyDerivationTestFile.cs @@ -0,0 +1,91 @@ +/* + * Comprehensive test file for System.Security.Cryptography KDF-family detection rules + * (DotNetKeyDerivation.java), excluding Rfc2898DeriveBytes (covered separately in + * DotNetRfc2898DeriveBytesTestFile.cs). + * + * Covers: + * - HKDF (static-only class: Extract, Expand, DeriveKey) + * - SP800108HmacCounterKdf (constructor + instance DeriveKey, and the static DeriveBytes + * one-shot overload) + * - PasswordDeriveBytes (constructor + instance GetBytes / CryptDeriveKey) + */ + +using System.Security.Cryptography; + +public class DotNetKeyDerivationTest +{ + // ------------------------------------------------------------------------- + // Section 1: HKDF (static-only, no instance) + // ------------------------------------------------------------------------- + + public void TestHkdfExtract() + { + byte[] ikm = new byte[32]; + byte[] salt = new byte[16]; + byte[] prk = HKDF.Extract(HashAlgorithmName.SHA256, ikm, salt); + } + + public void TestHkdfExpand() + { + byte[] prk = new byte[32]; + byte[] info = new byte[8]; + byte[] okm = HKDF.Expand(HashAlgorithmName.SHA256, prk, 32, info); + } + + public void TestHkdfDeriveKey() + { + byte[] ikm = new byte[32]; + byte[] salt = new byte[16]; + byte[] info = new byte[8]; + byte[] key = HKDF.DeriveKey(HashAlgorithmName.SHA256, ikm, 32, salt, info); + } + + // ------------------------------------------------------------------------- + // Section 2: SP800108HmacCounterKdf + // ------------------------------------------------------------------------- + + public void TestSp800108CtorAndDeriveKey() + { + byte[] key = new byte[32]; + var kdf = new SP800108HmacCounterKdf(key, HashAlgorithmName.SHA256); + byte[] label = new byte[8]; + byte[] context = new byte[8]; + byte[] derived = kdf.DeriveKey(label, context, 32); + } + + public void TestSp800108StaticDeriveBytes() + { + byte[] key = new byte[32]; + byte[] label = new byte[8]; + byte[] context = new byte[8]; + byte[] derived = SP800108HmacCounterKdf.DeriveBytes( + key, HashAlgorithmName.SHA256, label, context, 32); + } + + // ------------------------------------------------------------------------- + // Section 3: PasswordDeriveBytes + // ------------------------------------------------------------------------- + + public void TestPasswordDeriveBytesGetBytes() + { + byte[] salt = new byte[16]; + var pdb = new PasswordDeriveBytes("password", salt); + byte[] derived = pdb.GetBytes(16); + } + + public void TestPasswordDeriveBytesCryptDeriveKey() + { + byte[] salt = new byte[16]; + var pdb = new PasswordDeriveBytes("password", salt, "SHA1", 100); + byte[] iv = new byte[8]; + byte[] key = pdb.CryptDeriveKey("TripleDES", "SHA1", 192, iv); + } + + public void TestPasswordDeriveBytesProperties() + { + byte[] salt = new byte[16]; + var pdb = new PasswordDeriveBytes("password", salt); + pdb.IterationCount = 100000; + pdb.HashName = "SHA256"; + } +} diff --git a/csharp/src/test/files/rules/detection/dotnet/DotNetLegacyFormattersTestFile.cs b/csharp/src/test/files/rules/detection/dotnet/DotNetLegacyFormattersTestFile.cs new file mode 100644 index 000000000..774c2978d --- /dev/null +++ b/csharp/src/test/files/rules/detection/dotnet/DotNetLegacyFormattersTestFile.cs @@ -0,0 +1,138 @@ +/* + * Test file for the legacy Formatter/Deformatter and mask-generation detection rules + * (DotNetLegacyFormatters.java). + * + * Covers all nine classes: + * - DSASignatureFormatter / DSASignatureDeformatter + * - RSAPKCS1SignatureFormatter / RSAPKCS1SignatureDeformatter + * - RSAOAEPKeyExchangeFormatter / RSAOAEPKeyExchangeDeformatter + * - RSAPKCS1KeyExchangeFormatter / RSAPKCS1KeyExchangeDeformatter + * - PKCS1MaskGenerationMethod + * + * Note: the parameterless constructor overload is used throughout (rather than passing a + * DSA/RSA instance from DSA.Create()/RSA.Create()) so that each test method exercises exactly + * this file's rules in isolation, without also triggering DotNetDSA.java's/DotNetRSA.java's own + * DSA.Create()/RSA.Create() detection rules. This also directly demonstrates the documented + * SetKey gap: the Formatter/Deformatter is detected purely from its own class name, independent + * of whether (or how) a real key was ever wired up via SetKey. + */ + +using System.Security.Cryptography; + +public class DotNetLegacyFormattersTest +{ + // ------------------------------------------------------------------------- + // DSASignatureFormatter / DSASignatureDeformatter + // ------------------------------------------------------------------------- + + public void TestDsaSignatureFormatterCreateSignature() + { + var formatter = new DSASignatureFormatter(); // Noncompliant + byte[] hash = new byte[20]; + byte[] signature = formatter.CreateSignature(hash); + } + + public void TestDsaSignatureFormatterSetHashAlgorithm() + { + var formatter = new DSASignatureFormatter(); + formatter.SetHashAlgorithm("SHA1"); + } + + public void TestDsaSignatureDeformatterVerifySignature() + { + var deformatter = new DSASignatureDeformatter(); + byte[] hash = new byte[20]; + byte[] signature = new byte[40]; + bool valid = deformatter.VerifySignature(hash, signature); + } + + // ------------------------------------------------------------------------- + // RSAPKCS1SignatureFormatter / RSAPKCS1SignatureDeformatter + // ------------------------------------------------------------------------- + + public void TestRsaPkcs1SignatureFormatterCreateSignature() + { + var formatter = new RSAPKCS1SignatureFormatter(); + byte[] hash = new byte[32]; + byte[] signature = formatter.CreateSignature(hash); + } + + public void TestRsaPkcs1SignatureFormatterSetHashAlgorithm() + { + var formatter = new RSAPKCS1SignatureFormatter(); + formatter.SetHashAlgorithm("SHA256"); + } + + public void TestRsaPkcs1SignatureDeformatterVerifySignature() + { + var deformatter = new RSAPKCS1SignatureDeformatter(); + byte[] hash = new byte[32]; + byte[] signature = new byte[256]; + bool valid = deformatter.VerifySignature(hash, signature); + } + + // ------------------------------------------------------------------------- + // RSAOAEPKeyExchangeFormatter / RSAOAEPKeyExchangeDeformatter + // ------------------------------------------------------------------------- + + public void TestRsaOaepKeyExchangeFormatterCreateKeyExchange1() + { + var formatter = new RSAOAEPKeyExchangeFormatter(); + byte[] key = new byte[32]; + byte[] encryptedKey = formatter.CreateKeyExchange(key); + } + + public void TestRsaOaepKeyExchangeFormatterCreateKeyExchange2() + { + var formatter = new RSAOAEPKeyExchangeFormatter(); + byte[] key = new byte[32]; + byte[] encryptedKey = formatter.CreateKeyExchange(key, typeof(Aes)); + } + + public void TestRsaOaepKeyExchangeDeformatterDecryptKeyExchange() + { + var deformatter = new RSAOAEPKeyExchangeDeformatter(); + byte[] encryptedKey = new byte[256]; + byte[] key = deformatter.DecryptKeyExchange(encryptedKey); + } + + // ------------------------------------------------------------------------- + // RSAPKCS1KeyExchangeFormatter / RSAPKCS1KeyExchangeDeformatter + // ------------------------------------------------------------------------- + + public void TestRsaPkcs1KeyExchangeFormatterCreateKeyExchange1() + { + var formatter = new RSAPKCS1KeyExchangeFormatter(); + byte[] key = new byte[32]; + byte[] encryptedKey = formatter.CreateKeyExchange(key); + } + + public void TestRsaPkcs1KeyExchangeFormatterCreateKeyExchange2() + { + var formatter = new RSAPKCS1KeyExchangeFormatter(); + byte[] key = new byte[32]; + byte[] encryptedKey = formatter.CreateKeyExchange(key, typeof(Aes)); + } + + public void TestRsaPkcs1KeyExchangeDeformatterDecryptKeyExchange() + { + var deformatter = new RSAPKCS1KeyExchangeDeformatter(); + byte[] encryptedKey = new byte[256]; + byte[] key = deformatter.DecryptKeyExchange(encryptedKey); + } + + // ------------------------------------------------------------------------- + // PKCS1MaskGenerationMethod + // ------------------------------------------------------------------------- + + public void TestPkcs1MaskGenerationMethodCreate() + { + var mgf = new PKCS1MaskGenerationMethod(); + } + + public void TestPkcs1MaskGenerationMethodSetHashName() + { + var mgf = new PKCS1MaskGenerationMethod(); + mgf.HashName = "SHA256"; + } +} diff --git a/csharp/src/test/files/rules/detection/dotnet/DotNetMLDsaTestFile.cs b/csharp/src/test/files/rules/detection/dotnet/DotNetMLDsaTestFile.cs new file mode 100644 index 000000000..3a7145948 --- /dev/null +++ b/csharp/src/test/files/rules/detection/dotnet/DotNetMLDsaTestFile.cs @@ -0,0 +1,266 @@ +/* + * Comprehensive test file for System.Security.Cryptography ML-DSA (MLDsa, MLDsaCng, + * MLDsaOpenSsl) and Composite ML-DSA (CompositeMLDsa, CompositeMLDsaCng) detection rules. + * + * ML-DSA (FIPS 204) is documented for the net-10.0/net-11.0 monikers, with no [Experimental] + * attribute on the plain MLDsa/MLDsaCng/MLDsaOpenSsl surface. Composite ML-DSA additionally + * carries [Experimental("SYSLIB5006")] at the class level (still a preview API). See + * DotNetMLDsa.java's class javadoc for the full, verified explanation of what is/isn't modeled + * here and why (in particular the Composite ML-DSA modeling compromise). + */ + +using System.Security.Cryptography; + +public class DotNetMLDsaTest +{ + // ------------------------------------------------------------------------- + // Section 1: MLDsa algorithm-parameterized creation (parameter set captured from the + // MLDsaAlgorithm argument) + // ------------------------------------------------------------------------- + + public void TestGenerateKey44() + { + var dsa = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa44); + } + + public void TestGenerateKey65() + { + var dsa = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65); + } + + public void TestGenerateKey87() + { + var dsa = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa87); + } + + public void TestImportMLDsaPrivateKey() + { + byte[] privateKeyBytes = new byte[64]; + var dsa = MLDsa.ImportMLDsaPrivateKey(MLDsaAlgorithm.MLDsa65, privateKeyBytes); + } + + public void TestImportMLDsaPrivateSeed() + { + byte[] seedBytes = new byte[32]; + var dsa = MLDsa.ImportMLDsaPrivateSeed(MLDsaAlgorithm.MLDsa44, seedBytes); + } + + public void TestImportMLDsaPublicKey() + { + byte[] publicKeyBytes = new byte[64]; + var dsa = MLDsa.ImportMLDsaPublicKey(MLDsaAlgorithm.MLDsa87, publicKeyBytes); + } + + // ------------------------------------------------------------------------- + // Section 2: MLDsa structural imports (no MLDsaAlgorithm argument — the parameter set is + // embedded in the encoded key material / PEM text and cannot be recovered) + // ------------------------------------------------------------------------- + + public void TestImportPkcs8PrivateKey() + { + byte[] pkcs8Bytes = new byte[64]; + var dsa = MLDsa.ImportPkcs8PrivateKey(pkcs8Bytes); + } + + public void TestImportSubjectPublicKeyInfo() + { + byte[] spkiBytes = new byte[64]; + var dsa = MLDsa.ImportSubjectPublicKeyInfo(spkiBytes); + } + + public void TestImportFromPem() + { + string pem = "-----BEGIN PRIVATE KEY-----"; + var dsa = MLDsa.ImportFromPem(pem); + } + + public void TestImportEncryptedPkcs8PrivateKey() + { + byte[] passwordBytes = new byte[16]; + byte[] encryptedBytes = new byte[64]; + var dsa = MLDsa.ImportEncryptedPkcs8PrivateKey(passwordBytes, encryptedBytes); + } + + public void TestImportFromEncryptedPem() + { + string pem = "-----BEGIN ENCRYPTED PRIVATE KEY-----"; + string password = "hunter2"; + var dsa = MLDsa.ImportFromEncryptedPem(pem, password); + } + + // ------------------------------------------------------------------------- + // Section 3: MLDsa native-interop constructors (no MLDsaAlgorithm argument — wrap an + // already-existing native key handle) + // ------------------------------------------------------------------------- + + public void TestMLDsaCng() + { + CngKey cngKey = null; + var dsa = new MLDsaCng(cngKey); + } + + public void TestMLDsaOpenSsl() + { + SafeEvpPKeyHandle handle = null; + var dsa = new MLDsaOpenSsl(handle); + } + + // ------------------------------------------------------------------------- + // Section 4: MLDsa Sign / Verify operations + // ------------------------------------------------------------------------- + + public void TestSignData() + { + var dsa = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65); + byte[] data = new byte[32]; + byte[] context = new byte[0]; + byte[] signature = dsa.SignData(data, context); + } + + public void TestVerifyData() + { + var dsa = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65); + byte[] data = new byte[32]; + byte[] signature = new byte[3309]; + byte[] context = new byte[0]; + bool ok = dsa.VerifyData(data, signature, context); + } + + public void TestSignMu() + { + var dsa = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65); + byte[] mu = new byte[64]; + byte[] signature = dsa.SignMu(mu); + } + + public void TestVerifyMu() + { + var dsa = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65); + byte[] mu = new byte[64]; + byte[] signature = new byte[3309]; + bool ok = dsa.VerifyMu(mu, signature); + } + + public void TestSignPreHash() + { + var dsa = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65); + byte[] hash = new byte[64]; + string hashOid = "2.16.840.1.101.3.4.2.3"; + byte[] context = new byte[0]; + byte[] signature = dsa.SignPreHash(hash, hashOid, context); + } + + public void TestVerifyPreHash() + { + var dsa = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa65); + byte[] hash = new byte[64]; + byte[] signature = new byte[3309]; + string hashOid = "2.16.840.1.101.3.4.2.3"; + byte[] context = new byte[0]; + bool ok = dsa.VerifyPreHash(hash, signature, hashOid, context); + } + + // ------------------------------------------------------------------------- + // Section 5: CompositeMLDsa algorithm-parameterized creation (parameter set captured + // verbatim from the CompositeMLDsaAlgorithm argument, e.g. "MLDsa44WithECDsaP256") + // ------------------------------------------------------------------------- + + public void TestCompositeGenerateKey() + { + var dsa = CompositeMLDsa.GenerateKey(CompositeMLDsaAlgorithm.MLDsa44WithECDsaP256); + } + + public void TestCompositeImportPrivateKey() + { + byte[] privateKeyBytes = new byte[64]; + var dsa = CompositeMLDsa.ImportCompositeMLDsaPrivateKey( + CompositeMLDsaAlgorithm.MLDsa65WithRSA3072Pss, privateKeyBytes); + } + + public void TestCompositeImportPublicKey() + { + byte[] publicKeyBytes = new byte[64]; + var dsa = CompositeMLDsa.ImportCompositeMLDsaPublicKey( + CompositeMLDsaAlgorithm.MLDsa87WithEd448, publicKeyBytes); + } + + // ------------------------------------------------------------------------- + // Section 6: CompositeMLDsa structural imports (no CompositeMLDsaAlgorithm argument) + // ------------------------------------------------------------------------- + + public void TestCompositeImportPkcs8PrivateKey() + { + byte[] pkcs8Bytes = new byte[64]; + var dsa = CompositeMLDsa.ImportPkcs8PrivateKey(pkcs8Bytes); + } + + public void TestCompositeImportSubjectPublicKeyInfo() + { + byte[] spkiBytes = new byte[64]; + var dsa = CompositeMLDsa.ImportSubjectPublicKeyInfo(spkiBytes); + } + + public void TestCompositeImportFromPem() + { + string pem = "-----BEGIN PRIVATE KEY-----"; + var dsa = CompositeMLDsa.ImportFromPem(pem); + } + + public void TestCompositeImportEncryptedPkcs8PrivateKey() + { + byte[] passwordBytes = new byte[16]; + byte[] encryptedBytes = new byte[64]; + var dsa = CompositeMLDsa.ImportEncryptedPkcs8PrivateKey(passwordBytes, encryptedBytes); + } + + public void TestCompositeImportFromEncryptedPem() + { + string pem = "-----BEGIN ENCRYPTED PRIVATE KEY-----"; + string password = "hunter2"; + var dsa = CompositeMLDsa.ImportFromEncryptedPem(pem, password); + } + + // ------------------------------------------------------------------------- + // Section 7: CompositeMLDsaCng native-interop constructor + // ------------------------------------------------------------------------- + + public void TestCompositeMLDsaCng() + { + CngKey cngKey = null; + var dsa = new CompositeMLDsaCng(cngKey); + } + + // ------------------------------------------------------------------------- + // Section 8: CompositeMLDsa Sign / Verify operations + // ------------------------------------------------------------------------- + + public void TestCompositeSignData() + { + var dsa = CompositeMLDsa.GenerateKey(CompositeMLDsaAlgorithm.MLDsa44WithECDsaP256); + byte[] data = new byte[32]; + byte[] context = new byte[0]; + byte[] signature = dsa.SignData(data, context); + } + + public void TestCompositeVerifyData() + { + var dsa = CompositeMLDsa.GenerateKey(CompositeMLDsaAlgorithm.MLDsa44WithECDsaP256); + byte[] data = new byte[32]; + byte[] signature = new byte[2400]; + byte[] context = new byte[0]; + bool ok = dsa.VerifyData(data, signature, context); + } + + // ------------------------------------------------------------------------- + // Section 9: Combined usage pattern (generation + sign + verify) + // ------------------------------------------------------------------------- + + public void TestFullFlow() + { + var dsa = MLDsa.GenerateKey(MLDsaAlgorithm.MLDsa87); + byte[] data = new byte[32]; + byte[] context = new byte[0]; + byte[] signature = dsa.SignData(data, context); + bool ok = dsa.VerifyData(data, signature, context); + } +} diff --git a/csharp/src/test/files/rules/detection/dotnet/DotNetMLKemTestFile.cs b/csharp/src/test/files/rules/detection/dotnet/DotNetMLKemTestFile.cs new file mode 100644 index 000000000..8c27143fc --- /dev/null +++ b/csharp/src/test/files/rules/detection/dotnet/DotNetMLKemTestFile.cs @@ -0,0 +1,138 @@ +/* + * Comprehensive test file for System.Security.Cryptography ML-KEM (MLKem, MLKemCng, + * MLKemOpenSsl) detection rules. + * + * ML-KEM (FIPS 203) is documented for the net-10.0/net-11.0 monikers — still an evolving + * preview API (see the [Experimental("SYSLIB5006")] attribute on some overloads in the + * official reference). See DotNetMLKem.java's class javadoc for the full, verified + * explanation of what is/isn't modeled here and why. + */ + +using System.Security.Cryptography; + +public class DotNetMLKemTest +{ + // ------------------------------------------------------------------------- + // Section 1: Algorithm-parameterized creation (parameter set captured from the + // MLKemAlgorithm argument) + // ------------------------------------------------------------------------- + + public void TestGenerateKey512() + { + var kem = MLKem.GenerateKey(MLKemAlgorithm.MLKem512); + } + + public void TestGenerateKey768() + { + var kem = MLKem.GenerateKey(MLKemAlgorithm.MLKem768); + } + + public void TestGenerateKey1024() + { + var kem = MLKem.GenerateKey(MLKemAlgorithm.MLKem1024); + } + + public void TestImportDecapsulationKey() + { + byte[] decapsulationKeyBytes = new byte[64]; + var kem = MLKem.ImportDecapsulationKey(MLKemAlgorithm.MLKem768, decapsulationKeyBytes); + } + + public void TestImportEncapsulationKey() + { + byte[] encapsulationKeyBytes = new byte[64]; + var kem = MLKem.ImportEncapsulationKey(MLKemAlgorithm.MLKem768, encapsulationKeyBytes); + } + + public void TestImportPrivateSeed() + { + byte[] seedBytes = new byte[64]; + var kem = MLKem.ImportPrivateSeed(MLKemAlgorithm.MLKem512, seedBytes); + } + + // ------------------------------------------------------------------------- + // Section 2: Structural imports (no MLKemAlgorithm argument — the parameter set is + // embedded in the encoded key material / PEM text and cannot be recovered) + // ------------------------------------------------------------------------- + + public void TestImportPkcs8PrivateKey() + { + byte[] pkcs8Bytes = new byte[64]; + var kem = MLKem.ImportPkcs8PrivateKey(pkcs8Bytes); + } + + public void TestImportSubjectPublicKeyInfo() + { + byte[] spkiBytes = new byte[64]; + var kem = MLKem.ImportSubjectPublicKeyInfo(spkiBytes); + } + + public void TestImportFromPem() + { + string pem = "-----BEGIN PRIVATE KEY-----"; + var kem = MLKem.ImportFromPem(pem); + } + + public void TestImportEncryptedPkcs8PrivateKey() + { + byte[] passwordBytes = new byte[16]; + byte[] encryptedBytes = new byte[64]; + var kem = MLKem.ImportEncryptedPkcs8PrivateKey(passwordBytes, encryptedBytes); + } + + public void TestImportFromEncryptedPem() + { + string pem = "-----BEGIN ENCRYPTED PRIVATE KEY-----"; + string password = "hunter2"; + var kem = MLKem.ImportFromEncryptedPem(pem, password); + } + + // ------------------------------------------------------------------------- + // Section 3: Native-interop constructors (no MLKemAlgorithm argument — wrap an + // already-existing native key handle) + // ------------------------------------------------------------------------- + + public void TestMLKemCng() + { + CngKey cngKey = null; + var kem = new MLKemCng(cngKey); + } + + public void TestMLKemOpenSsl() + { + SafeEvpPKeyHandle handle = null; + var kem = new MLKemOpenSsl(handle); + } + + // ------------------------------------------------------------------------- + // Section 4: Encapsulate / Decapsulate operations + // ------------------------------------------------------------------------- + + public void TestEncapsulate() + { + var kem = MLKem.GenerateKey(MLKemAlgorithm.MLKem768); + byte[] ciphertext; + byte[] sharedSecret; + kem.Encapsulate(out ciphertext, out sharedSecret); + } + + public void TestDecapsulate() + { + var kem = MLKem.GenerateKey(MLKemAlgorithm.MLKem768); + byte[] ciphertext = new byte[1088]; + byte[] sharedSecret = kem.Decapsulate(ciphertext); + } + + // ------------------------------------------------------------------------- + // Section 5: Combined usage pattern (generation + encapsulate + decapsulate) + // ------------------------------------------------------------------------- + + public void TestFullFlow() + { + var kem = MLKem.GenerateKey(MLKemAlgorithm.MLKem1024); + byte[] ciphertext; + byte[] sharedSecret; + kem.Encapsulate(out ciphertext, out sharedSecret); + byte[] decapsulated = kem.Decapsulate(ciphertext); + } +} diff --git a/csharp/src/test/files/rules/detection/dotnet/DotNetProtectedDataTestFile.cs b/csharp/src/test/files/rules/detection/dotnet/DotNetProtectedDataTestFile.cs new file mode 100644 index 000000000..1c7b47e40 --- /dev/null +++ b/csharp/src/test/files/rules/detection/dotnet/DotNetProtectedDataTestFile.cs @@ -0,0 +1,84 @@ +/* + * Comprehensive test file for the DPAPI (Windows Data Protection API) detection rules + * (DotNetProtectedData.java). + * + * Covers: + * - ProtectedData.Protect(...) / Unprotect(...) / TryProtect(...) / TryUnprotect(...) + * - ProtectedMemory.Protect(...) / Unprotect(...) + * - DpapiDataProtector constructor + instance Protect(byte[]) / Unprotect(byte[]) + */ + +using System.Security.Cryptography; + +public class DotNetProtectedDataTest +{ + // ------------------------------------------------------------------------- + // Section 1: ProtectedData + // ------------------------------------------------------------------------- + + public void TestProtectedDataProtect() + { + byte[] secret = new byte[16]; + byte[] entropy = new byte[8]; + byte[] encrypted = ProtectedData.Protect(secret, entropy, DataProtectionScope.CurrentUser); + } + + public void TestProtectedDataUnprotect() + { + byte[] encrypted = new byte[32]; + byte[] entropy = new byte[8]; + byte[] secret = ProtectedData.Unprotect(encrypted, entropy, DataProtectionScope.CurrentUser); + } + + public void TestProtectedDataTryProtect() + { + byte[] secret = new byte[16]; + byte[] entropy = new byte[8]; + byte[] destination = new byte[64]; + int bytesWritten; + ProtectedData.TryProtect(secret, DataProtectionScope.LocalMachine, destination, out bytesWritten, entropy); + } + + public void TestProtectedDataTryUnprotect() + { + byte[] encrypted = new byte[32]; + byte[] entropy = new byte[8]; + byte[] destination = new byte[32]; + int bytesWritten; + ProtectedData.TryUnprotect(encrypted, DataProtectionScope.LocalMachine, destination, out bytesWritten, entropy); + } + + // ------------------------------------------------------------------------- + // Section 2: ProtectedMemory + // ------------------------------------------------------------------------- + + public void TestProtectedMemoryProtect() + { + byte[] secret = new byte[16]; + ProtectedMemory.Protect(secret, MemoryProtectionScope.SameProcess); + } + + public void TestProtectedMemoryUnprotect() + { + byte[] secret = new byte[16]; + ProtectedMemory.Unprotect(secret, MemoryProtectionScope.SameProcess); + } + + // ------------------------------------------------------------------------- + // Section 3: DpapiDataProtector + // ------------------------------------------------------------------------- + + public void TestDpapiDataProtectorProtect() + { + DpapiDataProtector protector = new DpapiDataProtector("MyApp", "Giftcard", "1234"); + byte[] secret = new byte[16]; + byte[] encrypted = protector.Protect(secret); + } + + public void TestDpapiDataProtectorUnprotect() + { + DpapiDataProtector protector = new DpapiDataProtector("MyApp", "Giftcard", "1234"); + byte[] encrypted = new byte[32]; + byte[] secret = protector.Unprotect(encrypted); + } +} diff --git a/csharp/src/test/files/rules/detection/dotnet/DotNetRC2ComprehensiveTestFile.cs b/csharp/src/test/files/rules/detection/dotnet/DotNetRC2ComprehensiveTestFile.cs new file mode 100644 index 000000000..4d42dd1a9 --- /dev/null +++ b/csharp/src/test/files/rules/detection/dotnet/DotNetRC2ComprehensiveTestFile.cs @@ -0,0 +1,353 @@ +/* + * Comprehensive test file for System.Security.Cryptography RC2 detection rules. + * + * Covers both RC2-related classes and their complete API surface: + * - RC2 (abstract base) + * - RC2CryptoServiceProvider (derived from RC2) + * + * Like DES, RC2 has no CNG-backed subclass and no AEAD variant, so there are + * fewer constructor scenarios in Section 1, and no AEAD sections. Unlike DES, + * RC2 additionally exposes an EffectiveKeySize property (Section 2), which is + * reused as a KeySize detection (see DotNetRC2.java). + * + * Architecture note: all methods inherited from SymmetricAlgorithm (EncryptCbc, + * CreateEncryptor, etc.) are covered once here. The detection engine tracks the + * variable and fires the same depending rules for every concrete RC2 subclass. + */ + +using System.Security.Cryptography; + +public class DotNetRC2ComprehensiveTest +{ + // ------------------------------------------------------------------------- + // Section 1: Factory methods / constructors + // ------------------------------------------------------------------------- + + public void TestRc2Create() + { + var rc2 = RC2.Create(); + } + + public void TestRc2CreateNamed() + { + var rc2 = RC2.Create("RC2"); + } + + public void TestRc2Csp() + { + var rc2 = new RC2CryptoServiceProvider(); + } + + // ------------------------------------------------------------------------- + // Section 2: Property setters (via assignment → synthetic set_X invocations) + // ------------------------------------------------------------------------- + + public void TestPropertyModeCBC() + { + var rc2 = RC2.Create(); + rc2.Mode = CipherMode.CBC; + } + + public void TestPropertyModeECB() + { + var rc2 = RC2.Create(); + rc2.Mode = CipherMode.ECB; + } + + public void TestPropertyModeCFB() + { + var rc2 = RC2.Create(); + rc2.Mode = CipherMode.CFB; + } + + public void TestPropertyModeOFB() + { + var rc2 = RC2.Create(); + rc2.Mode = CipherMode.OFB; + } + + public void TestPropertyModeCTS() + { + var rc2 = RC2.Create(); + rc2.Mode = CipherMode.CTS; + } + + public void TestPropertyKeySize() + { + var rc2 = RC2.Create(); + rc2.KeySize = 128; + } + + public void TestPropertyEffectiveKeySize() + { + var rc2 = RC2.Create(); + rc2.EffectiveKeySize = 64; + } + + public void TestPropertyPaddingPKCS7() + { + var rc2 = RC2.Create(); + rc2.Padding = PaddingMode.PKCS7; + } + + public void TestPropertyPaddingNone() + { + var rc2 = RC2.Create(); + rc2.Padding = PaddingMode.None; + } + + public void TestPropertyPaddingZeros() + { + var rc2 = RC2.Create(); + rc2.Padding = PaddingMode.Zeros; + } + + public void TestPropertyPaddingANSIX923() + { + var rc2 = RC2.Create(); + rc2.Padding = PaddingMode.ANSIX923; + } + + public void TestPropertyFeedbackSize() + { + var rc2 = RC2.Create(); + rc2.FeedbackSize = 8; + } + + public void TestPropertyIV() + { + var rc2 = RC2.Create(); + rc2.IV = new byte[8]; + } + + public void TestPropertyKey() + { + var rc2 = RC2.Create(); + rc2.Key = new byte[16]; + } + + // ------------------------------------------------------------------------- + // Section 3: CreateEncryptor / CreateDecryptor + // ------------------------------------------------------------------------- + + public void TestCreateEncryptorNoArgs() + { + var rc2 = RC2.Create(); + var encryptor = rc2.CreateEncryptor(); + } + + public void TestCreateEncryptorWithArgs() + { + var rc2 = RC2.Create(); + byte[] key = new byte[16]; + byte[] iv = new byte[8]; + var encryptor = rc2.CreateEncryptor(key, iv); + } + + public void TestCreateDecryptorNoArgs() + { + var rc2 = RC2.Create(); + var decryptor = rc2.CreateDecryptor(); + } + + public void TestCreateDecryptorWithArgs() + { + var rc2 = RC2.Create(); + byte[] key = new byte[16]; + byte[] iv = new byte[8]; + var decryptor = rc2.CreateDecryptor(key, iv); + } + + // ------------------------------------------------------------------------- + // Section 4: Direct mode-specific encrypt methods + // ------------------------------------------------------------------------- + + public void TestEncryptCbc() + { + var rc2 = RC2.Create(); + byte[] plaintext = new byte[16]; + byte[] iv = new byte[8]; + byte[] ciphertext = rc2.EncryptCbc(plaintext, iv, PaddingMode.PKCS7); + } + + public void TestEncryptEcb() + { + var rc2 = RC2.Create(); + byte[] plaintext = new byte[16]; + byte[] ciphertext = rc2.EncryptEcb(plaintext, PaddingMode.None); + } + + public void TestEncryptCfb() + { + var rc2 = RC2.Create(); + byte[] plaintext = new byte[16]; + byte[] iv = new byte[8]; + byte[] ciphertext = rc2.EncryptCfb(plaintext, iv, PaddingMode.None, 8); + } + + // ------------------------------------------------------------------------- + // Section 5: Direct mode-specific decrypt methods + // ------------------------------------------------------------------------- + + public void TestDecryptCbc() + { + var rc2 = RC2.Create(); + byte[] ciphertext = new byte[16]; + byte[] iv = new byte[8]; + byte[] plaintext = rc2.DecryptCbc(ciphertext, iv, PaddingMode.PKCS7); + } + + public void TestDecryptEcb() + { + var rc2 = RC2.Create(); + byte[] ciphertext = new byte[16]; + byte[] plaintext = rc2.DecryptEcb(ciphertext, PaddingMode.None); + } + + public void TestDecryptCfb() + { + var rc2 = RC2.Create(); + byte[] ciphertext = new byte[16]; + byte[] iv = new byte[8]; + byte[] plaintext = rc2.DecryptCfb(ciphertext, iv, PaddingMode.None, 8); + } + + // ------------------------------------------------------------------------- + // Section 6: Try* variants + // ------------------------------------------------------------------------- + + public void TestTryEncryptCbc() + { + var rc2 = RC2.Create(); + byte[] plaintext = new byte[16]; + byte[] iv = new byte[8]; + byte[] destination = new byte[24]; + int bytesWritten; + rc2.TryEncryptCbc(plaintext, iv, destination, out bytesWritten, PaddingMode.PKCS7); + } + + public void TestTryDecryptCbc() + { + var rc2 = RC2.Create(); + byte[] ciphertext = new byte[16]; + byte[] iv = new byte[8]; + byte[] destination = new byte[16]; + int bytesWritten; + rc2.TryDecryptCbc(ciphertext, iv, destination, out bytesWritten, PaddingMode.PKCS7); + } + + public void TestTryEncryptEcb() + { + var rc2 = RC2.Create(); + byte[] plaintext = new byte[16]; + byte[] destination = new byte[24]; + int bytesWritten; + rc2.TryEncryptEcb(plaintext, destination, PaddingMode.None, out bytesWritten); + } + + public void TestTryDecryptEcb() + { + var rc2 = RC2.Create(); + byte[] ciphertext = new byte[16]; + byte[] destination = new byte[16]; + int bytesWritten; + rc2.TryDecryptEcb(ciphertext, destination, PaddingMode.None, out bytesWritten); + } + + public void TestTryEncryptCfb() + { + var rc2 = RC2.Create(); + byte[] plaintext = new byte[16]; + byte[] iv = new byte[8]; + byte[] destination = new byte[24]; + int bytesWritten; + rc2.TryEncryptCfb(plaintext, iv, destination, out bytesWritten, PaddingMode.None, 8); + } + + public void TestTryDecryptCfb() + { + var rc2 = RC2.Create(); + byte[] ciphertext = new byte[16]; + byte[] iv = new byte[8]; + byte[] destination = new byte[16]; + int bytesWritten; + rc2.TryDecryptCfb(ciphertext, iv, destination, out bytesWritten, PaddingMode.None, 8); + } + + // ------------------------------------------------------------------------- + // Section 7: Key/IV generation + // ------------------------------------------------------------------------- + + public void TestGenerateKey() + { + var rc2 = RC2.Create(); + rc2.GenerateKey(); + } + + public void TestGenerateIV() + { + var rc2 = RC2.Create(); + rc2.GenerateIV(); + } + + // ------------------------------------------------------------------------- + // Section 8: Combined usage patterns (real-world scenarios) + // Demonstrates that depending rules fire correctly for both derived classes. + // ------------------------------------------------------------------------- + + public void TestRc2CbcFullFlow() + { + var rc2 = RC2.Create(); + rc2.Mode = CipherMode.CBC; + rc2.Padding = PaddingMode.PKCS7; + var encryptor = rc2.CreateEncryptor(); + } + + public void TestRc2CspEncryptCbc() + { + var rc2 = new RC2CryptoServiceProvider(); + rc2.Mode = CipherMode.CBC; + byte[] plaintext = new byte[16]; + byte[] iv = new byte[8]; + byte[] ciphertext = rc2.EncryptCbc(plaintext, iv, PaddingMode.PKCS7); + } + + public void TestRc2CspDecryptCbc() + { + var rc2 = new RC2CryptoServiceProvider(); + byte[] ciphertext = new byte[16]; + byte[] iv = new byte[8]; + byte[] plaintext = rc2.DecryptCbc(ciphertext, iv, PaddingMode.PKCS7); + } + + public void TestRc2CfbFeedback() + { + var rc2 = RC2.Create(); + rc2.Mode = CipherMode.CFB; + rc2.FeedbackSize = 8; + byte[] plaintext = new byte[16]; + byte[] iv = new byte[8]; + byte[] ciphertext = rc2.EncryptCfb(plaintext, iv, PaddingMode.None, 8); + } + + public void TestRc2CbcWithEncryptorOverload() + { + var rc2 = RC2.Create(); + byte[] key = new byte[16]; + byte[] iv = new byte[8]; + var encryptor = rc2.CreateEncryptor(key, iv); + } + + public void TestRc2EcbEncrypt() + { + var rc2 = new RC2CryptoServiceProvider(); + byte[] plaintext = new byte[16]; + byte[] ciphertext = rc2.EncryptEcb(plaintext, PaddingMode.None); + } + + public void TestRc2CspEffectiveKeySize() + { + var rc2 = new RC2CryptoServiceProvider(); + rc2.EffectiveKeySize = 40; + } +} diff --git a/csharp/src/test/files/rules/detection/dotnet/DotNetRSATestFile.cs b/csharp/src/test/files/rules/detection/dotnet/DotNetRSATestFile.cs index 147848228..4c4a90d2c 100755 --- a/csharp/src/test/files/rules/detection/dotnet/DotNetRSATestFile.cs +++ b/csharp/src/test/files/rules/detection/dotnet/DotNetRSATestFile.cs @@ -1,4 +1,182 @@ +/* + * Comprehensive test file for System.Security.Cryptography RSA detection rules. + * + * Covers all four RSA-related classes and their complete operational API surface: + * - RSA (abstract base) + * - RSACng, RSACryptoServiceProvider, RSAOpenSsl (derived from RSA) + * + * Architecture note: all methods inherited from RSA / AsymmetricAlgorithm (KeySize, + * Encrypt, Decrypt, SignData, VerifyData, SignHash, VerifyHash, Try* variants, etc.) + * are covered once here. The detection engine tracks the variable and fires the same + * depending rules for every concrete RSA subclass. + */ + using System.Security.Cryptography; -public class DotNetRSATest { - public void TestRsaCreate() { var rsa = RSA.Create(); } // Noncompliant + +public class DotNetRSATest +{ + // ------------------------------------------------------------------------- + // Section 1: Factory methods / constructors + // ------------------------------------------------------------------------- + + public void TestRsaCreate() + { + var rsa = RSA.Create(); // Noncompliant + } + + public void TestRsaCreateWithKeySize() + { + var rsa = RSA.Create(2048); + } + + public void TestRsaCsp() + { + var rsa = new RSACryptoServiceProvider(); + } + + public void TestRsaCng() + { + var rsa = new RSACng(); + } + + public void TestRsaOpenSsl() + { + var rsa = new RSAOpenSsl(); + } + + // ------------------------------------------------------------------------- + // Section 2: Property setters (via assignment → synthetic set_X invocations) + // ------------------------------------------------------------------------- + + public void TestPropertyKeySize2048() + { + var rsa = RSA.Create(); + rsa.KeySize = 2048; + } + + public void TestPropertyKeySize4096() + { + var rsa = RSA.Create(); + rsa.KeySize = 4096; + } + + // ------------------------------------------------------------------------- + // Section 3: Encrypt / Decrypt + // ------------------------------------------------------------------------- + + public void TestEncrypt() + { + var rsa = RSA.Create(); + byte[] data = new byte[32]; + byte[] ciphertext = rsa.Encrypt(data, RSAEncryptionPadding.OaepSHA256); + } + + public void TestDecrypt() + { + var rsa = RSA.Create(); + byte[] ciphertext = new byte[256]; + byte[] plaintext = rsa.Decrypt(ciphertext, RSAEncryptionPadding.OaepSHA256); + } + + public void TestTryEncrypt() + { + var rsa = RSA.Create(); + byte[] data = new byte[32]; + byte[] destination = new byte[256]; + int bytesWritten; + rsa.TryEncrypt(data, destination, RSAEncryptionPadding.Pkcs1, out bytesWritten); + } + + public void TestTryDecrypt() + { + var rsa = RSA.Create(); + byte[] ciphertext = new byte[256]; + byte[] destination = new byte[32]; + int bytesWritten; + rsa.TryDecrypt(ciphertext, destination, RSAEncryptionPadding.Pkcs1, out bytesWritten); + } + + // ------------------------------------------------------------------------- + // Section 4: SignData / TrySignData / VerifyData + // ------------------------------------------------------------------------- + + public void TestSignData() + { + var rsa = RSA.Create(); + byte[] data = new byte[64]; + byte[] signature = rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + } + + public void TestTrySignData() + { + var rsa = RSA.Create(); + byte[] data = new byte[64]; + byte[] destination = new byte[256]; + int bytesWritten; + rsa.TrySignData(data, destination, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1, out bytesWritten); + } + + public void TestVerifyData() + { + var rsa = RSA.Create(); + byte[] data = new byte[64]; + byte[] signature = new byte[256]; + bool valid = rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + } + + // ------------------------------------------------------------------------- + // Section 5: SignHash / TrySignHash / VerifyHash + // ------------------------------------------------------------------------- + + public void TestSignHash() + { + var rsa = RSA.Create(); + byte[] hash = new byte[32]; + byte[] signature = rsa.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + } + + public void TestTrySignHash() + { + var rsa = RSA.Create(); + byte[] hash = new byte[32]; + byte[] destination = new byte[256]; + int bytesWritten; + rsa.TrySignHash(hash, destination, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1, out bytesWritten); + } + + public void TestVerifyHash() + { + var rsa = RSA.Create(); + byte[] hash = new byte[32]; + byte[] signature = new byte[256]; + bool valid = rsa.VerifyHash(hash, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + } + + // ------------------------------------------------------------------------- + // Section 6: Combined usage patterns (real-world scenarios) + // Demonstrates that depending rules fire correctly for ALL derived classes. + // ------------------------------------------------------------------------- + + public void TestRsaCngFullFlow() + { + var rsa = new RSACng(); + rsa.KeySize = 3072; + byte[] data = new byte[32]; + byte[] ciphertext = rsa.Encrypt(data, RSAEncryptionPadding.OaepSHA256); + } + + public void TestRsaCspSignFlow() + { + var rsa = new RSACryptoServiceProvider(); + byte[] data = new byte[64]; + byte[] signature = rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + } + + public void TestRsaOpenSslVerifyFlow() + { + var rsa = new RSAOpenSsl(); + byte[] data = new byte[64]; + byte[] signature = new byte[256]; + bool valid = rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + } } diff --git a/csharp/src/test/files/rules/detection/dotnet/DotNetRandomNumberGeneratorTestFile.cs b/csharp/src/test/files/rules/detection/dotnet/DotNetRandomNumberGeneratorTestFile.cs new file mode 100644 index 000000000..7d4d470e3 --- /dev/null +++ b/csharp/src/test/files/rules/detection/dotnet/DotNetRandomNumberGeneratorTestFile.cs @@ -0,0 +1,121 @@ +/* + * Comprehensive test file for RandomNumberGenerator / RNGCryptoServiceProvider detection rules + * (DotNetRandomNumberGenerator.java). + * + * Covers: + * - RandomNumberGenerator.Create() / Create(string) + instance GetBytes/GetNonZeroBytes + * (including their Span overloads, which are virtual instance methods, not static -- + * confirmed by compiling this exact file against the real .NET 10 compiler) + * - RandomNumberGenerator's genuinely static-only methods: Fill, GetBytes(int), + * GetHexString, GetInt32, GetItems, GetString, Shuffle + * - RNGCryptoServiceProvider constructor overloads + instance GetBytes/GetNonZeroBytes + * + * Note: GetNonZeroBytes has NO static overload at all (unlike GetBytes, which has a genuinely + * static GetBytes(int) alongside its instance overloads) -- `RandomNumberGenerator.GetNonZeroBytes(data)` + * does not compile (CS0120). There is therefore no "static GetNonZeroBytes" test case in this file; + * TestCreateAndGetNonZeroBytes below already exercises the (only) instance form. + */ + +using System.Security.Cryptography; + +public class DotNetRandomNumberGeneratorTest +{ + // ------------------------------------------------------------------------- + // Section 1: RandomNumberGenerator.Create() / Create(string) + instance operations + // ------------------------------------------------------------------------- + + public void TestCreateAndGetBytes() + { + RandomNumberGenerator rng = RandomNumberGenerator.Create(); + byte[] data = new byte[32]; + rng.GetBytes(data); + } + + public void TestCreateAndGetNonZeroBytes() + { + RandomNumberGenerator rng = RandomNumberGenerator.Create(); + byte[] data = new byte[32]; + rng.GetNonZeroBytes(data); + } + + public void TestCreateNamed() + { + RandomNumberGenerator rng = RandomNumberGenerator.Create("RandomNumberGenerator"); + byte[] data = new byte[32]; + rng.GetBytes(data); + } + + // ------------------------------------------------------------------------- + // Section 2: RandomNumberGenerator static-only methods + // ------------------------------------------------------------------------- + + public void TestStaticFill() + { + byte[] data = new byte[32]; + RandomNumberGenerator.Fill(data); + } + + public void TestStaticGetBytesCount() + { + byte[] data = RandomNumberGenerator.GetBytes(32); + } + + public void TestStaticGetHexString() + { + string hex = RandomNumberGenerator.GetHexString(16); + } + + public void TestStaticGetInt32() + { + int value = RandomNumberGenerator.GetInt32(100); + } + + public void TestStaticGetInt32Range() + { + int value = RandomNumberGenerator.GetInt32(1, 100); + } + + public void TestStaticGetItems() + { + int[] choices = new int[] { 1, 2, 3, 4, 5 }; + int[] items = RandomNumberGenerator.GetItems(choices, 3); + } + + public void TestStaticGetString() + { + string alphabet = "abcdefghijklmnopqrstuvwxyz"; + string result = RandomNumberGenerator.GetString(alphabet, 10); + } + + public void TestStaticShuffle() + { + int[] values = new int[] { 1, 2, 3, 4, 5 }; + RandomNumberGenerator.Shuffle(values); + } + + // ------------------------------------------------------------------------- + // Section 3: RNGCryptoServiceProvider + // ------------------------------------------------------------------------- + + public void TestRngCspGetBytes() + { + RNGCryptoServiceProvider rngCsp = new RNGCryptoServiceProvider(); + byte[] data = new byte[32]; + rngCsp.GetBytes(data); + } + + public void TestRngCspGetNonZeroBytes() + { + RNGCryptoServiceProvider rngCsp = new RNGCryptoServiceProvider(); + byte[] data = new byte[32]; + rngCsp.GetNonZeroBytes(data); + } + + public void TestRngCspWithSeed() + { + byte[] seed = new byte[32]; + RNGCryptoServiceProvider rngCsp = new RNGCryptoServiceProvider(seed); + byte[] data = new byte[32]; + rngCsp.GetBytes(data); + } +} diff --git a/csharp/src/test/files/rules/detection/dotnet/DotNetRfc2898DeriveBytesTestFile.cs b/csharp/src/test/files/rules/detection/dotnet/DotNetRfc2898DeriveBytesTestFile.cs index eefd47425..ad065467b 100755 --- a/csharp/src/test/files/rules/detection/dotnet/DotNetRfc2898DeriveBytesTestFile.cs +++ b/csharp/src/test/files/rules/detection/dotnet/DotNetRfc2898DeriveBytesTestFile.cs @@ -3,4 +3,25 @@ public class DotNetRfc2898DeriveBytesTest { public void TestPbkdf2() { var kdf = new Rfc2898DeriveBytes("password", new byte[16], 10000, HashAlgorithmName.SHA256); // Noncompliant } + + public void TestPbkdf2GetBytes() { + var kdf = new Rfc2898DeriveBytes("password", new byte[16], 10000, HashAlgorithmName.SHA256); // Noncompliant + byte[] derived = kdf.GetBytes(32); + } + + public void TestPbkdf2CryptDeriveKey() { + var kdf = new Rfc2898DeriveBytes("password", new byte[16], 10000, HashAlgorithmName.SHA256); // Noncompliant + byte[] iv = new byte[8]; + byte[] key = kdf.CryptDeriveKey("TripleDES", "SHA1", 192, iv); + } + + public void TestPbkdf2StaticByteArray() { + byte[] key = Rfc2898DeriveBytes.Pbkdf2( + new byte[8], new byte[16], 10000, HashAlgorithmName.SHA256, 32); // Noncompliant + } + + public void TestPbkdf2StaticString() { + byte[] key = Rfc2898DeriveBytes.Pbkdf2( + "password", new byte[16], 10000, HashAlgorithmName.SHA256, 32); // Noncompliant + } } diff --git a/csharp/src/test/files/rules/detection/dotnet/DotNetSHA3TestFile.cs b/csharp/src/test/files/rules/detection/dotnet/DotNetSHA3TestFile.cs new file mode 100644 index 000000000..457709ce5 --- /dev/null +++ b/csharp/src/test/files/rules/detection/dotnet/DotNetSHA3TestFile.cs @@ -0,0 +1,8 @@ +using System.Security.Cryptography; +public class DotNetSHA3Test { + public void TestSha3_256Create() { var h = SHA3_256.Create(); } // Noncompliant + public void TestSha3_384Create() { var h = SHA3_384.Create(); } // Noncompliant + public void TestSha3_512Create() { var h = SHA3_512.Create(); } // Noncompliant + public void TestShake128() { var h = new Shake128(); } // Noncompliant + public void TestShake256() { var h = new Shake256(); } // Noncompliant +} diff --git a/csharp/src/test/files/rules/detection/dotnet/DotNetSHATestFile.cs b/csharp/src/test/files/rules/detection/dotnet/DotNetSHATestFile.cs index 9ab3b53cf..30fc8c085 100755 --- a/csharp/src/test/files/rules/detection/dotnet/DotNetSHATestFile.cs +++ b/csharp/src/test/files/rules/detection/dotnet/DotNetSHATestFile.cs @@ -5,4 +5,30 @@ public class DotNetSHATest { public void TestSha384Create() { var h = SHA384.Create(); } // Noncompliant public void TestSha512Create() { var h = SHA512.Create(); } // Noncompliant public void TestMd5Create() { var h = MD5.Create(); } // Noncompliant + + public void TestMd5CreateNamed() { var h = MD5.Create("MD5"); } // Noncompliant + public void TestSha1CreateNamed() { var h = SHA1.Create("SHA1"); } // Noncompliant + public void TestSha256CreateNamed() { var h = SHA256.Create("SHA256"); } // Noncompliant + public void TestSha384CreateNamed() { var h = SHA384.Create("SHA384"); } // Noncompliant + public void TestSha512CreateNamed() { var h = SHA512.Create("SHA512"); } // Noncompliant + + public void TestMd5Cng() { var h = new MD5Cng(); } // Noncompliant + public void TestSha1Cng() { var h = new SHA1Cng(); } // Noncompliant + public void TestSha1Csp() { var h = new SHA1CryptoServiceProvider(); } // Noncompliant + public void TestSha256Cng() { var h = new SHA256Cng(); } // Noncompliant + public void TestSha256Csp() { var h = new SHA256CryptoServiceProvider(); }// Noncompliant + public void TestSha384Cng() { var h = new SHA384Cng(); } // Noncompliant + public void TestSha384Csp() { var h = new SHA384CryptoServiceProvider(); }// Noncompliant + public void TestSha512Cng() { var h = new SHA512Cng(); } // Noncompliant + public void TestSha512Csp() { var h = new SHA512CryptoServiceProvider(); }// Noncompliant + + public void TestRipemd160Create() { var h = RIPEMD160.Create(); } // Noncompliant + public void TestRipemd160CreateNamed() { var h = RIPEMD160.Create("RIPEMD160"); } // Noncompliant + public void TestRipemd160Managed() { var h = new RIPEMD160Managed(); } // Noncompliant + + public void TestMd5Csp() { var h = new MD5CryptoServiceProvider(); } // Noncompliant + public void TestSha1Managed() { var h = new SHA1Managed(); } // Noncompliant + public void TestSha256Managed() { var h = new SHA256Managed(); } // Noncompliant + public void TestSha384Managed() { var h = new SHA384Managed(); } // Noncompliant + public void TestSha512Managed() { var h = new SHA512Managed(); } // Noncompliant } diff --git a/csharp/src/test/files/rules/detection/dotnet/DotNetSlhDsaTestFile.cs b/csharp/src/test/files/rules/detection/dotnet/DotNetSlhDsaTestFile.cs new file mode 100644 index 000000000..29bf6e05f --- /dev/null +++ b/csharp/src/test/files/rules/detection/dotnet/DotNetSlhDsaTestFile.cs @@ -0,0 +1,152 @@ +/* + * Comprehensive test file for System.Security.Cryptography SLH-DSA (SlhDsa, SlhDsaCng, + * SlhDsaOpenSsl) detection rules. + * + * SLH-DSA (FIPS 205, formerly SPHINCS+) is documented for the net-10.0/net-11.0 monikers, with + * the whole SlhDsa surface (base class, both derived classes, and SlhDsaAlgorithm) carrying a + * class-level [Experimental("SYSLIB5006")] attribute (still a preview API, unlike the now-stable + * plain ML-DSA/ML-KEM surface). See DotNetSlhDsa.java's class javadoc for the full, verified + * explanation of what is/isn't modeled here and why, and how it differs from ML-DSA (no + * ImportSlhDsaPrivateSeed, no SignMu/VerifyMu, no Composite variant, different parameter set + * names). + */ + +using System.Security.Cryptography; + +public class DotNetSlhDsaTest +{ + // ------------------------------------------------------------------------- + // Section 1: SlhDsa algorithm-parameterized creation (parameter set captured from the + // SlhDsaAlgorithm argument) + // ------------------------------------------------------------------------- + + public void TestGenerateKeySha2_128s() + { + var dsa = SlhDsa.GenerateKey(SlhDsaAlgorithm.SlhDsaSha2_128s); + } + + public void TestGenerateKeyShake256f() + { + var dsa = SlhDsa.GenerateKey(SlhDsaAlgorithm.SlhDsaShake256f); + } + + public void TestImportSlhDsaPrivateKey() + { + byte[] privateKeyBytes = new byte[64]; + var dsa = SlhDsa.ImportSlhDsaPrivateKey(SlhDsaAlgorithm.SlhDsaSha2_192f, privateKeyBytes); + } + + public void TestImportSlhDsaPublicKey() + { + byte[] publicKeyBytes = new byte[32]; + var dsa = SlhDsa.ImportSlhDsaPublicKey(SlhDsaAlgorithm.SlhDsaShake128s, publicKeyBytes); + } + + // ------------------------------------------------------------------------- + // Section 2: SlhDsa structural imports (no SlhDsaAlgorithm argument — the parameter set is + // embedded in the encoded key material / PEM text and cannot be recovered) + // ------------------------------------------------------------------------- + + public void TestImportPkcs8PrivateKey() + { + byte[] pkcs8Bytes = new byte[64]; + var dsa = SlhDsa.ImportPkcs8PrivateKey(pkcs8Bytes); + } + + public void TestImportSubjectPublicKeyInfo() + { + byte[] spkiBytes = new byte[64]; + var dsa = SlhDsa.ImportSubjectPublicKeyInfo(spkiBytes); + } + + public void TestImportFromPem() + { + string pem = "-----BEGIN PRIVATE KEY-----"; + var dsa = SlhDsa.ImportFromPem(pem); + } + + public void TestImportEncryptedPkcs8PrivateKey() + { + byte[] passwordBytes = new byte[16]; + byte[] encryptedBytes = new byte[64]; + var dsa = SlhDsa.ImportEncryptedPkcs8PrivateKey(passwordBytes, encryptedBytes); + } + + public void TestImportFromEncryptedPem() + { + string pem = "-----BEGIN ENCRYPTED PRIVATE KEY-----"; + string password = "hunter2"; + var dsa = SlhDsa.ImportFromEncryptedPem(pem, password); + } + + // ------------------------------------------------------------------------- + // Section 3: SlhDsa native-interop constructors (no SlhDsaAlgorithm argument — wrap an + // already-existing native key handle) + // ------------------------------------------------------------------------- + + public void TestSlhDsaCng() + { + CngKey cngKey = null; + var dsa = new SlhDsaCng(cngKey); + } + + public void TestSlhDsaOpenSsl() + { + SafeEvpPKeyHandle handle = null; + var dsa = new SlhDsaOpenSsl(handle); + } + + // ------------------------------------------------------------------------- + // Section 4: SlhDsa Sign / Verify operations (no SignMu/VerifyMu — confirmed absent from + // the official SlhDsa reference) + // ------------------------------------------------------------------------- + + public void TestSignData() + { + var dsa = SlhDsa.GenerateKey(SlhDsaAlgorithm.SlhDsaSha2_128s); + byte[] data = new byte[32]; + byte[] context = new byte[0]; + byte[] signature = dsa.SignData(data, context); + } + + public void TestVerifyData() + { + var dsa = SlhDsa.GenerateKey(SlhDsaAlgorithm.SlhDsaSha2_128s); + byte[] data = new byte[32]; + byte[] signature = new byte[7856]; + byte[] context = new byte[0]; + bool ok = dsa.VerifyData(data, signature, context); + } + + public void TestSignPreHash() + { + var dsa = SlhDsa.GenerateKey(SlhDsaAlgorithm.SlhDsaSha2_128s); + byte[] hash = new byte[32]; + string hashOid = "2.16.840.1.101.3.4.2.1"; + byte[] context = new byte[0]; + byte[] signature = dsa.SignPreHash(hash, hashOid, context); + } + + public void TestVerifyPreHash() + { + var dsa = SlhDsa.GenerateKey(SlhDsaAlgorithm.SlhDsaSha2_128s); + byte[] hash = new byte[32]; + byte[] signature = new byte[7856]; + string hashOid = "2.16.840.1.101.3.4.2.1"; + byte[] context = new byte[0]; + bool ok = dsa.VerifyPreHash(hash, signature, hashOid, context); + } + + // ------------------------------------------------------------------------- + // Section 5: combined usage pattern (generation + sign + verify) + // ------------------------------------------------------------------------- + + public void TestFullFlow() + { + var dsa = SlhDsa.GenerateKey(SlhDsaAlgorithm.SlhDsaShake256f); + byte[] data = new byte[32]; + byte[] context = new byte[0]; + byte[] signature = dsa.SignData(data, context); + bool ok = dsa.VerifyData(data, signature, context); + } +} diff --git a/csharp/src/test/files/rules/detection/dotnet/DotNetTripleDESComprehensiveTestFile.cs b/csharp/src/test/files/rules/detection/dotnet/DotNetTripleDESComprehensiveTestFile.cs new file mode 100644 index 000000000..68585e759 --- /dev/null +++ b/csharp/src/test/files/rules/detection/dotnet/DotNetTripleDESComprehensiveTestFile.cs @@ -0,0 +1,359 @@ +/* + * Comprehensive test file for System.Security.Cryptography Triple DES (3DES) detection rules. + * + * Covers all three TripleDES-related classes and their complete API surface: + * - TripleDES (abstract base) + * - TripleDESCryptoServiceProvider (derived from TripleDES, legacy CAPI) + * - TripleDESCng (derived from TripleDES, CNG-backed) + * + * Architecture note: all methods inherited from SymmetricAlgorithm (EncryptCbc, + * CreateEncryptor, etc.) are covered once here. The detection engine tracks the + * variable and fires the same depending rules for every concrete TripleDES subclass. + * Unlike RC2, TripleDES exposes no extra properties beyond those inherited from + * SymmetricAlgorithm, so Section 2 mirrors DotNetDESComprehensiveTestFile.cs. Unlike + * DES/RC2 (which have no CNG-backed subclass), TripleDES has TripleDESCng, mirroring + * AesCng in DotNetAESComprehensiveTestFile.cs. + */ + +using System.Security.Cryptography; + +public class DotNetTripleDESComprehensiveTest +{ + // ------------------------------------------------------------------------- + // Section 1: Factory methods / constructors + // ------------------------------------------------------------------------- + + public void TestTripleDesCreate() + { + var tdes = TripleDES.Create(); + } + + public void TestTripleDesCreateNamed() + { + var tdes = TripleDES.Create("TripleDES"); + } + + public void TestTripleDesCsp() + { + var tdes = new TripleDESCryptoServiceProvider(); + } + + public void TestTripleDesCng() + { + var tdes = new TripleDESCng(); + } + + public void TestTripleDesCngNamed() + { + var tdes = new TripleDESCng("myKey"); + } + + // ------------------------------------------------------------------------- + // Section 2: Property setters (via assignment → synthetic set_X invocations) + // ------------------------------------------------------------------------- + + public void TestPropertyModeCBC() + { + var tdes = TripleDES.Create(); + tdes.Mode = CipherMode.CBC; + } + + public void TestPropertyModeECB() + { + var tdes = TripleDES.Create(); + tdes.Mode = CipherMode.ECB; + } + + public void TestPropertyModeCFB() + { + var tdes = TripleDES.Create(); + tdes.Mode = CipherMode.CFB; + } + + public void TestPropertyModeOFB() + { + var tdes = TripleDES.Create(); + tdes.Mode = CipherMode.OFB; + } + + public void TestPropertyModeCTS() + { + var tdes = TripleDES.Create(); + tdes.Mode = CipherMode.CTS; + } + + public void TestPropertyKeySize() + { + var tdes = TripleDES.Create(); + tdes.KeySize = 192; + } + + public void TestPropertyPaddingPKCS7() + { + var tdes = TripleDES.Create(); + tdes.Padding = PaddingMode.PKCS7; + } + + public void TestPropertyPaddingNone() + { + var tdes = TripleDES.Create(); + tdes.Padding = PaddingMode.None; + } + + public void TestPropertyPaddingZeros() + { + var tdes = TripleDES.Create(); + tdes.Padding = PaddingMode.Zeros; + } + + public void TestPropertyPaddingANSIX923() + { + var tdes = TripleDES.Create(); + tdes.Padding = PaddingMode.ANSIX923; + } + + public void TestPropertyFeedbackSize() + { + var tdes = TripleDES.Create(); + tdes.FeedbackSize = 8; + } + + public void TestPropertyIV() + { + var tdes = TripleDES.Create(); + tdes.IV = new byte[8]; + } + + public void TestPropertyKey() + { + var tdes = TripleDES.Create(); + tdes.Key = new byte[24]; + } + + // ------------------------------------------------------------------------- + // Section 3: CreateEncryptor / CreateDecryptor + // ------------------------------------------------------------------------- + + public void TestCreateEncryptorNoArgs() + { + var tdes = TripleDES.Create(); + var encryptor = tdes.CreateEncryptor(); + } + + public void TestCreateEncryptorWithArgs() + { + var tdes = TripleDES.Create(); + byte[] key = new byte[24]; + byte[] iv = new byte[8]; + var encryptor = tdes.CreateEncryptor(key, iv); + } + + public void TestCreateDecryptorNoArgs() + { + var tdes = TripleDES.Create(); + var decryptor = tdes.CreateDecryptor(); + } + + public void TestCreateDecryptorWithArgs() + { + var tdes = TripleDES.Create(); + byte[] key = new byte[24]; + byte[] iv = new byte[8]; + var decryptor = tdes.CreateDecryptor(key, iv); + } + + // ------------------------------------------------------------------------- + // Section 4: Direct mode-specific encrypt methods + // ------------------------------------------------------------------------- + + public void TestEncryptCbc() + { + var tdes = TripleDES.Create(); + byte[] plaintext = new byte[16]; + byte[] iv = new byte[8]; + byte[] ciphertext = tdes.EncryptCbc(plaintext, iv, PaddingMode.PKCS7); + } + + public void TestEncryptEcb() + { + var tdes = TripleDES.Create(); + byte[] plaintext = new byte[16]; + byte[] ciphertext = tdes.EncryptEcb(plaintext, PaddingMode.None); + } + + public void TestEncryptCfb() + { + var tdes = TripleDES.Create(); + byte[] plaintext = new byte[16]; + byte[] iv = new byte[8]; + byte[] ciphertext = tdes.EncryptCfb(plaintext, iv, PaddingMode.None, 8); + } + + // ------------------------------------------------------------------------- + // Section 5: Direct mode-specific decrypt methods + // ------------------------------------------------------------------------- + + public void TestDecryptCbc() + { + var tdes = TripleDES.Create(); + byte[] ciphertext = new byte[16]; + byte[] iv = new byte[8]; + byte[] plaintext = tdes.DecryptCbc(ciphertext, iv, PaddingMode.PKCS7); + } + + public void TestDecryptEcb() + { + var tdes = TripleDES.Create(); + byte[] ciphertext = new byte[16]; + byte[] plaintext = tdes.DecryptEcb(ciphertext, PaddingMode.None); + } + + public void TestDecryptCfb() + { + var tdes = TripleDES.Create(); + byte[] ciphertext = new byte[16]; + byte[] iv = new byte[8]; + byte[] plaintext = tdes.DecryptCfb(ciphertext, iv, PaddingMode.None, 8); + } + + // ------------------------------------------------------------------------- + // Section 6: Try* variants + // ------------------------------------------------------------------------- + + public void TestTryEncryptCbc() + { + var tdes = TripleDES.Create(); + byte[] plaintext = new byte[16]; + byte[] iv = new byte[8]; + byte[] destination = new byte[24]; + int bytesWritten; + tdes.TryEncryptCbc(plaintext, iv, destination, out bytesWritten, PaddingMode.PKCS7); + } + + public void TestTryDecryptCbc() + { + var tdes = TripleDES.Create(); + byte[] ciphertext = new byte[16]; + byte[] iv = new byte[8]; + byte[] destination = new byte[16]; + int bytesWritten; + tdes.TryDecryptCbc(ciphertext, iv, destination, out bytesWritten, PaddingMode.PKCS7); + } + + public void TestTryEncryptEcb() + { + var tdes = TripleDES.Create(); + byte[] plaintext = new byte[16]; + byte[] destination = new byte[24]; + int bytesWritten; + tdes.TryEncryptEcb(plaintext, destination, PaddingMode.None, out bytesWritten); + } + + public void TestTryDecryptEcb() + { + var tdes = TripleDES.Create(); + byte[] ciphertext = new byte[16]; + byte[] destination = new byte[16]; + int bytesWritten; + tdes.TryDecryptEcb(ciphertext, destination, PaddingMode.None, out bytesWritten); + } + + public void TestTryEncryptCfb() + { + var tdes = TripleDES.Create(); + byte[] plaintext = new byte[16]; + byte[] iv = new byte[8]; + byte[] destination = new byte[24]; + int bytesWritten; + tdes.TryEncryptCfb(plaintext, iv, destination, out bytesWritten, PaddingMode.None, 8); + } + + public void TestTryDecryptCfb() + { + var tdes = TripleDES.Create(); + byte[] ciphertext = new byte[16]; + byte[] iv = new byte[8]; + byte[] destination = new byte[16]; + int bytesWritten; + tdes.TryDecryptCfb(ciphertext, iv, destination, out bytesWritten, PaddingMode.None, 8); + } + + // ------------------------------------------------------------------------- + // Section 7: Key/IV generation + // ------------------------------------------------------------------------- + + public void TestGenerateKey() + { + var tdes = TripleDES.Create(); + tdes.GenerateKey(); + } + + public void TestGenerateIV() + { + var tdes = TripleDES.Create(); + tdes.GenerateIV(); + } + + // ------------------------------------------------------------------------- + // Section 8: Combined usage patterns (real-world scenarios) + // Demonstrates that depending rules fire correctly for all derived classes. + // ------------------------------------------------------------------------- + + public void TestTripleDesCbcFullFlow() + { + var tdes = TripleDES.Create(); + tdes.Mode = CipherMode.CBC; + tdes.Padding = PaddingMode.PKCS7; + var encryptor = tdes.CreateEncryptor(); + } + + public void TestTripleDesCspEncryptCbc() + { + var tdes = new TripleDESCryptoServiceProvider(); + tdes.Mode = CipherMode.CBC; + byte[] plaintext = new byte[16]; + byte[] iv = new byte[8]; + byte[] ciphertext = tdes.EncryptCbc(plaintext, iv, PaddingMode.PKCS7); + } + + public void TestTripleDesCspDecryptCbc() + { + var tdes = new TripleDESCryptoServiceProvider(); + byte[] ciphertext = new byte[16]; + byte[] iv = new byte[8]; + byte[] plaintext = tdes.DecryptCbc(ciphertext, iv, PaddingMode.PKCS7); + } + + public void TestTripleDesCfbFeedback() + { + var tdes = TripleDES.Create(); + tdes.Mode = CipherMode.CFB; + tdes.FeedbackSize = 8; + byte[] plaintext = new byte[16]; + byte[] iv = new byte[8]; + byte[] ciphertext = tdes.EncryptCfb(plaintext, iv, PaddingMode.None, 8); + } + + public void TestTripleDesCbcWithEncryptorOverload() + { + var tdes = TripleDES.Create(); + byte[] key = new byte[24]; + byte[] iv = new byte[8]; + var encryptor = tdes.CreateEncryptor(key, iv); + } + + public void TestTripleDesEcbEncrypt() + { + var tdes = new TripleDESCryptoServiceProvider(); + byte[] plaintext = new byte[16]; + byte[] ciphertext = tdes.EncryptEcb(plaintext, PaddingMode.None); + } + + public void TestTripleDesCngEncryptCbc() + { + var tdes = new TripleDESCng(); + byte[] plaintext = new byte[16]; + byte[] iv = new byte[8]; + byte[] ciphertext = tdes.EncryptCbc(plaintext, iv, PaddingMode.PKCS7); + } +} diff --git a/csharp/src/test/files/rules/detection/dotnet/DotNetX25519DiffieHellmanTestFile.cs b/csharp/src/test/files/rules/detection/dotnet/DotNetX25519DiffieHellmanTestFile.cs new file mode 100644 index 000000000..eb1da1fdc --- /dev/null +++ b/csharp/src/test/files/rules/detection/dotnet/DotNetX25519DiffieHellmanTestFile.cs @@ -0,0 +1,102 @@ +/* + * Comprehensive test file for System.Security.Cryptography X25519DiffieHellman (X25519) + * detection rules. + * + * Covers all three X25519DiffieHellman-related classes and their complete verified operational + * API surface: + * - X25519DiffieHellman (abstract base) — entry point is the static GenerateKey() factory + * (there is no Create() method, unlike ECDiffieHellman) + * - X25519DiffieHellmanCng, X25519DiffieHellmanOpenSsl (derived from X25519DiffieHellman) + * + * Architecture note: DeriveRawSecretAgreement is the only key-agreement operation that exists on + * X25519DiffieHellman (verified against the official Microsoft Learn API reference — no + * DeriveKeyMaterial/DeriveKeyFromHash/DeriveKeyFromHmac/DeriveKeyTls, and no settable KeySize + * property, unlike ECDiffieHellman). See DotNetX25519DiffieHellman.java class javadoc for the + * full, verified rationale. + * + * Known gap: reading/exporting the public key (e.g. ExportPublicKey()) cannot be meaningfully + * modeled as a depending rule (see DotNetX25519DiffieHellman.java class javadoc) — no test + * attempts it. + * + * ImportPrivateKey/ImportPublicKey are static factories on X25519DiffieHellman itself (each with + * byte[] and ReadOnlySpan overloads collapsed into one rule) that build a brand-new instance + * directly from raw 32-byte key material — modeled as primary creation rules, mirroring + * MLKem.ImportDecapsulationKey/ImportEncapsulationKey in DotNetMLKem.java. + */ + +using System.Security.Cryptography; + +public class DotNetX25519DiffieHellmanTest +{ + // ------------------------------------------------------------------------- + // Section 1: Factory method / constructors + // ------------------------------------------------------------------------- + + public void TestX25519GenerateKey() + { + var x25519 = X25519DiffieHellman.GenerateKey(); + } + + public void TestX25519Cng() + { + CngKey cngKey = null; + var x25519 = new X25519DiffieHellmanCng(cngKey); + } + + public void TestX25519OpenSsl() + { + SafeEvpPKeyHandle handle = null; + var x25519 = new X25519DiffieHellmanOpenSsl(handle); + } + + public void TestX25519ImportPrivateKeyByteArray() + { + byte[] privateKey = new byte[32]; + var x25519 = X25519DiffieHellman.ImportPrivateKey(privateKey); + } + + public void TestX25519ImportPublicKeyByteArray() + { + byte[] publicKey = new byte[32]; + var x25519 = X25519DiffieHellman.ImportPublicKey(publicKey); + } + + // ------------------------------------------------------------------------- + // Section 2: DeriveRawSecretAgreement operation (all overloads collapse to one rule) + // ------------------------------------------------------------------------- + + public void TestDeriveRawSecretAgreementByteArray() + { + var x25519 = X25519DiffieHellman.GenerateKey(); + byte[] otherPartyPublicKey = new byte[32]; + byte[] secret = x25519.DeriveRawSecretAgreement(otherPartyPublicKey); + } + + public void TestDeriveRawSecretAgreementOtherParty() + { + var x25519 = X25519DiffieHellman.GenerateKey(); + var otherParty = X25519DiffieHellman.GenerateKey(); + byte[] secret = x25519.DeriveRawSecretAgreement(otherParty); + } + + // ------------------------------------------------------------------------- + // Section 3: Combined usage patterns (real-world scenarios) + // Demonstrates that the depending rule fires correctly for ALL derived classes. + // ------------------------------------------------------------------------- + + public void TestX25519CngDeriveFlow() + { + CngKey cngKey = null; + var x25519 = new X25519DiffieHellmanCng(cngKey); + byte[] otherPartyPublicKey = new byte[32]; + byte[] secret = x25519.DeriveRawSecretAgreement(otherPartyPublicKey); + } + + public void TestX25519OpenSslDeriveFlow() + { + SafeEvpPKeyHandle handle = null; + var x25519 = new X25519DiffieHellmanOpenSsl(handle); + byte[] otherPartyPublicKey = new byte[32]; + byte[] secret = x25519.DeriveRawSecretAgreement(otherPartyPublicKey); + } +} diff --git a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetAESComprehensiveTest.java b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetAESComprehensiveTest.java new file mode 100644 index 000000000..1bb96ae15 --- /dev/null +++ b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetAESComprehensiveTest.java @@ -0,0 +1,465 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ibm.engine.detection.DetectionStore; +import com.ibm.engine.language.csharp.CSharpCheck; +import com.ibm.engine.language.csharp.CSharpScanContext; +import com.ibm.engine.language.csharp.CSharpSymbol; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.BlockSize; +import com.ibm.engine.model.CipherAction; +import com.ibm.engine.model.IValue; +import com.ibm.engine.model.KeySize; +import com.ibm.engine.model.Mode; +import com.ibm.engine.model.Padding; +import com.ibm.engine.model.ValueAction; +import com.ibm.engine.model.context.CipherContext; +import com.ibm.mapper.model.BlockCipher; +import com.ibm.mapper.model.INode; +import com.ibm.mapper.model.KeyLength; +import com.ibm.mapper.model.functionality.Decrypt; +import com.ibm.mapper.model.functionality.Encrypt; +import com.ibm.mapper.model.functionality.Generate; +import com.ibm.mapper.model.functionality.KeyGeneration; +import com.ibm.plugin.CSharpVerifier; +import com.ibm.plugin.TestBase; +import java.util.List; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +/** + * Comprehensive test for all AES-related detection rules (DotNetAES.java). + * + *

Covers all five AES-related classes and their complete API surface: + * + *

    + *
  • Aes (abstract base) + *
  • AesManaged, AesCng, AesCryptoServiceProvider (derived from Aes) + *
  • AesGcm, AesCcm (AEAD, separate class hierarchy) + *
+ * + *

Finding mapping (one finding per test method in DotNetAESComprehensiveTestFile.cs): + * + *

+ * Section 1 – factory methods / constructors (findings 0–7):
+ *   0  TestAesCreate                → AES
+ *   1  TestAesCreateNamed           → AES
+ *   2  TestAesManaged               → AES
+ *   3  TestAesCng                   → AES
+ *   4  TestAesCngNamed              → AES
+ *   5  TestAesCsp                   → AES
+ *   6  TestAesGcm                   → AES
+ *   7  TestAesCcm                   → AES
+ *
+ * Section 2 – property setters (findings 8–22):
+ *   8  TestPropertyModeCBC          → AES-CBC
+ *   9  TestPropertyModeECB          → AES-ECB
+ *   10 TestPropertyModeCFB          → AES-CFB
+ *   11 TestPropertyModeOFB          → AES-OFB
+ *   12 TestPropertyModeCTS          → AES-CTS
+ *   13 TestPropertyKeySize128       → AES-128
+ *   14 TestPropertyKeySize192       → AES-192
+ *   15 TestPropertyKeySize256       → AES-256
+ *   16 TestPropertyPaddingPKCS7     → AES-PKCS7
+ *   17 TestPropertyPaddingNone      → AES-None
+ *   18 TestPropertyPaddingZeros     → AES-Zeros
+ *   19 TestPropertyPaddingANSIX923  → AES-ANSIX923
+ *   20 TestPropertyFeedbackSize     → AES (BlockSize=128 detected, merged into default)
+ *   21 TestPropertyIV               → AES (no IV rule)
+ *   22 TestPropertyKey              → AES (no Key rule)
+ *
+ * Section 3 – CreateEncryptor/CreateDecryptor (findings 23–26):
+ *   23 TestCreateEncryptorNoArgs     → AES + Encrypt
+ *   24 TestCreateEncryptorWithArgs   → AES + Encrypt
+ *   25 TestCreateDecryptorNoArgs     → AES + Decrypt
+ *   26 TestCreateDecryptorWithArgs   → AES + Decrypt
+ *
+ * Section 4 – direct encrypt (findings 27–29):
+ *   27 TestEncryptCbc               → AES-CBC-PKCS7
+ *   28 TestEncryptEcb               → AES-ECB-None
+ *   29 TestEncryptCfb               → AES-CFB-None
+ *
+ * Section 5 – direct decrypt (findings 30–32):
+ *   30 TestDecryptCbc               → AES-CBC-PKCS7
+ *   31 TestDecryptEcb               → AES-ECB-None
+ *   32 TestDecryptCfb               → AES-CFB-None
+ *
+ * Section 6 – Try* variants (findings 33–38):
+ *   33 TestTryEncryptCbc            → AES-CBC-PKCS7
+ *   34 TestTryDecryptCbc            → AES-CBC-PKCS7
+ *   35 TestTryEncryptEcb            → AES-ECB-None
+ *   36 TestTryDecryptEcb            → AES-ECB-None
+ *   37 TestTryEncryptCfb            → AES-CFB-None
+ *   38 TestTryDecryptCfb            → AES-CFB-None
+ *
+ * Section 7 – key/IV generation (findings 39–40):
+ *   39 TestGenerateKey              → AES + KeyGeneration
+ *   40 TestGenerateIV               → AES + Generate
+ *
+ * Section 8 – AesGcm AEAD ops (findings 41–42):
+ *   41 TestAesGcmEncrypt            → AES + Encrypt
+ *   42 TestAesGcmDecrypt            → AES + Decrypt
+ *
+ * Section 9 – AesCcm AEAD ops (findings 43–44):
+ *   43 TestAesCcmEncrypt            → AES + Encrypt
+ *   44 TestAesCcmDecrypt            → AES + Decrypt
+ *
+ * Section 10 – combined usage patterns (findings 45–52):
+ *   45 TestAesCbcFullFlow           → AES-256-CBC-PKCS7 + Encrypt
+ *   46 TestAesCngEncryptCbc         → AES-CBC-PKCS7
+ *   47 TestAesCspDecryptCbc         → AES-CBC-PKCS7
+ *   48 TestAesManagedCfbFeedback    → AES-CFB-None
+ *   49 TestAesCbcWithEncryptorOverload → AES + Encrypt
+ *   50 TestAesEcbEncrypt            → AES-ECB-None
+ *   51 TestAesGcmFullFlow           → AES + Encrypt
+ *   52 TestAesCcmFullFlow           → AES + Encrypt
+ *
+ * Section 11 – EncryptKeyWrapPadded / DecryptKeyWrapPadded /
+ * TryDecryptKeyWrapPadded (RFC 5649 AES Key Wrap with Padding, findings 53–57):
+ *   53 TestEncryptKeyWrapPadded1    → AES + Encrypt
+ *   54 TestEncryptKeyWrapPadded2    → AES + Encrypt
+ *   55 TestDecryptKeyWrapPadded1    → AES + Decrypt
+ *   56 TestDecryptKeyWrapPadded2    → AES + Decrypt
+ *   57 TestTryDecryptKeyWrapPadded  → AES + Decrypt
+ * 
+ */ +class DotNetAESComprehensiveTest extends TestBase { + + @Test + void test() throws Exception { + CSharpVerifier.verify("rules/detection/dotnet/DotNetAESComprehensiveTestFile.cs", this); + } + + @Override + public void asserts( + int findingId, + @Nonnull + DetectionStore + detectionStore, + @Nonnull List nodes) { + + // Every top-level finding must be AES + assertThat(detectionStore.getDetectionValueContext()).isInstanceOf(CipherContext.class); + assertThat(detectionStore.getDetectionValues()).hasSize(1); + IValue primary = detectionStore.getDetectionValues().get(0); + assertThat(primary).isInstanceOf(ValueAction.class); + assertThat(primary.asString()).isEqualTo("AES"); + + switch (findingId) { + + // ----------------------------------------------------------------- + // Section 1: simple constructors — only AES, no children fired + // ----------------------------------------------------------------- + case 0, 1, 2, 3, 4, 5, 6, 7 -> { + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo("AES"); + } + + // ----------------------------------------------------------------- + // Section 2a: property Mode setters + // ----------------------------------------------------------------- + case 8 -> assertModeFindings(detectionStore, nodes, "CBC", "AES-CBC"); + case 9 -> assertModeFindings(detectionStore, nodes, "ECB", "AES-ECB"); + case 10 -> assertModeFindings(detectionStore, nodes, "CFB", "AES-CFB"); + case 11 -> assertModeFindings(detectionStore, nodes, "OFB", "AES-OFB"); + case 12 -> assertModeFindings(detectionStore, nodes, "CTS", "AES-CTS"); + + // ----------------------------------------------------------------- + // Section 2b: property KeySize setters + // ----------------------------------------------------------------- + case 13 -> assertKeySizeFindings(detectionStore, nodes, "128", "AES-128"); + case 14 -> assertKeySizeFindings(detectionStore, nodes, "192", "AES-192"); + case 15 -> assertKeySizeFindings(detectionStore, nodes, "256", "AES-256"); + + // ----------------------------------------------------------------- + // Section 2c: property Padding setters + // ----------------------------------------------------------------- + case 16 -> assertPaddingFindings(detectionStore, nodes, "PKCS7", "AES-PKCS7"); + case 17 -> assertPaddingFindings(detectionStore, nodes, "None", "AES-None"); + case 18 -> assertPaddingFindings(detectionStore, nodes, "Zeros", "AES-Zeros"); + case 19 -> assertPaddingFindings(detectionStore, nodes, "ANSIX923", "AES-ANSIX923"); + + // ----------------------------------------------------------------- + // Section 2d: FeedbackSize setter — BlockSize(128) detected but + // matches the default AES block size so node string stays "AES" + // ----------------------------------------------------------------- + case 20 -> { + DetectionStore fbStore = + getStoreOfValueType(BlockSize.class, detectionStore.getChildren()); + assertThat(fbStore).isNotNull(); + assertThat(fbStore.getDetectionValues()).hasSize(1); + assertThat(fbStore.getDetectionValues().get(0).asString()).isEqualTo("128"); + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo("AES"); + } + + // ----------------------------------------------------------------- + // Section 2e: IV and Key setters — no detection rules for these + // ----------------------------------------------------------------- + case 21, 22 -> { + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo("AES"); + } + + // ----------------------------------------------------------------- + // Section 3: CreateEncryptor / CreateDecryptor + // ----------------------------------------------------------------- + case 23, 24 -> assertEncryptFindings(detectionStore, nodes, "AES"); + case 25, 26 -> assertDecryptFindings(detectionStore, nodes, "AES"); + + // ----------------------------------------------------------------- + // Section 4: direct mode-specific encrypt + // ----------------------------------------------------------------- + case 27 -> + assertModePaddingFindings( + detectionStore, nodes, "CBC", "PKCS7", "AES-CBC-PKCS7"); + case 28 -> + assertModePaddingFindings(detectionStore, nodes, "ECB", "None", "AES-ECB-None"); + case 29 -> + assertModePaddingFindings(detectionStore, nodes, "CFB", "None", "AES-CFB-None"); + + // ----------------------------------------------------------------- + // Section 5: direct mode-specific decrypt + // ----------------------------------------------------------------- + case 30 -> + assertModePaddingFindings( + detectionStore, nodes, "CBC", "PKCS7", "AES-CBC-PKCS7"); + case 31 -> + assertModePaddingFindings(detectionStore, nodes, "ECB", "None", "AES-ECB-None"); + case 32 -> + assertModePaddingFindings(detectionStore, nodes, "CFB", "None", "AES-CFB-None"); + + // ----------------------------------------------------------------- + // Section 6: Try* variants + // ----------------------------------------------------------------- + case 33, 34 -> + assertModePaddingFindings( + detectionStore, nodes, "CBC", "PKCS7", "AES-CBC-PKCS7"); + case 35, 36 -> + assertModePaddingFindings(detectionStore, nodes, "ECB", "None", "AES-ECB-None"); + case 37, 38 -> + assertModePaddingFindings(detectionStore, nodes, "CFB", "None", "AES-CFB-None"); + + // ----------------------------------------------------------------- + // Section 7: GenerateKey / GenerateIV + // ----------------------------------------------------------------- + case 39 -> { + // aes.GenerateKey() → KeyGeneration functionality node + DetectionStore + genKeyStore = + getStoreOfValueType( + ValueAction.class, detectionStore.getChildren()); + assertThat(genKeyStore).isNotNull(); + assertThat(genKeyStore.getDetectionValues().get(0).asString()) + .isEqualTo("GenerateKey"); + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).getChildren().get(KeyGeneration.class)).isNotNull(); + } + case 40 -> { + // aes.GenerateIV() → Generate functionality node + DetectionStore + genIvStore = + getStoreOfValueType( + ValueAction.class, detectionStore.getChildren()); + assertThat(genIvStore).isNotNull(); + assertThat(genIvStore.getDetectionValues().get(0).asString()) + .isEqualTo("GenerateIV"); + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).getChildren().get(Generate.class)).isNotNull(); + } + + // ----------------------------------------------------------------- + // Section 8: AesGcm AEAD operations + // ----------------------------------------------------------------- + case 41 -> assertEncryptFindings(detectionStore, nodes, "AES"); + case 42 -> assertDecryptFindings(detectionStore, nodes, "AES"); + + // ----------------------------------------------------------------- + // Section 9: AesCcm AEAD operations + // ----------------------------------------------------------------- + case 43 -> assertEncryptFindings(detectionStore, nodes, "AES"); + case 44 -> assertDecryptFindings(detectionStore, nodes, "AES"); + + // ----------------------------------------------------------------- + // Section 10: combined usage patterns + // ----------------------------------------------------------------- + case 45 -> { + // TestAesCbcFullFlow: Mode=CBC, KeySize=256, Padding=PKCS7, CreateEncryptor + assertThat(nodes).hasSize(1); + INode node = nodes.get(0); + assertThat(node.getKind()).isEqualTo(BlockCipher.class); + assertThat(node.asString()).isEqualTo("AES-256-CBC-PKCS7"); + assertThat(node.getChildren().get(KeyLength.class)).isNotNull(); + assertThat(node.getChildren().get(KeyLength.class).asString()).isEqualTo("256"); + assertThat(node.getChildren().get(com.ibm.mapper.model.Mode.class)).isNotNull(); + assertThat(node.getChildren().get(Encrypt.class)).isNotNull(); + } + case 46 -> + assertModePaddingFindings( + detectionStore, nodes, "CBC", "PKCS7", "AES-CBC-PKCS7"); + case 47 -> + assertModePaddingFindings( + detectionStore, nodes, "CBC", "PKCS7", "AES-CBC-PKCS7"); + case 48 -> + assertModePaddingFindings(detectionStore, nodes, "CFB", "None", "AES-CFB-None"); + case 49 -> assertEncryptFindings(detectionStore, nodes, "AES"); + case 50 -> + assertModePaddingFindings(detectionStore, nodes, "ECB", "None", "AES-ECB-None"); + case 51 -> assertEncryptFindings(detectionStore, nodes, "AES"); + case 52 -> assertEncryptFindings(detectionStore, nodes, "AES"); + + // ----------------------------------------------------------------- + // Section 11: EncryptKeyWrapPadded / DecryptKeyWrapPadded / + // TryDecryptKeyWrapPadded — generic Encrypt/Decrypt findings (no + // Mode/Padding concept for RFC 5649 Key Wrap with Padding) + // ----------------------------------------------------------------- + case 53, 54 -> assertEncryptFindings(detectionStore, nodes, "AES"); + case 55, 56, 57 -> assertDecryptFindings(detectionStore, nodes, "AES"); + + default -> throw new IllegalStateException("Unexpected findingId: " + findingId); + } + } + + // ------------------------------------------------------------------------- + // Assertion helpers + // ------------------------------------------------------------------------- + + private void assertModeFindings( + @Nonnull DetectionStore store, + @Nonnull List nodes, + @Nonnull String expectedMode, + @Nonnull String expectedNodeString) { + + DetectionStore modeStore = + getStoreOfValueType(Mode.class, store.getChildren()); + assertThat(modeStore).isNotNull(); + assertThat(modeStore.getDetectionValues()).hasSize(1); + assertThat(modeStore.getDetectionValues().get(0).asString()).isEqualTo(expectedMode); + + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo(expectedNodeString); + assertThat(nodes.get(0).getChildren().get(com.ibm.mapper.model.Mode.class)).isNotNull(); + } + + private void assertKeySizeFindings( + @Nonnull DetectionStore store, + @Nonnull List nodes, + @Nonnull String expectedKeySize, + @Nonnull String expectedNodeString) { + + DetectionStore keySizeStore = + getStoreOfValueType(KeySize.class, store.getChildren()); + assertThat(keySizeStore).isNotNull(); + assertThat(keySizeStore.getDetectionValues()).hasSize(1); + assertThat(keySizeStore.getDetectionValues().get(0).asString()).isEqualTo(expectedKeySize); + + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo(expectedNodeString); + assertThat(nodes.get(0).getChildren().get(KeyLength.class)).isNotNull(); + assertThat(nodes.get(0).getChildren().get(KeyLength.class).asString()) + .isEqualTo(expectedKeySize); + } + + private void assertPaddingFindings( + @Nonnull DetectionStore store, + @Nonnull List nodes, + @Nonnull String expectedPadding, + @Nonnull String expectedNodeString) { + + DetectionStore paddingStore = + getStoreOfValueType(Padding.class, store.getChildren()); + assertThat(paddingStore).isNotNull(); + assertThat(paddingStore.getDetectionValues()).hasSize(1); + assertThat(paddingStore.getDetectionValues().get(0).asString()).isEqualTo(expectedPadding); + + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo(expectedNodeString); + } + + private void assertEncryptFindings( + @Nonnull DetectionStore store, + @Nonnull List nodes, + @Nonnull String expectedNodeString) { + + DetectionStore encryptStore = + getStoreOfValueType(CipherAction.class, store.getChildren()); + assertThat(encryptStore).isNotNull(); + assertThat(encryptStore.getDetectionValues()).hasSize(1); + assertThat(encryptStore.getDetectionValues().get(0).asString()).isEqualTo("ENCRYPT"); + + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo(expectedNodeString); + assertThat(nodes.get(0).getChildren().get(Encrypt.class)).isNotNull(); + } + + private void assertDecryptFindings( + @Nonnull DetectionStore store, + @Nonnull List nodes, + @Nonnull String expectedNodeString) { + + DetectionStore decryptStore = + getStoreOfValueType(CipherAction.class, store.getChildren()); + assertThat(decryptStore).isNotNull(); + assertThat(decryptStore.getDetectionValues()).hasSize(1); + assertThat(decryptStore.getDetectionValues().get(0).asString()).isEqualTo("DECRYPT"); + + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo(expectedNodeString); + assertThat(nodes.get(0).getChildren().get(Decrypt.class)).isNotNull(); + } + + /** + * Asserts mode+padding findings using the translated node tree. Direct-mode methods + * (EncryptCbc, TryDecryptEcb, etc.) place Mode and Padding in the same child detection store, + * so we validate via the final node string rather than per-store inspection. + */ + private void assertModePaddingFindings( + @Nonnull DetectionStore store, + @Nonnull List nodes, + @Nonnull String expectedMode, + @Nonnull String expectedPadding, + @Nonnull String expectedNodeString) { + + // Verify that a Mode value with the expected string is detected somewhere in children + DetectionStore modeStore = + getStoreOfValueType(Mode.class, store.getChildren()); + assertThat(modeStore).isNotNull(); + assertThat(modeStore.getDetectionValues()) + .anySatisfy(v -> assertThat(v.asString()).isEqualTo(expectedMode)); + + // The translated node captures the combined result + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo(expectedNodeString); + } +} diff --git a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetAlgorithmFactoryTest.java b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetAlgorithmFactoryTest.java new file mode 100644 index 000000000..707631a9a --- /dev/null +++ b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetAlgorithmFactoryTest.java @@ -0,0 +1,235 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ibm.engine.detection.DetectionStore; +import com.ibm.engine.language.csharp.CSharpCheck; +import com.ibm.engine.language.csharp.CSharpScanContext; +import com.ibm.engine.language.csharp.CSharpSymbol; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.Algorithm; +import com.ibm.engine.model.CipherAction; +import com.ibm.engine.model.IValue; +import com.ibm.engine.model.context.CipherContext; +import com.ibm.engine.model.context.DigestContext; +import com.ibm.engine.model.context.KeyContext; +import com.ibm.engine.model.context.MacContext; +import com.ibm.mapper.model.BlockCipher; +import com.ibm.mapper.model.INode; +import com.ibm.mapper.model.functionality.Decrypt; +import com.ibm.mapper.model.functionality.Encrypt; +import com.ibm.plugin.CSharpVerifier; +import com.ibm.plugin.TestBase; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +/** + * Test for the generic, string-based {@code Create(string)} factory methods declared directly on + * the abstract base classes of {@code System.Security.Cryptography} ({@code + * DotNetAlgorithmFactory.java}): {@code SymmetricAlgorithm.Create(string)}, {@code + * HashAlgorithm.Create(string)}, {@code KeyedHashAlgorithm.Create(string)}, {@code + * HMAC.Create(string)}, {@code AsymmetricAlgorithm.Create(string)}. + * + *

Finding mapping (one finding per test method in {@code DotNetAlgorithmFactoryTestFile.cs}): + * + *

+ *  0 TestSymmetricAlgorithmCreateAes        → CipherContext, Algorithm "AES"
+ *  1 TestSymmetricAlgorithmCreateRc2        → CipherContext, Algorithm "RC2"
+ *  2 TestSymmetricAlgorithmCreateTripleDes  → CipherContext, Algorithm "3DES"
+ *  3 TestSymmetricAlgorithmCreateDes        → CipherContext, Algorithm "DES"
+ *  4 TestSymmetricAlgorithmCreateAesWithEncryptorAndDecryptor → CipherContext, Algorithm "AES",
+ *      plus two child findings (CipherAction "ENCRYPT" from CreateEncryptor(key, iv) and
+ *      CipherAction "DECRYPT" from CreateDecryptor(key, iv)) — exercises the depending rules now
+ *      attached to SYMMETRIC_ALGORITHM_CREATE.
+ *  5 TestHashAlgorithmCreateSha256          → DigestContext, Algorithm "SHA256"
+ *  6 TestHashAlgorithmCreateShaHyphenated   → DigestContext, Algorithm "SHA-512"
+ *  7 TestHashAlgorithmCreateMd5             → DigestContext, Algorithm "MD5"
+ *  8 TestHashAlgorithmCreateShaBare         → DigestContext, Algorithm "SHA"
+ *  9 TestKeyedHashAlgorithmCreateHmacSha256 → MacContext, Algorithm "HMACSHA256"
+ * 10 TestKeyedHashAlgorithmCreateMacTripleDes → MacContext, Algorithm "MACTripleDES"
+ * 11 TestHmacCreateHmacSha1                 → MacContext, Algorithm "HMACSHA1"
+ * 12 TestHmacCreateFullyQualified           → MacContext, Algorithm
+ *      "System.Security.Cryptography.HMACSHA256"
+ * 13 TestAsymmetricAlgorithmCreateRsa       → KeyContext, Algorithm "RSA"
+ * 14 TestAsymmetricAlgorithmCreateDsa       → KeyContext, Algorithm "DSA"
+ * 15 TestAsymmetricAlgorithmCreateEcdsa     → KeyContext, Algorithm "ECDsa"
+ * 16 TestAsymmetricAlgorithmCreateEcdh      → KeyContext, Algorithm "ECDH"
+ * 
+ */ +class DotNetAlgorithmFactoryTest extends TestBase { + + @Test + void test() throws Exception { + CSharpVerifier.verify("rules/detection/dotnet/DotNetAlgorithmFactoryTestFile.cs", this); + } + + @Override + public void asserts( + int findingId, + @Nonnull + DetectionStore + detectionStore, + @Nonnull List nodes) { + + assertThat(detectionStore.getDetectionValues()).hasSize(1); + IValue value = detectionStore.getDetectionValues().get(0); + assertThat(value).isInstanceOf(Algorithm.class); + + assertThat(nodes).hasSize(1); + INode node = nodes.get(0); + + switch (findingId) { + case 0 -> { + assertThat(detectionStore.getDetectionValueContext()) + .isInstanceOf(CipherContext.class); + assertThat(value.asString()).isEqualTo("AES"); + assertThat(node.asString()).isEqualTo("AES"); + } + case 1 -> { + assertThat(detectionStore.getDetectionValueContext()) + .isInstanceOf(CipherContext.class); + assertThat(value.asString()).isEqualTo("RC2"); + assertThat(node.asString()).isEqualTo("RC2"); + } + case 2 -> { + assertThat(detectionStore.getDetectionValueContext()) + .isInstanceOf(CipherContext.class); + assertThat(value.asString()).isEqualTo("3DES"); + assertThat(node.asString()).isEqualTo("DESede"); + } + case 3 -> { + assertThat(detectionStore.getDetectionValueContext()) + .isInstanceOf(CipherContext.class); + assertThat(value.asString()).isEqualTo("DES"); + assertThat(node.asString()).isEqualTo("DES-56"); + } + case 4 -> { + assertThat(detectionStore.getDetectionValueContext()) + .isInstanceOf(CipherContext.class); + assertThat(value.asString()).isEqualTo("AES"); + assertThat(node.asString()).isEqualTo("AES"); + assertThat(node.getKind()).isEqualTo(BlockCipher.class); + assertThat(node.getChildren().get(Encrypt.class)).isNotNull(); + assertThat(node.getChildren().get(Decrypt.class)).isNotNull(); + + // detectionStore.getChildren() has one entry per depending rule attached to + // SYMMETRIC_ALGORITHM_CREATE (28: 4 property setters + 24 cipher ops), most of + // them empty for this call site; only the two that actually matched + // (CreateEncryptor/CreateDecryptor) carry a detection value. + List> + populatedChildren = + detectionStore.getChildren().stream() + .filter(child -> !child.getDetectionValues().isEmpty()) + .toList(); + assertThat(populatedChildren).hasSize(2); + Set childActions = + populatedChildren.stream() + .map( + child -> { + assertThat(child.getDetectionValues()).hasSize(1); + IValue childValue = + child.getDetectionValues().get(0); + assertThat(childValue).isInstanceOf(CipherAction.class); + return childValue.asString(); + }) + .collect(Collectors.toSet()); + assertThat(childActions).containsExactlyInAnyOrder("ENCRYPT", "DECRYPT"); + } + case 5 -> { + assertThat(detectionStore.getDetectionValueContext()) + .isInstanceOf(DigestContext.class); + assertThat(value.asString()).isEqualTo("SHA256"); + assertThat(node.asString()).isEqualTo("SHA-256"); + } + case 6 -> { + assertThat(detectionStore.getDetectionValueContext()) + .isInstanceOf(DigestContext.class); + assertThat(value.asString()).isEqualTo("SHA-512"); + assertThat(node.asString()).isEqualTo("SHA-512"); + } + case 7 -> { + assertThat(detectionStore.getDetectionValueContext()) + .isInstanceOf(DigestContext.class); + assertThat(value.asString()).isEqualTo("MD5"); + assertThat(node.asString()).isEqualTo("MD5"); + } + case 8 -> { + assertThat(detectionStore.getDetectionValueContext()) + .isInstanceOf(DigestContext.class); + assertThat(value.asString()).isEqualTo("SHA"); + assertThat(node.asString()).isEqualTo("SHA-1"); + } + case 9 -> { + assertThat(detectionStore.getDetectionValueContext()) + .isInstanceOf(MacContext.class); + assertThat(value.asString()).isEqualTo("HMACSHA256"); + assertThat(node.asString()).isEqualTo("HMAC-SHA-256"); + } + case 10 -> { + assertThat(detectionStore.getDetectionValueContext()) + .isInstanceOf(MacContext.class); + assertThat(value.asString()).isEqualTo("MACTripleDES"); + assertThat(node.asString()).isEqualTo("DESede"); + } + case 11 -> { + assertThat(detectionStore.getDetectionValueContext()) + .isInstanceOf(MacContext.class); + assertThat(value.asString()).isEqualTo("HMACSHA1"); + assertThat(node.asString()).isEqualTo("HMAC-SHA-1"); + } + case 12 -> { + assertThat(detectionStore.getDetectionValueContext()) + .isInstanceOf(MacContext.class); + assertThat(value.asString()).isEqualTo("System.Security.Cryptography.HMACSHA256"); + assertThat(node.asString()).isEqualTo("HMAC-SHA-256"); + } + case 13 -> { + assertThat(detectionStore.getDetectionValueContext()) + .isInstanceOf(KeyContext.class); + assertThat(value.asString()).isEqualTo("RSA"); + assertThat(node.asString()).isEqualTo("RSA"); + } + case 14 -> { + assertThat(detectionStore.getDetectionValueContext()) + .isInstanceOf(KeyContext.class); + assertThat(value.asString()).isEqualTo("DSA"); + assertThat(node.asString()).isEqualTo("DSA"); + } + case 15 -> { + assertThat(detectionStore.getDetectionValueContext()) + .isInstanceOf(KeyContext.class); + assertThat(value.asString()).isEqualTo("ECDsa"); + assertThat(node.asString()).isEqualTo("ECDSA"); + } + case 16 -> { + assertThat(detectionStore.getDetectionValueContext()) + .isInstanceOf(KeyContext.class); + assertThat(value.asString()).isEqualTo("ECDH"); + assertThat(node.asString()).isEqualTo("ECDH"); + } + default -> throw new IllegalStateException("Unexpected findingId: " + findingId); + } + } +} diff --git a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetDESComprehensiveTest.java b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetDESComprehensiveTest.java new file mode 100644 index 000000000..738961952 --- /dev/null +++ b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetDESComprehensiveTest.java @@ -0,0 +1,419 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ibm.engine.detection.DetectionStore; +import com.ibm.engine.language.csharp.CSharpCheck; +import com.ibm.engine.language.csharp.CSharpScanContext; +import com.ibm.engine.language.csharp.CSharpSymbol; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.BlockSize; +import com.ibm.engine.model.CipherAction; +import com.ibm.engine.model.IValue; +import com.ibm.engine.model.KeySize; +import com.ibm.engine.model.Mode; +import com.ibm.engine.model.Padding; +import com.ibm.engine.model.ValueAction; +import com.ibm.engine.model.context.CipherContext; +import com.ibm.mapper.model.BlockCipher; +import com.ibm.mapper.model.INode; +import com.ibm.mapper.model.KeyLength; +import com.ibm.mapper.model.functionality.Decrypt; +import com.ibm.mapper.model.functionality.Encrypt; +import com.ibm.mapper.model.functionality.Generate; +import com.ibm.mapper.model.functionality.KeyGeneration; +import com.ibm.plugin.CSharpVerifier; +import com.ibm.plugin.TestBase; +import java.util.List; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +/** + * Comprehensive test for all DES-related detection rules (DotNetDES.java). + * + *

Covers both DES-related classes and their complete API surface: + * + *

    + *
  • DES (abstract base) + *
  • DESCryptoServiceProvider (derived from DES) + *
+ * + *

Unlike Aes, DES has no CNG-backed subclass and no AEAD variant, so there is no equivalent to + * Section 8/9 of DotNetAESComprehensiveTest (AesGcm/AesCcm). + * + *

Finding mapping (one finding per test method in DotNetDESComprehensiveTestFile.cs): + * + *

+ * Section 1 – factory methods / constructors (findings 0–2):
+ *   0  TestDesCreate                 → DES-56
+ *   1  TestDesCreateNamed            → DES-56
+ *   2  TestDesCsp                    → DES-56
+ *
+ * Section 2 – property setters (findings 3–15):
+ *   3  TestPropertyModeCBC           → DES-56-CBC
+ *   4  TestPropertyModeECB           → DES-56-ECB
+ *   5  TestPropertyModeCFB           → DES-56-CFB
+ *   6  TestPropertyModeOFB           → DES-56-OFB
+ *   7  TestPropertyModeCTS           → DES-56-CTS
+ *   8  TestPropertyKeySize           → DES-64 (overrides default 56)
+ *   9  TestPropertyPaddingPKCS7      → DES-56 (padding never rendered in asString)
+ *   10 TestPropertyPaddingNone       → DES-56 (padding never rendered in asString)
+ *   11 TestPropertyPaddingZeros      → DES-56 (padding never rendered in asString)
+ *   12 TestPropertyPaddingANSIX923   → DES-56 (padding never rendered in asString)
+ *   13 TestPropertyFeedbackSize      → DES-56 (BlockSize never rendered in asString)
+ *   14 TestPropertyIV                → DES-56 (no IV rule)
+ *   15 TestPropertyKey               → DES-56 (no Key rule)
+ *
+ * Section 3 – CreateEncryptor/CreateDecryptor (findings 16–19):
+ *   16 TestCreateEncryptorNoArgs      → DES-56 + Encrypt
+ *   17 TestCreateEncryptorWithArgs    → DES-56 + Encrypt
+ *   18 TestCreateDecryptorNoArgs      → DES-56 + Decrypt
+ *   19 TestCreateDecryptorWithArgs    → DES-56 + Decrypt
+ *
+ * Section 4 – direct encrypt (findings 20–22):
+ *   20 TestEncryptCbc                → DES-56-CBC
+ *   21 TestEncryptEcb                → DES-56-ECB
+ *   22 TestEncryptCfb                → DES-56-CFB
+ *
+ * Section 5 – direct decrypt (findings 23–25):
+ *   23 TestDecryptCbc                → DES-56-CBC
+ *   24 TestDecryptEcb                → DES-56-ECB
+ *   25 TestDecryptCfb                → DES-56-CFB
+ *
+ * Section 6 – Try* variants (findings 26–31):
+ *   26 TestTryEncryptCbc             → DES-56-CBC
+ *   27 TestTryDecryptCbc             → DES-56-CBC
+ *   28 TestTryEncryptEcb             → DES-56-ECB
+ *   29 TestTryDecryptEcb             → DES-56-ECB
+ *   30 TestTryEncryptCfb             → DES-56-CFB
+ *   31 TestTryDecryptCfb             → DES-56-CFB
+ *
+ * Section 7 – key/IV generation (findings 32–33):
+ *   32 TestGenerateKey               → DES-56 + KeyGeneration
+ *   33 TestGenerateIV                → DES-56 + Generate
+ *
+ * Section 8 – combined usage patterns (findings 34–39):
+ *   34 TestDesCbcFullFlow            → DES-56-CBC + Encrypt
+ *   35 TestDesCspEncryptCbc          → DES-56-CBC
+ *   36 TestDesCspDecryptCbc          → DES-56-CBC
+ *   37 TestDesCfbFeedback            → DES-56-CFB
+ *   38 TestDesCbcWithEncryptorOverload → DES-56 + Encrypt
+ *   39 TestDesEcbEncrypt             → DES-56-ECB
+ * 
+ * + *

NOTE: unlike AES, {@code DES.asString()} uses {@code composeName(true, true, false)} — key + * length IS rendered (and {@code DES}'s no-arg constructor always seeds a default {@code + * KeyLength.ofDefault(56, ...)}), while padding is NEVER rendered. So every node string below + * carries a {@code -56} (or overridden key size) suffix, and Padding never contributes to the + * string even when a Padding value is detected as a child. Verified against actual test-run debug + * output (see {@code Algorithm.composeName} and {@code DES.java}/{@code AES.java} for the + * asymmetry) rather than guessed. + */ +class DotNetDESComprehensiveTest extends TestBase { + + @Test + void test() throws Exception { + CSharpVerifier.verify("rules/detection/dotnet/DotNetDESComprehensiveTestFile.cs", this); + } + + @Override + public void asserts( + int findingId, + @Nonnull + DetectionStore + detectionStore, + @Nonnull List nodes) { + + // Every top-level finding must be DES + assertThat(detectionStore.getDetectionValueContext()).isInstanceOf(CipherContext.class); + assertThat(detectionStore.getDetectionValues()).hasSize(1); + IValue primary = detectionStore.getDetectionValues().get(0); + assertThat(primary).isInstanceOf(ValueAction.class); + assertThat(primary.asString()).isEqualTo("DES"); + + switch (findingId) { + + // ----------------------------------------------------------------- + // Section 1: simple constructors — only DES, no children fired + // ----------------------------------------------------------------- + case 0, 1, 2 -> { + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo("DES-56"); + } + + // ----------------------------------------------------------------- + // Section 2a: property Mode setters + // ----------------------------------------------------------------- + case 3 -> assertModeFindings(detectionStore, nodes, "CBC", "DES-56-CBC"); + case 4 -> assertModeFindings(detectionStore, nodes, "ECB", "DES-56-ECB"); + case 5 -> assertModeFindings(detectionStore, nodes, "CFB", "DES-56-CFB"); + case 6 -> assertModeFindings(detectionStore, nodes, "OFB", "DES-56-OFB"); + case 7 -> assertModeFindings(detectionStore, nodes, "CTS", "DES-56-CTS"); + + // ----------------------------------------------------------------- + // Section 2b: property KeySize setter — overrides the default 56 + // ----------------------------------------------------------------- + case 8 -> assertKeySizeFindings(detectionStore, nodes, "64", "DES-64"); + + // ----------------------------------------------------------------- + // Section 2c: property Padding setters — Padding is tracked as a + // child node but DES.asString() uses composeName(true, true, false), + // so padding never contributes to the rendered string. + // ----------------------------------------------------------------- + case 9 -> assertPaddingFindings(detectionStore, nodes, "PKCS7", "DES-56"); + case 10 -> assertPaddingFindings(detectionStore, nodes, "None", "DES-56"); + case 11 -> assertPaddingFindings(detectionStore, nodes, "Zeros", "DES-56"); + case 12 -> assertPaddingFindings(detectionStore, nodes, "ANSIX923", "DES-56"); + + // ----------------------------------------------------------------- + // Section 2d: FeedbackSize setter — BlockSize(8) detected but never + // contributes to asString() (composeName has no BlockSize branch) + // ----------------------------------------------------------------- + case 13 -> { + DetectionStore fbStore = + getStoreOfValueType(BlockSize.class, detectionStore.getChildren()); + assertThat(fbStore).isNotNull(); + assertThat(fbStore.getDetectionValues()).hasSize(1); + assertThat(fbStore.getDetectionValues().get(0).asString()).isEqualTo("8"); + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo("DES-56"); + } + + // ----------------------------------------------------------------- + // Section 2e: IV and Key setters — no detection rules for these + // ----------------------------------------------------------------- + case 14, 15 -> { + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo("DES-56"); + } + + // ----------------------------------------------------------------- + // Section 3: CreateEncryptor / CreateDecryptor + // ----------------------------------------------------------------- + case 16, 17 -> assertEncryptFindings(detectionStore, nodes, "DES-56"); + case 18, 19 -> assertDecryptFindings(detectionStore, nodes, "DES-56"); + + // ----------------------------------------------------------------- + // Section 4: direct mode-specific encrypt + // ----------------------------------------------------------------- + case 20 -> + assertModePaddingFindings(detectionStore, nodes, "CBC", "PKCS7", "DES-56-CBC"); + case 21 -> + assertModePaddingFindings(detectionStore, nodes, "ECB", "None", "DES-56-ECB"); + case 22 -> + assertModePaddingFindings(detectionStore, nodes, "CFB", "None", "DES-56-CFB"); + + // ----------------------------------------------------------------- + // Section 5: direct mode-specific decrypt + // ----------------------------------------------------------------- + case 23 -> + assertModePaddingFindings(detectionStore, nodes, "CBC", "PKCS7", "DES-56-CBC"); + case 24 -> + assertModePaddingFindings(detectionStore, nodes, "ECB", "None", "DES-56-ECB"); + case 25 -> + assertModePaddingFindings(detectionStore, nodes, "CFB", "None", "DES-56-CFB"); + + // ----------------------------------------------------------------- + // Section 6: Try* variants + // ----------------------------------------------------------------- + case 26, 27 -> + assertModePaddingFindings(detectionStore, nodes, "CBC", "PKCS7", "DES-56-CBC"); + case 28, 29 -> + assertModePaddingFindings(detectionStore, nodes, "ECB", "None", "DES-56-ECB"); + case 30, 31 -> + assertModePaddingFindings(detectionStore, nodes, "CFB", "None", "DES-56-CFB"); + + // ----------------------------------------------------------------- + // Section 7: GenerateKey / GenerateIV + // ----------------------------------------------------------------- + case 32 -> { + // des.GenerateKey() → KeyGeneration functionality node + DetectionStore + genKeyStore = + getStoreOfValueType( + ValueAction.class, detectionStore.getChildren()); + assertThat(genKeyStore).isNotNull(); + assertThat(genKeyStore.getDetectionValues().get(0).asString()) + .isEqualTo("GenerateKey"); + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).getChildren().get(KeyGeneration.class)).isNotNull(); + } + case 33 -> { + // des.GenerateIV() → Generate functionality node + DetectionStore + genIvStore = + getStoreOfValueType( + ValueAction.class, detectionStore.getChildren()); + assertThat(genIvStore).isNotNull(); + assertThat(genIvStore.getDetectionValues().get(0).asString()) + .isEqualTo("GenerateIV"); + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).getChildren().get(Generate.class)).isNotNull(); + } + + // ----------------------------------------------------------------- + // Section 8: combined usage patterns + // ----------------------------------------------------------------- + case 34 -> { + // TestDesCbcFullFlow: Mode=CBC, Padding=PKCS7 (unrendered), CreateEncryptor + assertThat(nodes).hasSize(1); + INode node = nodes.get(0); + assertThat(node.getKind()).isEqualTo(BlockCipher.class); + assertThat(node.asString()).isEqualTo("DES-56-CBC"); + assertThat(node.getChildren().get(com.ibm.mapper.model.Mode.class)).isNotNull(); + assertThat(node.getChildren().get(Encrypt.class)).isNotNull(); + } + case 35 -> + assertModePaddingFindings(detectionStore, nodes, "CBC", "PKCS7", "DES-56-CBC"); + case 36 -> + assertModePaddingFindings(detectionStore, nodes, "CBC", "PKCS7", "DES-56-CBC"); + case 37 -> + assertModePaddingFindings(detectionStore, nodes, "CFB", "None", "DES-56-CFB"); + case 38 -> assertEncryptFindings(detectionStore, nodes, "DES-56"); + case 39 -> + assertModePaddingFindings(detectionStore, nodes, "ECB", "None", "DES-56-ECB"); + + default -> throw new IllegalStateException("Unexpected findingId: " + findingId); + } + } + + // ------------------------------------------------------------------------- + // Assertion helpers + // ------------------------------------------------------------------------- + + private void assertModeFindings( + @Nonnull DetectionStore store, + @Nonnull List nodes, + @Nonnull String expectedMode, + @Nonnull String expectedNodeString) { + + DetectionStore modeStore = + getStoreOfValueType(Mode.class, store.getChildren()); + assertThat(modeStore).isNotNull(); + assertThat(modeStore.getDetectionValues()).hasSize(1); + assertThat(modeStore.getDetectionValues().get(0).asString()).isEqualTo(expectedMode); + + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo(expectedNodeString); + assertThat(nodes.get(0).getChildren().get(com.ibm.mapper.model.Mode.class)).isNotNull(); + } + + private void assertKeySizeFindings( + @Nonnull DetectionStore store, + @Nonnull List nodes, + @Nonnull String expectedKeySize, + @Nonnull String expectedNodeString) { + + DetectionStore keySizeStore = + getStoreOfValueType(KeySize.class, store.getChildren()); + assertThat(keySizeStore).isNotNull(); + assertThat(keySizeStore.getDetectionValues()).hasSize(1); + assertThat(keySizeStore.getDetectionValues().get(0).asString()).isEqualTo(expectedKeySize); + + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo(expectedNodeString); + assertThat(nodes.get(0).getChildren().get(KeyLength.class)).isNotNull(); + assertThat(nodes.get(0).getChildren().get(KeyLength.class).asString()) + .isEqualTo(expectedKeySize); + } + + private void assertPaddingFindings( + @Nonnull DetectionStore store, + @Nonnull List nodes, + @Nonnull String expectedPadding, + @Nonnull String expectedNodeString) { + + DetectionStore paddingStore = + getStoreOfValueType(Padding.class, store.getChildren()); + assertThat(paddingStore).isNotNull(); + assertThat(paddingStore.getDetectionValues()).hasSize(1); + assertThat(paddingStore.getDetectionValues().get(0).asString()).isEqualTo(expectedPadding); + + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo(expectedNodeString); + } + + private void assertEncryptFindings( + @Nonnull DetectionStore store, + @Nonnull List nodes, + @Nonnull String expectedNodeString) { + + DetectionStore encryptStore = + getStoreOfValueType(CipherAction.class, store.getChildren()); + assertThat(encryptStore).isNotNull(); + assertThat(encryptStore.getDetectionValues()).hasSize(1); + assertThat(encryptStore.getDetectionValues().get(0).asString()).isEqualTo("ENCRYPT"); + + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo(expectedNodeString); + assertThat(nodes.get(0).getChildren().get(Encrypt.class)).isNotNull(); + } + + private void assertDecryptFindings( + @Nonnull DetectionStore store, + @Nonnull List nodes, + @Nonnull String expectedNodeString) { + + DetectionStore decryptStore = + getStoreOfValueType(CipherAction.class, store.getChildren()); + assertThat(decryptStore).isNotNull(); + assertThat(decryptStore.getDetectionValues()).hasSize(1); + assertThat(decryptStore.getDetectionValues().get(0).asString()).isEqualTo("DECRYPT"); + + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo(expectedNodeString); + assertThat(nodes.get(0).getChildren().get(Decrypt.class)).isNotNull(); + } + + /** + * Asserts mode+padding findings using the translated node tree. Direct-mode methods + * (EncryptCbc, TryDecryptEcb, etc.) place Mode and Padding in the same child detection store, + * so we validate via the final node string rather than per-store inspection. + */ + private void assertModePaddingFindings( + @Nonnull DetectionStore store, + @Nonnull List nodes, + @Nonnull String expectedMode, + @Nonnull String expectedPadding, + @Nonnull String expectedNodeString) { + + DetectionStore modeStore = + getStoreOfValueType(Mode.class, store.getChildren()); + assertThat(modeStore).isNotNull(); + assertThat(modeStore.getDetectionValues()) + .anySatisfy(v -> assertThat(v.asString()).isEqualTo(expectedMode)); + + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo(expectedNodeString); + } +} diff --git a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetDSAComprehensiveTest.java b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetDSAComprehensiveTest.java new file mode 100644 index 000000000..72b692b17 --- /dev/null +++ b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetDSAComprehensiveTest.java @@ -0,0 +1,188 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ibm.engine.detection.DetectionStore; +import com.ibm.engine.language.csharp.CSharpCheck; +import com.ibm.engine.language.csharp.CSharpScanContext; +import com.ibm.engine.language.csharp.CSharpSymbol; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.context.KeyContext; +import com.ibm.mapper.model.INode; +import com.ibm.mapper.model.KeyLength; +import com.ibm.mapper.model.Oid; +import com.ibm.mapper.model.Signature; +import com.ibm.mapper.model.functionality.Sign; +import com.ibm.mapper.model.functionality.Verify; +import com.ibm.plugin.CSharpVerifier; +import com.ibm.plugin.TestBase; +import java.util.List; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +/** + * Comprehensive test for all DSA-related detection rules (DotNetDSA.java). + * + *

Covers all four DSA-related classes and their complete operational API surface: + * + *

    + *
  • DSA (abstract base) + *
  • DSACng, DSACryptoServiceProvider, DSAOpenSsl (derived from DSA) + *
+ * + *

Finding mapping (one finding per test method in DotNetDSAComprehensiveTestFile.cs): + * + *

+ * Section 1 – factory methods / constructors (findings 0–5):
+ *   0 TestDsaCreate              → DSA
+ *   1 TestDsaCreateWithKeySize   → DSA
+ *   2 TestDsaCng                 → DSA
+ *   3 TestDsaCngWithKeySize      → DSA
+ *   4 TestDsaCsp                 → DSA
+ *   5 TestDsaOpenSsl             → DSA
+ *
+ * Section 2 – property KeySize setters (findings 6–8):
+ *   6 TestPropertyKeySize1024    → DSA-1024
+ *   7 TestPropertyKeySize2048    → DSA-2048
+ *   8 TestPropertyKeySize3072    → DSA-3072
+ *
+ * Section 3 – CreateSignature / TryCreateSignature (findings 9–11):
+ *   9  TestCreateSignature            → DSA + Sign
+ *   10 TestCreateSignatureWithFormat  → DSA + Sign
+ *   11 TestTryCreateSignature         → DSA + Sign
+ *
+ * Section 4 – VerifySignature (findings 12–13):
+ *   12 TestVerifySignature            → DSA + Verify
+ *   13 TestVerifySignatureWithFormat  → DSA + Verify
+ *
+ * Section 5 – SignData / TrySignData (findings 14–15):
+ *   14 TestSignData    → DSA + Sign
+ *   15 TestTrySignData → DSA + Sign
+ *
+ * Section 6 – VerifyData (finding 16):
+ *   16 TestVerifyData → DSA + Verify
+ *
+ * Section 7 – SignHash / VerifyHash (findings 17–18):
+ *   17 TestSignHash   → DSA + Sign
+ *   18 TestVerifyHash → DSA + Verify
+ *
+ * Section 8 – combined usage patterns (findings 19–21):
+ *   19 TestDsaCngFullFlow      → DSA-2048 + Sign
+ *   20 TestDsaCspVerifyFlow    → DSA + Verify
+ *   21 TestDsaOpenSslSignFlow  → DSA + Sign
+ * 
+ */ +class DotNetDSAComprehensiveTest extends TestBase { + + @Test + void test() throws Exception { + CSharpVerifier.verify("rules/detection/dotnet/DotNetDSAComprehensiveTestFile.cs", this); + } + + @Override + public void asserts( + int findingId, + @Nonnull + DetectionStore + detectionStore, + @Nonnull List nodes) { + + // Every top-level finding must be DSA + assertThat(detectionStore.getDetectionValueContext()).isInstanceOf(KeyContext.class); + assertThat(nodes).hasSize(1); + INode node = nodes.get(0); + assertThat(node.getKind()).isEqualTo(Signature.class); + assertThat(node.getChildren().get(Oid.class)).isNotNull(); + assertThat(node.getChildren().get(Oid.class).asString()).isEqualTo("1.2.840.10040.4.1"); + + switch (findingId) { + + // ----------------------------------------------------------------- + // Section 1: simple constructors — only DSA, no children fired + // ----------------------------------------------------------------- + case 0, 1, 2, 3, 4, 5 -> assertThat(node.asString()).isEqualTo("DSA"); + + // ----------------------------------------------------------------- + // Section 2: property KeySize setters + // ----------------------------------------------------------------- + case 6 -> assertKeySize(node, "1024"); + case 7 -> assertKeySize(node, "2048"); + case 8 -> assertKeySize(node, "3072"); + + // ----------------------------------------------------------------- + // Section 3: CreateSignature / TryCreateSignature + // ----------------------------------------------------------------- + case 9, 10, 11 -> assertSign(node); + + // ----------------------------------------------------------------- + // Section 4: VerifySignature + // ----------------------------------------------------------------- + case 12, 13 -> assertVerify(node); + + // ----------------------------------------------------------------- + // Section 5: SignData / TrySignData + // ----------------------------------------------------------------- + case 14, 15 -> assertSign(node); + + // ----------------------------------------------------------------- + // Section 6: VerifyData + // ----------------------------------------------------------------- + case 16 -> assertVerify(node); + + // ----------------------------------------------------------------- + // Section 7: SignHash / VerifyHash + // ----------------------------------------------------------------- + case 17 -> assertSign(node); + case 18 -> assertVerify(node); + + // ----------------------------------------------------------------- + // Section 8: combined usage patterns + // ----------------------------------------------------------------- + case 19 -> { + assertThat(node.asString()).isEqualTo("DSA-2048"); + assertThat(node.getChildren().get(KeyLength.class)).isNotNull(); + assertThat(node.getChildren().get(KeyLength.class).asString()).isEqualTo("2048"); + assertThat(node.getChildren().get(Sign.class)).isNotNull(); + } + case 20 -> assertVerify(node); + case 21 -> assertSign(node); + + default -> throw new IllegalStateException("Unexpected findingId: " + findingId); + } + } + + private void assertKeySize(@Nonnull INode node, @Nonnull String expectedKeySize) { + assertThat(node.asString()).isEqualTo("DSA-" + expectedKeySize); + assertThat(node.getChildren().get(KeyLength.class)).isNotNull(); + assertThat(node.getChildren().get(KeyLength.class).asString()).isEqualTo(expectedKeySize); + } + + private void assertSign(@Nonnull INode node) { + assertThat(node.asString()).isEqualTo("DSA"); + assertThat(node.getChildren().get(Sign.class)).isNotNull(); + } + + private void assertVerify(@Nonnull INode node) { + assertThat(node.asString()).isEqualTo("DSA"); + assertThat(node.getChildren().get(Verify.class)).isNotNull(); + } +} diff --git a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetECDiffieHellmanTest.java b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetECDiffieHellmanTest.java index 8b88a4d3a..676cfb813 100755 --- a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetECDiffieHellmanTest.java +++ b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetECDiffieHellmanTest.java @@ -31,13 +31,57 @@ import com.ibm.engine.model.context.KeyContext; import com.ibm.mapper.model.INode; import com.ibm.mapper.model.KeyAgreement; +import com.ibm.mapper.model.KeyLength; import com.ibm.mapper.model.Oid; +import com.ibm.mapper.model.functionality.Generate; +import com.ibm.mapper.model.functionality.KeyDerivation; import com.ibm.plugin.CSharpVerifier; import com.ibm.plugin.TestBase; import java.util.List; import javax.annotation.Nonnull; import org.junit.jupiter.api.Test; +/** + * Comprehensive test for all ECDH (ECDiffieHellman) related detection rules + * (DotNetECDiffieHellman.java). + * + *

Covers all three ECDiffieHellman-related classes and their complete operational API surface: + * + *

    + *
  • ECDiffieHellman (abstract base) + *
  • ECDiffieHellmanCng, ECDiffieHellmanOpenSsl (derived from ECDiffieHellman) + *
+ * + *

Note: every top-level finding translates to a {@link com.ibm.mapper.model.algorithms.ECDH} + * node ({@code node.asString()} == "ECDH"), whose kind is {@link KeyAgreement}, and which always + * carries an {@link Oid} child ("1.3.132.1.12") added unconditionally by the {@code ECDH} algorithm + * model constructor. + * + *

Finding mapping (one finding per test method in DotNetECDiffieHellmanTestFile.cs): + * + *

+ * Section 1 – factory methods / constructors (findings 0–3):
+ *   0 TestECDHCreate             → ECDH
+ *   1 TestECDHCreateWithCurve    → ECDH
+ *   2 TestECDHCng                → ECDH
+ *   3 TestECDHOpenSsl            → ECDH
+ *
+ * Section 2 – property KeySize setters (findings 4–5):
+ *   4 TestPropertyKeySize256     → ECDH, KeyLength child "256"
+ *   5 TestPropertyKeySize384     → ECDH, KeyLength child "384"
+ *
+ * Section 3 – key-derivation operations (findings 6–10):
+ *   6  TestDeriveKeyMaterial          → ECDH + KeyDerivation child
+ *   7  TestDeriveKeyFromHash          → ECDH + KeyDerivation child
+ *   8  TestDeriveKeyFromHmac          → ECDH + KeyDerivation child
+ *   9  TestDeriveKeyTls               → ECDH + KeyDerivation child
+ *   10 TestDeriveRawSecretAgreement   → ECDH + Generate child
+ *
+ * Section 4 – combined usage patterns (findings 11–12):
+ *   11 TestECDHCngFullFlow        → ECDH + KeyDerivation (+ KeyLength child "384")
+ *   12 TestECDHOpenSslDeriveFlow  → ECDH + KeyDerivation
+ * 
+ */ class DotNetECDiffieHellmanTest extends TestBase { @Test @@ -52,11 +96,13 @@ public void asserts( DetectionStore detectionStore, @Nonnull List nodes) { - assertThat(detectionStore.getDetectionValues()).hasSize(1); + + // Every top-level finding must be ECDH assertThat(detectionStore.getDetectionValueContext()).isInstanceOf(KeyContext.class); - IValue value0 = detectionStore.getDetectionValues().get(0); - assertThat(value0).isInstanceOf(ValueAction.class); - assertThat(value0.asString()).isEqualTo("ECDH"); + assertThat(detectionStore.getDetectionValues()).hasSize(1); + IValue primary = detectionStore.getDetectionValues().get(0); + assertThat(primary).isInstanceOf(ValueAction.class); + assertThat(primary.asString()).isEqualTo("ECDH"); assertThat(nodes).hasSize(1); INode node = nodes.get(0); @@ -66,5 +112,72 @@ public void asserts( INode oid = node.getChildren().get(Oid.class); assertThat(oid).isNotNull(); assertThat(oid.asString()).isEqualTo("1.3.132.1.12"); + + switch (findingId) { + + // ----------------------------------------------------------------- + // Section 1: simple constructors — only ECDH, no extra children + // ----------------------------------------------------------------- + case 0, 1, 2, 3 -> { + // node.asString() already asserted to be "ECDH" above + } + + // ----------------------------------------------------------------- + // Section 2: property KeySize setters + // ----------------------------------------------------------------- + case 4 -> assertKeySize(node, "256"); + case 5 -> assertKeySize(node, "384"); + + // ----------------------------------------------------------------- + // Section 3: key-derivation operations + // ----------------------------------------------------------------- + case 6, 7, 8, 9 -> assertKeyDerivation(detectionStore, node); + case 10 -> assertRawSecretAgreement(detectionStore, node); + + // ----------------------------------------------------------------- + // Section 4: combined usage patterns + // ----------------------------------------------------------------- + case 11 -> { + assertKeyDerivation(detectionStore, node); + assertThat(node.getChildren().get(KeyLength.class)).isNotNull(); + assertThat(node.getChildren().get(KeyLength.class).asString()).isEqualTo("384"); + } + case 12 -> assertKeyDerivation(detectionStore, node); + + default -> throw new IllegalStateException("Unexpected findingId: " + findingId); + } + } + + // ------------------------------------------------------------------------- + // Assertion helpers + // ------------------------------------------------------------------------- + + private void assertKeySize(@Nonnull INode node, @Nonnull String expectedKeySize) { + assertThat(node.getChildren().get(KeyLength.class)).isNotNull(); + assertThat(node.getChildren().get(KeyLength.class).asString()).isEqualTo(expectedKeySize); + } + + private void assertKeyDerivation( + @Nonnull DetectionStore store, + @Nonnull INode node) { + + DetectionStore deriveStore = + getStoreOfValueType(ValueAction.class, store.getChildren()); + assertThat(deriveStore).isNotNull(); + assertThat(deriveStore.getDetectionValues()).hasSize(1); + + assertThat(node.getChildren().get(KeyDerivation.class)).isNotNull(); + } + + private void assertRawSecretAgreement( + @Nonnull DetectionStore store, + @Nonnull INode node) { + + DetectionStore deriveStore = + getStoreOfValueType(ValueAction.class, store.getChildren()); + assertThat(deriveStore).isNotNull(); + assertThat(deriveStore.getDetectionValues()).hasSize(1); + + assertThat(node.getChildren().get(Generate.class)).isNotNull(); } } diff --git a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetECDsaTest.java b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetECDsaTest.java index f7337a1c3..6ca1e6f07 100755 --- a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetECDsaTest.java +++ b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetECDsaTest.java @@ -27,16 +27,67 @@ import com.ibm.engine.language.csharp.CSharpSymbol; import com.ibm.engine.language.csharp.tree.CSharpTree; import com.ibm.engine.model.IValue; +import com.ibm.engine.model.SignatureAction; import com.ibm.engine.model.ValueAction; import com.ibm.engine.model.context.KeyContext; import com.ibm.mapper.model.INode; +import com.ibm.mapper.model.KeyLength; import com.ibm.mapper.model.Signature; +import com.ibm.mapper.model.functionality.Sign; +import com.ibm.mapper.model.functionality.Verify; import com.ibm.plugin.CSharpVerifier; import com.ibm.plugin.TestBase; import java.util.List; import javax.annotation.Nonnull; import org.junit.jupiter.api.Test; +/** + * Comprehensive test for all ECDSA-related detection rules (DotNetECDsa.java). + * + *

Covers all three ECDsa-related classes and their complete operational API surface: + * + *

    + *
  • ECDsa (abstract base) + *
  • ECDsaCng, ECDsaOpenSsl (derived from ECDsa) + *
+ * + *

Note: {@code ECDSA.asString()} (see {@code mapper/model/algorithms/ECDSA.java}) only appends + * an {@code EllipticCurve} or {@code MessageDigest} suffix, matching the CycloneDX cryptography + * registry pattern {@code ECDSA[-{ellipticCurve}][-{hashAlgorithm}]} (which has no {@code + * keyLength} placeholder, unlike RSA/DSA). Therefore a detected {@code KeySize} property still + * surfaces as a {@link KeyLength} child node, but does not change {@code node.asString()}. + * + *

Finding mapping (one finding per test method in DotNetECDsaTestFile.cs): + * + *

+ * Section 1 – factory methods / constructors (findings 0–6):
+ *   0 TestECDsaCreate             → ECDSA
+ *   1 TestECDsaCreateWithCurve    → ECDSA
+ *   2 TestECDsaCng                → ECDSA
+ *   3 TestECDsaOpenSsl            → ECDSA
+ *   4 TestECDsaCngWithKey         → ECDSA (new ECDsaCng(CngKey))
+ *   5 TestECDsaCngWithCurve       → ECDSA (new ECDsaCng(ECCurve))
+ *   6 TestECDsaCngWithKeySize     → ECDSA (new ECDsaCng(int))
+ *
+ * Section 2 – property KeySize setters (findings 7–8):
+ *   7 TestPropertyKeySize256      → ECDSA, KeyLength child "256"
+ *   8 TestPropertyKeySize384      → ECDSA, KeyLength child "384"
+ *
+ * Section 3 – SignData / TrySignData / VerifyData (findings 9–11):
+ *   9  TestSignData      → ECDSA + Sign
+ *   10 TestTrySignData   → ECDSA + Sign
+ *   11 TestVerifyData    → ECDSA + Verify
+ *
+ * Section 4 – SignHash / TrySignHash / VerifyHash (findings 12–14):
+ *   12 TestSignHash     → ECDSA + Sign
+ *   13 TestTrySignHash  → ECDSA + Sign
+ *   14 TestVerifyHash   → ECDSA + Verify
+ *
+ * Section 5 – combined usage patterns (findings 15–16):
+ *   15 TestECDsaCngFullFlow        → ECDSA + Sign (+ KeyLength child "384")
+ *   16 TestECDsaOpenSslVerifyFlow  → ECDSA + Verify
+ * 
+ */ class DotNetECDsaTest extends TestBase { @Test @@ -51,18 +102,92 @@ public void asserts( DetectionStore detectionStore, @Nonnull List nodes) { - assertThat(detectionStore.getDetectionValues()).hasSize(1); + + // Every top-level finding must be ECDSA assertThat(detectionStore.getDetectionValueContext()).isInstanceOf(KeyContext.class); - IValue value0 = detectionStore.getDetectionValues().get(0); - assertThat(value0).isInstanceOf(ValueAction.class); - assertThat(value0.asString()).isEqualTo("ECDSA"); + assertThat(detectionStore.getDetectionValues()).hasSize(1); + IValue primary = detectionStore.getDetectionValues().get(0); + assertThat(primary).isInstanceOf(ValueAction.class); + assertThat(primary.asString()).isEqualTo("ECDSA"); - /* - * Translation - */ assertThat(nodes).hasSize(1); INode node = nodes.get(0); assertThat(node.getKind()).isEqualTo(Signature.class); assertThat(node.asString()).isEqualTo("ECDSA"); + + switch (findingId) { + + // ----------------------------------------------------------------- + // Section 1: simple constructors — only ECDSA, no children fired + // ----------------------------------------------------------------- + case 0, 1, 2, 3, 4, 5, 6 -> { + // node.asString() already asserted to be "ECDSA" above + } + + // ----------------------------------------------------------------- + // Section 2: property KeySize setters + // ----------------------------------------------------------------- + case 7 -> assertKeySize(node, "256"); + case 8 -> assertKeySize(node, "384"); + + // ----------------------------------------------------------------- + // Section 3: SignData / TrySignData / VerifyData + // ----------------------------------------------------------------- + case 9, 10 -> assertSign(detectionStore, node); + case 11 -> assertVerify(detectionStore, node); + + // ----------------------------------------------------------------- + // Section 4: SignHash / TrySignHash / VerifyHash + // ----------------------------------------------------------------- + case 12, 13 -> assertSign(detectionStore, node); + case 14 -> assertVerify(detectionStore, node); + + // ----------------------------------------------------------------- + // Section 5: combined usage patterns + // ----------------------------------------------------------------- + case 15 -> { + assertSign(detectionStore, node); + assertThat(node.getChildren().get(KeyLength.class)).isNotNull(); + assertThat(node.getChildren().get(KeyLength.class).asString()).isEqualTo("384"); + } + case 16 -> assertVerify(detectionStore, node); + + default -> throw new IllegalStateException("Unexpected findingId: " + findingId); + } + } + + // ------------------------------------------------------------------------- + // Assertion helpers + // ------------------------------------------------------------------------- + + private void assertKeySize(@Nonnull INode node, @Nonnull String expectedKeySize) { + assertThat(node.getChildren().get(KeyLength.class)).isNotNull(); + assertThat(node.getChildren().get(KeyLength.class).asString()).isEqualTo(expectedKeySize); + } + + private void assertSign( + @Nonnull DetectionStore store, + @Nonnull INode node) { + + DetectionStore signStore = + getStoreOfValueType(SignatureAction.class, store.getChildren()); + assertThat(signStore).isNotNull(); + assertThat(signStore.getDetectionValues()).hasSize(1); + assertThat(signStore.getDetectionValues().get(0).asString()).isEqualTo("SIGN"); + + assertThat(node.getChildren().get(Sign.class)).isNotNull(); + } + + private void assertVerify( + @Nonnull DetectionStore store, + @Nonnull INode node) { + + DetectionStore verifyStore = + getStoreOfValueType(SignatureAction.class, store.getChildren()); + assertThat(verifyStore).isNotNull(); + assertThat(verifyStore.getDetectionValues()).hasSize(1); + assertThat(verifyStore.getDetectionValues().get(0).asString()).isEqualTo("VERIFY"); + + assertThat(node.getChildren().get(Verify.class)).isNotNull(); } } diff --git a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetHMACTest.java b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetHMACTest.java index 261e1dfc6..09b10e7fd 100755 --- a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetHMACTest.java +++ b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetHMACTest.java @@ -29,6 +29,7 @@ import com.ibm.engine.model.IValue; import com.ibm.engine.model.ValueAction; import com.ibm.engine.model.context.MacContext; +import com.ibm.mapper.model.BlockSize; import com.ibm.mapper.model.DigestSize; import com.ibm.mapper.model.INode; import com.ibm.mapper.model.Mac; @@ -39,6 +40,26 @@ import javax.annotation.Nonnull; import org.junit.jupiter.api.Test; +/** + * Tests for {@link DotNetHMAC}. + * + *

findingId → test method → expected translated node ({@code asString()}), in source order of + * {@code DotNetHMACTestFile.cs}: + * + *

    + *
  • 0 → {@code TestHmacSha1} → {@code HMAC-SHA-1} + *
  • 1 → {@code TestHmacSha256} → {@code HMAC-SHA-256} + *
  • 2 → {@code TestHmacSha384} → {@code HMAC-SHA-384} + *
  • 3 → {@code TestHmacSha512} → {@code HMAC-SHA-512} + *
  • 4 → {@code TestHmacMd5} → {@code HMAC-MD5} + *
  • 5 → {@code TestHmacRipemd160} → {@code HMAC-RIPEMD} (digest child: {@code RIPEMD-160}) + *
  • 6 → {@code TestHmacSha3_256} → {@code HMAC-SHA3-256} + *
  • 7 → {@code TestHmacSha3_384} → {@code HMAC-SHA3-384} + *
  • 8 → {@code TestHmacSha3_512} → {@code HMAC-SHA3-512} + *
  • 9 → {@code TestMacTripleDes} → {@code DESede} (not an HMAC(digest) node; see {@link + * DotNetHMAC} javadoc for why {@code MACTripleDES} is modeled as DESede-as-Mac) + *
+ */ class DotNetHMACTest extends TestBase { @Test @@ -120,6 +141,55 @@ public void asserts( assertThat(digestSize).isNotNull(); assertThat(digestSize.asString()).isEqualTo("128"); } + case 5 -> { + assertThat(value0.asString()).isEqualTo("HMACRIPEMD160"); + assertThat(node.asString()).isEqualTo("HMAC-RIPEMD"); + INode digest = node.getChildren().get(MessageDigest.class); + assertThat(digest).isNotNull(); + assertThat(digest.asString()).isEqualTo("RIPEMD-160"); + INode digestSize = digest.getChildren().get(DigestSize.class); + assertThat(digestSize).isNotNull(); + assertThat(digestSize.asString()).isEqualTo("160"); + } + case 6 -> { + assertThat(value0.asString()).isEqualTo("HMACSHA3_256"); + assertThat(node.asString()).isEqualTo("HMAC-SHA3-256"); + INode digest = node.getChildren().get(MessageDigest.class); + assertThat(digest).isNotNull(); + assertThat(digest.asString()).isEqualTo("SHA3-256"); + INode digestSize = digest.getChildren().get(DigestSize.class); + assertThat(digestSize).isNotNull(); + assertThat(digestSize.asString()).isEqualTo("256"); + } + case 7 -> { + assertThat(value0.asString()).isEqualTo("HMACSHA3_384"); + assertThat(node.asString()).isEqualTo("HMAC-SHA3-384"); + INode digest = node.getChildren().get(MessageDigest.class); + assertThat(digest).isNotNull(); + assertThat(digest.asString()).isEqualTo("SHA3-384"); + INode digestSize = digest.getChildren().get(DigestSize.class); + assertThat(digestSize).isNotNull(); + assertThat(digestSize.asString()).isEqualTo("384"); + } + case 8 -> { + assertThat(value0.asString()).isEqualTo("HMACSHA3_512"); + assertThat(node.asString()).isEqualTo("HMAC-SHA3-512"); + INode digest = node.getChildren().get(MessageDigest.class); + assertThat(digest).isNotNull(); + assertThat(digest.asString()).isEqualTo("SHA3-512"); + INode digestSize = digest.getChildren().get(DigestSize.class); + assertThat(digestSize).isNotNull(); + assertThat(digestSize.asString()).isEqualTo("512"); + } + case 9 -> { + // MACTripleDES is not HMAC-based: it translates to the DESede algorithm + // reinterpreted "as" a Mac (see DotNetHMAC javadoc), not to an HMAC(digest) node. + assertThat(value0.asString()).isEqualTo("MACTRIPLEDES"); + assertThat(node.asString()).isEqualTo("DESede"); + INode blockSize = node.getChildren().get(BlockSize.class); + assertThat(blockSize).isNotNull(); + assertThat(blockSize.asString()).isEqualTo("64"); + } default -> throw new IllegalStateException("Unexpected findingId: " + findingId); } } diff --git a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetKMACTest.java b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetKMACTest.java new file mode 100644 index 000000000..47ee60304 --- /dev/null +++ b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetKMACTest.java @@ -0,0 +1,106 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ibm.engine.detection.DetectionStore; +import com.ibm.engine.language.csharp.CSharpCheck; +import com.ibm.engine.language.csharp.CSharpScanContext; +import com.ibm.engine.language.csharp.CSharpSymbol; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.IValue; +import com.ibm.engine.model.ValueAction; +import com.ibm.engine.model.context.MacContext; +import com.ibm.mapper.model.INode; +import com.ibm.mapper.model.Mac; +import com.ibm.plugin.CSharpVerifier; +import com.ibm.plugin.TestBase; +import java.util.List; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link DotNetKMAC}. + * + *

findingId → test method → expected translated node ({@code asString()}), in source order of + * {@code DotNetKMACTestFile.cs}: + * + *

    + *
  • 0 → {@code TestKmac128} → {@code KMAC128} + *
  • 1 → {@code TestKmac256} → {@code KMAC256} + *
  • 2 → {@code TestKmacXof128} → {@code KMAC128} (raw detected value is {@code KMACXOF128}; the + * translated node collapses to the same {@code KMAC} model as {@code Kmac128} — see {@link + * DotNetKMAC} javadoc "Known modeling gap" section) + *
  • 3 → {@code TestKmacXof256} → {@code KMAC256} (raw detected value is {@code KMACXOF256}; + * same collapse as above, for the 256-bit pair) + *
+ */ +class DotNetKMACTest extends TestBase { + + @Test + void test() throws Exception { + CSharpVerifier.verify("rules/detection/dotnet/DotNetKMACTestFile.cs", this); + } + + @Override + public void asserts( + int findingId, + @Nonnull + DetectionStore + detectionStore, + @Nonnull List nodes) { + + /* + * Detection Store + */ + assertThat(detectionStore.getDetectionValues()).hasSize(1); + assertThat(detectionStore.getDetectionValueContext()).isInstanceOf(MacContext.class); + IValue value0 = detectionStore.getDetectionValues().get(0); + assertThat(value0).isInstanceOf(ValueAction.class); + + /* + * Translation + */ + assertThat(nodes).hasSize(1); + INode node = nodes.get(0); + assertThat(node.getKind()).isEqualTo(Mac.class); + + switch (findingId) { + case 0 -> { + assertThat(value0.asString()).isEqualTo("KMAC128"); + assertThat(node.asString()).isEqualTo("KMAC128"); + } + case 1 -> { + assertThat(value0.asString()).isEqualTo("KMAC256"); + assertThat(node.asString()).isEqualTo("KMAC256"); + } + case 2 -> { + assertThat(value0.asString()).isEqualTo("KMACXOF128"); + assertThat(node.asString()).isEqualTo("KMAC128"); + } + case 3 -> { + assertThat(value0.asString()).isEqualTo("KMACXOF256"); + assertThat(node.asString()).isEqualTo("KMAC256"); + } + default -> throw new IllegalStateException("Unexpected findingId: " + findingId); + } + } +} diff --git a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetKeyDerivationTest.java b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetKeyDerivationTest.java new file mode 100644 index 000000000..79ab41716 --- /dev/null +++ b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetKeyDerivationTest.java @@ -0,0 +1,200 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ibm.engine.detection.DetectionStore; +import com.ibm.engine.language.csharp.CSharpCheck; +import com.ibm.engine.language.csharp.CSharpScanContext; +import com.ibm.engine.language.csharp.CSharpSymbol; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.Algorithm; +import com.ibm.engine.model.IValue; +import com.ibm.engine.model.IterationCount; +import com.ibm.engine.model.ValueAction; +import com.ibm.engine.model.context.KeyContext; +import com.ibm.mapper.model.INode; +import com.ibm.mapper.model.KeyDerivationFunction; +import com.ibm.mapper.model.MessageDigest; +import com.ibm.mapper.model.NumberOfIterations; +import com.ibm.mapper.model.PasswordBasedKeyDerivationFunction; +import com.ibm.mapper.model.functionality.KeyDerivation; +import com.ibm.plugin.CSharpVerifier; +import com.ibm.plugin.TestBase; +import java.util.List; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +/** + * Comprehensive test for all KDF-family detection rules (DotNetKeyDerivation.java), excluding + * {@code Rfc2898DeriveBytes} (covered separately by {@code DotNetRfc2898DeriveBytesTest}). + * + *

Covers: + * + *

    + *
  • {@code HKDF} — static-only class ({@code Extract}, {@code Expand}, {@code DeriveKey}) + *
  • {@code SP800108HmacCounterKdf} — constructor + instance {@code DeriveKey}, and the static + * one-shot {@code DeriveBytes} overload + *
  • {@code PasswordDeriveBytes} — constructor + instance {@code GetBytes} / {@code + * CryptDeriveKey} + *
+ * + *

Node strings below were captured from an actual test run's debug log ({@code + * target/node-tree.log}), not guessed: + * + *

+ * Finding mapping (one finding per test method in DotNetKeyDerivationTestFile.cs):
+ *
+ * 0 TestHkdfExtract                     → KeyDerivationFunction "HKDF"
+ * 1 TestHkdfExpand                      → KeyDerivationFunction "HKDF"
+ * 2 TestHkdfDeriveKey                   → KeyDerivationFunction "HKDF"
+ * 3 TestSp800108CtorAndDeriveKey        → KeyDerivationFunction "SP800_108_CounterKDF"
+ *                                          + KeyDerivation "KEYDERIVATION" child
+ * 4 TestSp800108StaticDeriveBytes       → KeyDerivationFunction "SP800_108_CounterKDF"
+ *                                          (no children — static one-shot call)
+ * 5 TestPasswordDeriveBytesGetBytes     → PasswordBasedKeyDerivationFunction "PBKDF1"
+ *                                          + KeyDerivation "KEYDERIVATION" child
+ * 6 TestPasswordDeriveBytesCryptDeriveKey → PasswordBasedKeyDerivationFunction "PBKDF1"
+ *                                          + KeyDerivation "KEYDERIVATION" child
+ * 7 TestPasswordDeriveBytesProperties    → PasswordBasedKeyDerivationFunction "PBKDF1-SHA-256"
+ *                                          + NumberOfIterations "100000" child
+ *                                          + MessageDigest "SHA-256" child (from set_HashName)
+ * 
+ */ +class DotNetKeyDerivationTest extends TestBase { + + @Test + void test() throws Exception { + CSharpVerifier.verify("rules/detection/dotnet/DotNetKeyDerivationTestFile.cs", this); + } + + @Override + public void asserts( + int findingId, + @Nonnull + DetectionStore + detectionStore, + @Nonnull List nodes) { + + assertThat(detectionStore.getDetectionValueContext()).isInstanceOf(KeyContext.class); + assertThat(detectionStore.getDetectionValues()).hasSize(1); + IValue primary = detectionStore.getDetectionValues().get(0); + assertThat(primary).isInstanceOf(ValueAction.class); + + assertThat(nodes).hasSize(1); + INode node = nodes.get(0); + + switch (findingId) { + // ----------------------------------------------------------------- + // Section 1: HKDF static methods — no depending rules, no children + // ----------------------------------------------------------------- + case 0, 1, 2 -> { + assertThat(primary.asString()).isEqualTo("HKDF"); + assertThat(node.getKind()).isEqualTo(KeyDerivationFunction.class); + assertThat(node.asString()).isEqualTo("HKDF"); + assertThat(node.getChildren()).isEmpty(); + } + + // ----------------------------------------------------------------- + // Section 2: SP800108HmacCounterKdf + // ----------------------------------------------------------------- + case 3 -> { + assertThat(primary.asString()).isEqualTo("SP800108"); + assertThat(node.getKind()).isEqualTo(KeyDerivationFunction.class); + assertThat(node.asString()).isEqualTo("SP800_108_CounterKDF"); + assertKeyDerivationChild(detectionStore, node); + } + case 4 -> { + assertThat(primary.asString()).isEqualTo("SP800108"); + assertThat(node.getKind()).isEqualTo(KeyDerivationFunction.class); + assertThat(node.asString()).isEqualTo("SP800_108_CounterKDF"); + assertThat(node.getChildren()).isEmpty(); + } + + // ----------------------------------------------------------------- + // Section 3: PasswordDeriveBytes + // ----------------------------------------------------------------- + case 5, 6 -> { + assertThat(primary.asString()).isEqualTo("PBKDF1"); + assertThat(node.getKind()).isEqualTo(PasswordBasedKeyDerivationFunction.class); + assertThat(node.asString()).isEqualTo("PBKDF1"); + assertKeyDerivationChild(detectionStore, node); + } + case 7 -> { + /* + * TestPasswordDeriveBytesProperties: pdb.IterationCount = 100000; + * pdb.HashName = "SHA256"; — property-setter depending rules + */ + assertThat(primary.asString()).isEqualTo("PBKDF1"); + assertThat(node.getKind()).isEqualTo(PasswordBasedKeyDerivationFunction.class); + + // Depending rule: set_IterationCount detected IterationCount(100000) + DetectionStore + iterationStore = + getStoreOfValueType( + IterationCount.class, detectionStore.getChildren()); + assertThat(iterationStore).isNotNull(); + assertThat(iterationStore.getDetectionValues()).hasSize(1); + assertThat(iterationStore.getDetectionValues().get(0).asString()) + .isEqualTo("100000"); + + // Depending rule: set_HashName detected Algorithm("SHA256") + DetectionStore + hashNameStore = + getStoreOfValueType(Algorithm.class, detectionStore.getChildren()); + assertThat(hashNameStore).isNotNull(); + assertThat(hashNameStore.getDetectionValues()).hasSize(1); + assertThat(hashNameStore.getDetectionValues().get(0).asString()) + .isEqualTo("SHA256"); + + // Translation: PBKDF1 node with NumberOfIterations + MessageDigest children + // (PBKDF1#asString() appends the digest child's name, per its own source) + assertThat(node.asString()).isEqualTo("PBKDF1-SHA-256"); + INode iterations = node.getChildren().get(NumberOfIterations.class); + assertThat(iterations).isNotNull(); + assertThat(iterations.asString()).isEqualTo("100000"); + INode digest = node.getChildren().get(MessageDigest.class); + assertThat(digest).isNotNull(); + assertThat(digest.asString()).isEqualTo("SHA-256"); + } + + default -> throw new IllegalStateException("Unexpected findingId: " + findingId); + } + } + + // ------------------------------------------------------------------------- + // Assertion helpers + // ------------------------------------------------------------------------- + + private void assertKeyDerivationChild( + @Nonnull DetectionStore store, + @Nonnull INode node) { + + DetectionStore deriveStore = + getStoreOfValueType(ValueAction.class, store.getChildren()); + assertThat(deriveStore).isNotNull(); + assertThat(deriveStore.getDetectionValues()).hasSize(1); + + assertThat(node.getChildren().get(KeyDerivation.class)).isNotNull(); + assertThat(node.getChildren().get(KeyDerivation.class).asString()) + .isEqualTo("KEYDERIVATION"); + } +} diff --git a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetLegacyFormattersTest.java b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetLegacyFormattersTest.java new file mode 100644 index 000000000..4b3c18e92 --- /dev/null +++ b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetLegacyFormattersTest.java @@ -0,0 +1,264 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ibm.engine.detection.DetectionStore; +import com.ibm.engine.language.csharp.CSharpCheck; +import com.ibm.engine.language.csharp.CSharpScanContext; +import com.ibm.engine.language.csharp.CSharpSymbol; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.CipherAction; +import com.ibm.engine.model.IValue; +import com.ibm.engine.model.SignatureAction; +import com.ibm.engine.model.ValueAction; +import com.ibm.engine.model.context.KeyContext; +import com.ibm.mapper.model.INode; +import com.ibm.mapper.model.MaskGenerationFunction; +import com.ibm.mapper.model.MessageDigest; +import com.ibm.mapper.model.Oid; +import com.ibm.mapper.model.Padding; +import com.ibm.mapper.model.PublicKeyEncryption; +import com.ibm.mapper.model.Signature; +import com.ibm.mapper.model.functionality.Decrypt; +import com.ibm.mapper.model.functionality.Encrypt; +import com.ibm.mapper.model.functionality.Sign; +import com.ibm.mapper.model.functionality.Verify; +import com.ibm.plugin.CSharpVerifier; +import com.ibm.plugin.TestBase; +import java.util.List; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +/** + * Test for the legacy Formatter/Deformatter and mask-generation detection rules + * (DotNetLegacyFormatters.java). + * + *

Finding mapping (one finding per test method in DotNetLegacyFormattersTestFile.cs; the + * parameterless constructor overload is used throughout, so — unlike DotNetRSATest/DotNetDSATest — + * no extra findings from DSA.Create()/RSA.Create() are interleaved): + * + *

+ * 0  TestDsaSignatureFormatterCreateSignature          → DSA + Sign
+ * 1  TestDsaSignatureFormatterSetHashAlgorithm         → DSA-SHA-1 + MessageDigest(SHA-1)
+ * 2  TestDsaSignatureDeformatterVerifySignature        → DSA + Verify
+ * 3  TestRsaPkcs1SignatureFormatterCreateSignature     → RSA + Sign
+ * 4  TestRsaPkcs1SignatureFormatterSetHashAlgorithm    → RSA + MessageDigest(SHA-256)
+ * 5  TestRsaPkcs1SignatureDeformatterVerifySignature   → RSA + Verify
+ * 6  TestRsaOaepKeyExchangeFormatterCreateKeyExchange1 → RSA-OAEP + Encrypt + Padding(OAEP)
+ * 7  TestRsaOaepKeyExchangeFormatterCreateKeyExchange2 → RSA-OAEP + Encrypt + Padding(OAEP)
+ * 8  TestRsaOaepKeyExchangeDeformatterDecryptKeyExchange → RSA-OAEP + Decrypt + Padding(OAEP)
+ * 9  TestRsaPkcs1KeyExchangeFormatterCreateKeyExchange1  → RSA + Encrypt + Padding(PKCS1)
+ * 10 TestRsaPkcs1KeyExchangeFormatterCreateKeyExchange2  → RSA + Encrypt + Padding(PKCS1)
+ * 11 TestRsaPkcs1KeyExchangeDeformatterDecryptKeyExchange → RSA + Decrypt + Padding(PKCS1)
+ * 12 TestPkcs1MaskGenerationMethodCreate                → MGF1
+ * 13 TestPkcs1MaskGenerationMethodSetHashName           → MGF1 + MessageDigest(SHA-256)
+ * 
+ * + * These node-string / OID values were captured by first running this test with a no-op {@code + * asserts()} and inspecting {@code target/node-tree.log}, per the project's testing guidelines + * (never guessing {@code asString()} output). + */ +class DotNetLegacyFormattersTest extends TestBase { + + @Test + void test() throws Exception { + CSharpVerifier.verify("rules/detection/dotnet/DotNetLegacyFormattersTestFile.cs", this); + } + + @Override + public void asserts( + int findingId, + @Nonnull + DetectionStore + detectionStore, + @Nonnull List nodes) { + + // Every top-level finding in this file is a KeyContext ValueAction (DSA/RSA/MGF1) + assertThat(detectionStore.getDetectionValueContext()).isInstanceOf(KeyContext.class); + assertThat(detectionStore.getDetectionValues()).hasSize(1); + IValue primary = detectionStore.getDetectionValues().get(0); + assertThat(primary).isInstanceOf(ValueAction.class); + + assertThat(nodes).hasSize(1); + INode node = nodes.get(0); + + switch (findingId) { + case 0 -> { + assertThat(primary.asString()).isEqualTo("DSA"); + assertThat(node.getKind()).isEqualTo(Signature.class); + assertThat(node.asString()).isEqualTo("DSA"); + assertOid(node, "1.2.840.10040.4.1"); + assertSign(detectionStore, node); + } + case 1 -> { + assertThat(primary.asString()).isEqualTo("DSA"); + assertThat(node.getKind()).isEqualTo(Signature.class); + assertThat(node.asString()).isEqualTo("DSA-SHA-1"); + assertOid(node, "1.2.840.10040.4.3"); + assertDigest(node, "SHA-1"); + } + case 2 -> { + assertThat(primary.asString()).isEqualTo("DSA"); + assertThat(node.getKind()).isEqualTo(Signature.class); + assertThat(node.asString()).isEqualTo("DSA"); + assertOid(node, "1.2.840.10040.4.1"); + assertVerify(detectionStore, node); + } + case 3 -> { + assertThat(primary.asString()).isEqualTo("RSA"); + assertThat(node.getKind()).isEqualTo(PublicKeyEncryption.class); + assertThat(node.asString()).isEqualTo("RSA"); + assertOid(node, "1.2.840.113549.1.1.1"); + assertSign(detectionStore, node); + } + case 4 -> { + assertThat(primary.asString()).isEqualTo("RSA"); + assertThat(node.getKind()).isEqualTo(PublicKeyEncryption.class); + assertThat(node.asString()).isEqualTo("RSA"); + assertOid(node, "1.2.840.113549.1.1.1"); + assertDigest(node, "SHA-256"); + } + case 5 -> { + assertThat(primary.asString()).isEqualTo("RSA"); + assertThat(node.getKind()).isEqualTo(PublicKeyEncryption.class); + assertThat(node.asString()).isEqualTo("RSA"); + assertOid(node, "1.2.840.113549.1.1.1"); + assertVerify(detectionStore, node); + } + case 6, 7 -> { + assertThat(primary.asString()).isEqualTo("RSA"); + assertThat(node.getKind()).isEqualTo(PublicKeyEncryption.class); + assertThat(node.asString()).isEqualTo("RSA-OAEP"); + assertOid(node, "1.2.840.113549.1.1.7"); + assertPadding(node, "OAEP"); + assertEncrypt(detectionStore, node); + } + case 8 -> { + assertThat(primary.asString()).isEqualTo("RSA"); + assertThat(node.getKind()).isEqualTo(PublicKeyEncryption.class); + assertThat(node.asString()).isEqualTo("RSA-OAEP"); + assertOid(node, "1.2.840.113549.1.1.7"); + assertPadding(node, "OAEP"); + assertDecrypt(detectionStore, node); + } + case 9, 10 -> { + assertThat(primary.asString()).isEqualTo("RSA"); + assertThat(node.getKind()).isEqualTo(PublicKeyEncryption.class); + assertThat(node.asString()).isEqualTo("RSA"); + assertOid(node, "1.2.840.113549.1.1.1"); + assertPadding(node, "PKCS1"); + assertEncrypt(detectionStore, node); + } + case 11 -> { + assertThat(primary.asString()).isEqualTo("RSA"); + assertThat(node.getKind()).isEqualTo(PublicKeyEncryption.class); + assertThat(node.asString()).isEqualTo("RSA"); + assertOid(node, "1.2.840.113549.1.1.1"); + assertPadding(node, "PKCS1"); + assertDecrypt(detectionStore, node); + } + case 12 -> { + assertThat(primary.asString()).isEqualTo("MGF1"); + assertThat(node.getKind()).isEqualTo(MaskGenerationFunction.class); + assertThat(node.asString()).isEqualTo("MGF1"); + assertOid(node, "1.2.840.113549.1.1.8"); + } + case 13 -> { + assertThat(primary.asString()).isEqualTo("MGF1"); + assertThat(node.getKind()).isEqualTo(MaskGenerationFunction.class); + assertThat(node.asString()).isEqualTo("MGF1"); + assertOid(node, "1.2.840.113549.1.1.8"); + assertDigest(node, "SHA-256"); + } + default -> throw new IllegalStateException("Unexpected findingId: " + findingId); + } + } + + // ------------------------------------------------------------------------- + // Assertion helpers + // ------------------------------------------------------------------------- + + private void assertOid(@Nonnull INode node, @Nonnull String expectedOid) { + INode oid = node.getChildren().get(Oid.class); + assertThat(oid).isNotNull(); + assertThat(oid.asString()).isEqualTo(expectedOid); + } + + private void assertDigest(@Nonnull INode node, @Nonnull String expectedDigest) { + INode digest = node.getChildren().get(MessageDigest.class); + assertThat(digest).isNotNull(); + assertThat(digest.asString()).isEqualTo(expectedDigest); + } + + private void assertPadding(@Nonnull INode node, @Nonnull String expectedPadding) { + INode padding = node.getChildren().get(Padding.class); + assertThat(padding).isNotNull(); + assertThat(padding.asString()).isEqualTo(expectedPadding); + } + + private void assertSign( + @Nonnull DetectionStore store, + @Nonnull INode node) { + DetectionStore signStore = + getStoreOfValueType(SignatureAction.class, store.getChildren()); + assertThat(signStore).isNotNull(); + assertThat(signStore.getDetectionValues()).hasSize(1); + assertThat(signStore.getDetectionValues().get(0).asString()).isEqualTo("SIGN"); + assertThat(node.getChildren().get(Sign.class)).isNotNull(); + } + + private void assertVerify( + @Nonnull DetectionStore store, + @Nonnull INode node) { + DetectionStore verifyStore = + getStoreOfValueType(SignatureAction.class, store.getChildren()); + assertThat(verifyStore).isNotNull(); + assertThat(verifyStore.getDetectionValues()).hasSize(1); + assertThat(verifyStore.getDetectionValues().get(0).asString()).isEqualTo("VERIFY"); + assertThat(node.getChildren().get(Verify.class)).isNotNull(); + } + + private void assertEncrypt( + @Nonnull DetectionStore store, + @Nonnull INode node) { + // The CipherAction and the constant Padding value (see DotNetLegacyFormatters's + // CREATE_KEY_EXCHANGE_* rules) are captured by the same rule/level, so this child store's + // detection values contains both — unlike DotNetRSA's plain Encrypt/Decrypt rules. + DetectionStore encryptStore = + getStoreOfValueType(CipherAction.class, store.getChildren()); + assertThat(encryptStore).isNotNull(); + assertThat(encryptStore.getDetectionValues()) + .anyMatch(value -> "ENCRYPT".equals(value.asString())); + assertThat(node.getChildren().get(Encrypt.class)).isNotNull(); + } + + private void assertDecrypt( + @Nonnull DetectionStore store, + @Nonnull INode node) { + DetectionStore decryptStore = + getStoreOfValueType(CipherAction.class, store.getChildren()); + assertThat(decryptStore).isNotNull(); + assertThat(decryptStore.getDetectionValues()) + .anyMatch(value -> "DECRYPT".equals(value.asString())); + assertThat(node.getChildren().get(Decrypt.class)).isNotNull(); + } +} diff --git a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetMLDsaTest.java b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetMLDsaTest.java new file mode 100644 index 000000000..c0cc117e5 --- /dev/null +++ b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetMLDsaTest.java @@ -0,0 +1,274 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ibm.engine.detection.DetectionStore; +import com.ibm.engine.language.csharp.CSharpCheck; +import com.ibm.engine.language.csharp.CSharpScanContext; +import com.ibm.engine.language.csharp.CSharpSymbol; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.IValue; +import com.ibm.engine.model.ParameterIdentifier; +import com.ibm.engine.model.ValueAction; +import com.ibm.engine.model.context.KeyContext; +import com.ibm.mapper.model.INode; +import com.ibm.mapper.model.Oid; +import com.ibm.mapper.model.ParameterSetIdentifier; +import com.ibm.mapper.model.Signature; +import com.ibm.mapper.model.functionality.Sign; +import com.ibm.mapper.model.functionality.Verify; +import com.ibm.plugin.CSharpVerifier; +import com.ibm.plugin.TestBase; +import java.util.List; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +/** + * Comprehensive test for all ML-DSA and Composite ML-DSA related detection rules + * (DotNetMLDsa.java). + * + *

Covers the {@code MLDsa} abstract base (all of its static factory methods), the {@code + * MLDsaCng}/{@code MLDsaOpenSsl} native-interop derived classes, the {@code SignData}/{@code + * VerifyData}/{@code SignMu}/{@code VerifyMu}/{@code SignPreHash}/{@code VerifyPreHash} instance + * operations, and the {@code CompositeMLDsa}/{@code CompositeMLDsaCng} counterparts. + * + *

Every top-level finding translates to a {@link com.ibm.mapper.model.algorithms.MLDSA} node + * (kind {@link Signature}). When the {@code MLDsaAlgorithm} parameter set is captured, {@code + * node.asString()} is {@code "ML-DSA-44"}/{@code "ML-DSA-65"}/{@code "ML-DSA-87"}, the node carries + * a {@link ParameterSetIdentifier} child and — added by the enricher, confirmed from the actual + * debug output — an {@link Oid} child. For {@code CompositeMLDsa}, the full {@code + * CompositeMLDsaAlgorithm} name (e.g. {@code "MLDsa44WithECDsaP256"}) is captured verbatim as the + * {@link ParameterSetIdentifier} (see DotNetMLDsa's class javadoc for why), which the enricher does + * not recognize, so it falls back to the generic NIST signature base OID. + * + *

Finding mapping (one finding per test method in DotNetMLDsaTestFile.cs; verified against the + * actual {@code target/node-tree.log} output of a debug run, not guessed): + * + *

+ * Section 1 – MLDsa algorithm-parameterized creation (findings 0–5):
+ *   0 TestGenerateKey44            → ML-DSA-44
+ *   1 TestGenerateKey65            → ML-DSA-65
+ *   2 TestGenerateKey87            → ML-DSA-87
+ *   3 TestImportMLDsaPrivateKey    → ML-DSA-65
+ *   4 TestImportMLDsaPrivateSeed   → ML-DSA-44
+ *   5 TestImportMLDsaPublicKey     → ML-DSA-87
+ *
+ * Section 2 – MLDsa structural imports, no parameter set (findings 6–10):
+ *   6  TestImportPkcs8PrivateKey          → ML-DSA (generic)
+ *   7  TestImportSubjectPublicKeyInfo     → ML-DSA (generic)
+ *   8  TestImportFromPem                  → ML-DSA (generic)
+ *   9  TestImportEncryptedPkcs8PrivateKey → ML-DSA (generic)
+ *   10 TestImportFromEncryptedPem         → ML-DSA (generic)
+ *
+ * Section 3 – MLDsa native-interop constructors, no parameter set (findings 11–12):
+ *   11 TestMLDsaCng      → ML-DSA (generic)
+ *   12 TestMLDsaOpenSsl  → ML-DSA (generic)
+ *
+ * Section 4 – MLDsa Sign / Verify operations (findings 13–18):
+ *   13 TestSignData      → ML-DSA-65, Sign child
+ *   14 TestVerifyData    → ML-DSA-65, Verify child
+ *   15 TestSignMu        → ML-DSA-65, Sign child
+ *   16 TestVerifyMu      → ML-DSA-65, Verify child
+ *   17 TestSignPreHash   → ML-DSA-65, Sign child
+ *   18 TestVerifyPreHash → ML-DSA-65, Verify child
+ *
+ * Section 5 – CompositeMLDsa algorithm-parameterized creation (findings 19–21):
+ *   19 TestCompositeGenerateKey       → ML-DSA-MLDsa44WithECDsaP256
+ *   20 TestCompositeImportPrivateKey  → ML-DSA-MLDsa65WithRSA3072Pss
+ *   21 TestCompositeImportPublicKey   → ML-DSA-MLDsa87WithEd448
+ *
+ * Section 6 – CompositeMLDsa structural imports, no parameter set (findings 22–26):
+ *   22 TestCompositeImportPkcs8PrivateKey          → ML-DSA (generic)
+ *   23 TestCompositeImportSubjectPublicKeyInfo     → ML-DSA (generic)
+ *   24 TestCompositeImportFromPem                  → ML-DSA (generic)
+ *   25 TestCompositeImportEncryptedPkcs8PrivateKey → ML-DSA (generic)
+ *   26 TestCompositeImportFromEncryptedPem         → ML-DSA (generic)
+ *
+ * Section 7 – CompositeMLDsaCng native-interop constructor, no parameter set (finding 27):
+ *   27 TestCompositeMLDsaCng → ML-DSA (generic)
+ *
+ * Section 8 – CompositeMLDsa Sign / Verify operations (findings 28–29):
+ *   28 TestCompositeSignData   → ML-DSA-MLDsa44WithECDsaP256, Sign child
+ *   29 TestCompositeVerifyData → ML-DSA-MLDsa44WithECDsaP256, Verify child
+ *
+ * Section 9 – combined usage pattern (finding 30):
+ *   30 TestFullFlow → ML-DSA-87, Sign child, Verify child
+ * 
+ */ +class DotNetMLDsaTest extends TestBase { + + @Test + void test() throws Exception { + CSharpVerifier.verify("rules/detection/dotnet/DotNetMLDsaTestFile.cs", this); + } + + @Override + public void asserts( + int findingId, + @Nonnull + DetectionStore + detectionStore, + @Nonnull List nodes) { + + // Every top-level finding must be ML-DSA + assertThat(detectionStore.getDetectionValueContext()).isInstanceOf(KeyContext.class); + assertThat(detectionStore.getDetectionValues()).hasSize(1); + IValue primary = detectionStore.getDetectionValues().get(0); + assertThat(primary).isInstanceOf(ValueAction.class); + assertThat(primary.asString()).isEqualTo("ML-DSA"); + + assertThat(nodes).hasSize(1); + INode node = nodes.get(0); + assertThat(node.getKind()).isEqualTo(Signature.class); + + switch (findingId) { + + // ----------------------------------------------------------------- + // Section 1: MLDsa algorithm-parameterized creation + // ----------------------------------------------------------------- + case 0 -> assertParameterSet(detectionStore, node, "MLDsa44", "44"); + case 1 -> assertParameterSet(detectionStore, node, "MLDsa65", "65"); + case 2 -> assertParameterSet(detectionStore, node, "MLDsa87", "87"); + case 3 -> assertParameterSet(detectionStore, node, "MLDsa65", "65"); + case 4 -> assertParameterSet(detectionStore, node, "MLDsa44", "44"); + case 5 -> assertParameterSet(detectionStore, node, "MLDsa87", "87"); + + // ----------------------------------------------------------------- + // Section 2 & 3: generic ML-DSA, no parameter set + // ----------------------------------------------------------------- + case 6, 7, 8, 9, 10, 11, 12 -> assertGeneric(node); + + // ----------------------------------------------------------------- + // Section 4: MLDsa Sign / Verify operations + // ----------------------------------------------------------------- + case 13 -> { + assertParameterSet(detectionStore, node, "MLDsa65", "65"); + assertThat(node.getChildren().get(Sign.class)).isNotNull(); + } + case 14 -> { + assertParameterSet(detectionStore, node, "MLDsa65", "65"); + assertThat(node.getChildren().get(Verify.class)).isNotNull(); + } + case 15 -> { + assertParameterSet(detectionStore, node, "MLDsa65", "65"); + assertThat(node.getChildren().get(Sign.class)).isNotNull(); + } + case 16 -> { + assertParameterSet(detectionStore, node, "MLDsa65", "65"); + assertThat(node.getChildren().get(Verify.class)).isNotNull(); + } + case 17 -> { + assertParameterSet(detectionStore, node, "MLDsa65", "65"); + assertThat(node.getChildren().get(Sign.class)).isNotNull(); + } + case 18 -> { + assertParameterSet(detectionStore, node, "MLDsa65", "65"); + assertThat(node.getChildren().get(Verify.class)).isNotNull(); + } + + // ----------------------------------------------------------------- + // Section 5: CompositeMLDsa algorithm-parameterized creation + // ----------------------------------------------------------------- + case 19 -> assertCompositeParameterSet(detectionStore, node, "MLDsa44WithECDsaP256"); + case 20 -> assertCompositeParameterSet(detectionStore, node, "MLDsa65WithRSA3072Pss"); + case 21 -> assertCompositeParameterSet(detectionStore, node, "MLDsa87WithEd448"); + + // ----------------------------------------------------------------- + // Section 6 & 7: generic ML-DSA, no parameter set (CompositeMLDsa) + // ----------------------------------------------------------------- + case 22, 23, 24, 25, 26, 27 -> assertGeneric(node); + + // ----------------------------------------------------------------- + // Section 8: CompositeMLDsa Sign / Verify operations + // ----------------------------------------------------------------- + case 28 -> { + assertCompositeParameterSet(detectionStore, node, "MLDsa44WithECDsaP256"); + assertThat(node.getChildren().get(Sign.class)).isNotNull(); + } + case 29 -> { + assertCompositeParameterSet(detectionStore, node, "MLDsa44WithECDsaP256"); + assertThat(node.getChildren().get(Verify.class)).isNotNull(); + } + + // ----------------------------------------------------------------- + // Section 9: combined usage pattern + // ----------------------------------------------------------------- + case 30 -> { + assertParameterSet(detectionStore, node, "MLDsa87", "87"); + assertThat(node.getChildren().get(Sign.class)).isNotNull(); + assertThat(node.getChildren().get(Verify.class)).isNotNull(); + } + + default -> throw new IllegalStateException("Unexpected findingId: " + findingId); + } + } + + // ------------------------------------------------------------------------- + // Assertion helpers + // ------------------------------------------------------------------------- + + private void assertParameterSet( + @Nonnull DetectionStore store, + @Nonnull INode node, + @Nonnull String expectedRawIdentifier, + @Nonnull String expectedParameterSet) { + + DetectionStore + parameterIdentifierStore = + getStoreOfValueType(ParameterIdentifier.class, store.getChildren()); + assertThat(parameterIdentifierStore).isNotNull(); + assertThat(parameterIdentifierStore.getDetectionValues()).hasSize(1); + assertThat(parameterIdentifierStore.getDetectionValues().get(0).asString()) + .isEqualTo(expectedRawIdentifier); + + assertThat(node.asString()).isEqualTo("ML-DSA-" + expectedParameterSet); + + INode parameterSetIdentifier = node.getChildren().get(ParameterSetIdentifier.class); + assertThat(parameterSetIdentifier).isNotNull(); + assertThat(parameterSetIdentifier.asString()).isEqualTo(expectedParameterSet); + } + + private void assertCompositeParameterSet( + @Nonnull DetectionStore store, + @Nonnull INode node, + @Nonnull String expectedCompositeIdentifier) { + + DetectionStore + parameterIdentifierStore = + getStoreOfValueType(ParameterIdentifier.class, store.getChildren()); + assertThat(parameterIdentifierStore).isNotNull(); + assertThat(parameterIdentifierStore.getDetectionValues()).hasSize(1); + assertThat(parameterIdentifierStore.getDetectionValues().get(0).asString()) + .isEqualTo(expectedCompositeIdentifier); + + assertThat(node.asString()).isEqualTo("ML-DSA-" + expectedCompositeIdentifier); + + INode parameterSetIdentifier = node.getChildren().get(ParameterSetIdentifier.class); + assertThat(parameterSetIdentifier).isNotNull(); + assertThat(parameterSetIdentifier.asString()).isEqualTo(expectedCompositeIdentifier); + } + + private void assertGeneric(@Nonnull INode node) { + assertThat(node.asString()).isEqualTo("ML-DSA"); + assertThat(node.getChildren().get(ParameterSetIdentifier.class)).isNull(); + } +} diff --git a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetMLKemTest.java b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetMLKemTest.java new file mode 100644 index 000000000..32c9027f1 --- /dev/null +++ b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetMLKemTest.java @@ -0,0 +1,193 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ibm.engine.detection.DetectionStore; +import com.ibm.engine.language.csharp.CSharpCheck; +import com.ibm.engine.language.csharp.CSharpScanContext; +import com.ibm.engine.language.csharp.CSharpSymbol; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.IValue; +import com.ibm.engine.model.ParameterIdentifier; +import com.ibm.engine.model.ValueAction; +import com.ibm.engine.model.context.KeyContext; +import com.ibm.mapper.model.INode; +import com.ibm.mapper.model.KeyEncapsulationMechanism; +import com.ibm.mapper.model.Oid; +import com.ibm.mapper.model.ParameterSetIdentifier; +import com.ibm.mapper.model.functionality.Decapsulate; +import com.ibm.mapper.model.functionality.Encapsulate; +import com.ibm.plugin.CSharpVerifier; +import com.ibm.plugin.TestBase; +import java.util.List; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +/** + * Comprehensive test for all ML-KEM related detection rules (DotNetMLKem.java). + * + *

Covers the {@code MLKem} abstract base (all of its static factory methods), and the {@code + * MLKemCng}/{@code MLKemOpenSsl} native-interop derived classes, plus the {@code + * Encapsulate}/{@code Decapsulate} instance operations shared by all three. + * + *

Every top-level finding translates to a {@link com.ibm.mapper.model.algorithms.MLKEM} node + * (kind {@link KeyEncapsulationMechanism}). When the {@code MLKemAlgorithm} parameter set is + * captured, {@code node.asString()} is {@code "ML-KEM-512"}/{@code "ML-KEM-768"}/{@code + * "ML-KEM-1024"}, the node carries a {@link ParameterSetIdentifier} child ("512"/"768"/"1024") and + * — added by the enricher, confirmed from the actual debug output — an {@link Oid} child ({@code + * "2.16.840.1.101.3.4.4.1"}/{@code ".2"}/{@code ".3"} respectively). Otherwise {@code + * node.asString()} is the generic {@code "ML-KEM"}, with neither child. + * + *

Finding mapping (one finding per test method in DotNetMLKemTestFile.cs; verified against the + * actual {@code target/node-tree.log} output of a debug run, not guessed): + * + *

+ * Section 1 – algorithm-parameterized creation (findings 0–5):
+ *   0 TestGenerateKey512            → ML-KEM-512  (ParameterSetIdentifier "512", Oid ".4.1")
+ *   1 TestGenerateKey768            → ML-KEM-768  (ParameterSetIdentifier "768", Oid ".4.2")
+ *   2 TestGenerateKey1024           → ML-KEM-1024 (ParameterSetIdentifier "1024", Oid ".4.3")
+ *   3 TestImportDecapsulationKey    → ML-KEM-768
+ *   4 TestImportEncapsulationKey    → ML-KEM-768
+ *   5 TestImportPrivateSeed         → ML-KEM-512
+ *
+ * Section 2 – structural imports, no parameter set (findings 6–10):
+ *   6  TestImportPkcs8PrivateKey          → ML-KEM (generic)
+ *   7  TestImportSubjectPublicKeyInfo     → ML-KEM (generic)
+ *   8  TestImportFromPem                  → ML-KEM (generic)
+ *   9  TestImportEncryptedPkcs8PrivateKey → ML-KEM (generic)
+ *   10 TestImportFromEncryptedPem         → ML-KEM (generic)
+ *
+ * Section 3 – native-interop constructors, no parameter set (findings 11–12):
+ *   11 TestMLKemCng      → ML-KEM (generic)
+ *   12 TestMLKemOpenSsl  → ML-KEM (generic)
+ *
+ * Section 4 – Encapsulate / Decapsulate operations (findings 13–14):
+ *   13 TestEncapsulate → ML-KEM-768, Encapsulate child
+ *   14 TestDecapsulate → ML-KEM-768, Decapsulate child
+ *
+ * Section 5 – combined usage pattern (finding 15):
+ *   15 TestFullFlow → ML-KEM-1024, Encapsulate child, Decapsulate child
+ * 
+ */ +class DotNetMLKemTest extends TestBase { + + @Test + void test() throws Exception { + CSharpVerifier.verify("rules/detection/dotnet/DotNetMLKemTestFile.cs", this); + } + + @Override + public void asserts( + int findingId, + @Nonnull + DetectionStore + detectionStore, + @Nonnull List nodes) { + + // Every top-level finding must be ML-KEM + assertThat(detectionStore.getDetectionValueContext()).isInstanceOf(KeyContext.class); + assertThat(detectionStore.getDetectionValues()).hasSize(1); + IValue primary = detectionStore.getDetectionValues().get(0); + assertThat(primary).isInstanceOf(ValueAction.class); + assertThat(primary.asString()).isEqualTo("ML-KEM"); + + assertThat(nodes).hasSize(1); + INode node = nodes.get(0); + assertThat(node.getKind()).isEqualTo(KeyEncapsulationMechanism.class); + + switch (findingId) { + + // ----------------------------------------------------------------- + // Section 1: algorithm-parameterized creation + // ----------------------------------------------------------------- + case 0 -> assertParameterSet(detectionStore, node, "512", "2.16.840.1.101.3.4.4.1"); + case 1 -> assertParameterSet(detectionStore, node, "768", "2.16.840.1.101.3.4.4.2"); + case 2 -> assertParameterSet(detectionStore, node, "1024", "2.16.840.1.101.3.4.4.3"); + case 3 -> assertParameterSet(detectionStore, node, "768", "2.16.840.1.101.3.4.4.2"); + case 4 -> assertParameterSet(detectionStore, node, "768", "2.16.840.1.101.3.4.4.2"); + case 5 -> assertParameterSet(detectionStore, node, "512", "2.16.840.1.101.3.4.4.1"); + + // ----------------------------------------------------------------- + // Section 2 & 3: generic ML-KEM, no parameter set + // ----------------------------------------------------------------- + case 6, 7, 8, 9, 10, 11, 12 -> assertGeneric(node); + + // ----------------------------------------------------------------- + // Section 4: Encapsulate / Decapsulate operations + // ----------------------------------------------------------------- + case 13 -> { + assertParameterSet(detectionStore, node, "768", "2.16.840.1.101.3.4.4.2"); + assertThat(node.getChildren().get(Encapsulate.class)).isNotNull(); + } + case 14 -> { + assertParameterSet(detectionStore, node, "768", "2.16.840.1.101.3.4.4.2"); + assertThat(node.getChildren().get(Decapsulate.class)).isNotNull(); + } + + // ----------------------------------------------------------------- + // Section 5: combined usage pattern + // ----------------------------------------------------------------- + case 15 -> { + assertParameterSet(detectionStore, node, "1024", "2.16.840.1.101.3.4.4.3"); + assertThat(node.getChildren().get(Encapsulate.class)).isNotNull(); + assertThat(node.getChildren().get(Decapsulate.class)).isNotNull(); + } + + default -> throw new IllegalStateException("Unexpected findingId: " + findingId); + } + } + + // ------------------------------------------------------------------------- + // Assertion helpers + // ------------------------------------------------------------------------- + + private void assertParameterSet( + @Nonnull DetectionStore store, + @Nonnull INode node, + @Nonnull String expectedParameterSet, + @Nonnull String expectedOid) { + + DetectionStore + parameterIdentifierStore = + getStoreOfValueType(ParameterIdentifier.class, store.getChildren()); + assertThat(parameterIdentifierStore).isNotNull(); + assertThat(parameterIdentifierStore.getDetectionValues()).hasSize(1); + assertThat(parameterIdentifierStore.getDetectionValues().get(0).asString()) + .isEqualTo("MLKem" + expectedParameterSet); + + assertThat(node.asString()).isEqualTo("ML-KEM-" + expectedParameterSet); + + INode parameterSetIdentifier = node.getChildren().get(ParameterSetIdentifier.class); + assertThat(parameterSetIdentifier).isNotNull(); + assertThat(parameterSetIdentifier.asString()).isEqualTo(expectedParameterSet); + + INode oid = node.getChildren().get(Oid.class); + assertThat(oid).isNotNull(); + assertThat(oid.asString()).isEqualTo(expectedOid); + } + + private void assertGeneric(@Nonnull INode node) { + assertThat(node.asString()).isEqualTo("ML-KEM"); + assertThat(node.getChildren().get(ParameterSetIdentifier.class)).isNull(); + assertThat(node.getChildren().get(Oid.class)).isNull(); + } +} diff --git a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetProtectedDataTest.java b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetProtectedDataTest.java new file mode 100644 index 000000000..752f61a79 --- /dev/null +++ b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetProtectedDataTest.java @@ -0,0 +1,155 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ibm.engine.detection.DetectionStore; +import com.ibm.engine.language.csharp.CSharpCheck; +import com.ibm.engine.language.csharp.CSharpScanContext; +import com.ibm.engine.language.csharp.CSharpSymbol; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.CipherAction; +import com.ibm.engine.model.IValue; +import com.ibm.engine.model.ValueAction; +import com.ibm.engine.model.context.CipherContext; +import com.ibm.mapper.model.Cipher; +import com.ibm.mapper.model.INode; +import com.ibm.mapper.model.functionality.Decrypt; +import com.ibm.mapper.model.functionality.Encrypt; +import com.ibm.plugin.CSharpVerifier; +import com.ibm.plugin.TestBase; +import java.util.List; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +/** + * Comprehensive test for all DPAPI detection rules ({@code DotNetProtectedData.java}). + * + *

Covers: + * + *

    + *
  • {@code ProtectedData.Protect}/{@code Unprotect}/{@code TryProtect}/{@code TryUnprotect} + *
  • {@code ProtectedMemory.Protect}/{@code Unprotect} + *
  • {@code DpapiDataProtector} constructor + instance {@code Protect(byte[])}/{@code + * Unprotect(byte[])} + *
+ * + *

Node strings below were captured from an actual test run's debug log ({@code + * target/node-tree.log} / stdout), not guessed: + * + *

+ * Finding mapping (one finding per test method in DotNetProtectedDataTestFile.cs):
+ *
+ * 0 TestProtectedDataProtect        → detected ValueAction "DPAPI_PROTECT"   → Cipher "DPAPI" + Encrypt "ENCRYPT" child
+ * 1 TestProtectedDataUnprotect      → detected ValueAction "DPAPI_UNPROTECT" → Cipher "DPAPI" + Decrypt "DECRYPT" child
+ * 2 TestProtectedDataTryProtect     → detected ValueAction "DPAPI_PROTECT"   → Cipher "DPAPI" + Encrypt "ENCRYPT" child
+ * 3 TestProtectedDataTryUnprotect   → detected ValueAction "DPAPI_UNPROTECT" → Cipher "DPAPI" + Decrypt "DECRYPT" child
+ * 4 TestProtectedMemoryProtect      → detected ValueAction "DPAPI_PROTECT"   → Cipher "DPAPI" + Encrypt "ENCRYPT" child
+ * 5 TestProtectedMemoryUnprotect    → detected ValueAction "DPAPI_UNPROTECT" → Cipher "DPAPI" + Decrypt "DECRYPT" child
+ * 6 TestDpapiDataProtectorProtect   → detected ValueAction "DPAPI" + child CipherAction ENCRYPT → Cipher "DPAPI" + Encrypt "ENCRYPT" child
+ * 7 TestDpapiDataProtectorUnprotect → detected ValueAction "DPAPI" + child CipherAction DECRYPT → Cipher "DPAPI" + Decrypt "DECRYPT" child
+ * 
+ */ +class DotNetProtectedDataTest extends TestBase { + + @Test + void test() throws Exception { + CSharpVerifier.verify("rules/detection/dotnet/DotNetProtectedDataTestFile.cs", this); + } + + @Override + public void asserts( + int findingId, + @Nonnull + DetectionStore + detectionStore, + @Nonnull List nodes) { + + assertThat(detectionStore.getDetectionValueContext()).isInstanceOf(CipherContext.class); + assertThat(detectionStore.getDetectionValues()).hasSize(1); + IValue primary = detectionStore.getDetectionValues().get(0); + assertThat(primary).isInstanceOf(ValueAction.class); + + assertThat(nodes).hasSize(1); + INode node = nodes.get(0); + assertThat(node.getKind()).isEqualTo(Cipher.class); + assertThat(node.asString()).isEqualTo("DPAPI"); + + switch (findingId) { + // ----------------------------------------------------------------- + // Section 1 & 2: ProtectedData / ProtectedMemory static one-shot calls — identity + // and action are captured together as a single merged ValueAction. + // ----------------------------------------------------------------- + case 0, 2, 4 -> { + assertThat(primary.asString()).isEqualTo("DPAPI_PROTECT"); + INode encrypt = node.getChildren().get(Encrypt.class); + assertThat(encrypt).isNotNull(); + assertThat(encrypt.asString()).isEqualTo("ENCRYPT"); + } + case 1, 3, 5 -> { + assertThat(primary.asString()).isEqualTo("DPAPI_UNPROTECT"); + INode decrypt = node.getChildren().get(Decrypt.class); + assertThat(decrypt).isNotNull(); + assertThat(decrypt.asString()).isEqualTo("DECRYPT"); + } + + // ----------------------------------------------------------------- + // Section 3: DpapiDataProtector — real tracked instance, so identity ("DPAPI") is + // captured at the constructor and the action (CipherAction) is a depending-rule + // child of it. + // ----------------------------------------------------------------- + case 6 -> { + assertThat(primary.asString()).isEqualTo("DPAPI"); + assertCipherActionChild(detectionStore, node, CipherAction.Action.ENCRYPT); + INode encrypt = node.getChildren().get(Encrypt.class); + assertThat(encrypt).isNotNull(); + assertThat(encrypt.asString()).isEqualTo("ENCRYPT"); + } + case 7 -> { + assertThat(primary.asString()).isEqualTo("DPAPI"); + assertCipherActionChild(detectionStore, node, CipherAction.Action.DECRYPT); + INode decrypt = node.getChildren().get(Decrypt.class); + assertThat(decrypt).isNotNull(); + assertThat(decrypt.asString()).isEqualTo("DECRYPT"); + } + + default -> throw new IllegalStateException("Unexpected findingId: " + findingId); + } + } + + // ------------------------------------------------------------------------- + // Assertion helpers + // ------------------------------------------------------------------------- + + private void assertCipherActionChild( + @Nonnull DetectionStore store, + @Nonnull INode node, + @Nonnull CipherAction.Action expectedAction) { + + DetectionStore childStore = + getStoreOfValueType(CipherAction.class, store.getChildren()); + assertThat(childStore).isNotNull(); + assertThat(childStore.getDetectionValues()).hasSize(1); + IValue childValue = childStore.getDetectionValues().get(0); + assertThat(childValue).isInstanceOf(CipherAction.class); + assertThat(((CipherAction) childValue).getAction()).isEqualTo(expectedAction); + } +} diff --git a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetRC2ComprehensiveTest.java b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetRC2ComprehensiveTest.java new file mode 100644 index 000000000..224e2bfaf --- /dev/null +++ b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetRC2ComprehensiveTest.java @@ -0,0 +1,417 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ibm.engine.detection.DetectionStore; +import com.ibm.engine.language.csharp.CSharpCheck; +import com.ibm.engine.language.csharp.CSharpScanContext; +import com.ibm.engine.language.csharp.CSharpSymbol; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.BlockSize; +import com.ibm.engine.model.CipherAction; +import com.ibm.engine.model.IValue; +import com.ibm.engine.model.KeySize; +import com.ibm.engine.model.Mode; +import com.ibm.engine.model.Padding; +import com.ibm.engine.model.ValueAction; +import com.ibm.engine.model.context.CipherContext; +import com.ibm.mapper.model.BlockCipher; +import com.ibm.mapper.model.INode; +import com.ibm.mapper.model.KeyLength; +import com.ibm.mapper.model.functionality.Decrypt; +import com.ibm.mapper.model.functionality.Encrypt; +import com.ibm.mapper.model.functionality.Generate; +import com.ibm.mapper.model.functionality.KeyGeneration; +import com.ibm.plugin.CSharpVerifier; +import com.ibm.plugin.TestBase; +import java.util.List; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +/** + * Comprehensive test for all RC2-related detection rules (DotNetRC2.java). + * + *

Covers both RC2-related classes and their complete API surface: + * + *

    + *
  • RC2 (abstract base) + *
  • RC2CryptoServiceProvider (derived from RC2) + *
+ * + *

Like DES, RC2 has no CNG-backed subclass and no AEAD variant, so there is no equivalent to + * Section 8/9 of DotNetAESComprehensiveTest (AesGcm/AesCcm). Unlike DES, RC2 additionally exposes + * an {@code EffectiveKeySize} property, which is reused as a {@code KeySize} detection (see + * DotNetRC2.java) and is covered by an extra test method in Section 2 (property setters). + * + *

Finding mapping (one finding per test method in DotNetRC2ComprehensiveTestFile.cs): + * + *

+ * Section 1 – factory methods / constructors (findings 0–2):
+ *   0  TestRc2Create                  → RC2
+ *   1  TestRc2CreateNamed             → RC2
+ *   2  TestRc2Csp                     → RC2
+ *
+ * Section 2 – property setters (findings 3–16):
+ *   3  TestPropertyModeCBC            → RC2-CBC
+ *   4  TestPropertyModeECB            → RC2-ECB
+ *   5  TestPropertyModeCFB            → RC2-CFB
+ *   6  TestPropertyModeOFB            → RC2-OFB
+ *   7  TestPropertyModeCTS            → RC2-CTS
+ *   8  TestPropertyKeySize            → RC2-128
+ *   9  TestPropertyEffectiveKeySize   → RC2-64 (reused as KeySize, see DotNetRC2.java)
+ *   10 TestPropertyPaddingPKCS7       → RC2 (padding never rendered in asString)
+ *   11 TestPropertyPaddingNone        → RC2 (padding never rendered in asString)
+ *   12 TestPropertyPaddingZeros       → RC2 (padding never rendered in asString)
+ *   13 TestPropertyPaddingANSIX923    → RC2 (padding never rendered in asString)
+ *   14 TestPropertyFeedbackSize       → RC2 (BlockSize never rendered in asString)
+ *   15 TestPropertyIV                 → RC2 (no IV rule)
+ *   16 TestPropertyKey                → RC2 (no Key rule)
+ *
+ * Section 3 – CreateEncryptor/CreateDecryptor (findings 17–20):
+ *   17 TestCreateEncryptorNoArgs      → RC2 + Encrypt
+ *   18 TestCreateEncryptorWithArgs    → RC2 + Encrypt
+ *   19 TestCreateDecryptorNoArgs      → RC2 + Decrypt
+ *   20 TestCreateDecryptorWithArgs    → RC2 + Decrypt
+ *
+ * Section 4 – direct encrypt (findings 21–23):
+ *   21 TestEncryptCbc                 → RC2-CBC
+ *   22 TestEncryptEcb                 → RC2-ECB
+ *   23 TestEncryptCfb                 → RC2-CFB
+ *
+ * Section 5 – direct decrypt (findings 24–26):
+ *   24 TestDecryptCbc                 → RC2-CBC
+ *   25 TestDecryptEcb                 → RC2-ECB
+ *   26 TestDecryptCfb                 → RC2-CFB
+ *
+ * Section 6 – Try* variants (findings 27–32):
+ *   27 TestTryEncryptCbc              → RC2-CBC
+ *   28 TestTryDecryptCbc              → RC2-CBC
+ *   29 TestTryEncryptEcb              → RC2-ECB
+ *   30 TestTryDecryptEcb              → RC2-ECB
+ *   31 TestTryEncryptCfb              → RC2-CFB
+ *   32 TestTryDecryptCfb              → RC2-CFB
+ *
+ * Section 7 – key/IV generation (findings 33–34):
+ *   33 TestGenerateKey                → RC2 + KeyGeneration
+ *   34 TestGenerateIV                 → RC2 + Generate
+ *
+ * Section 8 – combined usage patterns (findings 35–41):
+ *   35 TestRc2CbcFullFlow             → RC2-CBC + Encrypt
+ *   36 TestRc2CspEncryptCbc           → RC2-CBC
+ *   37 TestRc2CspDecryptCbc           → RC2-CBC
+ *   38 TestRc2CfbFeedback             → RC2-CFB
+ *   39 TestRc2CbcWithEncryptorOverload → RC2 + Encrypt
+ *   40 TestRc2EcbEncrypt              → RC2-ECB
+ *   41 TestRc2CspEffectiveKeySize     → RC2-40
+ * 
+ * + *

NOTE: unlike DES, {@code RC2}'s abstract-base constructor {@code RC2(DetectionLocation)} does + * not seed a default {@code KeyLength} or {@code BlockSize} (see {@code RC2.java}) — + * {@code RC2.asString()} uses {@code composeName(true, true, false)} just like {@code DES}, but + * with nothing to render unless a KeySize/EffectiveKeySize/Mode is actually detected. So the plain + * "RC2" string appears whenever no such property was set, and Padding never contributes to the + * string even when a Padding value is detected as a child. Verified against actual test-run debug + * output ({@code target/node-tree.log}) rather than guessed. + */ +class DotNetRC2ComprehensiveTest extends TestBase { + + @Test + void test() throws Exception { + CSharpVerifier.verify("rules/detection/dotnet/DotNetRC2ComprehensiveTestFile.cs", this); + } + + @Override + public void asserts( + int findingId, + @Nonnull + DetectionStore + detectionStore, + @Nonnull List nodes) { + + // Every top-level finding must be RC2 + assertThat(detectionStore.getDetectionValueContext()).isInstanceOf(CipherContext.class); + assertThat(detectionStore.getDetectionValues()).hasSize(1); + IValue primary = detectionStore.getDetectionValues().get(0); + assertThat(primary).isInstanceOf(ValueAction.class); + assertThat(primary.asString()).isEqualTo("RC2"); + + switch (findingId) { + + // ----------------------------------------------------------------- + // Section 1: simple constructors — only RC2, no children fired + // ----------------------------------------------------------------- + case 0, 1, 2 -> { + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo("RC2"); + } + + // ----------------------------------------------------------------- + // Section 2a: property Mode setters + // ----------------------------------------------------------------- + case 3 -> assertModeFindings(detectionStore, nodes, "CBC", "RC2-CBC"); + case 4 -> assertModeFindings(detectionStore, nodes, "ECB", "RC2-ECB"); + case 5 -> assertModeFindings(detectionStore, nodes, "CFB", "RC2-CFB"); + case 6 -> assertModeFindings(detectionStore, nodes, "OFB", "RC2-OFB"); + case 7 -> assertModeFindings(detectionStore, nodes, "CTS", "RC2-CTS"); + + // ----------------------------------------------------------------- + // Section 2b: property KeySize / EffectiveKeySize setters — both + // reuse KeySizeFactory (see DotNetRC2.java), so both render as + // - in the node string via composeName's KeyLength branch. + // ----------------------------------------------------------------- + case 8 -> assertKeySizeFindings(detectionStore, nodes, "128", "RC2-128"); + case 9 -> assertKeySizeFindings(detectionStore, nodes, "64", "RC2-64"); + + // ----------------------------------------------------------------- + // Section 2c: property Padding setters — Padding is tracked as a + // child node but RC2.asString() uses composeName(true, true, false), + // so padding never contributes to the rendered string. + // ----------------------------------------------------------------- + case 10 -> assertPaddingFindings(detectionStore, nodes, "PKCS7", "RC2"); + case 11 -> assertPaddingFindings(detectionStore, nodes, "None", "RC2"); + case 12 -> assertPaddingFindings(detectionStore, nodes, "Zeros", "RC2"); + case 13 -> assertPaddingFindings(detectionStore, nodes, "ANSIX923", "RC2"); + + // ----------------------------------------------------------------- + // Section 2d: FeedbackSize setter — BlockSize(8) detected but never + // contributes to asString() (composeName has no BlockSize branch) + // ----------------------------------------------------------------- + case 14 -> { + DetectionStore fbStore = + getStoreOfValueType(BlockSize.class, detectionStore.getChildren()); + assertThat(fbStore).isNotNull(); + assertThat(fbStore.getDetectionValues()).hasSize(1); + assertThat(fbStore.getDetectionValues().get(0).asString()).isEqualTo("8"); + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo("RC2"); + } + + // ----------------------------------------------------------------- + // Section 2e: IV and Key setters — no detection rules for these + // ----------------------------------------------------------------- + case 15, 16 -> { + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo("RC2"); + } + + // ----------------------------------------------------------------- + // Section 3: CreateEncryptor / CreateDecryptor + // ----------------------------------------------------------------- + case 17, 18 -> assertEncryptFindings(detectionStore, nodes, "RC2"); + case 19, 20 -> assertDecryptFindings(detectionStore, nodes, "RC2"); + + // ----------------------------------------------------------------- + // Section 4: direct mode-specific encrypt + // ----------------------------------------------------------------- + case 21 -> assertModePaddingFindings(detectionStore, nodes, "CBC", "PKCS7", "RC2-CBC"); + case 22 -> assertModePaddingFindings(detectionStore, nodes, "ECB", "None", "RC2-ECB"); + case 23 -> assertModePaddingFindings(detectionStore, nodes, "CFB", "None", "RC2-CFB"); + + // ----------------------------------------------------------------- + // Section 5: direct mode-specific decrypt + // ----------------------------------------------------------------- + case 24 -> assertModePaddingFindings(detectionStore, nodes, "CBC", "PKCS7", "RC2-CBC"); + case 25 -> assertModePaddingFindings(detectionStore, nodes, "ECB", "None", "RC2-ECB"); + case 26 -> assertModePaddingFindings(detectionStore, nodes, "CFB", "None", "RC2-CFB"); + + // ----------------------------------------------------------------- + // Section 6: Try* variants + // ----------------------------------------------------------------- + case 27, 28 -> + assertModePaddingFindings(detectionStore, nodes, "CBC", "PKCS7", "RC2-CBC"); + case 29, 30 -> + assertModePaddingFindings(detectionStore, nodes, "ECB", "None", "RC2-ECB"); + case 31, 32 -> + assertModePaddingFindings(detectionStore, nodes, "CFB", "None", "RC2-CFB"); + + // ----------------------------------------------------------------- + // Section 7: GenerateKey / GenerateIV + // ----------------------------------------------------------------- + case 33 -> { + // rc2.GenerateKey() → KeyGeneration functionality node + DetectionStore + genKeyStore = + getStoreOfValueType( + ValueAction.class, detectionStore.getChildren()); + assertThat(genKeyStore).isNotNull(); + assertThat(genKeyStore.getDetectionValues().get(0).asString()) + .isEqualTo("GenerateKey"); + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).getChildren().get(KeyGeneration.class)).isNotNull(); + } + case 34 -> { + // rc2.GenerateIV() → Generate functionality node + DetectionStore + genIvStore = + getStoreOfValueType( + ValueAction.class, detectionStore.getChildren()); + assertThat(genIvStore).isNotNull(); + assertThat(genIvStore.getDetectionValues().get(0).asString()) + .isEqualTo("GenerateIV"); + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).getChildren().get(Generate.class)).isNotNull(); + } + + // ----------------------------------------------------------------- + // Section 8: combined usage patterns + // ----------------------------------------------------------------- + case 35 -> { + // TestRc2CbcFullFlow: Mode=CBC, Padding=PKCS7 (unrendered), CreateEncryptor + assertThat(nodes).hasSize(1); + INode node = nodes.get(0); + assertThat(node.getKind()).isEqualTo(BlockCipher.class); + assertThat(node.asString()).isEqualTo("RC2-CBC"); + assertThat(node.getChildren().get(com.ibm.mapper.model.Mode.class)).isNotNull(); + assertThat(node.getChildren().get(Encrypt.class)).isNotNull(); + } + case 36 -> assertModePaddingFindings(detectionStore, nodes, "CBC", "PKCS7", "RC2-CBC"); + case 37 -> assertModePaddingFindings(detectionStore, nodes, "CBC", "PKCS7", "RC2-CBC"); + case 38 -> assertModePaddingFindings(detectionStore, nodes, "CFB", "None", "RC2-CFB"); + case 39 -> assertEncryptFindings(detectionStore, nodes, "RC2"); + case 40 -> assertModePaddingFindings(detectionStore, nodes, "ECB", "None", "RC2-ECB"); + case 41 -> assertKeySizeFindings(detectionStore, nodes, "40", "RC2-40"); + + default -> throw new IllegalStateException("Unexpected findingId: " + findingId); + } + } + + // ------------------------------------------------------------------------- + // Assertion helpers + // ------------------------------------------------------------------------- + + private void assertModeFindings( + @Nonnull DetectionStore store, + @Nonnull List nodes, + @Nonnull String expectedMode, + @Nonnull String expectedNodeString) { + + DetectionStore modeStore = + getStoreOfValueType(Mode.class, store.getChildren()); + assertThat(modeStore).isNotNull(); + assertThat(modeStore.getDetectionValues()).hasSize(1); + assertThat(modeStore.getDetectionValues().get(0).asString()).isEqualTo(expectedMode); + + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo(expectedNodeString); + assertThat(nodes.get(0).getChildren().get(com.ibm.mapper.model.Mode.class)).isNotNull(); + } + + private void assertKeySizeFindings( + @Nonnull DetectionStore store, + @Nonnull List nodes, + @Nonnull String expectedKeySize, + @Nonnull String expectedNodeString) { + + DetectionStore keySizeStore = + getStoreOfValueType(KeySize.class, store.getChildren()); + assertThat(keySizeStore).isNotNull(); + assertThat(keySizeStore.getDetectionValues()).hasSize(1); + assertThat(keySizeStore.getDetectionValues().get(0).asString()).isEqualTo(expectedKeySize); + + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo(expectedNodeString); + assertThat(nodes.get(0).getChildren().get(KeyLength.class)).isNotNull(); + assertThat(nodes.get(0).getChildren().get(KeyLength.class).asString()) + .isEqualTo(expectedKeySize); + } + + private void assertPaddingFindings( + @Nonnull DetectionStore store, + @Nonnull List nodes, + @Nonnull String expectedPadding, + @Nonnull String expectedNodeString) { + + DetectionStore paddingStore = + getStoreOfValueType(Padding.class, store.getChildren()); + assertThat(paddingStore).isNotNull(); + assertThat(paddingStore.getDetectionValues()).hasSize(1); + assertThat(paddingStore.getDetectionValues().get(0).asString()).isEqualTo(expectedPadding); + + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo(expectedNodeString); + } + + private void assertEncryptFindings( + @Nonnull DetectionStore store, + @Nonnull List nodes, + @Nonnull String expectedNodeString) { + + DetectionStore encryptStore = + getStoreOfValueType(CipherAction.class, store.getChildren()); + assertThat(encryptStore).isNotNull(); + assertThat(encryptStore.getDetectionValues()).hasSize(1); + assertThat(encryptStore.getDetectionValues().get(0).asString()).isEqualTo("ENCRYPT"); + + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo(expectedNodeString); + assertThat(nodes.get(0).getChildren().get(Encrypt.class)).isNotNull(); + } + + private void assertDecryptFindings( + @Nonnull DetectionStore store, + @Nonnull List nodes, + @Nonnull String expectedNodeString) { + + DetectionStore decryptStore = + getStoreOfValueType(CipherAction.class, store.getChildren()); + assertThat(decryptStore).isNotNull(); + assertThat(decryptStore.getDetectionValues()).hasSize(1); + assertThat(decryptStore.getDetectionValues().get(0).asString()).isEqualTo("DECRYPT"); + + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo(expectedNodeString); + assertThat(nodes.get(0).getChildren().get(Decrypt.class)).isNotNull(); + } + + /** + * Asserts mode+padding findings using the translated node tree. Direct-mode methods + * (EncryptCbc, TryDecryptEcb, etc.) place Mode and Padding in the same child detection store, + * so we validate via the final node string rather than per-store inspection. + */ + private void assertModePaddingFindings( + @Nonnull DetectionStore store, + @Nonnull List nodes, + @Nonnull String expectedMode, + @Nonnull String expectedPadding, + @Nonnull String expectedNodeString) { + + DetectionStore modeStore = + getStoreOfValueType(Mode.class, store.getChildren()); + assertThat(modeStore).isNotNull(); + assertThat(modeStore.getDetectionValues()) + .anySatisfy(v -> assertThat(v.asString()).isEqualTo(expectedMode)); + + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo(expectedNodeString); + } +} diff --git a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetRSATest.java b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetRSATest.java index 5336bc3e1..c623bce63 100755 --- a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetRSATest.java +++ b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetRSATest.java @@ -26,18 +26,71 @@ import com.ibm.engine.language.csharp.CSharpScanContext; import com.ibm.engine.language.csharp.CSharpSymbol; import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.CipherAction; import com.ibm.engine.model.IValue; +import com.ibm.engine.model.SignatureAction; import com.ibm.engine.model.ValueAction; import com.ibm.engine.model.context.KeyContext; import com.ibm.mapper.model.INode; +import com.ibm.mapper.model.KeyLength; import com.ibm.mapper.model.Oid; import com.ibm.mapper.model.PublicKeyEncryption; +import com.ibm.mapper.model.functionality.Decrypt; +import com.ibm.mapper.model.functionality.Encrypt; +import com.ibm.mapper.model.functionality.Sign; +import com.ibm.mapper.model.functionality.Verify; import com.ibm.plugin.CSharpVerifier; import com.ibm.plugin.TestBase; import java.util.List; import javax.annotation.Nonnull; import org.junit.jupiter.api.Test; +/** + * Comprehensive test for all RSA-related detection rules (DotNetRSA.java). + * + *

Covers all four RSA-related classes and their complete operational API surface: + * + *

    + *
  • RSA (abstract base) + *
  • RSACng, RSACryptoServiceProvider, RSAOpenSsl (derived from RSA) + *
+ * + *

Finding mapping (one finding per test method in DotNetRSATestFile.cs): + * + *

+ * Section 1 – factory methods / constructors (findings 0–4):
+ *   0 TestRsaCreate              → RSA
+ *   1 TestRsaCreateWithKeySize   → RSA
+ *   2 TestRsaCsp                 → RSA
+ *   3 TestRsaCng                 → RSA
+ *   4 TestRsaOpenSsl             → RSA
+ *
+ * Section 2 – property KeySize setters (findings 5–6):
+ *   5 TestPropertyKeySize2048    → RSA-2048
+ *   6 TestPropertyKeySize4096    → RSA-4096
+ *
+ * Section 3 – Encrypt / Decrypt (findings 7–10):
+ *   7  TestEncrypt      → RSA + Encrypt
+ *   8  TestDecrypt      → RSA + Decrypt
+ *   9  TestTryEncrypt   → RSA + Encrypt
+ *   10 TestTryDecrypt   → RSA + Decrypt
+ *
+ * Section 4 – SignData / TrySignData / VerifyData (findings 11–13):
+ *   11 TestSignData     → RSA + Sign
+ *   12 TestTrySignData  → RSA + Sign
+ *   13 TestVerifyData   → RSA + Verify
+ *
+ * Section 5 – SignHash / TrySignHash / VerifyHash (findings 14–16):
+ *   14 TestSignHash     → RSA + Sign
+ *   15 TestTrySignHash  → RSA + Sign
+ *   16 TestVerifyHash   → RSA + Verify
+ *
+ * Section 6 – combined usage patterns (findings 17–19):
+ *   17 TestRsaCngFullFlow        → RSA-3072 + Encrypt
+ *   18 TestRsaCspSignFlow        → RSA + Sign
+ *   19 TestRsaOpenSslVerifyFlow  → RSA + Verify
+ * 
+ */ class DotNetRSATest extends TestBase { @Test @@ -52,24 +105,136 @@ public void asserts( DetectionStore detectionStore, @Nonnull List nodes) { - assertThat(detectionStore).isNotNull(); - assertThat(detectionStore.getDetectionValues()).hasSize(1); + + // Every top-level finding must be RSA assertThat(detectionStore.getDetectionValueContext()).isInstanceOf(KeyContext.class); - IValue value0 = detectionStore.getDetectionValues().get(0); - assertThat(value0).isInstanceOf(ValueAction.class); - assertThat(value0.asString()).isEqualTo("RSA"); + assertThat(detectionStore.getDetectionValues()).hasSize(1); + IValue primary = detectionStore.getDetectionValues().get(0); + assertThat(primary).isInstanceOf(ValueAction.class); + assertThat(primary.asString()).isEqualTo("RSA"); - /* - * Translation - */ assertThat(nodes).hasSize(1); INode node = nodes.get(0); assertThat(node.getKind()).isEqualTo(PublicKeyEncryption.class); - assertThat(node.asString()).isEqualTo("RSA"); - // RSA carries its OID as a child node + // RSA always carries its OID as a child node INode oid = node.getChildren().get(Oid.class); assertThat(oid).isNotNull(); assertThat(oid.asString()).isEqualTo("1.2.840.113549.1.1.1"); + + switch (findingId) { + + // ----------------------------------------------------------------- + // Section 1: simple constructors — only RSA, no children fired + // ----------------------------------------------------------------- + case 0, 1, 2, 3, 4 -> assertThat(node.asString()).isEqualTo("RSA"); + + // ----------------------------------------------------------------- + // Section 2: property KeySize setters + // ----------------------------------------------------------------- + case 5 -> assertKeySize(detectionStore, node, "2048"); + case 6 -> assertKeySize(detectionStore, node, "4096"); + + // ----------------------------------------------------------------- + // Section 3: Encrypt / Decrypt / TryEncrypt / TryDecrypt + // ----------------------------------------------------------------- + case 7, 9 -> assertEncrypt(detectionStore, node, "RSA"); + case 8, 10 -> assertDecrypt(detectionStore, node, "RSA"); + + // ----------------------------------------------------------------- + // Section 4: SignData / TrySignData / VerifyData + // ----------------------------------------------------------------- + case 11, 12 -> assertSign(detectionStore, node, "RSA"); + case 13 -> assertVerify(detectionStore, node, "RSA"); + + // ----------------------------------------------------------------- + // Section 5: SignHash / TrySignHash / VerifyHash + // ----------------------------------------------------------------- + case 14, 15 -> assertSign(detectionStore, node, "RSA"); + case 16 -> assertVerify(detectionStore, node, "RSA"); + + // ----------------------------------------------------------------- + // Section 6: combined usage patterns + // ----------------------------------------------------------------- + case 17 -> assertEncrypt(detectionStore, node, "RSA-3072"); + case 18 -> assertSign(detectionStore, node, "RSA"); + case 19 -> assertVerify(detectionStore, node, "RSA"); + + default -> throw new IllegalStateException("Unexpected findingId: " + findingId); + } + } + + // ------------------------------------------------------------------------- + // Assertion helpers + // ------------------------------------------------------------------------- + + private void assertKeySize( + @Nonnull DetectionStore store, + @Nonnull INode node, + @Nonnull String expectedKeySize) { + + assertThat(node.asString()).isEqualTo("RSA-" + expectedKeySize); + assertThat(node.getChildren().get(KeyLength.class)).isNotNull(); + assertThat(node.getChildren().get(KeyLength.class).asString()).isEqualTo(expectedKeySize); + } + + private void assertEncrypt( + @Nonnull DetectionStore store, + @Nonnull INode node, + @Nonnull String expectedNodeString) { + + DetectionStore encryptStore = + getStoreOfValueType(CipherAction.class, store.getChildren()); + assertThat(encryptStore).isNotNull(); + assertThat(encryptStore.getDetectionValues()).hasSize(1); + assertThat(encryptStore.getDetectionValues().get(0).asString()).isEqualTo("ENCRYPT"); + + assertThat(node.asString()).isEqualTo(expectedNodeString); + assertThat(node.getChildren().get(Encrypt.class)).isNotNull(); + } + + private void assertDecrypt( + @Nonnull DetectionStore store, + @Nonnull INode node, + @Nonnull String expectedNodeString) { + + DetectionStore decryptStore = + getStoreOfValueType(CipherAction.class, store.getChildren()); + assertThat(decryptStore).isNotNull(); + assertThat(decryptStore.getDetectionValues()).hasSize(1); + assertThat(decryptStore.getDetectionValues().get(0).asString()).isEqualTo("DECRYPT"); + + assertThat(node.asString()).isEqualTo(expectedNodeString); + assertThat(node.getChildren().get(Decrypt.class)).isNotNull(); + } + + private void assertSign( + @Nonnull DetectionStore store, + @Nonnull INode node, + @Nonnull String expectedNodeString) { + + DetectionStore signStore = + getStoreOfValueType(SignatureAction.class, store.getChildren()); + assertThat(signStore).isNotNull(); + assertThat(signStore.getDetectionValues()).hasSize(1); + assertThat(signStore.getDetectionValues().get(0).asString()).isEqualTo("SIGN"); + + assertThat(node.asString()).isEqualTo(expectedNodeString); + assertThat(node.getChildren().get(Sign.class)).isNotNull(); + } + + private void assertVerify( + @Nonnull DetectionStore store, + @Nonnull INode node, + @Nonnull String expectedNodeString) { + + DetectionStore verifyStore = + getStoreOfValueType(SignatureAction.class, store.getChildren()); + assertThat(verifyStore).isNotNull(); + assertThat(verifyStore.getDetectionValues()).hasSize(1); + assertThat(verifyStore.getDetectionValues().get(0).asString()).isEqualTo("VERIFY"); + + assertThat(node.asString()).isEqualTo(expectedNodeString); + assertThat(node.getChildren().get(Verify.class)).isNotNull(); } } diff --git a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetRandomNumberGeneratorTest.java b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetRandomNumberGeneratorTest.java new file mode 100644 index 000000000..7e5e08296 --- /dev/null +++ b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetRandomNumberGeneratorTest.java @@ -0,0 +1,150 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ibm.engine.detection.DetectionStore; +import com.ibm.engine.language.csharp.CSharpCheck; +import com.ibm.engine.language.csharp.CSharpScanContext; +import com.ibm.engine.language.csharp.CSharpSymbol; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.IValue; +import com.ibm.engine.model.ValueAction; +import com.ibm.engine.model.context.PRNGContext; +import com.ibm.mapper.model.INode; +import com.ibm.mapper.model.PseudorandomNumberGenerator; +import com.ibm.mapper.model.functionality.Generate; +import com.ibm.plugin.CSharpVerifier; +import com.ibm.plugin.TestBase; +import java.util.List; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +/** + * Comprehensive test for all {@code RandomNumberGenerator} / {@code RNGCryptoServiceProvider} + * detection rules ({@code DotNetRandomNumberGenerator.java}). + * + *

Covers: + * + *

    + *
  • {@code RandomNumberGenerator.Create()} / {@code Create(string)} + instance {@code + * GetBytes}/{@code GetNonZeroBytes} + *
  • {@code RandomNumberGenerator}'s genuinely static-only methods (verified against the real + * .NET compiler -- {@code GetNonZeroBytes} has no static overload at all): {@code Fill}, + * {@code GetBytes(int)}, {@code GetHexString}, {@code GetInt32} (both overloads), {@code + * GetItems}, {@code GetString}, {@code Shuffle} + *
  • {@code RNGCryptoServiceProvider} constructor overloads + instance {@code GetBytes}/{@code + * GetNonZeroBytes} + *
+ * + *

Node strings below were captured from an actual test run's debug log ({@code + * target/node-tree.log}), not guessed: + * + *

+ * Finding mapping (one finding per test method in DotNetRandomNumberGeneratorTestFile.cs):
+ *
+ *  0 TestCreateAndGetBytes         → PseudorandomNumberGenerator "NATIVEPRNG" + Generate "GENERATE" child
+ *  1 TestCreateAndGetNonZeroBytes  → PseudorandomNumberGenerator "NATIVEPRNG" + Generate "GENERATE" child
+ *  2 TestCreateNamed               → PseudorandomNumberGenerator "NATIVEPRNG" + Generate "GENERATE" child
+ *  3 TestStaticFill                → PseudorandomNumberGenerator "NATIVEPRNG" (no children)
+ *  4 TestStaticGetBytesCount       → PseudorandomNumberGenerator "NATIVEPRNG" (no children)
+ *  5 TestStaticGetHexString        → PseudorandomNumberGenerator "NATIVEPRNG" (no children)
+ *  6 TestStaticGetInt32            → PseudorandomNumberGenerator "NATIVEPRNG" (no children)
+ *  7 TestStaticGetInt32Range       → PseudorandomNumberGenerator "NATIVEPRNG" (no children)
+ *  8 TestStaticGetItems            → PseudorandomNumberGenerator "NATIVEPRNG" (no children)
+ *  9 TestStaticGetString           → PseudorandomNumberGenerator "NATIVEPRNG" (no children)
+ * 10 TestStaticShuffle             → PseudorandomNumberGenerator "NATIVEPRNG" (no children)
+ * 11 TestRngCspGetBytes            → PseudorandomNumberGenerator "NATIVEPRNG" + Generate "GENERATE" child
+ * 12 TestRngCspGetNonZeroBytes     → PseudorandomNumberGenerator "NATIVEPRNG" + Generate "GENERATE" child
+ * 13 TestRngCspWithSeed            → PseudorandomNumberGenerator "NATIVEPRNG" + Generate "GENERATE" child
+ * 
+ * + *

Note: {@code GetNonZeroBytes} has no static overload at all (unlike {@code GetBytes}, which + * has a genuinely static {@code GetBytes(int)} alongside its instance overloads) -- verified by + * compiling {@code RandomNumberGenerator.GetNonZeroBytes(data)} against the real .NET 10 compiler + * (CS0120). There is therefore no "static GetNonZeroBytes" finding; finding 1 ( {@code + * TestCreateAndGetNonZeroBytes}) already exercises the only instance form. + */ +class DotNetRandomNumberGeneratorTest extends TestBase { + + @Test + void test() throws Exception { + CSharpVerifier.verify( + "rules/detection/dotnet/DotNetRandomNumberGeneratorTestFile.cs", this); + } + + @Override + public void asserts( + int findingId, + @Nonnull + DetectionStore + detectionStore, + @Nonnull List nodes) { + + assertThat(detectionStore.getDetectionValueContext()).isInstanceOf(PRNGContext.class); + assertThat(detectionStore.getDetectionValues()).hasSize(1); + IValue primary = detectionStore.getDetectionValues().get(0); + assertThat(primary).isInstanceOf(ValueAction.class); + assertThat(primary.asString()).isEqualTo("NATIVEPRNG"); + + assertThat(nodes).hasSize(1); + INode node = nodes.get(0); + assertThat(node.getKind()).isEqualTo(PseudorandomNumberGenerator.class); + assertThat(node.asString()).isEqualTo("NATIVEPRNG"); + + switch (findingId) { + // ----------------------------------------------------------------- + // Section 1: RandomNumberGenerator.Create()/Create(string) + instance operations + // ----------------------------------------------------------------- + case 0, 1, 2 -> assertGenerateChild(detectionStore, node); + + // ----------------------------------------------------------------- + // Section 2: RandomNumberGenerator static-only methods — self-contained, + // no depending rules, no children. + // ----------------------------------------------------------------- + case 3, 4, 5, 6, 7, 8, 9, 10 -> assertThat(node.getChildren()).isEmpty(); + + // ----------------------------------------------------------------- + // Section 3: RNGCryptoServiceProvider + // ----------------------------------------------------------------- + case 11, 12, 13 -> assertGenerateChild(detectionStore, node); + + default -> throw new IllegalStateException("Unexpected findingId: " + findingId); + } + } + + // ------------------------------------------------------------------------- + // Assertion helpers + // ------------------------------------------------------------------------- + + private void assertGenerateChild( + @Nonnull DetectionStore store, + @Nonnull INode node) { + + DetectionStore childStore = + getStoreOfValueType(ValueAction.class, store.getChildren()); + assertThat(childStore).isNotNull(); + assertThat(childStore.getDetectionValues()).hasSize(1); + + assertThat(node.getChildren().get(Generate.class)).isNotNull(); + assertThat(node.getChildren().get(Generate.class).asString()).isEqualTo("GENERATE"); + } +} diff --git a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetRfc2898DeriveBytesTest.java b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetRfc2898DeriveBytesTest.java index 4cf6473a4..c62f7d860 100755 --- a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetRfc2898DeriveBytesTest.java +++ b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetRfc2898DeriveBytesTest.java @@ -31,6 +31,7 @@ import com.ibm.engine.model.context.KeyContext; import com.ibm.mapper.model.INode; import com.ibm.mapper.model.PasswordBasedKeyDerivationFunction; +import com.ibm.mapper.model.functionality.KeyDerivation; import com.ibm.plugin.CSharpVerifier; import com.ibm.plugin.TestBase; import java.util.List; @@ -51,14 +52,43 @@ public void asserts( DetectionStore detectionStore, @Nonnull List nodes) { - assertThat(detectionStore.getDetectionValues()).hasSize(1); assertThat(detectionStore.getDetectionValueContext()).isInstanceOf(KeyContext.class); - IValue value0 = detectionStore.getDetectionValues().get(0); - assertThat(value0).isInstanceOf(ValueAction.class); - assertThat(value0.asString()).isEqualTo("PBKDF2"); + assertThat(detectionStore.getDetectionValues()).hasSize(1); + IValue primary = detectionStore.getDetectionValues().get(0); + assertThat(primary).isInstanceOf(ValueAction.class); + assertThat(primary.asString()).isEqualTo("PBKDF2"); assertThat(nodes).hasSize(1); - assertThat(nodes.get(0).getKind()).isEqualTo(PasswordBasedKeyDerivationFunction.class); - assertThat(nodes.get(0).asString()).isEqualTo("PBKDF2"); + INode node = nodes.get(0); + assertThat(node.getKind()).isEqualTo(PasswordBasedKeyDerivationFunction.class); + assertThat(node.asString()).isEqualTo("PBKDF2"); + + switch (findingId) { + // 0: new Rfc2898DeriveBytes(...) alone — no operations called afterwards + case 0 -> assertThat(node.getChildren()).isEmpty(); + // 1: new Rfc2898DeriveBytes(...) followed by kdf.GetBytes(32) + case 1 -> assertKeyDerivationChild(detectionStore, node); + // 2: new Rfc2898DeriveBytes(...) followed by kdf.CryptDeriveKey(...) + case 2 -> assertKeyDerivationChild(detectionStore, node); + // 3: Rfc2898DeriveBytes.Pbkdf2(byte[], byte[], int, HashAlgorithmName, int) — static, + // self-contained, no depending operations + case 3 -> assertThat(node.getChildren()).isEmpty(); + // 4: Rfc2898DeriveBytes.Pbkdf2(string, byte[], int, HashAlgorithmName, int) — static + case 4 -> assertThat(node.getChildren()).isEmpty(); + default -> throw new IllegalStateException("Unexpected findingId: " + findingId); + } + } + + private void assertKeyDerivationChild( + @Nonnull DetectionStore store, + @Nonnull INode node) { + DetectionStore deriveStore = + getStoreOfValueType(ValueAction.class, store.getChildren()); + assertThat(deriveStore).isNotNull(); + assertThat(deriveStore.getDetectionValues()).hasSize(1); + + assertThat(node.getChildren().get(KeyDerivation.class)).isNotNull(); + assertThat(node.getChildren().get(KeyDerivation.class).asString()) + .isEqualTo("KEYDERIVATION"); } } diff --git a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetSHA3Test.java b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetSHA3Test.java new file mode 100644 index 000000000..bc4cb7204 --- /dev/null +++ b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetSHA3Test.java @@ -0,0 +1,127 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ibm.engine.detection.DetectionStore; +import com.ibm.engine.language.csharp.CSharpCheck; +import com.ibm.engine.language.csharp.CSharpScanContext; +import com.ibm.engine.language.csharp.CSharpSymbol; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.IValue; +import com.ibm.engine.model.ValueAction; +import com.ibm.engine.model.context.DigestContext; +import com.ibm.mapper.model.DigestSize; +import com.ibm.mapper.model.ExtendableOutputFunction; +import com.ibm.mapper.model.INode; +import com.ibm.mapper.model.MessageDigest; +import com.ibm.mapper.model.ParameterSetIdentifier; +import com.ibm.plugin.CSharpVerifier; +import com.ibm.plugin.TestBase; +import java.util.List; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link DotNetSHA3}, covering: + * + *

    + *
  • findingId 0 → {@code TestSha3_256Create} → {@code SHA3-256} + *
  • findingId 1 → {@code TestSha3_384Create} → {@code SHA3-384} + *
  • findingId 2 → {@code TestSha3_512Create} → {@code SHA3-512} + *
  • findingId 3 → {@code TestShake128} → {@code SHAKE128} + *
  • findingId 4 → {@code TestShake256} → {@code SHAKE256} + *
+ */ +class DotNetSHA3Test extends TestBase { + + @Test + void test() throws Exception { + CSharpVerifier.verify("rules/detection/dotnet/DotNetSHA3TestFile.cs", this); + } + + @Override + public void asserts( + int findingId, + @Nonnull + DetectionStore + detectionStore, + @Nonnull List nodes) { + + /* + * Detection Store + */ + assertThat(detectionStore.getDetectionValues()).hasSize(1); + assertThat(detectionStore.getDetectionValueContext()).isInstanceOf(DigestContext.class); + IValue value0 = detectionStore.getDetectionValues().get(0); + assertThat(value0).isInstanceOf(ValueAction.class); + + /* + * Translation + */ + assertThat(nodes).hasSize(1); + INode node = nodes.get(0); + + switch (findingId) { + case 0 -> { + assertThat(value0.asString()).isEqualTo("SHA3_256"); + assertThat(node.getKind()).isEqualTo(MessageDigest.class); + assertThat(node.asString()).isEqualTo("SHA3-256"); + INode digestSize = node.getChildren().get(DigestSize.class); + assertThat(digestSize).isNotNull(); + assertThat(digestSize.asString()).isEqualTo("256"); + } + case 1 -> { + assertThat(value0.asString()).isEqualTo("SHA3_384"); + assertThat(node.getKind()).isEqualTo(MessageDigest.class); + assertThat(node.asString()).isEqualTo("SHA3-384"); + INode digestSize = node.getChildren().get(DigestSize.class); + assertThat(digestSize).isNotNull(); + assertThat(digestSize.asString()).isEqualTo("384"); + } + case 2 -> { + assertThat(value0.asString()).isEqualTo("SHA3_512"); + assertThat(node.getKind()).isEqualTo(MessageDigest.class); + assertThat(node.asString()).isEqualTo("SHA3-512"); + INode digestSize = node.getChildren().get(DigestSize.class); + assertThat(digestSize).isNotNull(); + assertThat(digestSize.asString()).isEqualTo("512"); + } + case 3 -> { + assertThat(value0.asString()).isEqualTo("Shake128"); + assertThat(node.getKind()).isEqualTo(ExtendableOutputFunction.class); + assertThat(node.asString()).isEqualTo("SHAKE128"); + INode parameterSetIdentifier = node.getChildren().get(ParameterSetIdentifier.class); + assertThat(parameterSetIdentifier).isNotNull(); + assertThat(parameterSetIdentifier.asString()).isEqualTo("128"); + } + case 4 -> { + assertThat(value0.asString()).isEqualTo("Shake256"); + assertThat(node.getKind()).isEqualTo(ExtendableOutputFunction.class); + assertThat(node.asString()).isEqualTo("SHAKE256"); + INode parameterSetIdentifier = node.getChildren().get(ParameterSetIdentifier.class); + assertThat(parameterSetIdentifier).isNotNull(); + assertThat(parameterSetIdentifier.asString()).isEqualTo("256"); + } + default -> throw new IllegalStateException("Unexpected findingId: " + findingId); + } + } +} diff --git a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetSHATest.java b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetSHATest.java index 7f787d40d..70e4abe4a 100755 --- a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetSHATest.java +++ b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetSHATest.java @@ -104,6 +104,135 @@ public void asserts( assertThat(digestSize).isNotNull(); assertThat(digestSize.asString()).isEqualTo("128"); } + // MD5.Create("MD5") / SHA*.Create("SHA*") named-factory overloads: same detected + // value and translation as their no-arg Create() counterparts (cases 0-4). + case 5 -> { + assertThat(value0.asString()).isEqualTo("MD5"); + assertThat(node.asString()).isEqualTo("MD5"); + INode digestSize = node.getChildren().get(DigestSize.class); + assertThat(digestSize).isNotNull(); + assertThat(digestSize.asString()).isEqualTo("128"); + } + case 6 -> { + assertThat(value0.asString()).isEqualTo("SHA1"); + assertThat(node.asString()).isEqualTo("SHA-1"); + INode digestSize = node.getChildren().get(DigestSize.class); + assertThat(digestSize).isNotNull(); + assertThat(digestSize.asString()).isEqualTo("160"); + } + case 7 -> { + assertThat(value0.asString()).isEqualTo("SHA256"); + assertThat(node.asString()).isEqualTo("SHA-256"); + INode digestSize = node.getChildren().get(DigestSize.class); + assertThat(digestSize).isNotNull(); + assertThat(digestSize.asString()).isEqualTo("256"); + } + case 8 -> { + assertThat(value0.asString()).isEqualTo("SHA384"); + assertThat(node.asString()).isEqualTo("SHA-384"); + INode digestSize = node.getChildren().get(DigestSize.class); + assertThat(digestSize).isNotNull(); + assertThat(digestSize.asString()).isEqualTo("384"); + } + case 9 -> { + assertThat(value0.asString()).isEqualTo("SHA512"); + assertThat(node.asString()).isEqualTo("SHA-512"); + INode digestSize = node.getChildren().get(DigestSize.class); + assertThat(digestSize).isNotNull(); + assertThat(digestSize.asString()).isEqualTo("512"); + } + // new MD5Cng(): CNG-backed implementation, same translation as MD5.Create() (case 4). + case 10 -> { + assertThat(value0.asString()).isEqualTo("MD5"); + assertThat(node.asString()).isEqualTo("MD5"); + INode digestSize = node.getChildren().get(DigestSize.class); + assertThat(digestSize).isNotNull(); + assertThat(digestSize.asString()).isEqualTo("128"); + } + // new SHA1Cng() / new SHA1CryptoServiceProvider(): same translation as SHA1.Create(). + case 11, 12 -> { + assertThat(value0.asString()).isEqualTo("SHA1"); + assertThat(node.asString()).isEqualTo("SHA-1"); + INode digestSize = node.getChildren().get(DigestSize.class); + assertThat(digestSize).isNotNull(); + assertThat(digestSize.asString()).isEqualTo("160"); + } + // new SHA256Cng() / new SHA256CryptoServiceProvider(): same as SHA256.Create(). + case 13, 14 -> { + assertThat(value0.asString()).isEqualTo("SHA256"); + assertThat(node.asString()).isEqualTo("SHA-256"); + INode digestSize = node.getChildren().get(DigestSize.class); + assertThat(digestSize).isNotNull(); + assertThat(digestSize.asString()).isEqualTo("256"); + } + // new SHA384Cng() / new SHA384CryptoServiceProvider(): same as SHA384.Create(). + case 15, 16 -> { + assertThat(value0.asString()).isEqualTo("SHA384"); + assertThat(node.asString()).isEqualTo("SHA-384"); + INode digestSize = node.getChildren().get(DigestSize.class); + assertThat(digestSize).isNotNull(); + assertThat(digestSize.asString()).isEqualTo("384"); + } + // new SHA512Cng() / new SHA512CryptoServiceProvider(): same as SHA512.Create(). + case 17, 18 -> { + assertThat(value0.asString()).isEqualTo("SHA512"); + assertThat(node.asString()).isEqualTo("SHA-512"); + INode digestSize = node.getChildren().get(DigestSize.class); + assertThat(digestSize).isNotNull(); + assertThat(digestSize.asString()).isEqualTo("512"); + } + // RIPEMD160.Create() / RIPEMD160.Create("RIPEMD160") / new RIPEMD160Managed() + case 19, 20, 21 -> { + assertThat(value0.asString()).isEqualTo("RIPEMD160"); + assertThat(node.asString()).isEqualTo("RIPEMD-160"); + INode digestSize = node.getChildren().get(DigestSize.class); + assertThat(digestSize).isNotNull(); + assertThat(digestSize.asString()).isEqualTo("160"); + } + // new MD5CryptoServiceProvider(): legacy CAPI implementation, same translation as + // MD5.Create() (case 4). + case 22 -> { + assertThat(value0.asString()).isEqualTo("MD5"); + assertThat(node.asString()).isEqualTo("MD5"); + INode digestSize = node.getChildren().get(DigestSize.class); + assertThat(digestSize).isNotNull(); + assertThat(digestSize.asString()).isEqualTo("128"); + } + // new SHA1Managed(): pure-managed implementation, same translation as SHA1.Create(). + case 23 -> { + assertThat(value0.asString()).isEqualTo("SHA1"); + assertThat(node.asString()).isEqualTo("SHA-1"); + INode digestSize = node.getChildren().get(DigestSize.class); + assertThat(digestSize).isNotNull(); + assertThat(digestSize.asString()).isEqualTo("160"); + } + // new SHA256Managed(): pure-managed implementation, same translation as + // SHA256.Create(). + case 24 -> { + assertThat(value0.asString()).isEqualTo("SHA256"); + assertThat(node.asString()).isEqualTo("SHA-256"); + INode digestSize = node.getChildren().get(DigestSize.class); + assertThat(digestSize).isNotNull(); + assertThat(digestSize.asString()).isEqualTo("256"); + } + // new SHA384Managed(): pure-managed implementation, same translation as + // SHA384.Create(). + case 25 -> { + assertThat(value0.asString()).isEqualTo("SHA384"); + assertThat(node.asString()).isEqualTo("SHA-384"); + INode digestSize = node.getChildren().get(DigestSize.class); + assertThat(digestSize).isNotNull(); + assertThat(digestSize.asString()).isEqualTo("384"); + } + // new SHA512Managed(): pure-managed implementation, same translation as + // SHA512.Create(). + case 26 -> { + assertThat(value0.asString()).isEqualTo("SHA512"); + assertThat(node.asString()).isEqualTo("SHA-512"); + INode digestSize = node.getChildren().get(DigestSize.class); + assertThat(digestSize).isNotNull(); + assertThat(digestSize.asString()).isEqualTo("512"); + } default -> throw new IllegalStateException("Unexpected findingId: " + findingId); } } diff --git a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetSlhDsaTest.java b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetSlhDsaTest.java new file mode 100644 index 000000000..7147de276 --- /dev/null +++ b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetSlhDsaTest.java @@ -0,0 +1,192 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ibm.engine.detection.DetectionStore; +import com.ibm.engine.language.csharp.CSharpCheck; +import com.ibm.engine.language.csharp.CSharpScanContext; +import com.ibm.engine.language.csharp.CSharpSymbol; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.IValue; +import com.ibm.engine.model.ParameterIdentifier; +import com.ibm.engine.model.ValueAction; +import com.ibm.engine.model.context.KeyContext; +import com.ibm.mapper.model.INode; +import com.ibm.mapper.model.ParameterSetIdentifier; +import com.ibm.mapper.model.Signature; +import com.ibm.mapper.model.functionality.Sign; +import com.ibm.mapper.model.functionality.Verify; +import com.ibm.plugin.CSharpVerifier; +import com.ibm.plugin.TestBase; +import java.util.List; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +/** + * Comprehensive test for all SLH-DSA related detection rules (DotNetSlhDsa.java). + * + *

Covers the {@code SlhDsa} abstract base (all of its static factory methods), the {@code + * SlhDsaCng}/{@code SlhDsaOpenSsl} native-interop derived classes, and the {@code SignData}/{@code + * VerifyData}/{@code SignPreHash}/{@code VerifyPreHash} instance operations (no {@code SignMu}/ + * {@code VerifyMu} — confirmed absent from the official {@code SlhDsa} reference, see {@code + * DotNetSlhDsa}'s class javadoc). + * + *

Every top-level finding translates to a {@link com.ibm.mapper.model.algorithms.SPHINCSPlus} + * node (kind {@link Signature}, {@code NAME = "SLH-DSA"}). When the {@code SlhDsaAlgorithm} + * parameter set is captured, {@code node.asString()} is e.g. {@code "SLH-DSA-SHA2-128s"} and the + * node carries a {@link ParameterSetIdentifier} child. + * + *

Finding mapping (one finding per test method in DotNetSlhDsaTestFile.cs; verified against the + * actual {@code target/node-tree.log} output of a debug run, not guessed): + * + *

+ * Section 1 – SlhDsa algorithm-parameterized creation (findings 0–3):
+ *   0 TestGenerateKeySha2_128s      → SLH-DSA-SHA2-128s
+ *   1 TestGenerateKeyShake256f      → SLH-DSA-SHAKE-256f
+ *   2 TestImportSlhDsaPrivateKey    → SLH-DSA-SHA2-192f
+ *   3 TestImportSlhDsaPublicKey     → SLH-DSA-SHAKE-128s
+ *
+ * Section 2 – SlhDsa structural imports, no parameter set (findings 4–8):
+ *   4 TestImportPkcs8PrivateKey          → SLH-DSA (generic)
+ *   5 TestImportSubjectPublicKeyInfo     → SLH-DSA (generic)
+ *   6 TestImportFromPem                  → SLH-DSA (generic)
+ *   7 TestImportEncryptedPkcs8PrivateKey → SLH-DSA (generic)
+ *   8 TestImportFromEncryptedPem         → SLH-DSA (generic)
+ *
+ * Section 3 – SlhDsa native-interop constructors, no parameter set (findings 9–10):
+ *   9  TestSlhDsaCng      → SLH-DSA (generic)
+ *   10 TestSlhDsaOpenSsl  → SLH-DSA (generic)
+ *
+ * Section 4 – SlhDsa Sign / Verify operations (findings 11–14):
+ *   11 TestSignData      → SLH-DSA-SHA2-128s, Sign child
+ *   12 TestVerifyData    → SLH-DSA-SHA2-128s, Verify child
+ *   13 TestSignPreHash   → SLH-DSA-SHA2-128s, Sign child
+ *   14 TestVerifyPreHash → SLH-DSA-SHA2-128s, Verify child
+ *
+ * Section 5 – combined usage pattern (finding 15):
+ *   15 TestFullFlow → SLH-DSA-SHAKE-256f, Sign child, Verify child
+ * 
+ */ +class DotNetSlhDsaTest extends TestBase { + + @Test + void test() throws Exception { + CSharpVerifier.verify("rules/detection/dotnet/DotNetSlhDsaTestFile.cs", this); + } + + @Override + public void asserts( + int findingId, + @Nonnull + DetectionStore + detectionStore, + @Nonnull List nodes) { + + // Every top-level finding must be SLH-DSA + assertThat(detectionStore.getDetectionValueContext()).isInstanceOf(KeyContext.class); + assertThat(detectionStore.getDetectionValues()).hasSize(1); + IValue primary = detectionStore.getDetectionValues().get(0); + assertThat(primary).isInstanceOf(ValueAction.class); + assertThat(primary.asString()).isEqualTo("SLH-DSA"); + + assertThat(nodes).hasSize(1); + INode node = nodes.get(0); + assertThat(node.getKind()).isEqualTo(Signature.class); + + switch (findingId) { + + // ----------------------------------------------------------------- + // Section 1: SlhDsa algorithm-parameterized creation + // ----------------------------------------------------------------- + case 0 -> assertParameterSet(detectionStore, node, "SlhDsaSha2_128s", "SHA2-128s"); + case 1 -> assertParameterSet(detectionStore, node, "SlhDsaShake256f", "SHAKE-256f"); + case 2 -> assertParameterSet(detectionStore, node, "SlhDsaSha2_192f", "SHA2-192f"); + case 3 -> assertParameterSet(detectionStore, node, "SlhDsaShake128s", "SHAKE-128s"); + + // ----------------------------------------------------------------- + // Section 2 & 3: generic SLH-DSA, no parameter set + // ----------------------------------------------------------------- + case 4, 5, 6, 7, 8, 9, 10 -> assertGeneric(node); + + // ----------------------------------------------------------------- + // Section 4: SlhDsa Sign / Verify operations + // ----------------------------------------------------------------- + case 11 -> { + assertParameterSet(detectionStore, node, "SlhDsaSha2_128s", "SHA2-128s"); + assertThat(node.getChildren().get(Sign.class)).isNotNull(); + } + case 12 -> { + assertParameterSet(detectionStore, node, "SlhDsaSha2_128s", "SHA2-128s"); + assertThat(node.getChildren().get(Verify.class)).isNotNull(); + } + case 13 -> { + assertParameterSet(detectionStore, node, "SlhDsaSha2_128s", "SHA2-128s"); + assertThat(node.getChildren().get(Sign.class)).isNotNull(); + } + case 14 -> { + assertParameterSet(detectionStore, node, "SlhDsaSha2_128s", "SHA2-128s"); + assertThat(node.getChildren().get(Verify.class)).isNotNull(); + } + + // ----------------------------------------------------------------- + // Section 5: combined usage pattern + // ----------------------------------------------------------------- + case 15 -> { + assertParameterSet(detectionStore, node, "SlhDsaShake256f", "SHAKE-256f"); + assertThat(node.getChildren().get(Sign.class)).isNotNull(); + assertThat(node.getChildren().get(Verify.class)).isNotNull(); + } + + default -> throw new IllegalStateException("Unexpected findingId: " + findingId); + } + } + + // ------------------------------------------------------------------------- + // Assertion helpers + // ------------------------------------------------------------------------- + + private void assertParameterSet( + @Nonnull DetectionStore store, + @Nonnull INode node, + @Nonnull String expectedRawIdentifier, + @Nonnull String expectedParameterSet) { + + DetectionStore + parameterIdentifierStore = + getStoreOfValueType(ParameterIdentifier.class, store.getChildren()); + assertThat(parameterIdentifierStore).isNotNull(); + assertThat(parameterIdentifierStore.getDetectionValues()).hasSize(1); + assertThat(parameterIdentifierStore.getDetectionValues().get(0).asString()) + .isEqualTo(expectedRawIdentifier); + + assertThat(node.asString()).isEqualTo("SLH-DSA-" + expectedParameterSet); + + INode parameterSetIdentifier = node.getChildren().get(ParameterSetIdentifier.class); + assertThat(parameterSetIdentifier).isNotNull(); + assertThat(parameterSetIdentifier.asString()).isEqualTo(expectedParameterSet); + } + + private void assertGeneric(@Nonnull INode node) { + assertThat(node.asString()).isEqualTo("SLH-DSA"); + assertThat(node.getChildren().get(ParameterSetIdentifier.class)).isNull(); + } +} diff --git a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetTripleDESComprehensiveTest.java b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetTripleDESComprehensiveTest.java new file mode 100644 index 000000000..23ee4722b --- /dev/null +++ b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetTripleDESComprehensiveTest.java @@ -0,0 +1,438 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ibm.engine.detection.DetectionStore; +import com.ibm.engine.language.csharp.CSharpCheck; +import com.ibm.engine.language.csharp.CSharpScanContext; +import com.ibm.engine.language.csharp.CSharpSymbol; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.CipherAction; +import com.ibm.engine.model.IValue; +import com.ibm.engine.model.KeySize; +import com.ibm.engine.model.Mode; +import com.ibm.engine.model.Padding; +import com.ibm.engine.model.ValueAction; +import com.ibm.engine.model.context.CipherContext; +import com.ibm.mapper.model.BlockCipher; +import com.ibm.mapper.model.INode; +import com.ibm.mapper.model.KeyLength; +import com.ibm.mapper.model.functionality.Decrypt; +import com.ibm.mapper.model.functionality.Encrypt; +import com.ibm.mapper.model.functionality.Generate; +import com.ibm.mapper.model.functionality.KeyGeneration; +import com.ibm.plugin.CSharpVerifier; +import com.ibm.plugin.TestBase; +import java.util.List; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +/** + * Comprehensive test for all Triple DES (3DES)-related detection rules (DotNetTripleDES.java). + * + *

Covers all three TripleDES-related classes and their complete API surface: + * + *

    + *
  • TripleDES (abstract base) + *
  • TripleDESCryptoServiceProvider (derived from TripleDES, legacy CAPI) + *
  • TripleDESCng (derived from TripleDES, CNG-backed) + *
+ * + *

TripleDES exposes no extra properties beyond those inherited from {@code SymmetricAlgorithm} + * (unlike RC2's {@code EffectiveKeySize}), so the depending-rule set mirrors {@code DotNetDES} + * exactly, plus a CNG-backed creation rule (mirroring {@code AesCng} in {@code DotNetAES}). + * + *

Finding mapping (one finding per test method in DotNetTripleDESComprehensiveTestFile.cs): + * + *

+ * Section 1 – factory methods / constructors (findings 0–4):
+ *   0  TestTripleDesCreate            → DESede
+ *   1  TestTripleDesCreateNamed       → DESede
+ *   2  TestTripleDesCsp               → DESede
+ *   3  TestTripleDesCng               → DESede
+ *   4  TestTripleDesCngNamed          → DESede
+ *
+ * Section 2 – property setters (findings 5–17):
+ *   5  TestPropertyModeCBC            → DESede-CBC
+ *   6  TestPropertyModeECB            → DESede-ECB
+ *   7  TestPropertyModeCFB            → DESede-CFB
+ *   8  TestPropertyModeOFB            → DESede-OFB
+ *   9  TestPropertyModeCTS            → DESede-CTS
+ *   10 TestPropertyKeySize            → DESede192 (KeyLength appended with no separator)
+ *   11 TestPropertyPaddingPKCS7       → DESede-PKCS7
+ *   12 TestPropertyPaddingNone        → DESede-None
+ *   13 TestPropertyPaddingZeros       → DESede-Zeros
+ *   14 TestPropertyPaddingANSIX923    → DESede-ANSIX923
+ *   15 TestPropertyFeedbackSize       → DESede (BlockSize never rendered in asString)
+ *   16 TestPropertyIV                 → DESede (no IV rule)
+ *   17 TestPropertyKey                → DESede (no Key rule)
+ *
+ * Section 3 – CreateEncryptor/CreateDecryptor (findings 18–21):
+ *   18 TestCreateEncryptorNoArgs      → DESede + Encrypt
+ *   19 TestCreateEncryptorWithArgs    → DESede + Encrypt
+ *   20 TestCreateDecryptorNoArgs      → DESede + Decrypt
+ *   21 TestCreateDecryptorWithArgs    → DESede + Decrypt
+ *
+ * Section 4 – direct encrypt (findings 22–24):
+ *   22 TestEncryptCbc                 → DESede-CBC-PKCS7
+ *   23 TestEncryptEcb                 → DESede-ECB-None
+ *   24 TestEncryptCfb                 → DESede-CFB-None
+ *
+ * Section 5 – direct decrypt (findings 25–27):
+ *   25 TestDecryptCbc                 → DESede-CBC-PKCS7
+ *   26 TestDecryptEcb                 → DESede-ECB-None
+ *   27 TestDecryptCfb                 → DESede-CFB-None
+ *
+ * Section 6 – Try* variants (findings 28–33):
+ *   28 TestTryEncryptCbc              → DESede-CBC-PKCS7
+ *   29 TestTryDecryptCbc              → DESede-CBC-PKCS7
+ *   30 TestTryEncryptEcb              → DESede-ECB-None
+ *   31 TestTryDecryptEcb              → DESede-ECB-None
+ *   32 TestTryEncryptCfb              → DESede-CFB-None
+ *   33 TestTryDecryptCfb              → DESede-CFB-None
+ *
+ * Section 7 – key/IV generation (findings 34–35):
+ *   34 TestGenerateKey                → DESede + KeyGeneration
+ *   35 TestGenerateIV                 → DESede + Generate
+ *
+ * Section 8 – combined usage patterns (findings 36–42):
+ *   36 TestTripleDesCbcFullFlow           → DESede-CBC-PKCS7 + Encrypt
+ *   37 TestTripleDesCspEncryptCbc         → DESede-CBC-PKCS7
+ *   38 TestTripleDesCspDecryptCbc         → DESede-CBC-PKCS7
+ *   39 TestTripleDesCfbFeedback           → DESede-CFB-None
+ *   40 TestTripleDesCbcWithEncryptorOverload → DESede + Encrypt
+ *   41 TestTripleDesEcbEncrypt            → DESede-ECB-None
+ *   42 TestTripleDesCngEncryptCbc         → DESede-CBC-PKCS7
+ * 
+ * + *

NOTE: unlike {@code RC2} (whose {@code asString()} uses {@code composeName(true, true, false)} + * and never renders padding), {@code DESede.asString()} explicitly appends {@code KeyLength} (no + * separator), then {@code -}, then {@code -} (see {@code DESede.java}). Verified + * against actual test-run debug output ({@code target/node-tree.log}) rather than guessed. + */ +class DotNetTripleDESComprehensiveTest extends TestBase { + + @Test + void test() throws Exception { + CSharpVerifier.verify( + "rules/detection/dotnet/DotNetTripleDESComprehensiveTestFile.cs", this); + } + + @Override + public void asserts( + int findingId, + @Nonnull + DetectionStore + detectionStore, + @Nonnull List nodes) { + + // Every top-level finding must be TRIPLEDES + assertThat(detectionStore.getDetectionValueContext()).isInstanceOf(CipherContext.class); + assertThat(detectionStore.getDetectionValues()).hasSize(1); + IValue primary = detectionStore.getDetectionValues().get(0); + assertThat(primary).isInstanceOf(ValueAction.class); + assertThat(primary.asString()).isEqualTo("TRIPLEDES"); + + switch (findingId) { + + // ----------------------------------------------------------------- + // Section 1: simple constructors — only DESede, no children fired + // ----------------------------------------------------------------- + case 0, 1, 2, 3, 4 -> { + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo("DESede"); + } + + // ----------------------------------------------------------------- + // Section 2a: property Mode setters + // ----------------------------------------------------------------- + case 5 -> assertModeFindings(detectionStore, nodes, "CBC", "DESede-CBC"); + case 6 -> assertModeFindings(detectionStore, nodes, "ECB", "DESede-ECB"); + case 7 -> assertModeFindings(detectionStore, nodes, "CFB", "DESede-CFB"); + case 8 -> assertModeFindings(detectionStore, nodes, "OFB", "DESede-OFB"); + case 9 -> assertModeFindings(detectionStore, nodes, "CTS", "DESede-CTS"); + + // ----------------------------------------------------------------- + // Section 2b: property KeySize setter — KeyLength is appended + // directly after the name (no separator) by DESede.asString(). + // ----------------------------------------------------------------- + case 10 -> assertKeySizeFindings(detectionStore, nodes, "192", "DESede192"); + + // ----------------------------------------------------------------- + // Section 2c: property Padding setters — unlike RC2, DESede.asString() + // does render padding as a "-" suffix. + // ----------------------------------------------------------------- + case 11 -> assertPaddingFindings(detectionStore, nodes, "PKCS7", "DESede-PKCS7"); + case 12 -> assertPaddingFindings(detectionStore, nodes, "None", "DESede-None"); + case 13 -> assertPaddingFindings(detectionStore, nodes, "Zeros", "DESede-Zeros"); + case 14 -> assertPaddingFindings(detectionStore, nodes, "ANSIX923", "DESede-ANSIX923"); + + // ----------------------------------------------------------------- + // Section 2d: FeedbackSize setter — BlockSize(8) detected but never + // contributes to asString() (no BlockSize branch in DESede.asString()) + // ----------------------------------------------------------------- + case 15 -> { + DetectionStore fbStore = + getStoreOfValueType( + com.ibm.engine.model.BlockSize.class, detectionStore.getChildren()); + assertThat(fbStore).isNotNull(); + assertThat(fbStore.getDetectionValues()).hasSize(1); + assertThat(fbStore.getDetectionValues().get(0).asString()).isEqualTo("8"); + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo("DESede"); + } + + // ----------------------------------------------------------------- + // Section 2e: IV and Key setters — no detection rules for these + // ----------------------------------------------------------------- + case 16, 17 -> { + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo("DESede"); + } + + // ----------------------------------------------------------------- + // Section 3: CreateEncryptor / CreateDecryptor + // ----------------------------------------------------------------- + case 18, 19 -> assertEncryptFindings(detectionStore, nodes, "DESede"); + case 20, 21 -> assertDecryptFindings(detectionStore, nodes, "DESede"); + + // ----------------------------------------------------------------- + // Section 4: direct mode-specific encrypt + // ----------------------------------------------------------------- + case 22 -> + assertModePaddingFindings( + detectionStore, nodes, "CBC", "PKCS7", "DESede-CBC-PKCS7"); + case 23 -> + assertModePaddingFindings( + detectionStore, nodes, "ECB", "None", "DESede-ECB-None"); + case 24 -> + assertModePaddingFindings( + detectionStore, nodes, "CFB", "None", "DESede-CFB-None"); + + // ----------------------------------------------------------------- + // Section 5: direct mode-specific decrypt + // ----------------------------------------------------------------- + case 25 -> + assertModePaddingFindings( + detectionStore, nodes, "CBC", "PKCS7", "DESede-CBC-PKCS7"); + case 26 -> + assertModePaddingFindings( + detectionStore, nodes, "ECB", "None", "DESede-ECB-None"); + case 27 -> + assertModePaddingFindings( + detectionStore, nodes, "CFB", "None", "DESede-CFB-None"); + + // ----------------------------------------------------------------- + // Section 6: Try* variants + // ----------------------------------------------------------------- + case 28, 29 -> + assertModePaddingFindings( + detectionStore, nodes, "CBC", "PKCS7", "DESede-CBC-PKCS7"); + case 30, 31 -> + assertModePaddingFindings( + detectionStore, nodes, "ECB", "None", "DESede-ECB-None"); + case 32, 33 -> + assertModePaddingFindings( + detectionStore, nodes, "CFB", "None", "DESede-CFB-None"); + + // ----------------------------------------------------------------- + // Section 7: GenerateKey / GenerateIV + // ----------------------------------------------------------------- + case 34 -> { + // tdes.GenerateKey() → KeyGeneration functionality node + DetectionStore + genKeyStore = + getStoreOfValueType( + ValueAction.class, detectionStore.getChildren()); + assertThat(genKeyStore).isNotNull(); + assertThat(genKeyStore.getDetectionValues().get(0).asString()) + .isEqualTo("GenerateKey"); + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).getChildren().get(KeyGeneration.class)).isNotNull(); + } + case 35 -> { + // tdes.GenerateIV() → Generate functionality node + DetectionStore + genIvStore = + getStoreOfValueType( + ValueAction.class, detectionStore.getChildren()); + assertThat(genIvStore).isNotNull(); + assertThat(genIvStore.getDetectionValues().get(0).asString()) + .isEqualTo("GenerateIV"); + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).getChildren().get(Generate.class)).isNotNull(); + } + + // ----------------------------------------------------------------- + // Section 8: combined usage patterns + // ----------------------------------------------------------------- + case 36 -> { + // TestTripleDesCbcFullFlow: Mode=CBC, Padding=PKCS7, CreateEncryptor + assertThat(nodes).hasSize(1); + INode node = nodes.get(0); + assertThat(node.getKind()).isEqualTo(BlockCipher.class); + assertThat(node.asString()).isEqualTo("DESede-CBC-PKCS7"); + assertThat(node.getChildren().get(com.ibm.mapper.model.Mode.class)).isNotNull(); + assertThat(node.getChildren().get(Encrypt.class)).isNotNull(); + } + case 37 -> + assertModePaddingFindings( + detectionStore, nodes, "CBC", "PKCS7", "DESede-CBC-PKCS7"); + case 38 -> + assertModePaddingFindings( + detectionStore, nodes, "CBC", "PKCS7", "DESede-CBC-PKCS7"); + case 39 -> + assertModePaddingFindings( + detectionStore, nodes, "CFB", "None", "DESede-CFB-None"); + case 40 -> assertEncryptFindings(detectionStore, nodes, "DESede"); + case 41 -> + assertModePaddingFindings( + detectionStore, nodes, "ECB", "None", "DESede-ECB-None"); + case 42 -> + assertModePaddingFindings( + detectionStore, nodes, "CBC", "PKCS7", "DESede-CBC-PKCS7"); + + default -> throw new IllegalStateException("Unexpected findingId: " + findingId); + } + } + + // ------------------------------------------------------------------------- + // Assertion helpers + // ------------------------------------------------------------------------- + + private void assertModeFindings( + @Nonnull DetectionStore store, + @Nonnull List nodes, + @Nonnull String expectedMode, + @Nonnull String expectedNodeString) { + + DetectionStore modeStore = + getStoreOfValueType(Mode.class, store.getChildren()); + assertThat(modeStore).isNotNull(); + assertThat(modeStore.getDetectionValues()).hasSize(1); + assertThat(modeStore.getDetectionValues().get(0).asString()).isEqualTo(expectedMode); + + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo(expectedNodeString); + assertThat(nodes.get(0).getChildren().get(com.ibm.mapper.model.Mode.class)).isNotNull(); + } + + private void assertKeySizeFindings( + @Nonnull DetectionStore store, + @Nonnull List nodes, + @Nonnull String expectedKeySize, + @Nonnull String expectedNodeString) { + + DetectionStore keySizeStore = + getStoreOfValueType(KeySize.class, store.getChildren()); + assertThat(keySizeStore).isNotNull(); + assertThat(keySizeStore.getDetectionValues()).hasSize(1); + assertThat(keySizeStore.getDetectionValues().get(0).asString()).isEqualTo(expectedKeySize); + + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo(expectedNodeString); + assertThat(nodes.get(0).getChildren().get(KeyLength.class)).isNotNull(); + assertThat(nodes.get(0).getChildren().get(KeyLength.class).asString()) + .isEqualTo(expectedKeySize); + } + + private void assertPaddingFindings( + @Nonnull DetectionStore store, + @Nonnull List nodes, + @Nonnull String expectedPadding, + @Nonnull String expectedNodeString) { + + DetectionStore paddingStore = + getStoreOfValueType(Padding.class, store.getChildren()); + assertThat(paddingStore).isNotNull(); + assertThat(paddingStore.getDetectionValues()).hasSize(1); + assertThat(paddingStore.getDetectionValues().get(0).asString()).isEqualTo(expectedPadding); + + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo(expectedNodeString); + } + + private void assertEncryptFindings( + @Nonnull DetectionStore store, + @Nonnull List nodes, + @Nonnull String expectedNodeString) { + + DetectionStore encryptStore = + getStoreOfValueType(CipherAction.class, store.getChildren()); + assertThat(encryptStore).isNotNull(); + assertThat(encryptStore.getDetectionValues()).hasSize(1); + assertThat(encryptStore.getDetectionValues().get(0).asString()).isEqualTo("ENCRYPT"); + + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo(expectedNodeString); + assertThat(nodes.get(0).getChildren().get(Encrypt.class)).isNotNull(); + } + + private void assertDecryptFindings( + @Nonnull DetectionStore store, + @Nonnull List nodes, + @Nonnull String expectedNodeString) { + + DetectionStore decryptStore = + getStoreOfValueType(CipherAction.class, store.getChildren()); + assertThat(decryptStore).isNotNull(); + assertThat(decryptStore.getDetectionValues()).hasSize(1); + assertThat(decryptStore.getDetectionValues().get(0).asString()).isEqualTo("DECRYPT"); + + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo(expectedNodeString); + assertThat(nodes.get(0).getChildren().get(Decrypt.class)).isNotNull(); + } + + /** + * Asserts mode+padding findings using the translated node tree. Direct-mode methods + * (EncryptCbc, TryDecryptEcb, etc.) place Mode and Padding in the same child detection store, + * so we validate via the final node string rather than per-store inspection. + */ + private void assertModePaddingFindings( + @Nonnull DetectionStore store, + @Nonnull List nodes, + @Nonnull String expectedMode, + @Nonnull String expectedPadding, + @Nonnull String expectedNodeString) { + + DetectionStore modeStore = + getStoreOfValueType(Mode.class, store.getChildren()); + assertThat(modeStore).isNotNull(); + assertThat(modeStore.getDetectionValues()) + .anySatisfy(v -> assertThat(v.asString()).isEqualTo(expectedMode)); + + assertThat(nodes).hasSize(1); + assertThat(nodes.get(0).getKind()).isEqualTo(BlockCipher.class); + assertThat(nodes.get(0).asString()).isEqualTo(expectedNodeString); + } +} diff --git a/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetX25519DiffieHellmanTest.java b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetX25519DiffieHellmanTest.java new file mode 100644 index 000000000..e26c5d940 --- /dev/null +++ b/csharp/src/test/java/com/ibm/plugin/rules/detection/dotnet/DotNetX25519DiffieHellmanTest.java @@ -0,0 +1,155 @@ +/* + * Sonar Cryptography Plugin + * Copyright (C) 2024 PQCA + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.ibm.plugin.rules.detection.dotnet; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ibm.engine.detection.DetectionStore; +import com.ibm.engine.language.csharp.CSharpCheck; +import com.ibm.engine.language.csharp.CSharpScanContext; +import com.ibm.engine.language.csharp.CSharpSymbol; +import com.ibm.engine.language.csharp.tree.CSharpTree; +import com.ibm.engine.model.IValue; +import com.ibm.engine.model.ValueAction; +import com.ibm.engine.model.context.KeyContext; +import com.ibm.mapper.model.EllipticCurve; +import com.ibm.mapper.model.INode; +import com.ibm.mapper.model.KeyAgreement; +import com.ibm.mapper.model.Oid; +import com.ibm.mapper.model.functionality.Generate; +import com.ibm.plugin.CSharpVerifier; +import com.ibm.plugin.TestBase; +import java.util.List; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +/** + * Comprehensive test for all X25519 (X25519DiffieHellman) related detection rules + * (DotNetX25519DiffieHellman.java). + * + *

Covers all three X25519DiffieHellman-related classes and their complete verified operational + * API surface: + * + *

    + *
  • X25519DiffieHellman (abstract base — entry point is the static {@code GenerateKey()} + * factory, there is no {@code Create()}) + *
  • X25519DiffieHellmanCng, X25519DiffieHellmanOpenSsl (derived from X25519DiffieHellman) + *
+ * + *

Note: every top-level finding translates to a {@link com.ibm.mapper.model.algorithms.X25519} + * node ({@code node.asString()} == "x25519"), whose kind is {@link KeyAgreement}, and which always + * carries a {@link EllipticCurve} child ({@code Curve25519}, {@code asString()} == "Curve25519") + * and an {@link Oid} child ("1.3.101.110") added unconditionally by the {@code X25519} algorithm + * model constructor. + * + *

Finding mapping (one finding per test method in DotNetX25519DiffieHellmanTestFile.cs): + * + *

+ * Section 1 – factory method / constructors / static Import* factories (findings 0–4):
+ *   0 TestX25519GenerateKey              → X25519
+ *   1 TestX25519Cng                      → X25519
+ *   2 TestX25519OpenSsl                  → X25519
+ *   3 TestX25519ImportPrivateKeyByteArray → X25519
+ *   4 TestX25519ImportPublicKeyByteArray  → X25519
+ *
+ * Section 2 – DeriveRawSecretAgreement operation (findings 5–6):
+ *   5 TestDeriveRawSecretAgreementByteArray  → X25519 + Generate child
+ *   6 TestDeriveRawSecretAgreementOtherParty → X25519 + Generate child
+ *
+ * Section 3 – combined usage patterns (findings 7–8):
+ *   7 TestX25519CngDeriveFlow       → X25519 + Generate child
+ *   8 TestX25519OpenSslDeriveFlow   → X25519 + Generate child
+ * 
+ */ +class DotNetX25519DiffieHellmanTest extends TestBase { + + @Test + void test() throws Exception { + CSharpVerifier.verify("rules/detection/dotnet/DotNetX25519DiffieHellmanTestFile.cs", this); + } + + @Override + public void asserts( + int findingId, + @Nonnull + DetectionStore + detectionStore, + @Nonnull List nodes) { + + // Every top-level finding must be X25519 + assertThat(detectionStore.getDetectionValueContext()).isInstanceOf(KeyContext.class); + assertThat(detectionStore.getDetectionValues()).hasSize(1); + IValue primary = detectionStore.getDetectionValues().get(0); + assertThat(primary).isInstanceOf(ValueAction.class); + assertThat(primary.asString()).isEqualTo("X25519"); + + assertThat(nodes).hasSize(1); + INode node = nodes.get(0); + assertThat(node.getKind()).isEqualTo(KeyAgreement.class); + assertThat(node.asString()).isEqualTo("x25519"); + + INode curve = node.getChildren().get(EllipticCurve.class); + assertThat(curve).isNotNull(); + assertThat(curve.asString()).isEqualTo("Curve25519"); + + INode oid = node.getChildren().get(Oid.class); + assertThat(oid).isNotNull(); + assertThat(oid.asString()).isEqualTo("1.3.101.110"); + + switch (findingId) { + + // ----------------------------------------------------------------- + // Section 1: simple constructors / static Import* factories — only X25519, no extra + // children + // ----------------------------------------------------------------- + case 0, 1, 2, 3, 4 -> { + // node.asString() already asserted to be "x25519" above + } + + // ----------------------------------------------------------------- + // Section 2: DeriveRawSecretAgreement operation + // ----------------------------------------------------------------- + case 5, 6 -> assertRawSecretAgreement(detectionStore, node); + + // ----------------------------------------------------------------- + // Section 3: combined usage patterns + // ----------------------------------------------------------------- + case 7, 8 -> assertRawSecretAgreement(detectionStore, node); + + default -> throw new IllegalStateException("Unexpected findingId: " + findingId); + } + } + + // ------------------------------------------------------------------------- + // Assertion helpers + // ------------------------------------------------------------------------- + + private void assertRawSecretAgreement( + @Nonnull DetectionStore store, + @Nonnull INode node) { + + DetectionStore deriveStore = + getStoreOfValueType(ValueAction.class, store.getChildren()); + assertThat(deriveStore).isNotNull(); + assertThat(deriveStore.getDetectionValues()).hasSize(1); + + assertThat(node.getChildren().get(Generate.class)).isNotNull(); + } +} diff --git a/engine/pom.xml b/engine/pom.xml index 7b772606b..d4a6ed370 100644 --- a/engine/pom.xml +++ b/engine/pom.xml @@ -33,7 +33,7 @@ io.github.fynnth cbomkit-csharp-parser - 0.1.2 + 0.1.3 diff --git a/engine/src/main/java/com/ibm/engine/model/factory/ModeFactory.java b/engine/src/main/java/com/ibm/engine/model/factory/ModeFactory.java index 2517b09cf..f4d889969 100644 --- a/engine/src/main/java/com/ibm/engine/model/factory/ModeFactory.java +++ b/engine/src/main/java/com/ibm/engine/model/factory/ModeFactory.java @@ -24,11 +24,30 @@ import com.ibm.engine.model.Mode; import java.util.Optional; import javax.annotation.Nonnull; +import javax.annotation.Nullable; public class ModeFactory implements IValueFactory { + @Nullable private final String constant; + + public ModeFactory() { + this.constant = null; + } + + /** + * Creates a factory that always emits {@code Mode(constant)} regardless of the resolved + * parameter value. Useful when the mode is encoded in the method name (e.g. {@code EncryptCbc}) + * rather than in a parameter. + */ + public ModeFactory(@Nonnull String constant) { + this.constant = constant; + } + @Override public Optional> apply(@Nonnull ResolvedValue objectTResolvedValue) { + if (constant != null) { + return Optional.of(new Mode<>(constant, objectTResolvedValue.tree())); + } if (objectTResolvedValue.value() instanceof String s) { return Optional.of(new Mode<>(s, objectTResolvedValue.tree())); } diff --git a/mapper/src/main/java/com/ibm/mapper/model/algorithms/SPHINCSPlus.java b/mapper/src/main/java/com/ibm/mapper/model/algorithms/SPHINCSPlus.java index 29a92f845..dc8a01e76 100644 --- a/mapper/src/main/java/com/ibm/mapper/model/algorithms/SPHINCSPlus.java +++ b/mapper/src/main/java/com/ibm/mapper/model/algorithms/SPHINCSPlus.java @@ -20,9 +20,12 @@ package com.ibm.mapper.model.algorithms; import com.ibm.mapper.model.Algorithm; +import com.ibm.mapper.model.INode; import com.ibm.mapper.model.MessageDigest; +import com.ibm.mapper.model.ParameterSetIdentifier; import com.ibm.mapper.model.Signature; import com.ibm.mapper.utils.DetectionLocation; +import java.util.Optional; import javax.annotation.Nonnull; /** @@ -52,9 +55,12 @@ public class SPHINCSPlus extends Algorithm implements Signature { @Override public @Nonnull String asString() { - return this.hasChildOfType(MessageDigest.class) - .map(node -> this.name + node.asString()) - .orElse(this.name); + StringBuilder builtName = new StringBuilder(this.name); + Optional parameterSetIdentifier = this.hasChildOfType(ParameterSetIdentifier.class); + parameterSetIdentifier.ifPresent(node -> builtName.append("-").append(node.asString())); + this.hasChildOfType(MessageDigest.class) + .ifPresent(node -> builtName.append(node.asString())); + return builtName.toString(); } public SPHINCSPlus(@Nonnull DetectionLocation detectionLocation) { @@ -65,4 +71,17 @@ public SPHINCSPlus(MessageDigest messageDigest) { this(messageDigest.getDetectionContext()); this.put(messageDigest); } + + /** + * Constructs an SLH-DSA node carrying a FIPS 205 parameter set identifier, e.g. {@code + * "SHA2-128s"} or {@code "SHAKE-256f"}, so that {@link #asString()} yields {@code + * "SLH-DSA-SHA2-128s"}. Used by the C# {@code SlhDsaAlgorithm}-parameterized detection rules + * (see {@code DotNetSlhDsa}), which can recover the exact parameter set from the enum member + * name at the call site. + */ + public SPHINCSPlus( + @Nonnull String parameterSetIdentifier, @Nonnull DetectionLocation detectionLocation) { + this(detectionLocation); + this.put(new ParameterSetIdentifier(parameterSetIdentifier, detectionLocation)); + } }