Skip to content

Commit 23140df

Browse files
committed
Python: Add CryptographicOperation modeling for Cryptodome
1 parent 1b2ed9d commit 23140df

File tree

7 files changed

+139
-31
lines changed

7 files changed

+139
-31
lines changed

python/ql/src/semmle/python/frameworks/Cryptodome.qll

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ private import semmle.python.ApiGraphs
1717
* See https://pycryptodome.readthedocs.io/en/latest/
1818
*/
1919
private module CryptodomeModel {
20-
// ---------------------------------------------------------------------------
2120
/**
2221
* A call to `Cryptodome.PublicKey.RSA.generate`/`Crypto.PublicKey.RSA.generate`
2322
*
@@ -101,4 +100,115 @@ private module CryptodomeModel {
101100
// Note: There is not really a key-size argument, since it's always specified by the curve.
102101
override DataFlow::Node getKeySizeArg() { none() }
103102
}
103+
104+
/**
105+
* A cryptographic operation on an instance from the `Cipher` subpackage of `Cryptodome`/`Crypto`.
106+
*/
107+
class CryptodomeGenericCipherOperation extends Cryptography::CryptographicOperation::Range,
108+
DataFlow::CallCfgNode {
109+
string methodName;
110+
string cipherName;
111+
112+
CryptodomeGenericCipherOperation() {
113+
methodName in [
114+
"encrypt", "decrypt", "verify", "update", "hexverify", "encrypt_and_digest",
115+
"decrypt_and_verify"
116+
] and
117+
this =
118+
API::moduleImport(["Crypto", "Cryptodome"])
119+
.getMember(["Cipher"])
120+
.getMember(cipherName)
121+
.getMember("new")
122+
.getReturn()
123+
.getMember(methodName)
124+
.getACall()
125+
}
126+
127+
override Cryptography::CryptographicAlgorithm getAlgorithm() { result.matchesName(cipherName) }
128+
129+
override DataFlow::Node getAnInput() {
130+
methodName = "encrypt" and
131+
result in [this.getArg(0), this.getArgByName(["message", "plaintext"])]
132+
or
133+
methodName = "decrypt" and
134+
result in [this.getArg(0), this.getArgByName("ciphertext")]
135+
or
136+
// for the following methods, method signatures can be found in
137+
// https://pycryptodome.readthedocs.io/en/latest/src/cipher/modern.html
138+
methodName in ["update"] and
139+
result in [this.getArg(0), this.getArgByName("data")]
140+
or
141+
methodName in ["verify"] and
142+
result in [this.getArg(0), this.getArgByName(["mac_tag", "received_mac_tag"])]
143+
or
144+
methodName in ["hexverify"] and
145+
result in [this.getArg(0), this.getArgByName("mac_tag_hex")]
146+
or
147+
methodName in ["encrypt_and_digest"] and
148+
result in [this.getArg(0), this.getArgByName("plaintext")]
149+
or
150+
methodName in ["decrypt_and_verify"] and
151+
result in [this.getArg(0), this.getArgByName("ciphertext")]
152+
}
153+
}
154+
155+
/**
156+
* A cryptographic operation on an instance from the `Signature` subpackage of `Cryptodome`/`Crypto`.
157+
*/
158+
class CryptodomeGenericSignatureOperation extends Cryptography::CryptographicOperation::Range,
159+
DataFlow::CallCfgNode {
160+
string methodName;
161+
string signatureName;
162+
163+
CryptodomeGenericSignatureOperation() {
164+
methodName in ["sign", "verify"] and
165+
this =
166+
API::moduleImport(["Crypto", "Cryptodome"])
167+
.getMember(["Signature"])
168+
.getMember(signatureName)
169+
.getMember("new")
170+
.getReturn()
171+
.getMember(methodName)
172+
.getACall()
173+
}
174+
175+
override Cryptography::CryptographicAlgorithm getAlgorithm() {
176+
result.matchesName(signatureName)
177+
}
178+
179+
override DataFlow::Node getAnInput() {
180+
methodName = "sign" and
181+
result in [this.getArg(0), this.getArgByName("msg_hash")] // Cryptodome.Hash instance
182+
or
183+
methodName in ["verify"] and
184+
(
185+
result in [this.getArg(0), this.getArgByName(["msg_hash"])] // Cryptodome.Hash instance
186+
or
187+
result in [this.getArg(1), this.getArgByName(["signature"])]
188+
)
189+
}
190+
}
191+
192+
/**
193+
* A cryptographic operation on an instance from the `Hash` subpackage of `Cryptodome`/`Crypto`.
194+
*/
195+
class CryptodomeGenericHashOperation extends Cryptography::CryptographicOperation::Range,
196+
DataFlow::CallCfgNode {
197+
string hashName;
198+
199+
CryptodomeGenericHashOperation() {
200+
exists(API::Node hashModule |
201+
hashModule =
202+
API::moduleImport(["Crypto", "Cryptodome"]).getMember(["Hash"]).getMember(hashName)
203+
|
204+
this = hashModule.getMember("new").getACall()
205+
or
206+
this = hashModule.getMember("new").getReturn().getMember("update").getACall()
207+
)
208+
}
209+
210+
override Cryptography::CryptographicAlgorithm getAlgorithm() { result.matchesName(hashName) }
211+
212+
override DataFlow::Node getAnInput() { result in [this.getArg(0), this.getArgByName("data")] }
213+
}
104214
}

