Add modelling for JCA key gen cipher algorithm

This commit is contained in:
Nicolas Will
2025-04-30 16:28:31 +02:00
parent 1958c192ec
commit 7f24a2557d
8 changed files with 234 additions and 71 deletions

View File

@@ -2,7 +2,6 @@ import java
import semmle.code.java.dataflow.DataFlow
import semmle.code.java.dataflow.TaintTracking
import semmle.code.java.controlflow.Dominance
import codeql.util.Option
module JCAModel {
import Language
@@ -354,9 +353,11 @@ module JCAModel {
else result instanceof KeyOpAlg::TUnknownKeyOperationAlgorithmType
}
override string getKeySize() {
override string getKeySizeFixed() {
none() // TODO: implement to handle variants such as AES-128
}
override Crypto::ConsumerInputDataFlowNode getKeySizeConsumer() { none() }
}
bindingset[input]
@@ -394,8 +395,6 @@ module JCAModel {
override Crypto::HashAlgorithmInstance getOAEPEncodingHashAlgorithm() { result = this }
override Crypto::HashAlgorithmInstance getMGF1HashAlgorithm() { none() } // TODO
override string getKeySize() { none() }
}
/**
@@ -446,8 +445,6 @@ module JCAModel {
predicate isIntermediate();
}
module MethodCallOption = Option<MethodCall>;
/**
* An generic analysis module for analyzing the `getInstance` to `initialize` to `doOperation` pattern in the JCA.
*
@@ -568,6 +565,14 @@ module JCAModel {
GetInstanceToInitToUseFlow::flowPath(src, sink)
}
GetInstance getInstantiationFromInit(
Init init, GetInstanceToInitToUseFlow::PathNode src, GetInstanceToInitToUseFlow::PathNode sink
) {
src.getNode().asExpr() = result and
sink.getNode().asExpr() = init.(MethodCall).getQualifier() and
GetInstanceToInitToUseFlow::flowPath(src, sink)
}
Init getInitFromUse(
Use use, GetInstanceToInitToUseFlow::PathNode src, GetInstanceToInitToUseFlow::PathNode sink
) {
@@ -829,6 +834,9 @@ module JCAModel {
}
}
module MessageDigestFlowAnalysisImpl =
GetInstanceInitUseFlowAnalysis<MessageDigestGetInstanceCall, DUMMY_UNUSED_METHODCALL, DigestCall>;
class MessageDigestGetInstanceAlgorithmValueConsumer extends HashAlgorithmValueConsumer {
MessageDigestGetInstanceCall call;
@@ -849,17 +857,18 @@ module JCAModel {
}
Expr getAlgorithmArg() { result = this.getArgument(0) }
DigestHashOperation getDigestCall() {
DigestGetInstanceToDigestFlow::flow(DataFlow::exprNode(this),
DataFlow::exprNode(result.(DigestCall).getQualifier()))
}
}
class DigestCall extends MethodCall {
DigestCall() { this.getCallee().hasQualifiedName("java.security", "MessageDigest", "digest") }
DigestCall() {
this.getCallee().hasQualifiedName("java.security", "MessageDigest", ["update", "digest"])
}
Expr getDigestArtifactOutput() { result = this }
Expr getInputArg() { result = this.getArgument(0) }
predicate isIntermediate() { this.getMethod().getName() = "update" }
}
// flow config from MessageDigest.getInstance to MessageDigest.digest
@@ -873,23 +882,22 @@ module JCAModel {
module DigestGetInstanceToDigestFlow = DataFlow::Global<DigestGetInstanceToDigestConfig>;
class DigestArtifact extends Crypto::DigestArtifactInstance {
DigestArtifact() { this = any(DigestCall call).getDigestArtifactOutput() }
override DataFlow::Node getOutputNode() { result.asExpr() = this }
}
class DigestHashOperation extends Crypto::HashOperationInstance instanceof DigestCall {
override Crypto::DigestArtifactInstance getDigestArtifact() {
result = this.(DigestCall).getDigestArtifactOutput()
DigestHashOperation() { not super.isIntermediate() }
override Crypto::ArtifactOutputDataFlowNode getOutputArtifact() {
result.asExpr() = super.getDigestArtifactOutput()
}
override Crypto::AlgorithmValueConsumer getAnAlgorithmValueConsumer() {
exists(MessageDigestGetInstanceCall getInstanceCall |
getInstanceCall.getDigestCall() = this and
getInstanceCall =
result.(MessageDigestGetInstanceAlgorithmValueConsumer).getInstantiationCall()
)
MessageDigestFlowAnalysisImpl::getInstantiationFromUse(this, _, _) =
result.(MessageDigestGetInstanceAlgorithmValueConsumer).getInstantiationCall()
}
override Crypto::ConsumerInputDataFlowNode getInputConsumer() {
result.asExpr() = super.getInputArg() or
result.asExpr() =
MessageDigestFlowAnalysisImpl::getAnIntermediateUseFromFinalUse(this, _, _).getInputArg()
}
}
@@ -997,6 +1005,7 @@ module JCAModel {
or
// However, for general elliptic curves, getInstance("EC") is used
// and java.security.spec.ECGenParameterSpec("<CURVE NAME>") is what sets the specific curve.
// If init is not specified, the default (P-)
// The result of ECGenParameterSpec is passed to KeyPairGenerator.initialize
// If the curve is not specified, the default is used.
// We would trace the use of this inside a KeyPairGenerator.initialize
@@ -1096,6 +1105,30 @@ module JCAModel {
override string getKeySizeFixed() { none() }
}
class KeyGeneratorCipherAlgorithm extends CipherStringLiteralAlgorithmInstance {
KeyGeneratorCipherAlgorithm() { consumer instanceof KeyGenerationAlgorithmValueConsumer }
override Crypto::ConsumerInputDataFlowNode getKeySizeConsumer() {
exists(KeyGeneratorGetInstanceCall getInstance, KeyGeneratorInitCall init |
getInstance =
this.getConsumer().(KeyGenerationAlgorithmValueConsumer).getInstantiationCall() and
getInstance = KeyGeneratorFlowAnalysisImpl::getInstantiationFromInit(init, _, _) and
init.getKeySizeArg() = result.asExpr()
)
}
predicate isOnlyConsumedByKeyGen() {
forall(Crypto::AlgorithmValueConsumer c |
c = this.getConsumer() and
c instanceof KeyGenerationAlgorithmValueConsumer
)
}
override predicate shouldHaveModeOfOperation() { this.isOnlyConsumedByKeyGen() }
override predicate shouldHavePaddingScheme() { this.isOnlyConsumedByKeyGen() }
}
/*
* Key Derivation Functions (KDFs)
*/

View File

@@ -38,7 +38,7 @@ module CryptoInput implements InputSig<Language::Location> {
predicate artifactOutputFlowsToGenericInput(
DataFlow::Node artifactOutput, DataFlow::Node otherInput
) {
ArtifactUniversalFlow::flow(artifactOutput, otherInput)
ArtifactFlow::flow(artifactOutput, otherInput)
}
}
@@ -60,7 +60,7 @@ class GenericUnreferencedParameterSource extends Crypto::GenericUnreferencedPara
}
override predicate flowsTo(Crypto::FlowAwareElement other) {
GenericDataSourceUniversalFlow::flow(this.getOutputNode(), other.getInputNode())
GenericDataSourceFlow::flow(this.getOutputNode(), other.getInputNode())
}
override DataFlow::Node getOutputNode() { result.asParameter() = this }
@@ -76,7 +76,7 @@ class GenericLocalDataSource extends Crypto::GenericLocalDataSource {
override DataFlow::Node getOutputNode() { result.asExpr() = this }
override predicate flowsTo(Crypto::FlowAwareElement other) {
GenericDataSourceUniversalFlow::flow(this.getOutputNode(), other.getInputNode())
GenericDataSourceFlow::flow(this.getOutputNode(), other.getInputNode())
}
override string getAdditionalDescription() { result = this.toString() }
@@ -88,7 +88,7 @@ class GenericRemoteDataSource extends Crypto::GenericRemoteDataSource {
override DataFlow::Node getOutputNode() { result.asExpr() = this }
override predicate flowsTo(Crypto::FlowAwareElement other) {
GenericDataSourceUniversalFlow::flow(this.getOutputNode(), other.getInputNode())
GenericDataSourceFlow::flow(this.getOutputNode(), other.getInputNode())
}
override string getAdditionalDescription() { result = this.toString() }
@@ -107,7 +107,7 @@ class ConstantDataSource extends Crypto::GenericConstantSourceInstance instanceo
override predicate flowsTo(Crypto::FlowAwareElement other) {
// TODO: separate config to avoid blowing up data-flow analysis
GenericDataSourceUniversalFlow::flow(this.getOutputNode(), other.getInputNode())
GenericDataSourceFlow::flow(this.getOutputNode(), other.getInputNode())
}
override string getAdditionalDescription() { result = this.toString() }
@@ -122,15 +122,24 @@ abstract class RandomnessInstance extends Crypto::RandomNumberGenerationInstance
}
class SecureRandomnessInstance extends RandomnessInstance {
RandomDataSource source;
SecureRandomnessInstance() {
exists(RandomDataSource s | this = s.getOutput() |
s.getSourceOfRandomness() instanceof SecureRandomNumberGenerator
)
this = source.getOutput() and
source.getSourceOfRandomness() instanceof SecureRandomNumberGenerator
}
override string getGeneratorName() { result = source.getSourceOfRandomness().getQualifiedName() }
}
class InsecureRandomnessInstance extends RandomnessInstance {
InsecureRandomnessInstance() { exists(InsecureRandomnessSource node | this = node.asExpr()) }
RandomDataSource source;
InsecureRandomnessInstance() {
any(InsecureRandomnessSource src).asExpr() = this and source.getOutput() = this
}
override string getGeneratorName() { result = source.getSourceOfRandomness().getQualifiedName() }
}
/**
@@ -142,12 +151,12 @@ abstract class AdditionalFlowInputStep extends DataFlow::Node {
final DataFlow::Node getInput() { result = this }
}
module ArtifactUniversalFlow = DataFlow::Global<ArtifactUniversalFlowConfig>;
module ArtifactFlow = DataFlow::Global<ArtifactFlowConfig>;
/**
* Generic data source to node input configuration
*/
module GenericDataSourceUniversalFlowConfig implements DataFlow::ConfigSig {
module GenericDataSourceFlowConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
source = any(Crypto::GenericSourceInstance i).getOutputNode()
}
@@ -175,7 +184,7 @@ module GenericDataSourceUniversalFlowConfig implements DataFlow::ConfigSig {
}
}
module ArtifactUniversalFlowConfig implements DataFlow::ConfigSig {
module ArtifactFlowConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) {
source = any(Crypto::ArtifactInstance artifact).getOutputNode()
}
@@ -203,7 +212,7 @@ module ArtifactUniversalFlowConfig implements DataFlow::ConfigSig {
}
}
module GenericDataSourceUniversalFlow = TaintTracking::Global<GenericDataSourceUniversalFlowConfig>;
module GenericDataSourceFlow = TaintTracking::Global<GenericDataSourceFlowConfig>;
// Import library-specific modeling
import JCA

