Skip to content

Commit

Permalink
Signature validation for file now working
Browse files Browse the repository at this point in the history
  • Loading branch information
scheibling committed Nov 10, 2023
1 parent d4a6bbd commit fc2d5dc
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 51 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,6 @@ core
/id_dsa*
/id_ed25519*
hello.txt
hello.txt.sig
hello.txt.sig

/testkeys
41 changes: 0 additions & 41 deletions decode_signature.py

This file was deleted.

9 changes: 7 additions & 2 deletions make_signature.sh
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
#!/bin/bash
ssh-keygen -t rsa -b 4096 -f id_rsa -N ''
echo "Hello World" > hello.txt
ssh-keygen -Y sign -n hello@world -f id_rsa hello.txt
ssh-keygen -t ecdsa -f id_ecdsa -N ''
ssh-keygen -t ed25519 -f id_ed25519 -N ''
echo "Hello World" | tee rsa.txt | tee ecdsa.txt | tee ed25519.txt

ssh-keygen -Y sign -n hello@world -f id_rsa rsa.txt
ssh-keygen -Y sign -n hello@world -f id_ecdsa ecdsa.txt
ssh-keygen -Y sign -n hello@world -f id_ed25519 ed25519.txt
31 changes: 28 additions & 3 deletions src/sshkey_tools/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -1712,7 +1712,7 @@ def sign(self, data: bytes, **kwargs) -> None:
Args:
data (bytes): The data to be signed
hash_alg (RsaAlgs, optional): The RSA algorithm to use for hashing.
Defaults to RsaAlgs.SHA256.
Defaults to RsaAlgs.SHA256.
"""
self.value = self.private_key.sign(data)
self.is_signed = True
Expand Down Expand Up @@ -1759,8 +1759,23 @@ def __validate_value__(self) -> Union[bool, Exception]:
class SignatureVersionField(Integer32Field):
DATA_TYPE = int
DEFAULT = 1

def __validate_value__(self) -> Union[bool, Exception]:
"""
Validates the contents of the field
"""
if self.value != 1:
return _EX.InvalidCertificateFieldException(
"The certificate version is invalid"
)

return True

class SignatureNamespaceField(StringField):
DATA_TYPE = (str, bytes)
DEFAULT = ""

class SignatureNamespace(StringField):
class SignatureNamespaceField(StringField):
DATA_TYPE = (str, bytes)
DEFAULT = ""

Expand All @@ -1769,7 +1784,17 @@ def __validate_value__(self) -> Union[bool, Exception]:
return _EX.InvalidFieldDataException(
f"{self.get_name()} must be a non-empty string"
)

return True

class SignatureHashAlgorithmField(StringField):
DATA_TYPE = (str, bytes)
DEFAULT = ""
DEFAULT = "sha512"

def __validate_value__(self) -> Union[bool, Exception]:
if self.value not in ("sha256", "sha512"):
return _EX.InvalidFieldDataException(
f"{self.get_name()} must be one of 'sha256' or 'sha512'"
)

return True
116 changes: 112 additions & 4 deletions src/sshkey_tools/signatures.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
# string reserved
# string hash_algorithm
# string signature

from base64 import b64decode
from .cert import Fieldset, dataclass, Union
from prettytable import PrettyTable
from .utils import concat_to_bytestring, concat_to_string, ensure_bytestring
from . import (
fields as _FIELD,
keys as _KEY,
exceptions as _EX
exceptions as _EX,
utils as _U
)

@dataclass
Expand All @@ -33,11 +36,19 @@ class SignatureFieldset(Fieldset):
sig_version: _FIELD.SignatureVersionField = _FIELD.SignatureVersionField.factory
public_key: _FIELD.PublicKeyField = _FIELD.PublicKeyField.factory

namespace: _FIELD.StringField = _FIELD.StringField.factory
namespace: _FIELD.SignatureNamespaceField = _FIELD.StringField.factory
reserved: _FIELD.ReservedField = _FIELD.ReservedField.factory
hash_algorithm: _FIELD.SignatureHashAlgorithmField = _FIELD.SignatureHashAlgorithmField.factory
signature: _FIELD.SignatureField = _FIELD.SignatureField.factory

def __bytes__(self):
return concat_to_bytestring(
bytes(self.magic_preamble),
bytes(self.namespace),
bytes(self.reserved),
bytes(self.hash_algorithm)
)

class SSHSignature:
"""
General class for SSH Signatures, used for loading and parsing.
Expand All @@ -53,6 +64,47 @@ def __init__(
"signature", _FIELD.SignatureField.from_object(signer_privkey)
)

@classmethod
def from_file(cls, path: str, encoding: str = 'none') -> "SSHSignature":
"""
Loads an existing SSH Signature from a file
Args:
path (str): The path to the file
encoding (str, optional): The encoding of the file. None will load the byte content directly. Defaults to 'utf-8'.
Returns:
SSHSignature: SSH Signature Object
"""
with open(path, 'rb' if encoding == 'none' else 'r') as f:
data = f.read()