python/ql/test/experimental/library-tests/frameworks/cryptodome/test_aes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@
2121
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
2222
# using separate .encrypt calls on individual lines does not work
2323
whole_plantext = secret_message + padding
24-
encrypted = cipher.encrypt(whole_plantext)
24+
encrypted = cipher.encrypt(whole_plantext) # $ CryptographicOperation CryptographicOperationAlgorithm=AES CryptographicOperationInput=whole_plantext
2525

2626
print("encrypted={}".format(encrypted))
2727

2828
print()
2929

3030
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
31-
decrypted = cipher.decrypt(encrypted)
31+
decrypted = cipher.decrypt(encrypted) # $ CryptographicOperation CryptographicOperationAlgorithm=AES CryptographicOperationInput=encrypted
3232

3333
decrypted = decrypted[:-padding_len]
3434

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from Cryptodome.Hash import MD5
22

3-
hasher = MD5.new(b"secret message") # $ MISSING: CryptographicOperation CryptographicOperationInput=b"secret message" CryptographicOperationAlgorithm=MD5
3+
hasher = MD5.new(b"secret message") # $ CryptographicOperation CryptographicOperationInput=b"secret message" CryptographicOperationAlgorithm=MD5
44
print(hasher.hexdigest())
55

66

7-
hasher = MD5.new() # $ MISSING: CryptographicOperation CryptographicOperationAlgorithm=MD5
8-
hasher.update(b"secret") # $ MISSING: CryptographicOperation CryptographicOperationInput=b"secret" CryptographicOperationAlgorithm=MD5
9-
hasher.update(b" message") # $ MISSING: CryptographicOperation CryptographicOperationInput=b" message" CryptographicOperationAlgorithm=MD5
7+
hasher = MD5.new() # $ CryptographicOperation CryptographicOperationAlgorithm=MD5
8+
hasher.update(b"secret") # $ CryptographicOperation CryptographicOperationInput=b"secret" CryptographicOperationAlgorithm=MD5
9+
hasher.update(b" message") # $ CryptographicOperation CryptographicOperationInput=b" message" CryptographicOperationAlgorithm=MD5
1010
print(hasher.hexdigest())

python/ql/test/experimental/library-tests/frameworks/cryptodome/test_rc4.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@
1717
secret_message = b"secret message"
1818

1919
cipher = ARC4.new(key)
20-
encrypted = cipher.encrypt(secret_message)
20+
encrypted = cipher.encrypt(secret_message) # $ CryptographicOperation CryptographicOperationAlgorithm=ARC4 CryptographicOperationInput=secret_message
2121

2222
print("encrypted={}".format(encrypted))
2323

2424
print()
2525

2626
cipher = ARC4.new(key)
27-
decrypted = cipher.decrypt(encrypted)
27+
decrypted = cipher.decrypt(encrypted) # $ CryptographicOperation CryptographicOperationAlgorithm=ARC4 CryptographicOperationInput=encrypted
2828

2929
print("decrypted={}".format(decrypted))
3030
assert decrypted == secret_message

python/ql/test/library-tests/frameworks/cryptodome/test_dsa.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,22 @@
2020

2121
signer = DSS.new(private_key, mode='fips-186-3')
2222

23-
hasher = SHA256.new(message)
24-
signature = signer.sign(hasher)
23+
hasher = SHA256.new(message) # $ CryptographicOperation CryptographicOperationAlgorithm=SHA256 CryptographicOperationInput=message
24+
signature = signer.sign(hasher) # $ CryptographicOperation CryptographicOperationInput=hasher
2525

2626
print("signature={}".format(signature))
2727

2828
print()
2929

3030
verifier = DSS.new(public_key, mode='fips-186-3')
3131

32-
hasher = SHA256.new(message)
33-
verifier.verify(hasher, signature)
32+
hasher = SHA256.new(message) # $ CryptographicOperation CryptographicOperationAlgorithm=SHA256 CryptographicOperationInput=message
33+
verifier.verify(hasher, signature) # $ CryptographicOperation CryptographicOperationInput=hasher CryptographicOperationInput=signature
3434
print("Signature verified (as expected)")
3535