View File

@@ -0,0 +1,20 @@
/**
* @name Insecure nonce at a cipher operation
* @id java/insecure-nonce
* @kind problem
* @problem.severity error
* @precision high
* @description A nonce is generated from a source that is not secure. This can lead to
* vulnerabilities such as replay attacks or key recovery.
*/
import experimental.Quantum.Language
predicate isInsecureNonceSource(Crypto::NonceArtifactNode n, Crypto::NodeBase src) {
src = n.getSourceNode() and
not src.asElement() instanceof SecureRandomnessInstance
}
from Crypto::KeyOperationNode op, Crypto::NodeBase src
where isInsecureNonceSource(op.getANonce(), src)
select op, "Operation uses insecure nonce source $@", src, src.toString()

View File

@@ -0,0 +1,16 @@
/**
* @name "PQC Test"
*/
import experimental.Quantum.Language
class AESGCMAlgorithmNode extends Crypto::KeyOperationAlgorithmNode {
AESGCMAlgorithmNode() {
this.getAlgorithmType() = Crypto::KeyOpAlg::TSymmetricCipher(Crypto::KeyOpAlg::AES()) and
this.getModeOfOperation().getModeType() = Crypto::GCM()
}
}
from Crypto::KeyOperationNode op, Crypto::NonceArtifactNode nonce
where op.getAKnownAlgorithm() instanceof AESGCMAlgorithmNode and nonce = op.getANonce()
select op, nonce.getSourceNode()

