Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ It is part of **the [CBOMKit](https://github.com/cbomkit) toolset**.
[^1]: We only cover the BouncyCastle *light-weight API* according to [this specification](https://javadoc.io/static/org.bouncycastle/bctls-jdk14/1.80/specifications.html)
[^2]: All packages under [`crypto`](https://pkg.go.dev/crypto@go1.25.6#section-directories) are covered except `crypto/x509`
[^3]: Covers `golang.org/x/crypto/hkdf`, `golang.org/x/crypto/pbkdf2`, and `golang.org/x/crypto/sha3`
[^4]: C# support uses a custom ANTLR grammar (targeting up to C# 13; published from [github.com/fynnth/cbomkit-csharp-parser](https://github.com/fynnth/cbomkit-csharp-parser)) to parse source files directly. Detection rules cover 95 of the 97 algorithm-bearing classes of `System.Security.Cryptography`; the two exceptions, `CryptoStream` and `PemEncoding`, name no algorithm of their own. **Not yet meant for production usage!** Beyond the algorithm, the rules capture the parameters that decide whether a component is sound: key sizes, elliptic curves, PBKDF2 and HKDF iteration counts, salt and derived-key lengths, signature digests, RSA signature and OAEP paddings including the OAEP digest, AEAD nonce and tag lengths, cipher modes and paddings, and post-quantum parameter sets. Parameters are matched by their .NET names and declared types rather than by argument position, so a value is placed correctly across overloads that reorder it and across keyword arguments written out of order. The engine resolves a value when it is syntactically certain: literals, locals with a single assignment, `const`s, class fields the class never reassigns, `new byte[n]` and `stackalloc byte[n]` lengths, simple arithmetic, and — file-wide — a parameter whose callers all pass the same value and a call to a local method with a single return value. Object initializers, alias variables, chained calls and expression-bodied members are understood; control flow inside a method (`if`/`for`/`while`/`using`/`try`/`lock`/`fixed`/`switch`) does not interrupt variable tracking; and code inside a `#if` region is analysed, in every branch where that is valid C# and otherwise in the first branch of each chain, since a bill of materials should list the cryptography a build configuration can reach without losing the file when a conditional splits a single construct. The static one-shot helpers `HashData` and `TryHashData` are detected in their own right, because they have no creation call for an algorithm to be attached to. A creation that is not a statement of its own is a call site too: a field or auto-property initializer, one handed straight to another constructor or method, and object- and collection-initializer values. A key taken from an X.509 certificate is recorded as private or public key material — in both the extension-method and the static spelling of those accessors — so a CBOM reader can tell a key whose length only the certificate knows from one the engine failed to resolve. **Known limitations:** nothing resolves across file boundaries; there is no semantic type checker, so two overloads that share an arity and differ only in a parameter type this engine cannot tell apart are not separated; and values that depend on evaluating control flow (a ternary with differing branches, an element access, a `switch` expression) are deliberately left unresolved rather than guessed. Having no value is not the same as containing nothing, though: the creations written in a ternary or `switch` expression arm, in a collection expression (including under the spread operator) and in a lambda's expression body are reported as the call sites they are, since whichever branch runs, the code constructs what the arm says it does. The factory-delegate idiom `symmetricAlgorithmFactory: () => Aes.Create()` is covered for that reason, and so is the initializer of a target-typed `new()`, which names no type to report but is where such code often puts the algorithm.
[^4]: C# support uses a custom ANTLR grammar (targeting up to C# 13; published from [github.com/fynnth/cbomkit-csharp-parser](https://github.com/fynnth/cbomkit-csharp-parser)) to parse source files directly. Detection rules cover 95 of the 97 algorithm-bearing classes of `System.Security.Cryptography`; the two exceptions, `CryptoStream` and `PemEncoding`, name no algorithm of their own. **Not yet meant for production usage!** Beyond the algorithm, the rules capture the parameters that decide whether a component is sound: key sizes, elliptic curves, PBKDF2 and HKDF iteration counts, salt and derived-key lengths, signature digests, RSA signature and OAEP paddings including the OAEP digest, AEAD nonce and tag lengths, cipher modes and paddings, and post-quantum parameter sets. Parameters are matched by their .NET names and declared types rather than by argument position, so a value is placed correctly across overloads that reorder it and across keyword arguments written out of order. The engine resolves a value when it is syntactically certain: literals, locals with a single assignment, `const`s, class fields the class never reassigns, `new byte[n]` and `stackalloc byte[n]` lengths, simple arithmetic, and — file-wide — a parameter whose callers all pass the same value and a call to a local method with a single return value. Object initializers, alias variables, chained calls and expression-bodied members are understood; control flow inside a method (`if`/`for`/`while`/`using`/`try`/`lock`/`fixed`/`switch`) does not interrupt variable tracking; and code inside a `#if` region is analysed, in every branch where that is valid C# and otherwise in the first branch of each chain, since a bill of materials should list the cryptography a build configuration can reach without losing the file when a conditional splits a single construct. The static one-shot helpers `HashData` and `TryHashData` are detected in their own right, because they have no creation call for an algorithm to be attached to. A creation that is not a statement of its own is a call site too: a field or auto-property initializer, one handed straight to another constructor or method, and object- and collection-initializer values. A key taken from an X.509 certificate is recorded as private or public key material — in both the extension-method and the static spelling of those accessors — so a CBOM reader can tell a key whose length only the certificate knows from one the engine failed to resolve. The neighbouring namespaces whose calls state an algorithm are covered too: certificate requests, signature generators and thumbprints (`X509Certificates`); PFX integrity MACs and RFC 3161 timestamp requests (`Pkcs`); the digest, signature and encryption URIs of XML Signature and XML Encryption (`Xml`); and `CoseSigner` (`Cose`). The `PbeParameters` that protect an exported key or certificate are detected with their cipher, hash and iteration count. **Known limitations:** nothing resolves across file boundaries; there is no semantic type checker, so two overloads that share an arity and differ only in a parameter type this engine cannot tell apart are not separated; and values that depend on evaluating control flow (a ternary with differing branches, an element access, a `switch` expression) are deliberately left unresolved rather than guessed. Having no value is not the same as containing nothing, though: the creations written in a ternary or `switch` expression arm, in a collection expression (including under the spread operator) and in a lambda's expression body are reported as the call sites they are, since whichever branch runs, the code constructs what the arm says it does. The factory-delegate idiom `symmetricAlgorithmFactory: () => Aes.Create()` is covered for that reason, and so is the initializer of a target-typed `new()`, which names no type to report but is where such code often puts the algorithm.

> [!NOTE]
> The plugin is designed in a modular way so that it can be extended to support additional languages and recognition rules to support more libraries.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@
import com.ibm.engine.rule.RuleSets;
import com.ibm.plugin.rules.detection.dotnet.DotNetAES;
import com.ibm.plugin.rules.detection.dotnet.DotNetAlgorithmFactory;
import com.ibm.plugin.rules.detection.dotnet.DotNetCertificateHash;
import com.ibm.plugin.rules.detection.dotnet.DotNetCertificateKeys;
import com.ibm.plugin.rules.detection.dotnet.DotNetCertificateRequest;
import com.ibm.plugin.rules.detection.dotnet.DotNetChaCha20Poly1305;
import com.ibm.plugin.rules.detection.dotnet.DotNetCngKey;
import com.ibm.plugin.rules.detection.dotnet.DotNetCose;
import com.ibm.plugin.rules.detection.dotnet.DotNetDES;
import com.ibm.plugin.rules.detection.dotnet.DotNetDSA;
import com.ibm.plugin.rules.detection.dotnet.DotNetECDiffieHellman;
Expand All @@ -39,6 +42,8 @@
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.DotNetPbeParameters;
import com.ibm.plugin.rules.detection.dotnet.DotNetPkcs;
import com.ibm.plugin.rules.detection.dotnet.DotNetProtectedData;
import com.ibm.plugin.rules.detection.dotnet.DotNetRC2;
import com.ibm.plugin.rules.detection.dotnet.DotNetRSA;
Expand All @@ -49,6 +54,7 @@
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 com.ibm.plugin.rules.detection.dotnet.DotNetXml;
import java.util.List;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
Expand Down Expand Up @@ -79,6 +85,12 @@ protected List<IDetectionRule<CSharpTree>> buildRules() {
RuleSets.rulesOf(DotNetHMAC.class).stream(),
RuleSets.rulesOf(DotNetKMAC.class).stream(),
RuleSets.rulesOf(DotNetRfc2898DeriveBytes.class).stream(),
RuleSets.rulesOf(DotNetPbeParameters.class).stream(),
RuleSets.rulesOf(DotNetCertificateRequest.class).stream(),
RuleSets.rulesOf(DotNetCertificateHash.class).stream(),
RuleSets.rulesOf(DotNetPkcs.class).stream(),
RuleSets.rulesOf(DotNetXml.class).stream(),
RuleSets.rulesOf(DotNetCose.class).stream(),
RuleSets.rulesOf(DotNetKeyDerivation.class).stream(),
RuleSets.rulesOf(DotNetRandomNumberGenerator.class).stream(),
RuleSets.rulesOf(DotNetProtectedData.class).stream(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* 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.factory.AlgorithmFactory;
import com.ibm.engine.model.factory.ValueActionFactory;
import com.ibm.engine.rule.DetectionRuleSet;
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 certificate thumbprint methods of {@code X509Certificate}.
*
* <p>{@code GetCertHash}, {@code GetCertHashString} and {@code TryGetCertHash} take a {@code
* HashAlgorithmName}. Called without an argument, {@code GetCertHash()} and {@code
* GetCertHashString()} return the SHA-1 thumbprint.
*
* <p>The two rules cannot overlap: the first requires the hash argument, the second matches only a
* call with no arguments. The digest is the finding itself. An unresolved hash gives no finding.
*/
@SuppressWarnings("java:S1192")
public final class DotNetCertificateHash extends DetectionRuleSet<CSharpTree> {

// cert.GetCertHash(hashAlgorithm) / cert.GetCertHashString(hashAlgorithm) /
// cert.TryGetCertHash(hashAlgorithm, destination, out bytesWritten)
private static final IDetectionRule<CSharpTree> CERT_HASH_WITH_ALGORITHM =
new DetectionRuleBuilder<CSharpTree>()
.createDetectionRule()
.forObjectTypes(MethodMatcher.ANY)
.forMethods("GetCertHash", "GetCertHashString", "TryGetCertHash")
.withNamedMethodParameter("hashAlgorithm", "HashAlgorithmName")
.shouldBeDetectedAs(new AlgorithmFactory<>())
.withOptionalNamedMethodParameter("destination", MethodMatcher.ANY)
.withOptionalNamedMethodParameter("bytesWritten", MethodMatcher.ANY)
.buildForContext(new DigestContext())
.inBundle(() -> "DotNet")
.withoutDependingDetectionRules();

// cert.GetCertHash() / cert.GetCertHashString() — the SHA-1 thumbprint
private static final IDetectionRule<CSharpTree> CERT_HASH_SHA1 =
new DetectionRuleBuilder<CSharpTree>()
.createDetectionRule()
.forObjectTypes(MethodMatcher.ANY)
.forMethods("GetCertHash", "GetCertHashString")
.shouldBeDetectedAs(new ValueActionFactory<>("SHA1"))
.withoutParameters()
.buildForContext(new DigestContext())
.inBundle(() -> "DotNet")
.withoutDependingDetectionRules();

@Nonnull
@Override
protected List<IDetectionRule<CSharpTree>> buildRules() {
return List.of(CERT_HASH_WITH_ALGORITHM, CERT_HASH_SHA1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* 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.AlgorithmFactory;
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.DetectionRuleSet;
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 how a certificate is signed ({@code X509Certificates}).
*
* <ul>
* <li>{@code new CertificateRequest(subjectName, key, hashAlgorithm[, padding])}
* <li>{@code X509SignatureGenerator.CreateForRSA(key, signaturePadding)}
* <li>{@code X509SignatureGenerator.CreateForECDsa(key)}
* </ul>
*
* <p>A request is reported as a {@code Sign} with the hash and, for RSA, the padding as children,
* like {@code SignData}. {@code Create} and {@code CreateSelfSigned} add no information and are not
* matched. One rule covers all constructor overloads; the {@code PublicKey} overload names its
* second parameter {@code publicKey}, so only a keyword call to it is missed.
*
* <p>The key is not linked to the finding that created it. An unresolved hash or padding is left
* off.
*/
@SuppressWarnings("java:S1192")
public final class DotNetCertificateRequest extends DetectionRuleSet<CSharpTree> {

// new CertificateRequest(subjectName, key, hashAlgorithm [, padding])
private static final IDetectionRule<CSharpTree> CERTIFICATE_REQUEST =
new DetectionRuleBuilder<CSharpTree>()
.createDetectionRule()
.forObjectTypes("CertificateRequest")
.forMethods("<init>")
.shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.SIGN))
.withNamedMethodParameter("subjectName", MethodMatcher.ANY)
.withNamedMethodParameter("key", MethodMatcher.ANY)
.withNamedMethodParameter("hashAlgorithm", "HashAlgorithmName")
.shouldBeDetectedAs(new AlgorithmFactory<>())
.asChildOfParameterWithId(-1)
.withOptionalNamedMethodParameter("padding", "RSASignaturePadding")
.shouldBeDetectedAs(new PaddingFactory<>())
.asChildOfParameterWithId(-1)
.buildForContext(new SignatureContext())
.inBundle(() -> "DotNet")
.withoutDependingDetectionRules();

// X509SignatureGenerator.CreateForRSA(key, signaturePadding)
private static final IDetectionRule<CSharpTree> CREATE_FOR_RSA =
new DetectionRuleBuilder<CSharpTree>()
.createDetectionRule()
.forObjectTypes("X509SignatureGenerator")
.forMethods("CreateForRSA")
.shouldBeDetectedAs(new SignatureActionFactory<>(SignatureAction.Action.SIGN))
.withNamedMethodParameter("key", MethodMatcher.ANY)
.withNamedMethodParameter("signaturePadding", "RSASignaturePadding")
.shouldBeDetectedAs(new PaddingFactory<>())
.asChildOfParameterWithId(-1)
.buildForContext(new SignatureContext())
.inBundle(() -> "DotNet")
.withoutDependingDetectionRules();

// X509SignatureGenerator.CreateForECDsa(key)
private static final IDetectionRule<CSharpTree> CREATE_FOR_ECDSA =
new DetectionRuleBuilder<CSharpTree>()
.createDetectionRule()
.forObjectTypes("X509SignatureGenerator")
.forMethods("CreateForECDsa")
.shouldBeDetectedAs(new ValueActionFactory<>("ECDSA"))
.withNamedMethodParameter("key", MethodMatcher.ANY)
.buildForContext(new KeyContext(Map.of("kind", "ECDSA")))
.inBundle(() -> "DotNet")
.withoutDependingDetectionRules();

@Nonnull
@Override
protected List<IDetectionRule<CSharpTree>> buildRules() {
return List.of(CERTIFICATE_REQUEST, CREATE_FOR_RSA, CREATE_FOR_ECDSA);
}
}
Loading
Loading