Skip to content

Commit 7f24a25

Browse files
committed
Add modelling for JCA key gen cipher algorithm
1 parent 1958c19 commit 7f24a25

File tree

8 files changed

+234
-71
lines changed

8 files changed

+234
-71
lines changed

java/ql/lib/experimental/Quantum/JCA.qll

Lines changed: 58 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import java
22
import semmle.code.java.dataflow.DataFlow
33
import semmle.code.java.dataflow.TaintTracking
44
import semmle.code.java.controlflow.Dominance
5-
import codeql.util.Option
65

76
module JCAModel {
87
import Language
@@ -354,9 +353,11 @@ module JCAModel {
354353
else result instanceof KeyOpAlg::TUnknownKeyOperationAlgorithmType
355354
}
356355

357-
override string getKeySize() {
356+
override string getKeySizeFixed() {
358357
none() // TODO: implement to handle variants such as AES-128
359358
}
359+
360+
override Crypto::ConsumerInputDataFlowNode getKeySizeConsumer() { none() }
360361
}
361362

362363
bindingset[input]
@@ -394,8 +395,6 @@ module JCAModel {
394395
override Crypto::HashAlgorithmInstance getOAEPEncodingHashAlgorithm() { result = this }
395396

396397
override Crypto::HashAlgorithmInstance getMGF1HashAlgorithm() { none() } // TODO
397-
398-
override string getKeySize() { none() }
399398
}
400399

401400
/**
@@ -446,8 +445,6 @@ module JCAModel {
446445
predicate isIntermediate();
447446
}
448447

449-
module MethodCallOption = Option<MethodCall>;
450-
451448
/**
452449
* An generic analysis module for analyzing the `getInstance` to `initialize` to `doOperation` pattern in the JCA.
453450
*
@@ -568,6 +565,14 @@ module JCAModel {
568565
GetInstanceToInitToUseFlow::flowPath(src, sink)
569566
}
570567

568+
GetInstance getInstantiationFromInit(
569+
Init init, GetInstanceToInitToUseFlow::PathNode src, GetInstanceToInitToUseFlow::PathNode sink
570+
) {
571+
src.getNode().asExpr() = result and
572+
sink.getNode().asExpr() = init.(MethodCall).getQualifier() and
573+
GetInstanceToInitToUseFlow::flowPath(src, sink)
574+
}
575+
571576
Init getInitFromUse(
572577
Use use, GetInstanceToInitToUseFlow::PathNode src, GetInstanceToInitToUseFlow::PathNode sink
573578
) {
@@ -829,6 +834,9 @@ module JCAModel {
829834
}
830835
}
831836

837+
module MessageDigestFlowAnalysisImpl =
838+
GetInstanceInitUseFlowAnalysis<MessageDigestGetInstanceCall, DUMMY_UNUSED_METHODCALL, DigestCall>;
839+
832840
class MessageDigestGetInstanceAlgorithmValueConsumer extends HashAlgorithmValueConsumer {
833841
MessageDigestGetInstanceCall call;
834842

@@ -849,17 +857,18 @@ module JCAModel {
849857
}
850858

851859
Expr getAlgorithmArg() { result = this.getArgument(0) }
852-
853-
DigestHashOperation getDigestCall() {
854-
DigestGetInstanceToDigestFlow::flow(DataFlow::exprNode(this),
855-
DataFlow::exprNode(result.(DigestCall).getQualifier()))
856-
}
857860
}
858861

859862
class DigestCall extends MethodCall {
860-
DigestCall() { this.getCallee().hasQualifiedName("java.security", "MessageDigest", "digest") }
863+
DigestCall() {
864+
this.getCallee().hasQualifiedName("java.security", "MessageDigest", ["update", "digest"])
865+
}
861866

862867
Expr getDigestArtifactOutput() { result = this }
868+
869+
Expr getInputArg() { result = this.getArgument(0) }
870+
871+
predicate isIntermediate() { this.getMethod().getName() = "update" }
863872
}
864873

865874
// flow config from MessageDigest.getInstance to MessageDigest.digest
@@ -873,23 +882,22 @@ module JCAModel {
873882

874883
module DigestGetInstanceToDigestFlow = DataFlow::Global<DigestGetInstanceToDigestConfig>;
875884

876-
class DigestArtifact extends Crypto::DigestArtifactInstance {
877-
DigestArtifact() { this = any(DigestCall call).getDigestArtifactOutput() }
878-
879-
override DataFlow::Node getOutputNode() { result.asExpr() = this }
880-
}
881-
882885
class DigestHashOperation extends Crypto::HashOperationInstance instanceof DigestCall {
883-
override Crypto::DigestArtifactInstance getDigestArtifact() {
884-
result = this.(DigestCall).getDigestArtifactOutput()
886+
DigestHashOperation() { not super.isIntermediate() }
887+
888+
override Crypto::ArtifactOutputDataFlowNode getOutputArtifact() {
889+
result.asExpr() = super.getDigestArtifactOutput()
885890
}
886891

887892
override Crypto::AlgorithmValueConsumer getAnAlgorithmValueConsumer() {
888-
exists(MessageDigestGetInstanceCall getInstanceCall |
889-
getInstanceCall.getDigestCall() = this and
890-
getInstanceCall =
891-
result.(MessageDigestGetInstanceAlgorithmValueConsumer).getInstantiationCall()
892-
)
893+
MessageDigestFlowAnalysisImpl::getInstantiationFromUse(this, _, _) =
894+
result.(MessageDigestGetInstanceAlgorithmValueConsumer).getInstantiationCall()
895+
}
896+
897+
override Crypto::ConsumerInputDataFlowNode getInputConsumer() {
898+
result.asExpr() = super.getInputArg() or
899+
result.asExpr() =
900+
MessageDigestFlowAnalysisImpl::getAnIntermediateUseFromFinalUse(this, _, _).getInputArg()
893901
}
894902
}
895903

@@ -997,6 +1005,7 @@ module JCAModel {
9971005
or
9981006
// However, for general elliptic curves, getInstance("EC") is used
9991007
// and java.security.spec.ECGenParameterSpec("<CURVE NAME>") is what sets the specific curve.
1008+
// If init is not specified, the default (P-)
10001009
// The result of ECGenParameterSpec is passed to KeyPairGenerator.initialize
10011010
// If the curve is not specified, the default is used.
10021011
// We would trace the use of this inside a KeyPairGenerator.initialize
@@ -1096,6 +1105,30 @@ module JCAModel {
10961105
override string getKeySizeFixed() { none() }
10971106
}
10981107

1108+
class KeyGeneratorCipherAlgorithm extends CipherStringLiteralAlgorithmInstance {
1109+
KeyGeneratorCipherAlgorithm() { consumer instanceof KeyGenerationAlgorithmValueConsumer }
1110+
1111+
override Crypto::ConsumerInputDataFlowNode getKeySizeConsumer() {
1112+
exists(KeyGeneratorGetInstanceCall getInstance, KeyGeneratorInitCall init |
1113+
getInstance =
1114+
this.getConsumer().(KeyGenerationAlgorithmValueConsumer).getInstantiationCall() and
1115+
getInstance = KeyGeneratorFlowAnalysisImpl::getInstantiationFromInit(init, _, _) and
1116+
init.getKeySizeArg() = result.asExpr()
1117+
)
1118+
}
1119+
1120+
predicate isOnlyConsumedByKeyGen() {
1121+
forall(Crypto::AlgorithmValueConsumer c |
1122+
c = this.getConsumer() and
1123+
c instanceof KeyGenerationAlgorithmValueConsumer
1124+
)
1125+
}
1126+
1127+
override predicate shouldHaveModeOfOperation() { this.isOnlyConsumedByKeyGen() }
1128+
1129+
override predicate shouldHavePaddingScheme() { this.isOnlyConsumedByKeyGen() }
1130+
}
1131+
10991132
/*
11001133
* Key Derivation Functions (KDFs)
11011134
*/

java/ql/lib/experimental/Quantum/Language.qll

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ module CryptoInput implements InputSig<Language::Location> {
3838
predicate artifactOutputFlowsToGenericInput(
3939
DataFlow::Node artifactOutput, DataFlow::Node otherInput
4040
) {
41-
ArtifactUniversalFlow::flow(artifactOutput, otherInput)
41+
ArtifactFlow::flow(artifactOutput, otherInput)
4242
}
4343
}
4444

@@ -60,7 +60,7 @@ class GenericUnreferencedParameterSource extends Crypto::GenericUnreferencedPara
6060
}
6161