View File

@@ -1,5 +1,5 @@
/**
* @name "PQC Test"
* @name "Key operation slice table demo query"
*/
import experimental.Quantum.Language

View File

@@ -1,9 +1,9 @@
/**
* @name TestHashOperations
* @name "Hash operation slice table demo query"
*/
import experimental.Quantum.Language
from Crypto::HashOperationNode op, Crypto::HashAlgorithmNode alg
where alg = op.getAKnownHashAlgorithm()
where alg = op.getAKnownAlgorithm()
select op, op.getDigest(), alg, alg.getRawAlgorithmName()

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python3
import os
import re
import sys
import argparse
import subprocess
@@ -86,6 +87,7 @@ def main():
parser.add_argument("-c", "--codeql", required=True, help="Path to CodeQL CLI executable.")
parser.add_argument("-d", "--database", required=True, help="Path to the CodeQL database.")
parser.add_argument("-q", "--query", required=True, help="Path to the .ql query file.")
parser.add_argument("--queryid", required=True, help="Query ID for the analysis.")
parser.add_argument("-o", "--output", required=True, help="Output directory for analysis results.")
args = parser.parse_args()
@@ -94,7 +96,13 @@ def main():
run_codeql_analysis(args.codeql, args.database, args.query, args.output)
# Locate DGML file
dgml_file = os.path.join(args.output, "java", "print-cbom-graph.dgml")
ALLOWED_QUERY_ID = re.compile(r'^[a-zA-Z0-9_\-]+$')
if not ALLOWED_QUERY_ID.match(args.queryid):
print("Invalid query_id provided: '%s'. Allowed characters: letters, digits, '_', and '-'.", args.queryid)
sys.exit(1)
dgml_file = os.path.join(args.output, "java", '{}.dgml'.format(args.queryid))
dot_file = dgml_file.replace(".dgml", ".dot")
if os.path.exists(dgml_file):