return cls.from_string(data, encoding if encoding != 'none' else None)

@classmethod
def from_string(cls, data: Union[str, bytes], encoding: str = 'utf-8') -> "SSHSignature":
"""
Loads an existing SSH Signature from file contents/string
Args:
data (str): The normalized string data from the .sig-file
Returns:
SSHSignature: The parsed SSH Signature
"""
if isinstance(data, str):
data = data.encode(encoding)

if b'BEGIN SSH SIGNATURE' in data:
data = data.replace(b'-----BEGIN SSH SIGNATURE-----\n', b'')

if b'END SSH SIGNATURE' in data:
data = data.replace(b'-----END SSH SIGNATURE-----', b'')

data = data.strip(b"\n \t")

return cls.decode(b64decode(data))

@classmethod
def decode(cls, data: bytes) -> "SSHSignature":
"""
Expand All @@ -65,4 +117,60 @@ def decode(cls, data: bytes) -> "SSHSignature":
SSHSignature: The parsed SSH Signature
"""
sig_fields, data = SignatureFieldset.decode(data)
return cls(fields=sig_fields)
return cls(fields=sig_fields)

def get_signable(self, data: Union[str, bytes]) -> bytes:
"""
Returns the signable data for the signature or verification
Returns:
bytes: The signable data
"""
hash = b""
if self.fields.hash_algorithm.value == "sha256":
hash = _U.sha256_hash(ensure_bytestring(data))
elif self.fields.hash_algorithm.value == "sha512":
hash = _U.sha512_hash(ensure_bytestring(data))
else:
raise _EX.InvalidHashAlgorithmException(
f"Unknown hash algorithm {self.fields.hash_algorithm}"
)

return bytes(self.fields) + _FIELD.StringField.encode(hash)

def __str__(self) -> str:
table = PrettyTable(["Field", "Value"])

for item in (self.header, self.fields, self.footer):
for row in item.__table__():
table.add_row(row)

return str(table)

def get(self, field: str):
if field in self.fields.getattrs():
return self.fields.get(field, None)

raise _EX.InvalidCertificateFieldException(f"Unknown field {field}")

def set(self, field: str, data):
if field in self.fields.getattrs():
self.fields.set(field, data)

raise _EX.InvalidCertificateFieldException(f"Unknown field {field}")

def verify(
self, data, public_key: _KEY.PublicKey = None, raise_on_error: bool = False
) -> bool:
if not public_key:
public_key = self.get('public_key').value

public_key.verify(
self.get_signable(data),
self.fields.signature.value
)



print()

23 changes: 23 additions & 0 deletions src/sshkey_tools/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,29 @@ def md5_fingerprint(data: bytes, prefix: bool = True) -> str:
a + b for a, b in zip(digest[::2], digest[1::2])
)

def sha256_hash(data: bytes) -> str:
"""
Returns a SHA256 hash of the given data.
Args:
data (bytes): The data to hash
Returns:
str: The hash
"""
return hl.sha256(data).digest()

def sha512_hash(data: bytes) -> str:
"""
Returns a SHA512 hash of the given data.
Args:
data (bytes): The data to hash
Returns:
str: The hash
"""
return hl.sha512(data).digest()

def sha256_fingerprint(data: bytes, prefix: bool = True) -> str:
"""
Expand Down
41 changes: 41 additions & 0 deletions validate_signatures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from src.sshkey_tools import (
fields as _F,
keys as _K,
exceptions as _E,
signatures as _S
)


# Validate files created with ssh-keygen (WORKS!)
rsa_pub = _K.PublicKey.from_file('testkeys/id_rsa.pub')
ecdsa_pub = _K.PublicKey.from_file('testkeys/id_ecdsa.pub')
ed25519_pub = _K.PublicKey.from_file('testkeys/id_ed25519.pub')

rsa_sign = _S.SSHSignature.from_file('testkeys/rsa.txt.sig')
ecdsa_sign = _S.SSHSignature.from_file('testkeys/ecdsa.txt.sig')
ed25519_sign = _S.SSHSignature.from_file('testkeys/ed25519.txt.sig')

rsa_data = open('rsa.txt', 'rb').read()
ecdsa_data = open('ecdsa.txt', 'rb').read()
ed25519_data = open('ed25519.txt', 'rb').read()

rsa_signable = rsa_sign.get_signable(rsa_data)
ecdsa_signable = ecdsa_sign.get_signable(ecdsa_data)
ed25519_signable = ed25519_sign.get_signable(ed25519_data)

try:
rsa_pub.verify(rsa_signable, rsa_sign.fields.signature.value)
except:
print("RSA validation failed")

try:
ecdsa_pub.verify(ecdsa_signable, ecdsa_sign.fields.signature.value)
except:
print("ECDSA validation failed")

try:
ed25519_pub.verify(ed25519_signable, ed25519_sign.fields.signature.value)
except:
print("Ed25519 validation failed")

print()

0 comments on commit fc2d5dc

Please sign in to comment.