6262
override predicate flowsTo(Crypto::FlowAwareElement other) {
63-
GenericDataSourceUniversalFlow::flow(this.getOutputNode(), other.getInputNode())
63+
GenericDataSourceFlow::flow(this.getOutputNode(), other.getInputNode())
6464
}
6565

6666
override DataFlow::Node getOutputNode() { result.asParameter() = this }
@@ -76,7 +76,7 @@ class GenericLocalDataSource extends Crypto::GenericLocalDataSource {
7676
override DataFlow::Node getOutputNode() { result.asExpr() = this }
7777

7878
override predicate flowsTo(Crypto::FlowAwareElement other) {
79-
GenericDataSourceUniversalFlow::flow(this.getOutputNode(), other.getInputNode())
79+
GenericDataSourceFlow::flow(this.getOutputNode(), other.getInputNode())
8080
}
8181

8282
override string getAdditionalDescription() { result = this.toString() }
@@ -88,7 +88,7 @@ class GenericRemoteDataSource extends Crypto::GenericRemoteDataSource {
8888
override DataFlow::Node getOutputNode() { result.asExpr() = this }
8989

9090
override predicate flowsTo(Crypto::FlowAwareElement other) {
91-
GenericDataSourceUniversalFlow::flow(this.getOutputNode(), other.getInputNode())
91+
GenericDataSourceFlow::flow(this.getOutputNode(), other.getInputNode())
9292
}
9393

9494
override string getAdditionalDescription() { result = this.toString() }
@@ -107,7 +107,7 @@ class ConstantDataSource extends Crypto::GenericConstantSourceInstance instanceo
107107

108108
override predicate flowsTo(Crypto::FlowAwareElement other) {
109109
// TODO: separate config to avoid blowing up data-flow analysis
110-
GenericDataSourceUniversalFlow::flow(this.getOutputNode(), other.getInputNode())
110+
GenericDataSourceFlow::flow(this.getOutputNode(), other.getInputNode())
111111
}
112112

113113
override string getAdditionalDescription() { result = this.toString() }
@@ -122,15 +122,24 @@ abstract class RandomnessInstance extends Crypto::RandomNumberGenerationInstance
122122
}
123123

124124
class SecureRandomnessInstance extends RandomnessInstance {
125+
RandomDataSource source;
126+
125127
SecureRandomnessInstance() {
126-
exists(RandomDataSource s | this = s.getOutput() |
127-
s.getSourceOfRandomness() instanceof SecureRandomNumberGenerator
128-
)
128+
this = source.getOutput() and
129+
source.getSourceOfRandomness() instanceof SecureRandomNumberGenerator
129130
}
131+
132+
override string getGeneratorName() { result = source.getSourceOfRandomness().getQualifiedName() }
130133
}
131134

132135
class InsecureRandomnessInstance extends RandomnessInstance {
133-
InsecureRandomnessInstance() { exists(InsecureRandomnessSource node | this = node.asExpr()) }
136+
RandomDataSource source;
137+
138+
InsecureRandomnessInstance() {
139+
any(InsecureRandomnessSource src).asExpr() = this and source.getOutput() = this
140+
}
141+
142+
override string getGeneratorName() { result = source.getSourceOfRandomness().getQualifiedName() }
134143
}
135144

136145
/**
@@ -142,12 +151,12 @@ abstract class AdditionalFlowInputStep extends DataFlow::Node {
142151
final DataFlow::Node getInput() { result = this }
143152
}
144153

145-
module ArtifactUniversalFlow = DataFlow::Global<ArtifactUniversalFlowConfig>;
154+
module ArtifactFlow = DataFlow::Global<ArtifactFlowConfig>;
146155

147156
/**
148157
* Generic data source to node input configuration
149158
*/
150-
module GenericDataSourceUniversalFlowConfig implements DataFlow::ConfigSig {
159+
module GenericDataSourceFlowConfig implements DataFlow::ConfigSig {
151160
predicate isSource(DataFlow::Node source) {
152161
source = any(Crypto::GenericSourceInstance i).getOutputNode()
153162
}
@@ -175,7 +184,7 @@ module GenericDataSourceUniversalFlowConfig implements DataFlow::ConfigSig {
175184
}
176185
}
177186

178-
module ArtifactUniversalFlowConfig implements DataFlow::ConfigSig {
187+
module ArtifactFlowConfig implements DataFlow::ConfigSig {
179188
predicate isSource(DataFlow::Node source) {
180189
source = any(Crypto::ArtifactInstance artifact).getOutputNode()
181190
}
@@ -203,7 +212,7 @@ module ArtifactUniversalFlowConfig implements DataFlow::ConfigSig {
203212
}
204213
}
205214

206-
module GenericDataSourceUniversalFlow = TaintTracking::Global<GenericDataSourceUniversalFlowConfig>;
215+
module GenericDataSourceFlow = TaintTracking::Global<GenericDataSourceFlowConfig>;
207216

208217
// Import library-specific modeling
209218
import JCA
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @name Insecure nonce at a cipher operation
3+
* @id java/insecure-nonce
4+
* @kind problem
5+
* @problem.severity error
6+
* @precision high
7+
* @description A nonce is generated from a source that is not secure. This can lead to
8+
* vulnerabilities such as replay attacks or key recovery.
9+
*/
10+
11+
import experimental.Quantum.Language
12+
13+
predicate isInsecureNonceSource(Crypto::NonceArtifactNode n, Crypto::NodeBase src) {
14+
src = n.getSourceNode() and
15+
not src.asElement() instanceof SecureRandomnessInstance
16+
}
17+
18+
from Crypto::KeyOperationNode op, Crypto::NodeBase src
19+
where isInsecureNonceSource(op.getANonce(), src)
20+
select op, "Operation uses insecure nonce source $@", src, src.toString()
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* @name "PQC Test"
3+
*/
4+
5+
import experimental.Quantum.Language
6+
7+
class AESGCMAlgorithmNode extends Crypto::KeyOperationAlgorithmNode {
8+
AESGCMAlgorithmNode() {
9+
this.getAlgorithmType() = Crypto::KeyOpAlg::TSymmetricCipher(Crypto::KeyOpAlg::AES()) and
10+
this.getModeOfOperation().getModeType() = Crypto::GCM()
11+
}
12+
}
13+
14+
from Crypto::KeyOperationNode op, Crypto::NonceArtifactNode nonce
15+
where op.getAKnownAlgorithm() instanceof AESGCMAlgorithmNode and nonce = op.getANonce()
16+
select op, nonce.getSourceNode()

java/ql/src/experimental/Quantum/TestCipher.ql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* @name "PQC Test"
2+
* @name "Key operation slice table demo query"
33
*/
44

55
import experimental.Quantum.Language
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/**
2-
* @name TestHashOperations
2+
* @name "Hash operation slice table demo query"
33
*/
44

55
import experimental.Quantum.Language
66

77
from Crypto::HashOperationNode op, Crypto::HashAlgorithmNode alg
8-
where alg = op.getAKnownHashAlgorithm()
8+
where alg = op.getAKnownAlgorithm()
99
select op, op.getDigest(), alg, alg.getRawAlgorithmName()

misc/scripts/cryptography/generate_cbom.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env python3
22

33
import os
4+
import re
45
import sys
56
import argparse
67
import subprocess
@@ -86,6 +87,7 @@ def main():
8687
parser.add_argument("-c", "--codeql", required=True, help="Path to CodeQL CLI executable.")
8788
parser.add_argument("-d", "--database", required=True, help="Path to the CodeQL database.")
8889
parser.add_argument("-q", "--query", required=True, help="Path to the .ql query file.")
90+
parser.add_argument("--queryid", required=True, help="Query ID for the analysis.")
8991
parser.add_argument("-o", "--output", required=True, help="Output directory for analysis results.")
9092

9193
args = parser.parse_args()
@@ -94,7 +96,13 @@ def main():
9496
run_codeql_analysis(args.codeql, args.database, args.query, args.output)
9597

9698
# Locate DGML file
97-
dgml_file = os.path.join(args.output, "java", "print-cbom-graph.dgml")
99+
ALLOWED_QUERY_ID = re.compile(r'^[a-zA-Z0-9_\-]+$')
100+
101+
if not ALLOWED_QUERY_ID.match(args.queryid):
102+
print("Invalid query_id provided: '%s'. Allowed characters: letters, digits, '_', and '-'.", args.queryid)
103+
sys.exit(1)
104+
105+
dgml_file = os.path.join(args.output, "java", '{}.dgml'.format(args.queryid))
98106
dot_file = dgml_file.replace(".dgml", ".dot")
99107

100108
if os.path.exists(dgml_file):

0 commit comments

Comments
 (0)