support for Ed25519 signatures
This increases the versioned dependency on the cryptography module to 2.6, since that is the version that provides the necessary ed25519 functionality. We also add a "pure" 25519 OpenPGP certificate for testing purposes. Closes #221, #222, #247 Signed-off-by: Daniel Kahn Gillmor <dkg@fifthhorseman.net>
This commit is contained in:
committed by
Daniel Kahn Gillmor
parent
53c6c3ba94
commit
d601655c27
@@ -26,10 +26,10 @@ def _openssl_get_supported_curves():
|
||||
|
||||
# store the result so we don't have to do all of this every time
|
||||
curves = { b.ffi.string(b.lib.OBJ_nid2sn(c.nid)).decode('utf-8') for c in cs }
|
||||
# X25519 is always present in cryptography>=2.5
|
||||
# The python cryptography lib provides a different interface for this curve,
|
||||
# so it is handled differently in the ECDHPriv/ECDHPub classes
|
||||
curves |= {'X25519'}
|
||||
# Ed25519 and X25519 are always present in cryptography>=2.6
|
||||
# The python cryptography lib provides a different interface for these curves,
|
||||
# so they are handled differently in the ECDHPriv/Pub and EdDSAPriv/Pub classes
|
||||
curves |= {'X25519', 'ed25519'}
|
||||
_openssl_get_supported_curves._curves = curves
|
||||
return curves
|
||||
|
||||
|
||||
@@ -59,9 +59,6 @@ class EllipticCurveOID(Enum):
|
||||
#: DJB's fast elliptic curve
|
||||
Curve25519 = ('1.3.6.1.4.1.3029.1.5.1', X25519)
|
||||
#: Twisted Edwards variant of Curve25519
|
||||
#:
|
||||
#: .. warning::
|
||||
#: This curve is not currently usable by PGPy
|
||||
Ed25519 = ('1.3.6.1.4.1.11591.15.1', Ed25519)
|
||||
#: NIST P-256, also known as SECG curve secp256r1
|
||||
NIST_P256 = ('1.2.840.10045.3.1.7', ec.SECP256R1)
|
||||
@@ -268,7 +265,8 @@ class PubKeyAlgorithm(IntEnum):
|
||||
return self in {PubKeyAlgorithm.RSAEncryptOrSign,
|
||||
PubKeyAlgorithm.DSA,
|
||||
PubKeyAlgorithm.ECDSA,
|
||||
PubKeyAlgorithm.ECDH}
|
||||
PubKeyAlgorithm.ECDH,
|
||||
PubKeyAlgorithm.EdDSA}
|
||||
|
||||
@property
|
||||
def can_encrypt(self): # pragma: no cover
|
||||
@@ -276,7 +274,7 @@ class PubKeyAlgorithm(IntEnum):
|
||||
|
||||
@property
|
||||
def can_sign(self):
|
||||
return self in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.DSA, PubKeyAlgorithm.ECDSA}
|
||||
return self in {PubKeyAlgorithm.RSAEncryptOrSign, PubKeyAlgorithm.DSA, PubKeyAlgorithm.ECDSA, PubKeyAlgorithm.EdDSA}
|
||||
|
||||
@property
|
||||
def deprecated(self):
|
||||
|
||||
@@ -27,6 +27,7 @@ from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.hazmat.primitives.asymmetric import dsa
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from cryptography.hazmat.primitives.asymmetric import x25519
|
||||
|
||||
@@ -71,6 +72,7 @@ __all__ = ['SubPackets',
|
||||
'RSASignature',
|
||||
'DSASignature',
|
||||
'ECDSASignature',
|
||||
'EdDSASignature',
|
||||
'PubKey',
|
||||
'OpaquePubKey',
|
||||
'RSAPub',
|
||||
@@ -78,6 +80,7 @@ __all__ = ['SubPackets',
|
||||
'ElGPub',
|
||||
'ECPoint',
|
||||
'ECDSAPub',
|
||||
'EdDSAPub',
|
||||
'ECDHPub',
|
||||
'String2Key',
|
||||
'ECKDF',
|
||||
@@ -87,6 +90,7 @@ __all__ = ['SubPackets',
|
||||
'DSAPriv',
|
||||
'ElGPriv',
|
||||
'ECDSAPriv',
|
||||
'EdDSAPriv',
|
||||
'ECDHPriv',
|
||||
'CipherText',
|
||||
'RSACipherText',
|
||||
@@ -351,6 +355,21 @@ class ECDSASignature(DSASignature):
|
||||
self.s = MPI(seq[1])
|
||||
|
||||
|
||||
class EdDSASignature(DSASignature):
|
||||
def from_signer(self, sig):
|
||||
lsig = len(sig)
|
||||
if lsig % 2 != 0:
|
||||
raise PGPError("malformed EdDSA signature")
|
||||
split = lsig // 2
|
||||
self.r = MPI(self.bytes_to_int(sig[:split]))
|
||||
self.s = MPI(self.bytes_to_int(sig[split:]))
|
||||
|
||||
def __sig__(self):
|
||||
# TODO: change this length when EdDSA can be used with another curve (Ed448)
|
||||
l = (EllipticCurveOID.Ed25519.key_size + 7) // 8
|
||||
return self.int_to_bytes(self.r, l) + self.int_to_bytes(self.s, l)
|
||||
|
||||
|
||||
class PubKey(MPIs):
|
||||
__pubfields__ = ()
|
||||
|
||||
@@ -571,6 +590,57 @@ class ECDSAPub(PubKey):
|
||||
raise PGPIncompatibleECPointFormat("Only Standard format is valid for ECDSA")
|
||||
|
||||
|
||||
class EdDSAPub(PubKey):
|
||||
__pubfields__ = ('p', )
|
||||
|
||||
def __init__(self):
|
||||
super(EdDSAPub, self).__init__()
|
||||
self.oid = None
|
||||
|
||||
def __len__(self):
|
||||
return len(self.p) + len(encoder.encode(self.oid.value)) - 1
|
||||
|
||||
def __bytearray__(self):
|
||||
_b = bytearray()
|
||||
_b += encoder.encode(self.oid.value)[1:]
|
||||
_b += self.p.to_mpibytes()
|
||||
return _b
|
||||
|
||||
def __pubkey__(self):
|
||||
return ed25519.Ed25519PublicKey.from_public_bytes(self.p.x)
|
||||
|
||||
def __copy__(self):
|
||||
pkt = super(EdDSAPub, self).__copy__()
|
||||
pkt.oid = self.oid
|
||||
return pkt
|
||||
|
||||
def verify(self, subj, sigbytes, hash_alg):
|
||||
# GnuPG requires a pre-hashing with EdDSA
|
||||
# https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-06#section-14.8
|
||||
digest = hashes.Hash(hash_alg, backend=default_backend())
|
||||
digest.update(subj)
|
||||
subj = digest.finalize()
|
||||
try:
|
||||
self.__pubkey__().verify(sigbytes, subj)
|
||||
except InvalidSignature:
|
||||
return False
|
||||
return True
|
||||
|
||||
def parse(self, packet):
|
||||
oidlen = packet[0]
|
||||
del packet[0]
|
||||
_oid = bytearray(b'\x06')
|
||||
_oid.append(oidlen)
|
||||
_oid += bytearray(packet[:oidlen])
|
||||
oid, _ = decoder.decode(bytes(_oid))
|
||||
self.oid = EllipticCurveOID(oid)
|
||||
del packet[:oidlen]
|
||||
|
||||
self.p = ECPoint(packet)
|
||||
if self.p.format != ECPointFormat.Native:
|
||||
raise PGPIncompatibleECPointFormat("Only Native format is valid for EdDSA")
|
||||
|
||||
|
||||
class ECDHPub(PubKey):
|
||||
__pubfields__ = ('p',)
|
||||
|
||||
@@ -1434,6 +1504,63 @@ class ECDSAPriv(PrivKey, ECDSAPub):
|
||||
return self.__privkey__().sign(sigdata, ec.ECDSA(hash_alg))
|
||||
|
||||
|
||||
class EdDSAPriv(PrivKey, EdDSAPub):
|
||||
__privfields__ = ('s', )
|
||||
|
||||
def __privkey__(self):
|
||||
s = self.int_to_bytes(self.s, (self.oid.key_size + 7) // 8)
|
||||
return ed25519.Ed25519PrivateKey.from_private_bytes(s)
|
||||
|
||||
def _compute_chksum(self):
|
||||
chs = sum(bytearray(self.s.to_mpibytes())) % 65536
|
||||
self.chksum = bytearray(self.int_to_bytes(chs, 2))
|
||||
|
||||
def _generate(self, oid):
|
||||
if any(c != 0 for c in self): # pragma: no cover
|
||||
raise PGPError("Key is already populated!")
|
||||
|
||||
self.oid = EllipticCurveOID(oid)
|
||||
|
||||
if self.oid != EllipticCurveOID.Ed25519:
|
||||
raise ValueError("EdDSA only supported with {}".format(EllipticCurveOID.Ed25519))
|
||||
|
||||
pk = ed25519.Ed25519PrivateKey.generate()
|
||||
x = pk.public_key().public_bytes(encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw)
|
||||
self.p = ECPoint.from_values(self.oid.key_size, ECPointFormat.Native, x)
|
||||
self.s = MPI(self.bytes_to_int(pk.private_bytes(
|
||||
encoding=serialization.Encoding.Raw,
|
||||
format=serialization.PrivateFormat.Raw,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
)))
|
||||
self._compute_chksum()
|
||||
|
||||
def parse(self, packet):
|
||||
super(EdDSAPriv, self).parse(packet)
|
||||
self.s2k.parse(packet)
|
||||
|
||||
if not self.s2k:
|
||||
self.s = MPI(packet)
|
||||
if self.s2k.usage == 0:
|
||||
self.chksum = packet[:2]
|
||||
del packet[:2]
|
||||
else:
|
||||
##TODO: this needs to be bounded to the length of the encrypted key material
|
||||
self.encbytes = packet
|
||||
|
||||
def decrypt_keyblob(self, passphrase):
|
||||
kb = super(EdDSAPriv, self).decrypt_keyblob(passphrase)
|
||||
del passphrase
|
||||
self.s = MPI(kb)
|
||||
|
||||
def sign(self, sigdata, hash_alg):
|
||||
# GnuPG requires a pre-hashing with EdDSA
|
||||
# https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-06#section-14.8
|
||||
digest = hashes.Hash(hash_alg, backend=default_backend())
|
||||
digest.update(sigdata)
|
||||
sigdata = digest.finalize()
|
||||
return self.__privkey__().sign(sigdata)
|
||||
|
||||
|
||||
class ECDHPriv(ECDSAPriv, ECDHPub):
|
||||
def __bytearray__(self):
|
||||
_b = ECDHPub.__bytearray__(self)
|
||||
|
||||
@@ -18,6 +18,7 @@ from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from .fields import DSAPriv, DSAPub, DSASignature
|
||||
from .fields import ECDSAPub, ECDSAPriv, ECDSASignature
|
||||
from .fields import ECDHPub, ECDHPriv, ECDHCipherText
|
||||
from .fields import EdDSAPub, EdDSAPriv, EdDSASignature
|
||||
from .fields import ElGCipherText, ElGPriv, ElGPub
|
||||
from .fields import OpaquePubKey
|
||||
from .fields import OpaquePrivKey
|
||||
@@ -364,7 +365,7 @@ class SignatureV4(Signature):
|
||||
PubKeyAlgorithm.RSASign: RSASignature,
|
||||
PubKeyAlgorithm.DSA: DSASignature,
|
||||
PubKeyAlgorithm.ECDSA: ECDSASignature,
|
||||
PubKeyAlgorithm.EdDSA: ECDSASignature,}
|
||||
PubKeyAlgorithm.EdDSA: EdDSASignature,}
|
||||
|
||||
self.signature = sigs.get(self.pubalg, OpaqueSignature)()
|
||||
|
||||
@@ -762,6 +763,7 @@ class PubKeyV4(PubKey):
|
||||
(True, PubKeyAlgorithm.FormerlyElGamalEncryptOrSign): ElGPub,
|
||||
(True, PubKeyAlgorithm.ECDSA): ECDSAPub,
|
||||
(True, PubKeyAlgorithm.ECDH): ECDHPub,
|
||||
(True, PubKeyAlgorithm.EdDSA): EdDSAPub,
|
||||
# False means private
|
||||
(False, PubKeyAlgorithm.RSAEncryptOrSign): RSAPriv,
|
||||
(False, PubKeyAlgorithm.RSAEncrypt): RSAPriv,
|
||||
@@ -771,6 +773,7 @@ class PubKeyV4(PubKey):
|
||||
(False, PubKeyAlgorithm.FormerlyElGamalEncryptOrSign): ElGPriv,
|
||||
(False, PubKeyAlgorithm.ECDSA): ECDSAPriv,
|
||||
(False, PubKeyAlgorithm.ECDH): ECDHPriv,
|
||||
(False, PubKeyAlgorithm.EdDSA): EdDSAPriv,
|
||||
}
|
||||
|
||||
k = (self.public, self.pkalg)
|
||||
@@ -878,7 +881,7 @@ class PrivKeyV4(PrivKey, PubKeyV4):
|
||||
for pm in self.keymaterial.__pubfields__:
|
||||
setattr(pk.keymaterial, pm, copy.copy(getattr(self.keymaterial, pm)))
|
||||
|
||||
if self.pkalg == PubKeyAlgorithm.ECDSA:
|
||||
if self.pkalg in [PubKeyAlgorithm.ECDSA, PubKeyAlgorithm.EdDSA]:
|
||||
pk.keymaterial.oid = self.keymaterial.oid
|
||||
|
||||
if self.pkalg == PubKeyAlgorithm.ECDH:
|
||||
|
||||
@@ -1324,7 +1324,7 @@ class PGPKey(Armorable, ParentRef, PGPObject):
|
||||
"""*new in 0.4.1*
|
||||
The size pertaining to this key. ``int`` for non-EC key algorithms; :py:obj:`constants.EllipticCurveOID` for EC keys.
|
||||
"""
|
||||
if self.key_algorithm in {PubKeyAlgorithm.ECDSA, PubKeyAlgorithm.ECDH}:
|
||||
if self.key_algorithm in {PubKeyAlgorithm.ECDSA, PubKeyAlgorithm.ECDH, PubKeyAlgorithm.EdDSA}:
|
||||
return self._key.keymaterial.oid
|
||||
# check if keymaterial is not an Opaque class containing a bytearray
|
||||
param = next(iter(self._key.keymaterial))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
cryptography>=2.5
|
||||
cryptography>=2.6
|
||||
enum34
|
||||
pyasn1
|
||||
six>=1.9.0
|
||||
|
||||
2
setup.py
2
setup.py
@@ -22,7 +22,7 @@ with open('README.rst') as readme:
|
||||
|
||||
|
||||
_requires = [
|
||||
'cryptography>=2.5',
|
||||
'cryptography>=2.6',
|
||||
'pyasn1',
|
||||
'six>=1.9.0',
|
||||
]
|
||||
|
||||
@@ -194,7 +194,8 @@ def userphoto():
|
||||
# TODO: add more keyspecs
|
||||
pkeyspecs = ((PubKeyAlgorithm.RSAEncryptOrSign, 1024),
|
||||
(PubKeyAlgorithm.DSA, 1024),
|
||||
(PubKeyAlgorithm.ECDSA, EllipticCurveOID.NIST_P256),)
|
||||
(PubKeyAlgorithm.ECDSA, EllipticCurveOID.NIST_P256),
|
||||
(PubKeyAlgorithm.EdDSA, EllipticCurveOID.Ed25519),)
|
||||
|
||||
|
||||
skeyspecs = ((PubKeyAlgorithm.RSAEncryptOrSign, 1024),
|
||||
@@ -202,6 +203,7 @@ skeyspecs = ((PubKeyAlgorithm.RSAEncryptOrSign, 1024),
|
||||
(PubKeyAlgorithm.ElGamal, 1024),
|
||||
(PubKeyAlgorithm.ECDSA, EllipticCurveOID.SECP256K1),
|
||||
(PubKeyAlgorithm.ECDH, EllipticCurveOID.Brainpool_P256),
|
||||
(PubKeyAlgorithm.EdDSA, EllipticCurveOID.Ed25519),
|
||||
(PubKeyAlgorithm.ECDH, EllipticCurveOID.Curve25519),)
|
||||
|
||||
|
||||
@@ -889,3 +891,16 @@ class TestPGPKey_Actions(object):
|
||||
assert emsg.is_signed
|
||||
assert emsg.is_encrypted
|
||||
assert isinstance(next(iter(emsg)), PGPSignature)
|
||||
|
||||
def test_gpg_ed25519_verify(self, abe):
|
||||
# test verification of Ed25519 signature generated by GnuPG
|
||||
pubkey, _ = PGPKey.from_file('tests/testdata/keys/ecc.2.pub.asc')
|
||||
sig = PGPSignature.from_file('tests/testdata/signatures/ecc.2.sig.asc')
|
||||
assert pubkey.verify("This is a test signature message", sig)
|
||||
|
||||
def test_gpg_cv25519_decrypt(self, abe):
|
||||
# test the decryption of X25519 generated by GnuPG
|
||||
seckey, _ = PGPKey.from_file('tests/testdata/keys/ecc.2.sec.asc')
|
||||
emsg = PGPMessage.from_file('tests/testdata/messages/message.ecdh.cv25519.asc')
|
||||
dmsg = seckey.decrypt(emsg)
|
||||
assert bytes(dmsg.message) == b"This message will have been encrypted"
|
||||
|
||||
@@ -204,6 +204,8 @@ seckm = [
|
||||
_seckeys['RSAEncryptOrSign']._key, # RSA private key packet
|
||||
_seckeys['ECDSA']._key, # ECDSA private key packet
|
||||
_seckeys['ECDSA'].subkeys['A81B93FD16BD9806']._key, # ECDH private key packet
|
||||
_seckeys['EdDSA']._key, # EdDSA private key packet
|
||||
_seckeys['EdDSA'].subkeys['AFC377493D8E897D']._key, # Curve25519 private key packet
|
||||
_mixed1._key, # RSA private key packet
|
||||
_mixed1.subkeys['B345506C90A428C5']._key, # ECDH Curve25519 private key packet
|
||||
]
|
||||
|
||||
14
tests/testdata/keys/ecc.2.pub.asc
vendored
Normal file
14
tests/testdata/keys/ecc.2.pub.asc
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mDMEXL8mcRYJKwYBBAHaRw8BAQdASFm0/fvw3kw5Vjz+vVjKq2Xhy8aX4WAcr+YF
|
||||
n72YSHy0PkN1cnZlMjU1MTkgdm9uIFRlc3RLZXkgKGVkMjU1MTkgY3YyNTUxOSkg
|
||||
PGN1cnZlMjU1MTlAdGVzdC5rZXk+iJAEExYIADgWIQR/D5etU51R90sBi9EGLmrF
|
||||
IF2HHgUCXL8mcQIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRAGLmrFIF2H
|
||||
Hq3uAQCEY5BFGUt+yKQ0nzMv18EPH/AArvctRR4efdTXPAc6JgEAz/Gp4THGptmf
|
||||
Pn5JALETkH86zNUApJgSWeF/HifRwAi4OARcvyZxEgorBgEEAZdVAQUBAQdAMzdC
|
||||
zRVztPW7BXo+eZl9J06cclRV6rcw7udWUE4Rvj0DAQgHiHgEGBYIACAWIQR/D5et
|
||||
U51R90sBi9EGLmrFIF2HHgUCXL8mcQIbDAAKCRAGLmrFIF2HHgK/AP0XT+98jOMo
|
||||
6G8qoFU7ANLfn4E3bxV0sAH9g/q/rnU7FwEAkpn5woqcil5DexELJoKruxEQ0M84
|
||||
3wriYvL2DwktCAs=
|
||||
=OgJa
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
15
tests/testdata/keys/ecc.2.sec.asc
vendored
Normal file
15
tests/testdata/keys/ecc.2.sec.asc
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
lFgEXL8mcRYJKwYBBAHaRw8BAQdASFm0/fvw3kw5Vjz+vVjKq2Xhy8aX4WAcr+YF
|
||||
n72YSHwAAQCamKiDL90+GqgU5W7Y/givu0p9aUcW8XAaKh8y8f122A/YtD5DdXJ2
|
||||
ZTI1NTE5IHZvbiBUZXN0S2V5IChlZDI1NTE5IGN2MjU1MTkpIDxjdXJ2ZTI1NTE5
|
||||
QHRlc3Qua2V5PoiQBBMWCAA4FiEEfw+XrVOdUfdLAYvRBi5qxSBdhx4FAly/JnEC
|
||||
GwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQBi5qxSBdhx6t7gEAhGOQRRlL
|
||||
fsikNJ8zL9fBDx/wAK73LUUeHn3U1zwHOiYBAM/xqeExxqbZnz5+SQCxE5B/OszV
|
||||
AKSYElnhfx4n0cAInF0EXL8mcRIKKwYBBAGXVQEFAQEHQDM3Qs0Vc7T1uwV6PnmZ
|
||||
fSdOnHJUVeq3MO7nVlBOEb49AwEIBwAA/1yFWz6KQ1cHO+dce/Moq56wvUqJxVMB
|
||||
EGDIFjyxs9IQDyqIeAQYFggAIBYhBH8Pl61TnVH3SwGL0QYuasUgXYceBQJcvyZx
|
||||
AhsMAAoJEAYuasUgXYceAr8A/RdP73yM4yjobyqgVTsA0t+fgTdvFXSwAf2D+r+u
|
||||
dTsXAQCSmfnCipyKXkN7EQsmgqu7ERDQzzjfCuJi8vYPCS0ICw==
|
||||
=siss
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
||||
9
tests/testdata/messages/message.ecdh.cv25519.asc
vendored
Normal file
9
tests/testdata/messages/message.ecdh.cv25519.asc
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
hF4Dr8N3ST2OiX0SAQdAjBvn1lUIHo/MoXG0iizRRIs3zmU+q/H2nk8iU+ksgXgw
|
||||
YxiH/gRKL/QH0ozo4mKQBy9bzY0jt0V1ldVhP4C5nh3YKLLpasHyhn/Hd+QgmKBS
|
||||
0mABoKVxFzehABvQa1+QRkiegFGTjouEjRik/MCmfyT+kusqnj1dOgY/sAzFJoEy
|
||||
7duCJ4YsXRrh1VAa+eJsdm7h9eeXRUvbn7QPA6VLnhYeRsQU8cCscHj+jvklSdzs
|
||||
rR0=
|
||||
=PzYF
|
||||
-----END PGP MESSAGE-----
|
||||
7
tests/testdata/signatures/ecc.2.sig.asc
vendored
Normal file
7
tests/testdata/signatures/ecc.2.sig.asc
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
|
||||
iHUEABYIAB0WIQR/D5etU51R90sBi9EGLmrFIF2HHgUCXMILLAAKCRAGLmrFIF2H
|
||||
HmI3AQC70mikthv4+gadsD7E8STKEPKtrkGaWTLQ6Q+U9Rc7sAD/dkUfbeRB7UvU
|
||||
COB64/0yz5kupCYt5+an6jH85yWNTgc=
|
||||
=RSff
|
||||
-----END PGP SIGNATURE-----
|
||||
Reference in New Issue
Block a user