View File

@@ -3,7 +3,6 @@
*/
import codeql.util.Location
import codeql.util.Option
import codeql.util.Either
signature module InputSig<LocationSig Location> {
@@ -379,6 +378,8 @@ module CryptographyBase<LocationSig Location, InputSig<Location> Input> {
exists(KeyDerivationOperationInstance op | inputNode = op.getInputConsumer())
or
exists(MACOperationInstance op | inputNode = op.getMessageConsumer())
or
exists(HashOperationInstance op | inputNode = op.getInputConsumer())
) and
this = Input::dfn_to_element(inputNode)
}
@@ -410,16 +411,11 @@ module CryptographyBase<LocationSig Location, InputSig<Location> Input> {
}
}
/**
* An artifact representing a hash function's digest output.
*/
abstract class DigestArtifactInstance extends OutputArtifactInstance { }
/**
* An artifact representing a random number generator's output.
*/
abstract class RandomNumberGenerationInstance extends OutputArtifactInstance {
// TODO: input seed?
abstract string getGeneratorName();
}
/**
@@ -438,6 +434,17 @@ module CryptographyBase<LocationSig Location, InputSig<Location> Input> {
override DataFlowNode getOutputNode() { result = creator.getOutputArtifact() }
}
/**
* An artifact representing the message digest output of a hash operation.
*/
final class HashOutputArtifactInstance extends OutputArtifactInstance {
HashOperationInstance creator;
HashOutputArtifactInstance() { Input::dfn_to_element(creator.getOutputArtifact()) = this }
override DataFlowNode getOutputNode() { result = creator.getOutputArtifact() }
}
/**
* An artifact representing the shared secret generated by key agreement operations.
*/
@@ -489,8 +496,14 @@ module CryptographyBase<LocationSig Location, InputSig<Location> Input> {
// TODO: key type hint? e.g. hint: private || public
KeyArtifactConsumer() {
(
exists(KeyOperationInstance op | inputNode = op.getKeyConsumer()) or
exists(KeyOperationInstance op | inputNode = op.getKeyConsumer())
or
exists(MACOperationInstance op | inputNode = op.getKeyConsumer())
or
exists(KeyAgreementSecretGenerationOperationInstance op |
inputNode = op.getServerKeyConsumer() or
inputNode = op.getPeerKeyConsumer()
)
) and
this = Input::dfn_to_element(inputNode)
}
@@ -770,7 +783,22 @@ module CryptographyBase<LocationSig Location, InputSig<Location> Input> {
*
* If the algorithm accepts a range of key sizes without a particular one specified, this predicate should be implemented as `none()`.
*/
abstract string getKeySize();
abstract string getKeySizeFixed();
/**
* Gets a consumer for the key size in bits specified for this algorithm variant.
*/
abstract ConsumerInputDataFlowNode getKeySizeConsumer();
/**
* Holds if this algorithm is expected to have a mode specified.
*/
predicate shouldHaveModeOfOperation() { any() }
/**
* Holds if this algorithm is expected to have a padding scheme specified.
*/
predicate shouldHavePaddingScheme() { any() }
}
newtype TBlockCipherModeOfOperationType =
@@ -904,7 +932,9 @@ module CryptographyBase<LocationSig Location, InputSig<Location> Input> {
}
abstract class HashOperationInstance extends OperationInstance {
abstract DigestArtifactInstance getDigestArtifact();
abstract ArtifactOutputDataFlowNode getOutputArtifact();
abstract ConsumerInputDataFlowNode getInputConsumer();
}
abstract class HashAlgorithmInstance extends AlgorithmInstance {
@@ -1151,7 +1181,7 @@ module CryptographyBase<LocationSig Location, InputSig<Location> Input> {
private newtype TNode =
// Output artifacts (data that is not an operation or algorithm, e.g., a key)
TDigest(DigestArtifactInstance e) or
TDigest(HashOutputArtifactInstance e) or
TKey(KeyArtifactInstance e) or
TSharedSecret(KeyAgreementSharedSecretOutputArtifactInstance e) or
// Input artifacts (synthetic nodes, used to differentiate input as entities)
@@ -1442,6 +1472,15 @@ module CryptographyBase<LocationSig Location, InputSig<Location> Input> {
override LocatableElement asElement() { result = instance }
override string getSourceNodeRelationship() { none() } // TODO: seed?
override predicate properties(string key, string value, Location location) {
super.properties(key, value, location)
or
// [ONLY_KNOWN]
key = "Description" and
value = instance.getGeneratorName() and
location = this.getLocation()
}
}
/**
@@ -1518,7 +1557,7 @@ module CryptographyBase<LocationSig Location, InputSig<Location> Input> {
* A digest artifact produced by a hash operation.
*/
final class DigestArtifactNode extends ArtifactNode, TDigest {
DigestArtifactInstance instance;
HashOutputArtifactInstance instance;
DigestArtifactNode() { this = TDigest(instance) }
@@ -1664,12 +1703,28 @@ module CryptographyBase<LocationSig Location, InputSig<Location> Input> {
result.asElement() = instance.getOutputArtifact().getArtifact()
}
KeyArtifactNode getServerKey() {
result.asElement() = instance.getServerKeyConsumer().getConsumer()
}
KeyArtifactNode getPeerKey() {
result.asElement() = instance.getPeerKeyConsumer().getConsumer()
}
override NodeBase getChild(string key) {
result = super.getChild(key)
or
// [ALWAYS_KNOWN]
key = "Output" and
result = this.getOutput()
or
// [KNOWN_OR_UNKOWN]
key = "ServerKey" and
if exists(this.getServerKey()) then result = this.getServerKey() else result = this
or
// [KNOWN_OR_UNKOWN]
key = "PeerKey" and
if exists(this.getPeerKey()) then result = this.getPeerKey() else result = this
}
}
@@ -2115,7 +2170,14 @@ module CryptographyBase<LocationSig Location, InputSig<Location> Input> {
/**
* Gets the key size variant of this algorithm in bits, e.g., 128 for "AES-128".
*/
string getKeySize() { result = instance.asAlg().getKeySize() } // TODO: key sizes for known algorithms
string getKeySizeFixed() { result = instance.asAlg().getKeySizeFixed() } // TODO: key sizes for known algorithms
/**
* Gets the key size generic source node.
*/
GenericSourceNode getKeySize() {
result = instance.asAlg().getKeySizeConsumer().getConsumer().getAGenericSourceNode()
}
/**
* Gets the type of this key operation algorithm, e.g., "SymmetricEncryption(_)" or ""
@@ -2139,17 +2201,23 @@ module CryptographyBase<LocationSig Location, InputSig<Location> Input> {
override NodeBase getChild(string edgeName) {
result = super.getChild(edgeName)
or
// [KNOWN_OR_UNKNOWN]
// [KNOWN_OR_UNKNOWN] - but only if not suppressed
edgeName = "Mode" and
if exists(this.getModeOfOperation())
then result = this.getModeOfOperation()
else result = this
(
if exists(this.getModeOfOperation())
then result = this.getModeOfOperation()
else result = this
) and
instance.asAlg().shouldHaveModeOfOperation()
or
// [KNOWN_OR_UNKNOWN]
// [KNOWN_OR_UNKNOWN] - but only if not suppressed
edgeName = "Padding" and
if exists(this.getPaddingAlgorithm())
then result = this.getPaddingAlgorithm()
else result = this
(
if exists(this.getPaddingAlgorithm())
then result = this.getPaddingAlgorithm()
else result = this
) and
instance.asAlg().shouldHavePaddingScheme()
}
override predicate properties(string key, string value, Location location) {
@@ -2160,14 +2228,13 @@ module CryptographyBase<LocationSig Location, InputSig<Location> Input> {
this.getSymmetricCipherStructure().toString() = value and
location = this.getLocation()
or
// [ONLY_KNOWN]
key = "KeySize" and
(
// [KNOWN_OR_UNKNOWN]
key = "KeySize" and
if exists(this.getKeySize())
then value = this.getKeySize()
else (
value instanceof UnknownPropertyValue and location instanceof UnknownLocation
)
value = this.getKeySizeFixed() and
location = this.getLocation()
or
node_as_property(this.getKeySize(), value, location)
)
}
}
@@ -2191,10 +2258,16 @@ module CryptographyBase<LocationSig Location, InputSig<Location> Input> {
node instanceof HashAlgorithmNode
}
MessageArtifactNode getInputArtifact() {
result.asElement() = instance.getInputConsumer().getConsumer()
}
/**
* Gets the output digest node
*/
DigestArtifactNode getDigest() { result.asElement() = instance.getDigestArtifact() }
DigestArtifactNode getDigest() {
result.asElement() = instance.getOutputArtifact().getArtifact()
}
override NodeBase getChild(string key) {
result = super.getChild(key)
@@ -2202,6 +2275,10 @@ module CryptographyBase<LocationSig Location, InputSig<Location> Input> {
// [KNOWN_OR_UNKNOWN]
key = "Digest" and
if exists(this.getDigest()) then result = this.getDigest() else result = this
or
// [KNOWN_OR_UNKNOWN]
key = "Message" and
if exists(this.getInputArtifact()) then result = this.getInputArtifact() else result = this
}
}