Improve CI/CD: RSA sign+decrypt and EC decrypt
This commit is contained in:
parent
0b43929a83
commit
e84853414f
6
.github/workflows/main.yml
vendored
6
.github/workflows/main.yml
vendored
@ -23,10 +23,10 @@ jobs:
|
|||||||
sudo apt-get update && sudo apt-get install --reinstall adoptopenjdk-8-hotspot;
|
sudo apt-get update && sudo apt-get install --reinstall adoptopenjdk-8-hotspot;
|
||||||
sudo update-java-alternatives -s adoptopenjdk-8-hotspot-amd64;
|
sudo update-java-alternatives -s adoptopenjdk-8-hotspot-amd64;
|
||||||
# 3) Get Javacard SDKs
|
# 3) Get Javacard SDKs
|
||||||
git clone https://github.com/martinpaljak/oracle_javacard_sdks && mv oracle_javacard_sdks/jc305u3_kit/ /tmp/ && rm -rf oracle_javacard_sdks;
|
git clone https://github.com/martinpaljak/oracle_javacard_sdks && mv oracle_javacard_sdks/jc303_kit/ /tmp/ && mv oracle_javacard_sdks/jc305u3_kit/ /tmp/ &&rm -rf oracle_javacard_sdks;
|
||||||
# 4) Compile our applet
|
# 4) Compile our applet
|
||||||
cat build.xml | sed 's/<cap /<cap export="SmartPGPApplet" /' > /tmp/build.xml && mv /tmp/build.xml ./; # export jar for simulator
|
cat build.xml | sed 's/<cap /<cap export="SmartPGPApplet" /' > /tmp/build.xml && mv /tmp/build.xml ./; # export jar for simulator
|
||||||
JC_HOME=/tmp/jc305u3_kit/ ant;
|
JC_HOME=/tmp/jc303_kit/ ant;
|
||||||
# 5) Clone jcardsim repository, compile and install
|
# 5) Clone jcardsim repository, compile and install
|
||||||
git clone https://github.com/licel/jcardsim && cd jcardsim && git fetch origin pull/155/head:pr155 && git config --global user.name "John Doe" && git merge pr155 -m "AA" && cd -;
|
git clone https://github.com/licel/jcardsim && cd jcardsim && git fetch origin pull/155/head:pr155 && git config --global user.name "John Doe" && git merge pr155 -m "AA" && cd -;
|
||||||
# Patch random
|
# Patch random
|
||||||
@ -54,7 +54,7 @@ jobs:
|
|||||||
opensc-tool -s 80b800001810d276000124010304AFAF000000000000050000020F0F00;
|
opensc-tool -s 80b800001810d276000124010304AFAF000000000000050000020F0F00;
|
||||||
# Get card status
|
# Get card status
|
||||||
python3 .github/workflows/card-status.py;
|
python3 .github/workflows/card-status.py;
|
||||||
# Test ECDSA signatures
|
# Main tests
|
||||||
python3 .github/workflows/test_SmartPGP.py;
|
python3 .github/workflows/test_SmartPGP.py;
|
||||||
# Reset
|
# Reset
|
||||||
python3 .github/workflows/reset.py;
|
python3 .github/workflows/reset.py;
|
||||||
|
382
.github/workflows/test_SmartPGP.py
vendored
382
.github/workflows/test_SmartPGP.py
vendored
@ -1,8 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
# Testing SmartPGP applet up to a crash because of a memory leak
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from sys import path
|
from sys import path
|
||||||
import subprocess
|
import subprocess
|
||||||
@ -15,19 +13,28 @@ path.append(".")
|
|||||||
import OpenPGPpy
|
import OpenPGPpy
|
||||||
|
|
||||||
|
|
||||||
|
class BadTestResult(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def sha256(data):
|
def sha256(data):
|
||||||
return hashlib.sha256(data).digest()
|
return hashlib.sha256(data).digest()
|
||||||
|
|
||||||
|
|
||||||
def pubkey_to_der(pubkey):
|
def rsa_2048_pubkey_to_der(pubkey):
|
||||||
# Add ASN1 DER header (EC parameters)
|
# Add ASN1 DER header
|
||||||
# ECP 256 r1 header
|
# RSA 2048
|
||||||
header_hex = "3059301306072A8648CE3D020106082A8648CE3D030107034200"
|
header_modulus = "30820122300d06092a864886f70d01010105000382010f003082010a0282010100"
|
||||||
return bytes.fromhex(header_hex + pubkey.hex())
|
header_exponent = "0203"
|
||||||
|
|
||||||
|
modulus = pubkey[9:265].hex()
|
||||||
|
exponent = pubkey[267:].hex()
|
||||||
|
|
||||||
|
return bytes.fromhex(header_modulus + modulus + header_exponent + exponent)
|
||||||
|
|
||||||
|
|
||||||
def check_signature(msg, signature, pubkeyd):
|
def check_rsa_2048_signature(message, signature, pubkeyd):
|
||||||
pubkey = pubkey_to_der(pubkeyd)
|
pubkey = rsa_2048_pubkey_to_der(pubkeyd)
|
||||||
fpk = tempfile.NamedTemporaryFile(delete=False)
|
fpk = tempfile.NamedTemporaryFile(delete=False)
|
||||||
fpk.write(pubkey)
|
fpk.write(pubkey)
|
||||||
fpk.close()
|
fpk.close()
|
||||||
@ -40,79 +47,326 @@ def check_signature(msg, signature, pubkeyd):
|
|||||||
sigOK = False
|
sigOK = False
|
||||||
try:
|
try:
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
verify_cmd, input=msg, shell=True, check=True, stdout=subprocess.PIPE
|
verify_cmd, input=message, shell=True, check=True, stdout=subprocess.PIPE
|
||||||
)
|
)
|
||||||
sigOK = True
|
sigOK = True
|
||||||
except Exception:
|
except Exception:
|
||||||
print("Error in signature verification")
|
print("[-] ERROR in signature verification:")
|
||||||
print(">>> Requires openssl in path to check signatures")
|
print(">>> openssl is required in path to check signatures")
|
||||||
sigOK = False
|
sigOK = False
|
||||||
os.remove(fpk.name)
|
os.remove(fpk.name)
|
||||||
os.remove(fsig.name)
|
os.remove(fsig.name)
|
||||||
return sigOK
|
return sigOK
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def encrypt_rsa_2048(message, pubkeyd):
|
||||||
|
pubkey = rsa_2048_pubkey_to_der(pubkeyd)
|
||||||
|
fpk = tempfile.NamedTemporaryFile(delete=False)
|
||||||
|
fpk.write(pubkey)
|
||||||
|
fpk.close()
|
||||||
|
fenc = tempfile.NamedTemporaryFile(delete=False)
|
||||||
|
fenc.close()
|
||||||
|
encrypt_cmd = (
|
||||||
|
f"openssl rsautl -keyform DER -pubin -inkey {fpk.name} -pkcs -encrypt -out {fenc.name}"
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
# instanciated with (True) to enable debug mode
|
subprocess.run(
|
||||||
mydevice = OpenPGPpy.OpenPGPcard(True)
|
encrypt_cmd, input=message, shell=True, check=True, stdout=subprocess.PIPE
|
||||||
except OpenPGPpy.ConnectionException as exc:
|
)
|
||||||
print(exc)
|
except Exception:
|
||||||
return
|
print("[-] ERROR in encrpytion")
|
||||||
print("OpenPGP device detected")
|
print(">>> openssl is required in path to encrypt message")
|
||||||
pubkey_card_all = None
|
raise
|
||||||
|
os.remove(fpk.name)
|
||||||
|
fres = open(fenc.name, 'rb')
|
||||||
|
res = fres.read()
|
||||||
|
fres.close()
|
||||||
|
os.remove(fenc.name)
|
||||||
|
return bytes.fromhex("00") + res
|
||||||
|
|
||||||
|
|
||||||
|
def test_rsa_2048_signature(token, PIN1, PIN3, repeat):
|
||||||
|
print(f"[+] == Test RSA 2048 signature ({repeat} signatures) ==")
|
||||||
|
|
||||||
|
# Verify PIN3, required for the next command
|
||||||
|
print("[+] Verify PIN3")
|
||||||
|
token.verify_pin(3, PIN3)
|
||||||
|
|
||||||
|
# Setup RSA 2048 for SIG key
|
||||||
|
print("[+] Set SIG key to RSA 2048")
|
||||||
|
token.put_data("00C1", "010800001103")
|
||||||
|
|
||||||
|
# Generate key for SIG key
|
||||||
|
print("[+] Generate SIG key")
|
||||||
|
pubkey_card = token.gen_key("B600")
|
||||||
|
|
||||||
|
# Digest message and sign
|
||||||
|
message = "Hello SmartPGP! Take that message.".encode("ascii")
|
||||||
|
header_sha256 = "3031300D060960864801650304020105000420";
|
||||||
|
hash = bytes.fromhex(header_sha256) + sha256(message)
|
||||||
|
|
||||||
|
for i in range(repeat):
|
||||||
|
print(f"[+] Test #{i + 1}")
|
||||||
|
|
||||||
|
# Verify PIN1, required for signature
|
||||||
|
token.verify_pin(1, PIN1)
|
||||||
|
|
||||||
|
print("[+] Sign SHA256 hash")
|
||||||
|
sig_card = token.sign(hash)
|
||||||
|
|
||||||
|
print("[+] Verify signature")
|
||||||
|
if not check_rsa_2048_signature(message, sig_card, pubkey_card):
|
||||||
|
print("[-] BAD signature")
|
||||||
|
raise BadTestResult
|
||||||
|
|
||||||
|
|
||||||
|
def test_rsa_2048_decrypt(token, PIN1, PIN3, repeat):
|
||||||
|
print(f"[+] == Test RSA 2048 decrypt ({repeat} deciphers) ==")
|
||||||
|
|
||||||
|
# Verify PIN3, required for the next command
|
||||||
|
print("[+] Verify PIN3")
|
||||||
|
token.verify_pin(3, PIN3)
|
||||||
|
|
||||||
|
# Setup RSA 2048 for DEC key
|
||||||
|
print("[+] Set DEC key to RSA 2048")
|
||||||
|
token.put_data("00C2", "010800001103")
|
||||||
|
|
||||||
|
# Generate key for DEC key
|
||||||
|
print("[+] Generate DEC key")
|
||||||
|
pubkey_card = token.gen_key("B800")
|
||||||
|
|
||||||
|
# Verify PIN2, required for decrypt
|
||||||
|
token.verify_pin(2, PIN1)
|
||||||
|
|
||||||
|
# Cipher message
|
||||||
|
message = "Hello SmartPGP! Take that message.".encode("ascii")
|
||||||
|
|
||||||
|
print("[+] Prepare encrypted message")
|
||||||
|
encrypted = encrypt_rsa_2048(message, pubkey_card)
|
||||||
|
|
||||||
|
for i in range(repeat):
|
||||||
|
print(f"[+] Test #{i + 1}")
|
||||||
|
|
||||||
|
print("[+] Decipher message")
|
||||||
|
decrypted = token.decipher(encrypted)
|
||||||
|
|
||||||
|
print("[+] Verify message")
|
||||||
|
if message == decrypted:
|
||||||
|
print("[+] Good deciphering")
|
||||||
|
else:
|
||||||
|
print("[-] BAD deciphering")
|
||||||
|
raise BadTestResult
|
||||||
|
|
||||||
|
|
||||||
|
def test_rsa_2048(token, PIN1, PIN3, repeat):
|
||||||
|
print("[+] === Test RSA 2048 ===")
|
||||||
|
|
||||||
|
test_rsa_2048_signature(token, PIN1, PIN3, repeat)
|
||||||
|
|
||||||
|
test_rsa_2048_decrypt(token, PIN1, PIN3, repeat)
|
||||||
|
|
||||||
|
|
||||||
|
def ec_prime256v1_pubkey_to_der(pubkey):
|
||||||
|
# Add ASN1 DER header (EC parameters)
|
||||||
|
# ECP 256 r1 header
|
||||||
|
header_hex = "3059301306072A8648CE3D020106082A8648CE3D030107034200"
|
||||||
|
return bytes.fromhex(header_hex + pubkey.hex())
|
||||||
|
|
||||||
|
|
||||||
|
def check_ec_prime256v1_signature(message, signature, pubkeyd):
|
||||||
|
pubkey = ec_prime256v1_pubkey_to_der(pubkeyd)
|
||||||
|
fpk = tempfile.NamedTemporaryFile(delete=False)
|
||||||
|
fpk.write(pubkey)
|
||||||
|
fpk.close()
|
||||||
|
fsig = tempfile.NamedTemporaryFile(delete=False)
|
||||||
|
fsig.write(signature)
|
||||||
|
fsig.close()
|
||||||
|
verify_cmd = (
|
||||||
|
f"openssl dgst -sha256 -keyform DER -verify {fpk.name} -signature {fsig.name}"
|
||||||
|
)
|
||||||
|
sigOK = False
|
||||||
try:
|
try:
|
||||||
pubkey_card_all = mydevice.get_public_key("B600")
|
subprocess.run(
|
||||||
except OpenPGPpy.PGPCardException as exc:
|
verify_cmd, input=message, shell=True, check=True, stdout=subprocess.PIPE
|
||||||
# SW = 0x6581 or 0x6A88 ?
|
)
|
||||||
if exc.sw_code != 0x6581 and exc.sw_code != 0x6A88:
|
sigOK = True
|
||||||
raise
|
except Exception:
|
||||||
# SIGn key was not created, continue to setup this key
|
print("[-] ERROR in signature verification")
|
||||||
if pubkey_card_all is None:
|
print(">>> openssl is required in path to check signatures")
|
||||||
print("Setup the new device")
|
sigOK = False
|
||||||
PIN3 = "12345678" #getpass.getpass("Enter PIN3 (PUK) : ")
|
os.remove(fpk.name)
|
||||||
try:
|
os.remove(fsig.name)
|
||||||
mydevice.verify_pin(3, PIN3)
|
return sigOK
|
||||||
except OpenPGPpy.PGPCardException as exc:
|
|
||||||
if exc.sw_code == 0x6982 or exc.sw_code == 0x6A80:
|
|
||||||
print("Error: Wrong PUK")
|
|
||||||
return
|
|
||||||
# Setup EC256r1 for SIG key
|
|
||||||
try:
|
|
||||||
mydevice.put_data("00C1", "132A8648CE3D030107")
|
|
||||||
except OpenPGPpy.PGPCardException as exc:
|
|
||||||
if exc.sw_code == 0x6A80:
|
|
||||||
raise Exception(
|
|
||||||
"This device is not compatible with ECDSA 256r1."
|
|
||||||
) from exc
|
|
||||||
raise
|
|
||||||
# Generate key for sign
|
|
||||||
pubkey_card_all = mydevice.gen_key("B600")
|
|
||||||
pubkey_card = pubkey_card_all[-65:]
|
|
||||||
print('Device "SIG" public key read')
|
|
||||||
|
|
||||||
PIN1 = "123456" #getpass.getpass("Enter PIN1 : ")
|
|
||||||
|
|
||||||
# Make 200 ECDSA
|
def encrypt_ec_prime256v1(pubkeyd):
|
||||||
print(f"\nPublicKey for signature : 0x{pubkey_card.hex()}")
|
pubkey = ec_prime256v1_pubkey_to_der(pubkeyd)
|
||||||
|
fpub = tempfile.NamedTemporaryFile(delete=False)
|
||||||
|
fpub.write(pubkey)
|
||||||
|
fpub.close()
|
||||||
|
|
||||||
|
fpriv2 = tempfile.NamedTemporaryFile(delete=False)
|
||||||
|
fpriv2.close()
|
||||||
|
generate_private_cmd = (
|
||||||
|
f"openssl genpkey -outform DER -out {fpriv2.name} -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 -pkeyopt ec_param_enc:named_curve"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
generate_private_cmd, shell=True, check=True, stdout=subprocess.PIPE
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
print("[-] ERROR in encrpytion")
|
||||||
|
print(">>> openssl is required in path to encrypt message")
|
||||||
|
raise
|
||||||
|
|
||||||
|
fpub2 = tempfile.NamedTemporaryFile(delete=False)
|
||||||
|
fpub2.close()
|
||||||
|
generate_pub_cmd = (
|
||||||
|
f"openssl pkey -pubout -inform DER -in {fpriv2.name} -outform DER -out {fpub2.name}"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
generate_pub_cmd, shell=True, check=True, stdout=subprocess.PIPE
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
print("[-] ERROR in encrpytion")
|
||||||
|
print(">>> openssl is required in path to encrypt message")
|
||||||
|
raise
|
||||||
|
|
||||||
|
fenc = tempfile.NamedTemporaryFile(delete=False)
|
||||||
|
fenc.close()
|
||||||
|
generate_ecdh_cmd = (
|
||||||
|
f"openssl pkeyutl -derive -out {fenc.name} -keyform DER -inkey {fpriv2.name} -peerform DER -peerkey {fpub.name}"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
generate_ecdh_cmd, shell=True, check=True, stdout=subprocess.PIPE
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
print("[-] ERROR in encrpytion")
|
||||||
|
print(">>> openssl is required in path to encrypt message")
|
||||||
|
raise
|
||||||
|
|
||||||
|
f = open(fpub2.name, 'rb')
|
||||||
|
pub2 = f.read()
|
||||||
|
pub2 = pub2[26:]
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
f = open(fenc.name, 'rb')
|
||||||
|
enc = f.read()
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
os.remove(fpub.name)
|
||||||
|
os.remove(fpriv2.name)
|
||||||
|
os.remove(fpub2.name)
|
||||||
|
os.remove(fenc.name)
|
||||||
|
|
||||||
|
return (bytes.fromhex("A646") + bytes.fromhex("7F4943") + bytes.fromhex("8641") + pub2, enc)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ec_prime256v1_signature(token, PIN1, PIN3, repeat):
|
||||||
|
print(f"[+] == Test EC prime256v1 signature ({repeat} signatures) ==")
|
||||||
|
|
||||||
|
# Verify PIN3, required for the next command
|
||||||
|
print("[+] Verify PIN3")
|
||||||
|
token.verify_pin(3, PIN3)
|
||||||
|
|
||||||
|
# Setup EC prime256v1 for SIG key
|
||||||
|
print("[+] Set SIG key to EC prime256v1")
|
||||||
|
token.put_data("00C1", "132A8648CE3D030107")
|
||||||
|
|
||||||
|
# Generate key for SIG key
|
||||||
|
print("[+] Generate SIG key")
|
||||||
|
pubkey_card = token.gen_key("B600")
|
||||||
|
pubkey_card = pubkey_card[-65:]
|
||||||
|
|
||||||
|
# Digest message and sign
|
||||||
message = "Hello SmartPGP! Take that message.".encode("ascii")
|
message = "Hello SmartPGP! Take that message.".encode("ascii")
|
||||||
hash = sha256(message)
|
hash = sha256(message)
|
||||||
for _ in range(200):
|
|
||||||
mydevice.verify_pin(1, PIN1)
|
for i in range(repeat):
|
||||||
sig_card = mydevice.sign_ec_der(hash)
|
print(f"[+] Test #{i + 1}")
|
||||||
print(f"Signature : 0x{sig_card.hex()}")
|
|
||||||
if check_signature(message, sig_card, pubkey_card):
|
# Verify PIN1, required for signature
|
||||||
print("OK")
|
token.verify_pin(1, PIN1)
|
||||||
|
|
||||||
|
print("[+] Sign SHA256 hash")
|
||||||
|
sig_card = token.sign_ec_der(hash)
|
||||||
|
|
||||||
|
print("[+] Verify signature")
|
||||||
|
if not check_ec_prime256v1_signature(message, sig_card, pubkey_card):
|
||||||
|
print("[-] BAD signature")
|
||||||
|
raise BadTestResult
|
||||||
|
|
||||||
|
|
||||||
|
def test_ec_prime256v1_decrypt(token, PIN1, PIN3, repeat):
|
||||||
|
print(f"[+] == Test EC prime256v1 decrypt ({repeat} deciphers) ==")
|
||||||
|
|
||||||
|
# Verify PIN3, required for the next command
|
||||||
|
print("[+] Verify PIN3")
|
||||||
|
token.verify_pin(3, PIN3)
|
||||||
|
|
||||||
|
# Setup EC prime256v1 for DEC key
|
||||||
|
print("[+] Set DEC key to EC prime256v1")
|
||||||
|
token.put_data("00C2", "122A8648CE3D030107")
|
||||||
|
|
||||||
|
# Generate key for DEC key
|
||||||
|
print("[+] Generate DEC key")
|
||||||
|
pubkey_card = token.gen_key("B800")
|
||||||
|
pubkey_card = pubkey_card[-65:]
|
||||||
|
|
||||||
|
# Verify PIN2, required for decrypt
|
||||||
|
token.verify_pin(2, PIN1)
|
||||||
|
|
||||||
|
print("[+] Prepare ECDH")
|
||||||
|
(encrypted, secret) = encrypt_ec_prime256v1(pubkey_card)
|
||||||
|
|
||||||
|
for i in range(repeat):
|
||||||
|
print(f"[+] Test #{i + 1}")
|
||||||
|
|
||||||
|
print("[+] Decipher (compute ECDH secret)")
|
||||||
|
decrypted = token.decipher(encrypted)
|
||||||
|
|
||||||
|
print("[+] Verify message")
|
||||||
|
if secret == decrypted:
|
||||||
|
print("[+] Good deciphering")
|
||||||
else:
|
else:
|
||||||
print("Can't check signature")
|
print("[-] BAD deciphering")
|
||||||
return
|
raise BadTestResult
|
||||||
|
|
||||||
|
|
||||||
|
def test_ec_prime256v1(token, PIN1, PIN3, repeat):
|
||||||
|
print("[+] === Test EC prime256v1 ===")
|
||||||
|
|
||||||
|
test_ec_prime256v1_signature(token, PIN1, PIN3, repeat)
|
||||||
|
|
||||||
|
test_ec_prime256v1_decrypt(token, PIN1, PIN3, repeat)
|
||||||
|
|
||||||
|
|
||||||
|
def main(rsa, ec, repeat):
|
||||||
|
# True/False == with/without debug
|
||||||
|
token = OpenPGPpy.OpenPGPcard(False)
|
||||||
|
|
||||||
|
PIN1 = "123456"
|
||||||
|
PIN3 = "12345678"
|
||||||
|
|
||||||
|
if rsa:
|
||||||
|
test_rsa_2048(token, PIN1, PIN3, repeat)
|
||||||
|
|
||||||
|
if ec:
|
||||||
|
test_ec_prime256v1(token, PIN1, PIN3, repeat)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
main()
|
main(rsa=True, ec=False, repeat=200)
|
||||||
|
except OpenPGPpy.openpgp_card.ConnectionException:
|
||||||
|
print(f"[-] FAILED to find OpenPGP token")
|
||||||
|
exit(1)
|
||||||
except OpenPGPpy.PGPCardException as exc:
|
except OpenPGPpy.PGPCardException as exc:
|
||||||
if exc.sw_code == 0x6F00:
|
if exc.sw_code != 0x9000:
|
||||||
print("Crash, game over.")
|
print(f"[-] FAILED with: 0x{exc.sw_code:02x}")
|
||||||
print("SFYL !")
|
exit(2)
|
||||||
print(exc)
|
except BadTestResult:
|
||||||
|
print(f"[-] FAILED test")
|
||||||
|
exit(3)
|
||||||
|
Loading…
Reference in New Issue
Block a user