Added some security checks: * a check of self-signatures (doesn't work - no self-signatures are detected on primary key (_signatures is empty), BouncyCastle detects them fine) * a check of cryptoprimitives used
This commit is contained in:
@@ -6,10 +6,14 @@ import imghdr
|
||||
import os
|
||||
import time
|
||||
import zlib
|
||||
import warnings
|
||||
|
||||
from collections import namedtuple
|
||||
from enum import Enum
|
||||
from enum import IntEnum
|
||||
from enum import IntFlag
|
||||
from enum import EnumMeta
|
||||
|
||||
from pyasn1.type.univ import ObjectIdentifier
|
||||
|
||||
import six
|
||||
@@ -19,7 +23,6 @@ from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.primitives.ciphers import algorithms
|
||||
|
||||
from .decorators import classproperty
|
||||
from .types import FlagEnum
|
||||
from ._curves import BrainpoolP256R1, BrainpoolP384R1, BrainpoolP512R1, X25519, Ed25519
|
||||
|
||||
__all__ = ['Backend',
|
||||
@@ -35,19 +38,41 @@ __all__ = ['Backend',
|
||||
'SignatureType',
|
||||
'KeyServerPreferences',
|
||||
'S2KGNUExtension',
|
||||
'SecurityIssues',
|
||||
'String2KeyType',
|
||||
'TrustLevel',
|
||||
'KeyFlags',
|
||||
'Features',
|
||||
'FlagEnumMeta',
|
||||
'RevocationKeyClass',
|
||||
'NotationDataFlags',
|
||||
'TrustFlags']
|
||||
'TrustFlags',
|
||||
'check_assymetric_algo_and_its_parameters',
|
||||
'is_hash_considered_secure']
|
||||
|
||||
|
||||
# this is 50 KiB
|
||||
_hashtunedata = bytearray([10, 11, 12, 13, 14, 15, 16, 17] * 128 * 50)
|
||||
|
||||
|
||||
class FlagEnumMeta(EnumMeta):
|
||||
def __and__(self, other):
|
||||
return { f for f in iter(self) if f.value & other }
|
||||
|
||||
def __rand__(self, other): # pragma: no cover
|
||||
return self & other
|
||||
|
||||
|
||||
if six.PY2:
|
||||
class FlagEnum(IntEnum):
|
||||
__metaclass__ = FlagEnumMeta
|
||||
|
||||
else:
|
||||
namespace = FlagEnumMeta.__prepare__('FlagEnum', (IntEnum,))
|
||||
FlagEnum = FlagEnumMeta('FlagEnum', (IntEnum,), namespace)
|
||||
|
||||
|
||||
|
||||
class Backend(Enum):
|
||||
OpenSSL = openssl.backend
|
||||
|
||||
@@ -343,6 +368,10 @@ class HashAlgorithm(IntEnum):
|
||||
SHA384 = 0x09
|
||||
SHA512 = 0x0A
|
||||
SHA224 = 0x0B
|
||||
#SHA3_256 = 13
|
||||
#SHA3_384 = 14
|
||||
#SHA3_512 = 15
|
||||
|
||||
|
||||
def __init__(self, *args):
|
||||
super(self.__class__, self).__init__()
|
||||
@@ -365,6 +394,9 @@ class HashAlgorithm(IntEnum):
|
||||
return True
|
||||
|
||||
|
||||
secondPreimageResistantHashes = {HashAlgorithm.SHA1}
|
||||
collisionResistantHashses = {HashAlgorithm.SHA256, HashAlgorithm.SHA384, HashAlgorithm.SHA512}
|
||||
|
||||
class RevocationReason(IntEnum):
|
||||
"""Reasons explaining why a key or certificate was revoked."""
|
||||
#: No reason was specified. This is the default reason.
|
||||
@@ -540,3 +572,71 @@ class TrustFlags(FlagEnum):
|
||||
SubRevoked = 0x40
|
||||
Disabled = 0x80
|
||||
PendingCheck = 0x100
|
||||
|
||||
|
||||
class SecurityIssues(IntFlag):
|
||||
OK = 0
|
||||
wrongSig = (1 << 0)
|
||||
expired = (1 << 1)
|
||||
disabled = (1 << 2)
|
||||
revoked = (1 << 3)
|
||||
invalid = (1 << 4)
|
||||
brokenAssymetricFunc = (1 << 5)
|
||||
hashFunctionNotCollisionResistant = (1 << 6)
|
||||
hashFunctionNotSecondPreimageResistant = (1 << 7)
|
||||
assymetricKeyLengthIsTooShort = (1 << 8)
|
||||
insecureCurve = (1 << 9)
|
||||
noSelfSignature = (1 << 10)
|
||||
|
||||
# https://safecurves.cr.yp.to/
|
||||
safeCurves = {
|
||||
EllipticCurveOID.Curve25519,
|
||||
EllipticCurveOID.Ed25519,
|
||||
}
|
||||
|
||||
minimumAssymetricKeyLegths = {
|
||||
PubKeyAlgorithm.RSAEncryptOrSign: 2048,
|
||||
PubKeyAlgorithm.RSASign: 2048,
|
||||
PubKeyAlgorithm.ElGamal: 2048,
|
||||
PubKeyAlgorithm.DSA: 2048,
|
||||
|
||||
PubKeyAlgorithm.ECDSA: safeCurves,
|
||||
PubKeyAlgorithm.EdDSA: safeCurves,
|
||||
PubKeyAlgorithm.ECDH: safeCurves,
|
||||
}
|
||||
|
||||
|
||||
|
||||
def is_hash_considered_secure(hash):
|
||||
if hash in collisionResistantHashses:
|
||||
return SecurityIssues.OK
|
||||
|
||||
warnings.warn("Hash function " + repr(hash) + " is not considered collision resistant")
|
||||
issues = SecurityIssues.hashFunctionNotCollisionResistant
|
||||
|
||||
if hash not in secondPreimageResistantHashes:
|
||||
issues |= hashFunctionNotSecondPreimageResistant
|
||||
|
||||
return issues
|
||||
|
||||
def check_assymetric_algo_and_its_parameters(algo, size):
|
||||
if algo in minimumAssymetricKeyLegths:
|
||||
minLOrSetOfSecureCurves = minimumAssymetricKeyLegths[algo]
|
||||
if isinstance(minLOrSetOfSecureCurves, set): # ECC
|
||||
curve = size
|
||||
safeCurvesForThisAlg = minLOrSetOfSecureCurves
|
||||
if curve in safeCurvesForThisAlg:
|
||||
return SecurityIssues.OK
|
||||
else:
|
||||
warnings.warn("Curve " + repr(curve) + " is not considered secure for " + repr(algo))
|
||||
return SecurityIssues.insecureCurve
|
||||
else:
|
||||
minL = minLOrSetOfSecureCurves
|
||||
if size < minL:
|
||||
warnings.warn("Assymetric algo " + repr(algo) + " needs key at least of " + repr(minL) + " bits effective length to be considered secure")
|
||||
return SecurityIssues.assymetricKeyLengthIsTooShort
|
||||
else:
|
||||
return SecurityIssues.OK
|
||||
else:
|
||||
warnings.warn("Assymetric algo " + repr(algo) + " is not considered secure")
|
||||
return SecurityIssues.brokenAssymetricFunc
|
||||
|
||||
88
pgpy/pgp.py
88
pgpy/pgp.py
@@ -36,6 +36,9 @@ from .constants import RevocationKeyClass
|
||||
from .constants import RevocationReason
|
||||
from .constants import SignatureType
|
||||
from .constants import SymmetricKeyAlgorithm
|
||||
from .constants import SecurityIssues
|
||||
from .constants import check_assymetric_algo_and_its_parameters
|
||||
from .constants import is_hash_considered_secure
|
||||
|
||||
from .decorators import KeyAction
|
||||
|
||||
@@ -171,6 +174,13 @@ class PGPSignature(Armorable, ParentRef, PGPObject):
|
||||
The :py:obj:`~constants.HashAlgorithm` used when computing this signature.
|
||||
"""
|
||||
return self._signature.halg
|
||||
|
||||
|
||||
def check_primitives(self):
|
||||
return is_hash_considered_secure(self.hash_algorithm)
|
||||
|
||||
def check_soundness(self):
|
||||
return self.check_primitives()
|
||||
|
||||
@property
|
||||
def is_expired(self):
|
||||
@@ -1627,6 +1637,7 @@ class PGPKey(Armorable, ParentRef, PGPObject):
|
||||
self._signatures = SorteDeque()
|
||||
self._uids = SorteDeque()
|
||||
self._sibling = None
|
||||
self._self_verified = None
|
||||
self._require_usage_flags = True
|
||||
|
||||
def __bytearray__(self):
|
||||
@@ -2350,6 +2361,60 @@ class PGPKey(Armorable, ParentRef, PGPObject):
|
||||
|
||||
return self._sign(key, sig, **prefs)
|
||||
|
||||
def is_considered_insecure(self, self_verifying=False):
|
||||
res = self.check_soundness(self_verifying=self_verifying)
|
||||
|
||||
for sk in self.subkeys.values():
|
||||
res |= sk.check_soundness(self_verifying=self_verifying)
|
||||
return res
|
||||
|
||||
def self_verify(self):
|
||||
selfSigs = list(self.self_signatures)
|
||||
res = SecurityIssues.OK
|
||||
if selfSigs:
|
||||
for s in selfSigs:
|
||||
if not self.verify(self, s):
|
||||
res |= SecurityIssues.invalid
|
||||
break
|
||||
else:
|
||||
return SecurityIssues.noSelfSignature
|
||||
return res
|
||||
|
||||
def _do_self_signatures_verification(self):
|
||||
try:
|
||||
self._self_verified = SecurityIssues.OK
|
||||
self._self_verified = self.self_verify()
|
||||
except:
|
||||
self._self_verified = None
|
||||
raise
|
||||
|
||||
@property
|
||||
def self_verified(self):
|
||||
warnings.warn("TODO: Self-sigs verification is not yet working because self-sigs are not parsed!!!")
|
||||
return SecurityIssues.OK
|
||||
|
||||
if self._self_verified is None:
|
||||
self._do_self_signatures_verification()
|
||||
|
||||
return self._self_verified
|
||||
|
||||
def check_primitives(self):
|
||||
return check_assymetric_algo_and_its_parameters(self.key_algorithm, self.key_size)
|
||||
|
||||
def check_management(self, self_verifying=False):
|
||||
res = self.self_verified
|
||||
if self.is_expired:
|
||||
warnings.warn("Key " + repr(self) + " has expired at " + str(self.expires_at))
|
||||
res |= SecurityIssues.expired
|
||||
|
||||
warnings.warn("TODO: Revocation checks are not yet implemented!!!")
|
||||
warnings.warn("TODO: Flags (s.a. `disabled`) checks are not yet implemented!!!")
|
||||
res |= int(bool(list(self.revocation_signatures))) * SecurityIssues.revoked
|
||||
return res
|
||||
|
||||
def check_soundness(self, self_verifying=False):
|
||||
return self.check_management(self_verifying) | self.check_primitives()
|
||||
|
||||
def verify(self, subject, signature=None):
|
||||
"""
|
||||
Verify a subject with a signature using this key.
|
||||
@@ -2412,11 +2477,26 @@ class PGPKey(Armorable, ParentRef, PGPObject):
|
||||
sigv &= self.subkeys[sig.signer].verify(subj, sig)
|
||||
|
||||
else:
|
||||
verified = self._key.verify(sig.hashdata(subj), sig.__sig__, getattr(hashes, sig.hash_algorithm.name)())
|
||||
if verified is NotImplemented:
|
||||
raise NotImplementedError(sig.key_algorithm)
|
||||
if isinstance(subj, PGPKey):
|
||||
self_verifying = signerFp == subj.fingerprint
|
||||
else:
|
||||
self_verifying = False
|
||||
|
||||
subkey_issues = self.check_soundness(self_verifying)
|
||||
signature_issues = self.check_primitives()
|
||||
|
||||
if self_verifying:
|
||||
signature_issues &= ~SecurityIssues.hashFunctionNotCollisionResistant
|
||||
|
||||
issues = signature_issues | subkey_issues
|
||||
if issues:
|
||||
sigv.add_sigsubj(sig, self, subj, issues)
|
||||
else:
|
||||
verified = self._key.verify(sig.hashdata(subj), sig.__sig__, getattr(hashes, sig.hash_algorithm.name)())
|
||||
if verified is NotImplemented:
|
||||
raise NotImplementedError(sig.key_algorithm)
|
||||
|
||||
sigv.add_sigsubj(sig, self, subj, verified)
|
||||
sigv.add_sigsubj(sig, self, subj, SecurityIssues.wrongSig if not verified else SecurityIssues.OK)
|
||||
|
||||
return sigv
|
||||
|
||||
|
||||
2879
pgpy/pgp1.py
Normal file
2879
pgpy/pgp1.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,7 @@ import six
|
||||
from .decorators import sdproperty
|
||||
|
||||
from .errors import PGPError
|
||||
from .constants import SecurityIssues
|
||||
|
||||
__all__ = ['Armorable',
|
||||
'ParentRef',
|
||||
@@ -31,7 +32,6 @@ __all__ = ['Armorable',
|
||||
'MetaDispatchable',
|
||||
'Dispatchable',
|
||||
'SignatureVerification',
|
||||
'FlagEnumMeta',
|
||||
'FlagEnum',
|
||||
'Fingerprint',
|
||||
'SorteDeque']
|
||||
@@ -578,7 +578,8 @@ class Dispatchable(six.with_metaclass(MetaDispatchable, PGPObject)):
|
||||
|
||||
|
||||
class SignatureVerification(object):
|
||||
_sigsubj = collections.namedtuple('sigsubj', ['verified', 'by', 'signature', 'subject'])
|
||||
__slots__ = ("_subjects",)
|
||||
_sigsubj = collections.namedtuple('sigsubj', ['issues', 'by', 'signature', 'subject'])
|
||||
|
||||
@property
|
||||
def good_signatures(self):
|
||||
@@ -586,7 +587,7 @@ class SignatureVerification(object):
|
||||
A generator yielding namedtuples of all signatures that were successfully verified
|
||||
in the operation that returned this instance. The namedtuple has the following attributes:
|
||||
|
||||
``sigsubj.verified`` - ``bool`` of whether the signature verified successfully or not.
|
||||
``sigsubj.issues`` - ``SecurityIssues`` of whether the signature verified successfully or not. Must be 0 for success.
|
||||
|
||||
``sigsubj.by`` - the :py:obj:`~pgpy.PGPKey` that was used in this verify operation.
|
||||
|
||||
@@ -594,7 +595,7 @@ class SignatureVerification(object):
|
||||
|
||||
``sigsubj.subject`` - the subject that was verified using the signature.
|
||||
"""
|
||||
for s in [ i for i in self._subjects if i.verified ]:
|
||||
for s in [ i for i in self._subjects if not i.issues ]:
|
||||
yield s
|
||||
|
||||
@property
|
||||
@@ -611,7 +612,7 @@ class SignatureVerification(object):
|
||||
|
||||
``sigsubj.subject`` - the subject that was verified using the signature.
|
||||
"""
|
||||
for s in [ i for i in self._subjects if not i.verified ]:
|
||||
for s in [ i for i in self._subjects if i.issues ]:
|
||||
yield s
|
||||
|
||||
def __init__(self):
|
||||
@@ -630,7 +631,7 @@ class SignatureVerification(object):
|
||||
return len(self._subjects)
|
||||
|
||||
def __bool__(self):
|
||||
return all(s.verified for s in self._subjects)
|
||||
return all(not s.issues for s in self._subjects)
|
||||
|
||||
def __nonzero__(self):
|
||||
return self.__bool__()
|
||||
@@ -643,27 +644,10 @@ class SignatureVerification(object):
|
||||
return self
|
||||
|
||||
def __repr__(self):
|
||||
return "<SignatureVerification({verified})>".format(verified=str(bool(self)))
|
||||
return "<"+ self.__class__.__name__ + "({" + str(bool(self)) + "})>"
|
||||
|
||||
def add_sigsubj(self, signature, by, subject=None, verified=False):
|
||||
self._subjects.append(self._sigsubj(verified, by, signature, subject))
|
||||
|
||||
|
||||
class FlagEnumMeta(EnumMeta):
|
||||
def __and__(self, other):
|
||||
return { f for f in iter(self) if f.value & other }
|
||||
|
||||
def __rand__(self, other): # pragma: no cover
|
||||
return self & other
|
||||
|
||||
|
||||
if six.PY2:
|
||||
class FlagEnum(IntEnum):
|
||||
__metaclass__ = FlagEnumMeta
|
||||
|
||||
else:
|
||||
namespace = FlagEnumMeta.__prepare__('FlagEnum', (IntEnum,))
|
||||
FlagEnum = FlagEnumMeta('FlagEnum', (IntEnum,), namespace)
|
||||
def add_sigsubj(self, signature, by, subject=None, issues=SecurityIssues(0xFF)):
|
||||
self._subjects.append(self._sigsubj(issues, by, signature, subject))
|
||||
|
||||
|
||||
class Fingerprint(str):
|
||||
|
||||
Reference in New Issue
Block a user