3636
try:
37-
hasher = SHA256.new(b"other message")
38-
verifier.verify(hasher, signature)
37+
hasher = SHA256.new(b"other message") # $ CryptographicOperation CryptographicOperationAlgorithm=SHA256 CryptographicOperationInput=b"other message"
38+
verifier.verify(hasher, signature) # $ CryptographicOperation CryptographicOperationInput=hasher CryptographicOperationInput=signature
3939
raise Exception("Signature verified (unexpected)")
4040
except ValueError:
4141
print("Signature mismatch (as expected)")

python/ql/test/library-tests/frameworks/cryptodome/test_ec.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,22 @@
1717

1818
signer = DSS.new(private_key, mode='fips-186-3')
1919

20-
hasher = SHA256.new(message)
21-
signature = signer.sign(hasher)
20+
hasher = SHA256.new(message) # $ CryptographicOperation CryptographicOperationAlgorithm=SHA256 CryptographicOperationInput=message
21+
signature = signer.sign(hasher) # $ CryptographicOperation CryptographicOperationInput=hasher
2222

2323
print("signature={}".format(signature))
2424

2525
print()
2626

2727
verifier = DSS.new(public_key, mode='fips-186-3')
2828

29-
hasher = SHA256.new(message)
30-
verifier.verify(hasher, signature)
29+
hasher = SHA256.new(message) # $ CryptographicOperation CryptographicOperationAlgorithm=SHA256 CryptographicOperationInput=message
30+
verifier.verify(hasher, signature) # $ CryptographicOperation CryptographicOperationInput=hasher CryptographicOperationInput=signature
3131
print("Signature verified (as expected)")
3232

3333
try:
34-
hasher = SHA256.new(b"other message")
35-
verifier.verify(hasher, signature)
34+
hasher = SHA256.new(b"other message") # $ CryptographicOperation CryptographicOperationAlgorithm=SHA256 CryptographicOperationInput=b"other message"
35+
verifier.verify(hasher, signature) # $ CryptographicOperation CryptographicOperationInput=hasher CryptographicOperationInput=signature
3636
raise Exception("Signature verified (unexpected)")
3737
except ValueError:
3838
print("Signature mismatch (as expected)")

python/ql/test/library-tests/frameworks/cryptodome/test_rsa.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,15 @@
2323

2424
encrypt_cipher = PKCS1_OAEP.new(public_key)
2525

26-
encrypted = encrypt_cipher.encrypt(secret_message)
26+
encrypted = encrypt_cipher.encrypt(secret_message) # $ CryptographicOperation CryptographicOperationInput=secret_message
2727

2828
print("encrypted={}".format(encrypted))
2929

3030
print()
3131

3232
decrypt_cipher = PKCS1_OAEP.new(private_key)
3333

34-
decrypted = decrypt_cipher.decrypt(
35-
encrypted,
36-
)
34+
decrypted = decrypt_cipher.decrypt(encrypted) # $ CryptographicOperation CryptographicOperationInput=encrypted
3735

3836
print("decrypted={}".format(decrypted))
3937
assert decrypted == secret_message
@@ -51,23 +49,23 @@
5149

5250
signer = pss.new(private_key)
5351

54-
hasher = SHA256.new(message)
55-
signature = signer.sign(hasher)
52+
hasher = SHA256.new(message) # $ CryptographicOperation CryptographicOperationAlgorithm=SHA256 CryptographicOperationInput=message
53+
signature = signer.sign(hasher) # $ CryptographicOperation CryptographicOperationInput=hasher
5654

5755
print("signature={}".format(signature))
5856

5957
print()
6058

6159
verifier = pss.new(public_key)
6260

63-
hasher = SHA256.new(message)
64-
verifier.verify(hasher, signature)
61+
hasher = SHA256.new(message) # $ CryptographicOperation CryptographicOperationAlgorithm=SHA256 CryptographicOperationInput=message
62+
verifier.verify(hasher, signature) # $ CryptographicOperation CryptographicOperationInput=hasher CryptographicOperationInput=signature
6563
print("Signature verified (as expected)")
6664

6765
try:
6866
verifier = pss.new(public_key)
69-
hasher = SHA256.new(b"other message")
70-
verifier.verify(hasher, signature)
67+
hasher = SHA256.new(b"other message") # $ CryptographicOperation CryptographicOperationAlgorithm=SHA256 CryptographicOperationInput=b"other message"
68+
verifier.verify(hasher, signature) # $ CryptographicOperation CryptographicOperationInput=hasher CryptographicOperationInput=signature
7169
raise Exception("Signature verified (unexpected)")
7270
except ValueError:
7371
print("Signature mismatch (as expected)")

0 commit comments

Comments
 (0)