mirror of
https://github.com/saltstack/salt.git
synced 2025-04-10 14:51:40 +00:00
Add asymmetric
execution module for low-level signature operations
This commit is contained in:
parent
cb892be59e
commit
b171fae4e2
6 changed files with 1435 additions and 0 deletions
1
changelog/66528.added.md
Normal file
1
changelog/66528.added.md
Normal file
|
@ -0,0 +1 @@
|
|||
Added an `asymmetric` execution module for signing/verifying data using raw asymmetric algorithms
|
|
@ -34,6 +34,7 @@ execution modules
|
|||
archive
|
||||
arista_pyeapi
|
||||
artifactory
|
||||
asymmetric
|
||||
at
|
||||
at_solaris
|
||||
baredoc
|
||||
|
|
6
doc/ref/modules/all/salt.modules.asymmetric.rst
Normal file
6
doc/ref/modules/all/salt.modules.asymmetric.rst
Normal file
|
@ -0,0 +1,6 @@
|
|||
salt.modules.asymmetric
|
||||
=======================
|
||||
|
||||
.. automodule:: salt.modules.asymmetric
|
||||
:members:
|
||||
:undoc-members:
|
330
salt/modules/asymmetric.py
Normal file
330
salt/modules/asymmetric.py
Normal file
|
@ -0,0 +1,330 @@
|
|||
"""
|
||||
.. versionadded:: 3008.0
|
||||
|
||||
Low-level asymmetric cryptographic operations.
|
||||
|
||||
:depends: cryptography
|
||||
|
||||
.. note::
|
||||
|
||||
All parameters that take a public key or private key can be specified either
|
||||
as a PEM/hex/base64 string or a path to a local file encoded in all supported
|
||||
formats for the type.
|
||||
|
||||
A signature can be specified as a base64 string or a path to a file with the
|
||||
raw signature or its base64 encoding.
|
||||
|
||||
Public keys and signatures can additionally be specified as a URL that can be
|
||||
retrieved using :py:func:`cp.cache_file <salt.modules.cp.cache_file>`.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import salt.utils.files
|
||||
from salt.exceptions import CommandExecutionError, SaltInvocationError
|
||||
|
||||
try:
|
||||
from salt.utils import asymmetric as asym
|
||||
from salt.utils import x509
|
||||
|
||||
HAS_CRYPTOGRAPHY = True
|
||||
except ImportError:
|
||||
HAS_CRYPTOGRAPHY = False
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
__virtualname__ = "asymmetric"
|
||||
|
||||
|
||||
def __virtual__():
|
||||
if HAS_CRYPTOGRAPHY:
|
||||
return __virtualname__
|
||||
return False, "Missing `cryptography` library"
|
||||
|
||||
|
||||
def sign(
|
||||
privkey, passphrase=None, text=None, filename=None, digest=None, raw=None, path=None
|
||||
):
|
||||
"""
|
||||
Sign a file or text using an (RSA|ECDSA|Ed25519|Ed448) private key.
|
||||
You can employ :py:func:`x509.create_private_key <salt.modules.x509_v2.create_private_key>`
|
||||
to generate one. Returns the signature encoded in base64 by default.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' asymmetric.sign /root/my_privkey.pem text='I like you'
|
||||
salt '*' asymmetric.sign /root/my_privkey.pem filename=/data/to/be/signed
|
||||
|
||||
privkey
|
||||
The private key to sign with.
|
||||
|
||||
passphrase
|
||||
If the private key is encrypted, the passphrase to decrypt it. Optional.
|
||||
|
||||
text
|
||||
Pass the text to sign. Either this or ``filename`` is required.
|
||||
|
||||
filename
|
||||
Pass the path of a file to sign. Either this or ``text`` is required.
|
||||
|
||||
digest
|
||||
The name of the hashing algorithm to use when creating signatures.
|
||||
Defaults to ``sha256``. Only relevant for ECDSA or RSA.
|
||||
|
||||
raw
|
||||
Return the raw bytes instead of encoding them to base64. Defaults to false.
|
||||
|
||||
path
|
||||
Instead of returning the data, write it to a path on the local filesystem.
|
||||
Optional.
|
||||
"""
|
||||
if text is not None:
|
||||
try:
|
||||
data = text.encode()
|
||||
except AttributeError:
|
||||
data = text
|
||||
elif filename:
|
||||
data = Path(filename)
|
||||
else:
|
||||
raise SaltInvocationError("Either `text` or `filename` is required")
|
||||
raw = raw if raw is not None else bool(path)
|
||||
sig = asym.sign(privkey, data, digest, passphrase=passphrase)
|
||||
mode = "wb"
|
||||
if not raw:
|
||||
sig = base64.b64encode(sig).decode()
|
||||
mode = "w"
|
||||
if path:
|
||||
with salt.utils.files.fopen(path, mode) as out:
|
||||
out.write(sig)
|
||||
return f"Signature written to '{path}'"
|
||||
return sig
|
||||
|
||||
|
||||
def verify(
|
||||
text=None,
|
||||
filename=None,
|
||||
pubkey=None,
|
||||
signature=None,
|
||||
digest=None,
|
||||
signed_by_any=None,
|
||||
signed_by_all=None,
|
||||
**kwargs, # pylint: disable=unused-argument
|
||||
):
|
||||
"""
|
||||
Verify signatures on a specific input against (RSA|ECDSA|Ed25519|Ed448) public keys.
|
||||
|
||||
.. note::
|
||||
|
||||
This function is supposed to be compatible with the same interface
|
||||
as :py:func:`gpg.verify <salt.modules.gpg.verify>`` regarding keyword
|
||||
arguments and return value format.
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' asymmetric.verify pubkey=/root/my_pubkey.pem text='I like you' signature=/root/ilikeyou.sig
|
||||
salt '*' asymmetric.verify pubkey=/root/my_pubkey.pem path=/root/confidential signature=/root/confidential.sig
|
||||
|
||||
text
|
||||
The text to verify. Either this or ``filename`` is required.
|
||||
|
||||
filename
|
||||
The path of a file to verify. Either this or ``text`` is required.
|
||||
|
||||
pubkey
|
||||
The single public key to verify ``signature`` against. Specify either
|
||||
this or make use of ``signed_by_any``/``signed_by_all`` for compound checks.
|
||||
|
||||
signature
|
||||
If ``pubkey`` is specified, the single signature to verify.
|
||||
If ``signed_by_any`` and/or ``signed_by_all`` is specified, this can be
|
||||
a list of multiple signatures to check against the provided keys.
|
||||
Required.
|
||||
|
||||
digest
|
||||
The name of the hashing algorithm to use when verifying signatures.
|
||||
Defaults to ``sha256``. Only relevant for ECDSA or RSA.
|
||||
|
||||
signed_by_any
|
||||
A list of pubkeys from which any valid signature will mark verification
|
||||
as passed. If none of the listed pubkeys provided a signature,
|
||||
verification fails. Works with ``signed_by_all``, but mutually
|
||||
exclusive with ``pubkey``.
|
||||
|
||||
signed_by_all
|
||||
A list of pubkeys, all of which must provide a signature for verification
|
||||
to pass. If a single one of the listed pubkeys did not provide a signature,
|
||||
verification fails. Works with ``signed_by_any``, but mutually
|
||||
exclusive with ``pubkey``.
|
||||
"""
|
||||
# Basic compatibility with gpg.verify
|
||||
ret = {"res": False, "message": "internal error"}
|
||||
|
||||
signed_by_any = signed_by_any or []
|
||||
signed_by_all = signed_by_all or []
|
||||
if text and filename:
|
||||
raise SaltInvocationError(
|
||||
"`text` and `filename` arguments are mutually exclusive"
|
||||
)
|
||||
if not signature:
|
||||
raise SaltInvocationError("Missing `signature` parameter")
|
||||
# We're constrained by compatibility with gpg.verify, so ensure the parameters
|
||||
# are as expected.
|
||||
multi_check = bool(signed_by_any or signed_by_all)
|
||||
if multi_check:
|
||||
if pubkey:
|
||||
raise SaltInvocationError(
|
||||
"Either specify pubkey + signature or signed_by_(any|all)"
|
||||
)
|
||||
if isinstance(signature, (str, bytes)):
|
||||
signature = [signature]
|
||||
if not isinstance(signed_by_any, list):
|
||||
signed_by_any = [signed_by_any]
|
||||
if not isinstance(signed_by_all, list):
|
||||
signed_by_all = [signed_by_all]
|
||||
elif not pubkey:
|
||||
raise SaltInvocationError("Missing pubkey(s) to check against")
|
||||
elif not isinstance(signature, (str, bytes)):
|
||||
raise SaltInvocationError(
|
||||
"`signature` must be a string or bytes when verifying a single signing `pubkey`"
|
||||
)
|
||||
else:
|
||||
signed_by_all = [pubkey]
|
||||
if not isinstance(signature, list):
|
||||
signature = [signature]
|
||||
|
||||
file_digest = None
|
||||
if text:
|
||||
try:
|
||||
data = text.encode()
|
||||
except AttributeError:
|
||||
data = text
|
||||
elif filename:
|
||||
data = Path(filename)
|
||||
if not data.exists():
|
||||
raise CommandExecutionError(f"Path '{filename}' does not exist")
|
||||
else:
|
||||
raise SaltInvocationError(
|
||||
"Missing data to verify. Either specify `text` or `filename`"
|
||||
)
|
||||
any_check = all_check = False
|
||||
sigs = []
|
||||
for sig in signature:
|
||||
try:
|
||||
sigs.append(_fetch(sig))
|
||||
except CommandExecutionError as err:
|
||||
if pubkey:
|
||||
return {"res": False, "message": str(err)}
|
||||
log.error(str(err), exc_info_on_loglevel=logging.DEBUG)
|
||||
if not sigs:
|
||||
raise CommandExecutionError("Unable to locate any of the provided signatures")
|
||||
if signed_by_any:
|
||||
for signer in signed_by_any:
|
||||
try:
|
||||
# Since we don't know if the signature algorithm supports
|
||||
# `prehashed` (only rsa/ec), don't calculate it early, but
|
||||
# cache it once it has been calculated. If a verification fails,
|
||||
# it throws an exception.
|
||||
_, data, file_digest = _verify_pubkey_against_list(
|
||||
signer, sigs, data, digest, file_digest=file_digest
|
||||
)
|
||||
any_check = True
|
||||
break
|
||||
except asym.InvalidSignature as err:
|
||||
log.info(str(err), exc_info_on_loglevel=logging.DEBUG)
|
||||
if err.file_digest is not None:
|
||||
file_digest = err.file_digest
|
||||
if err.data is not None:
|
||||
data = err.data
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
log.error(str(err), exc_info_on_loglevel=logging.DEBUG)
|
||||
else:
|
||||
ret["res"] = False
|
||||
ret["message"] = (
|
||||
"None of the public keys listed in signed_by_any provided a valid signature"
|
||||
)
|
||||
return ret
|
||||
|
||||
if signed_by_all:
|
||||
all_sigs = sigs.copy()
|
||||
for signer in signed_by_all:
|
||||
try:
|
||||
match, data, file_digest = _verify_pubkey_against_list(
|
||||
signer, all_sigs, data, digest, file_digest=file_digest
|
||||
)
|
||||
# Remove already associated signatures from list of possible ones
|
||||
# Since pubkeys can be specified in different ways, this fails if
|
||||
# the user passes in the same pubkey twice
|
||||
all_sigs = list(set(all_sigs).difference(match))
|
||||
continue
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
log.error(str(err), exc_info_on_loglevel=logging.DEBUG)
|
||||
ret["res"] = False
|
||||
if pubkey:
|
||||
ret["message"] = f"Failed checking signature: {err}"
|
||||
else:
|
||||
ret["message"] = f"Failed while checking `signed_by_all`: {err}"
|
||||
return ret
|
||||
all_check = True
|
||||
|
||||
if bool(signed_by_any) is any_check and bool(signed_by_all) is all_check:
|
||||
ret["res"] = True
|
||||
if pubkey:
|
||||
ret["message"] = "The signature is valid"
|
||||
else:
|
||||
ret["message"] = "All required keys have provided a signature"
|
||||
return ret
|
||||
# This should never be reached
|
||||
ret["res"] = False
|
||||
return ret
|
||||
|
||||
|
||||
def _verify_pubkey_against_list(pub, sigs, data, digest, file_digest=None):
|
||||
pubkey = _fetch(pub)
|
||||
pubkey = x509.load_pubkey(pubkey)
|
||||
match = []
|
||||
for sig in sigs:
|
||||
try:
|
||||
data, file_digest = asym.verify(
|
||||
pubkey, sig, data, digest, file_digest=file_digest
|
||||
)
|
||||
match.append(sig)
|
||||
except asym.InvalidSignature as err:
|
||||
if err.file_digest is not None:
|
||||
file_digest = err.file_digest
|
||||
if err.data is not None:
|
||||
data = err.data
|
||||
if not match:
|
||||
raise asym.InvalidSignature(
|
||||
f"Invalid signature for key {asym.fingerprint(pubkey)}",
|
||||
file_digest=file_digest,
|
||||
data=data,
|
||||
pubkey=pubkey,
|
||||
)
|
||||
return match, data, file_digest
|
||||
|
||||
|
||||
def _fetch(url):
|
||||
try:
|
||||
parsed = urlparse(url)
|
||||
except (TypeError, ValueError):
|
||||
return url
|
||||
sfn = None
|
||||
if parsed.scheme == "":
|
||||
sfn = url
|
||||
elif parsed.scheme == "file":
|
||||
sfn = parsed.path
|
||||
else:
|
||||
sfn = __salt__["cp.cache_file"](url)
|
||||
if not sfn:
|
||||
raise CommandExecutionError(f"Failed fetching '{url}'")
|
||||
if parsed.scheme != "":
|
||||
if not Path(sfn).exists():
|
||||
raise CommandExecutionError(f"Failed fetching '{url}'")
|
||||
return sfn
|
263
salt/utils/asymmetric.py
Normal file
263
salt/utils/asymmetric.py
Normal file
|
@ -0,0 +1,263 @@
|
|||
import base64
|
||||
import hashlib
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from cryptography.exceptions import InvalidSignature as CryptographyInvalidSig
|
||||
from cryptography.hazmat.primitives.asymmetric import ec, padding, utils
|
||||
|
||||
from salt.exceptions import CommandExecutionError, SaltInvocationError
|
||||
from salt.utils import x509
|
||||
from salt.utils.hashutils import get_hash
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_HASHALG = "sha256"
|
||||
|
||||
|
||||
class InvalidSignature(CommandExecutionError):
|
||||
"""
|
||||
Raised when a signature is invalid.
|
||||
We save the file hash to avoid computing it multiple times for
|
||||
signed_by_any, if a path has been passed in and the signature algorithm
|
||||
supports prehashed data.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, file_digest=None, data=None, pubkey=None):
|
||||
super().__init__(*args)
|
||||
self.file_digest = file_digest
|
||||
self.data = data
|
||||
self.pubkey = pubkey
|
||||
|
||||
|
||||
def fingerprint(pubkey):
|
||||
"""
|
||||
Return the SHA256 hexdigest of a pubkey's DER representation.
|
||||
|
||||
pub
|
||||
The public key to calculate the fingerprint of.
|
||||
Can be any reference that can be passed to ``salt.utils.x509.load_pubkey``.
|
||||
"""
|
||||
pubkey = x509.load_pubkey(pubkey)
|
||||
hsh = getattr(hashlib, DEFAULT_HASHALG)()
|
||||
hsh.update(x509.to_der(pubkey))
|
||||
return hsh.hexdigest()
|
||||
|
||||
|
||||
def sign(privkey, data, digest=None, passphrase=None):
|
||||
"""
|
||||
Sign data with a private key.
|
||||
|
||||
privkey
|
||||
The private key to sign with. Can be any reference that can be passed
|
||||
to ``salt.utils.x509.load_privkey``.
|
||||
|
||||
data
|
||||
The data to sign. Should be either ``str``, ``bytes`` or ``pathlib.Path`` object.
|
||||
|
||||
digest
|
||||
The name of the hashing algorithm to use when creating signatures.
|
||||
Defaults to ``sha256``. Only relevant for ECDSA or RSA.
|
||||
|
||||
passphrase
|
||||
If the private key is encrypted, the passphrase to decrypt it. Optional.
|
||||
"""
|
||||
privkey = x509.load_privkey(privkey, passphrase=passphrase)
|
||||
key_type = x509.get_key_type(privkey)
|
||||
prehashed = False
|
||||
if isinstance(data, Path):
|
||||
if key_type in (x509.KEY_TYPE.RSA, x509.KEY_TYPE.EC):
|
||||
data = _get_file_digest(data, digest)
|
||||
prehashed = True
|
||||
else:
|
||||
data = data.read_bytes()
|
||||
elif isinstance(data, str):
|
||||
data = data.encode()
|
||||
if key_type == x509.KEY_TYPE.RSA:
|
||||
return _sign_rsa(privkey, data, digest, prehashed=prehashed)
|
||||
if key_type == x509.KEY_TYPE.EC:
|
||||
return _sign_ec(privkey, data, digest, prehashed=prehashed)
|
||||
if key_type == x509.KEY_TYPE.ED25519:
|
||||
return _sign_ed25519(privkey, data)
|
||||
if key_type == x509.KEY_TYPE.ED448:
|
||||
return _sign_ed448(privkey, data)
|
||||
raise CommandExecutionError(f"Unknown private key type: {privkey.__class__}")
|
||||
|
||||
|
||||
def try_base64(data):
|
||||
"""
|
||||
Check if the data is valid base64 and return the decoded
|
||||
bytes if so, otherwise return the data untouched.
|
||||
"""
|
||||
try:
|
||||
if isinstance(data, str):
|
||||
data = data.encode("ascii", "strict")
|
||||
elif isinstance(data, bytes):
|
||||
pass
|
||||
else:
|
||||
raise CommandExecutionError("is_base64 only works with strings and bytes")
|
||||
decoded = base64.b64decode(data)
|
||||
if base64.b64encode(decoded) == data.replace(b"\n", b""):
|
||||
return decoded
|
||||
return data
|
||||
except (TypeError, ValueError):
|
||||
return data
|
||||
|
||||
|
||||
def load_sig(sig):
|
||||
"""
|
||||
Try to load an input that represents a signature into the signature's bytes.
|
||||
|
||||
sig
|
||||
The reference to load. Can either be a base64-encoded string or a path
|
||||
to a local file in base64 encoding or raw bytes.
|
||||
"""
|
||||
if x509.isfile(sig):
|
||||
sig = Path(sig).read_bytes()
|
||||
sig = try_base64(sig)
|
||||
if isinstance(sig, bytes):
|
||||
return sig
|
||||
raise CommandExecutionError(
|
||||
f"Failed loading signature '{sig}' as file and/or base64 string"
|
||||
)
|
||||
|
||||
|
||||
def verify(pubkey, sig, data, digest=None, file_digest=None):
|
||||
"""
|
||||
Verify a signature against a public key.
|
||||
|
||||
On success, returns a tuple of (data, file_digest), which can reused
|
||||
by the callee when multiple signatures are checked against the same file.
|
||||
This avoids reading the file/calculating the digest multiple times.
|
||||
|
||||
On failure, raises an InvalidSignature exception which carries
|
||||
``data`` and ``file_digest`` attributes with the corresponding values.
|
||||
|
||||
pub
|
||||
The public key to verify the signature against.
|
||||
Can be any reference that can be passed to ``salt.utils.x509.load_pubkey``.
|
||||
|
||||
sig
|
||||
The signature to verify.
|
||||
Can be any reference that can be passed to ``salt.utils.asymmetric.load_sig``.
|
||||
|
||||
data
|
||||
The data to sign. Should be either ``str``, ``bytes`` or ``pathlib.Path`` object.
|
||||
Ignored when ``file_digest`` is passed and the signing algorithm is either
|
||||
ECDSA or RSA.
|
||||
|
||||
digest
|
||||
The name of the hashing algorithm to use when creating signatures.
|
||||
Defaults to ``sha256``. Only relevant for ECDSA or RSA.
|
||||
|
||||
file_digest
|
||||
The ECDSA and RSA algorithms can be invoked with a precalculated digest
|
||||
in order to avoid loading the whole file into memory. This happens automatically
|
||||
during the execution of this function, but when checking multiple signatures,
|
||||
you can cache the calculated value and pass it back in.
|
||||
"""
|
||||
pubkey = x509.load_pubkey(pubkey)
|
||||
signature = load_sig(sig)
|
||||
key_type = x509.get_key_type(pubkey)
|
||||
sig_data = data
|
||||
if key_type in (x509.KEY_TYPE.RSA, x509.KEY_TYPE.EC):
|
||||
if file_digest:
|
||||
sig_data = file_digest
|
||||
elif isinstance(data, Path):
|
||||
file_digest = sig_data = _get_file_digest(data, digest)
|
||||
elif isinstance(data, str):
|
||||
sig_data = data.encode()
|
||||
elif isinstance(data, Path):
|
||||
data = sig_data = data.read_bytes()
|
||||
elif isinstance(data, str):
|
||||
data = sig_data = data.encode()
|
||||
|
||||
try:
|
||||
if key_type == x509.KEY_TYPE.RSA:
|
||||
_verify_rsa(
|
||||
pubkey,
|
||||
signature,
|
||||
sig_data,
|
||||
digest or DEFAULT_HASHALG,
|
||||
prehashed=bool(file_digest),
|
||||
)
|
||||
return data, file_digest
|
||||
if key_type == x509.KEY_TYPE.EC:
|
||||
_verify_ec(
|
||||
pubkey,
|
||||
signature,
|
||||
sig_data,
|
||||
digest or DEFAULT_HASHALG,
|
||||
prehashed=bool(file_digest),
|
||||
)
|
||||
return data, file_digest
|
||||
if key_type == x509.KEY_TYPE.ED25519:
|
||||
_verify_ed25519(pubkey, signature, sig_data)
|
||||
return data, file_digest
|
||||
if key_type == x509.KEY_TYPE.ED448:
|
||||
_verify_ed448(pubkey, signature, sig_data)
|
||||
return data, file_digest
|
||||
except CryptographyInvalidSig as err:
|
||||
raise InvalidSignature(
|
||||
f"Invalid signature for key {fingerprint(pubkey)}",
|
||||
file_digest=file_digest,
|
||||
data=data,
|
||||
pubkey=pubkey,
|
||||
) from err
|
||||
raise SaltInvocationError(f"Unknown public key type: {pubkey.__class__}")
|
||||
|
||||
|
||||
def _sign_rsa(priv, data, digest, prehashed=False):
|
||||
pad_hashalg = sig_hashalg = x509.get_hashing_algorithm(digest or DEFAULT_HASHALG)
|
||||
if prehashed:
|
||||
sig_hashalg = utils.Prehashed(sig_hashalg)
|
||||
return priv.sign(
|
||||
data,
|
||||
padding.PSS(mgf=padding.MGF1(pad_hashalg), salt_length=padding.PSS.MAX_LENGTH),
|
||||
sig_hashalg,
|
||||
)
|
||||
|
||||
|
||||
def _sign_ec(priv, data, digest, prehashed=False):
|
||||
hashalg = x509.get_hashing_algorithm(digest or DEFAULT_HASHALG)
|
||||
if prehashed:
|
||||
hashalg = utils.Prehashed(hashalg)
|
||||
return priv.sign(data, ec.ECDSA(hashalg))
|
||||
|
||||
|
||||
def _sign_ed25519(priv, data):
|
||||
return priv.sign(data)
|
||||
|
||||
|
||||
def _sign_ed448(priv, data):
|
||||
return priv.sign(data)
|
||||
|
||||
|
||||
def _verify_rsa(pub, sig, data, digest=DEFAULT_HASHALG, prehashed=False):
|
||||
pad_hashalg = sig_hashalg = x509.get_hashing_algorithm(digest)
|
||||
if prehashed:
|
||||
sig_hashalg = utils.Prehashed(sig_hashalg)
|
||||
# Technically, scheme hash function and the MGF hash function can be different,
|
||||
# but that's not common practice.
|
||||
pad = padding.PSS(mgf=padding.MGF1(pad_hashalg), salt_length=padding.PSS.AUTO)
|
||||
pub.verify(sig, data, pad, sig_hashalg)
|
||||
|
||||
|
||||
def _verify_ec(pub, sig, data, digest=DEFAULT_HASHALG, prehashed=False):
|
||||
hashalg = x509.get_hashing_algorithm(digest)
|
||||
if prehashed:
|
||||
hashalg = utils.Prehashed(hashalg)
|
||||
pub.verify(sig, data, ec.ECDSA(hashalg))
|
||||
|
||||
|
||||
def _verify_ed25519(pub, sig, data):
|
||||
pub.verify(sig, data)
|
||||
|
||||
|
||||
def _verify_ed448(pub, sig, data):
|
||||
pub.verify(sig, data)
|
||||
|
||||
|
||||
def _get_file_digest(file, digest):
|
||||
hexdigest = get_hash(file, digest or DEFAULT_HASHALG)
|
||||
return bytes.fromhex(hexdigest)
|
834
tests/pytests/functional/modules/test_asymmetric.py
Normal file
834
tests/pytests/functional/modules/test_asymmetric.py
Normal file
|
@ -0,0 +1,834 @@
|
|||
import base64
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
|
||||
import pytest
|
||||
|
||||
from salt.exceptions import CommandExecutionError, SaltInvocationError
|
||||
|
||||
util = pytest.importorskip("salt.utils.asymmetric")
|
||||
|
||||
|
||||
@pytest.fixture(params=(False,))
|
||||
def signed_data(request, tmp_path):
|
||||
data = "I am an important message"
|
||||
if request.param:
|
||||
with pytest.helpers.temp_file("signed_data", data, tmp_path) as f:
|
||||
yield str(f)
|
||||
else:
|
||||
yield data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def priv_rsa():
|
||||
return dedent(
|
||||
"""
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5f9OHbgZcXzqx
|
||||
ZfH5DSicaYCTreYBGt+gQAClufIdrl+Tu147o2/tXJQNFWjvyl+FJXaDYTj2PdZp
|
||||
fdPNWw4k7v5ndqwVdevZGD/nrjzd9eX0WXN/OPPKmnLe7pVYOO179jKEGWIgbewv
|
||||
B9VhNodvYLbGmgHSDA9ijytzhTV7gJEswN89vhi0ovVR3uyEOYDXvWCkkG5UIdJz
|
||||
cKa8DHRLd+JoaZ5E5i3VtUgqn8wVQzW8CK8FcM/GdYMmcbHf5YNaWCZ5DZlM+83h
|
||||
ggUNrnsIao0x5LjFQ9uKgbLUvJnQBRmI+nKd5xhyMtwMidTxMj5Qlr0E3jlRBI1k
|
||||
JlT+BDCbAgMBAAECggEAAR8sn74++RJxiFVH7vG4kwDeF+wugJLL8TseFtNrXhYN
|
||||
uI9e8/S6gjoMXK9OFBA1nud6l/dyk6YrM5+zu7JzW3nhOa3VP3WBbEvgKDwTTb5S
|
||||
cPGy1THrW4DrEzY7JfmjVVCMGQLwhUw2wpQMwUMga8frROz1UoHUk0gnt7snOuje
|
||||
sIZCUlXi9yyRu00upFjKpPCONO5lp6ipTCxMZeD6cLw4SNE893kmmJXv0wVIKvhA
|
||||
PSqxPGas74h/+7x4/jrVNnuai8YYIMHtQd60YVoWOLV4l3s8m12HARaOUzyMTG/m
|
||||
vBAYx+4vCyNCGb+m7S+fDzaQdlsY+qsnE3RL9q6kAQKBgQDueHZLc1JgoahxWM26
|
||||
9ZhHZKAFclOQOVMgHPAfFEpmetrIgIZk9qiHaCIyo8sGSN2PLBwwS/Gc1/FikldS
|
||||
ACcsOC8w7Zfha4n1tyWvxY1btnbim1YPUDg1ciJuVTOh8vc949pLmyjKUZBvhZTZ
|
||||
qah416upxCv7I2hdsKRE1IicmwKBgQDHIo0/CTBr3xJ535bc8I0XlB5Oed6jXUSD
|
||||
Z83rpFx+IILotu8MFiOxH4wElr1vZz8VJYWH6paYm6F82hoEdQeFMKQf4DIq1tPO
|
||||
6WSC/icipM+y3TBuXTEtqtjRFeRr7siEj8kb11r7EaPYyw3Z2jEjcvfJHdV++tte
|
||||
QOrfN6n8AQKBgACfSde6jk14PoNFMww41dPh3FUHTlaC/8eGq8249NS9n1KEm1Uq
|
||||
G5h22hf9u2rhx8o22D/8Ar5hBd02+olZPMDtyJm9FPdem3aLqsqBnnPNzxOaSigy
|
||||
EmN5T8Ov7zmN870ymgA2gG2+trzDwXar7aebEHSZ8W9vUTdlXZhcYZrfAoGBAIzM
|
||||
b1Y8pxH+fc/SOZcqNniPcAZIwRR9I65NvRl58zPyxNzKS6ceGEpqZdPwySx1sfK/
|
||||
vvRk9+obUEk45OB15sVTqRgoqxADKWvJNhownXcvVPPA1TeTiOwjOn5LnmB6Syj/
|
||||
iVC4KkoPJOxqVfbNAaVw6qY3A/duY6D3AZqmfvgBAoGAQ6OyxYh6kAkGDZi8ITkc
|
||||
8i7Eqjb9C+DMURmaycXGm/9Ft11PQDjNIkJA1tiYKacs4v7ELk/dv5swvgOHthZi
|
||||
GqcVceAozUEluAj/crlD+IuwD4ohlW0HMqtoHtgsEg02n5q52e3ubmEqJzoeFTJf
|
||||
ofJwPHKSkcvnRSH9ZQFbhqo=
|
||||
-----END PRIVATE KEY-----
|
||||
"""
|
||||
).strip()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pub_rsa():
|
||||
return dedent(
|
||||
"""
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuX/Th24GXF86sWXx+Q0o
|
||||
nGmAk63mARrfoEAApbnyHa5fk7teO6Nv7VyUDRVo78pfhSV2g2E49j3WaX3TzVsO
|
||||
JO7+Z3asFXXr2Rg/56483fXl9Flzfzjzyppy3u6VWDjte/YyhBliIG3sLwfVYTaH
|
||||
b2C2xpoB0gwPYo8rc4U1e4CRLMDfPb4YtKL1Ud7shDmA171gpJBuVCHSc3CmvAx0
|
||||
S3fiaGmeROYt1bVIKp/MFUM1vAivBXDPxnWDJnGx3+WDWlgmeQ2ZTPvN4YIFDa57
|
||||
CGqNMeS4xUPbioGy1LyZ0AUZiPpynecYcjLcDInU8TI+UJa9BN45UQSNZCZU/gQw
|
||||
mwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
"""
|
||||
).strip()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sig_rsa():
|
||||
return dedent(
|
||||
"""
|
||||
JQcgQPkK4ys7Nfipn/lZvVvXd4xSRKhgd+Z/zmwB25z9uAXLFB0nVUgIUUk+r2eP8H7LX9lWqNO3
|
||||
hNRwkEaEijnnCILW8YezgBXpmKccG7Or50Us3okp52aTvxrCb0BK7CA0h/Tg+aRxNYmM+3RAgttk
|
||||
QGSKEkEHHZ2X0DEr8BDPb2bm27ghy5HPYB7DeFb+vJBWn2gCHbIaXy3nNCahApU+UnstdB6Cbe78
|
||||
A3TmOoPSVsdekV31FYztb3RBsjnlx76/t2zZ7B9BH1HsbFkz4fSblWJR+W36vg9gpKH4Ife82NlM
|
||||
YOhJapxsaXtAfxDlwMGk7NUacj66d0EJ+NJpUg==
|
||||
"""
|
||||
).strip()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def priv_ec():
|
||||
return dedent(
|
||||
"""
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgv25d59EOFqxFC07k
|
||||
Vzlc+7wOwBjTRYxuvMrcVqEeG3WhRANCAAQAJcGq7ad0wtRL9nRf98pQYCBFR96d
|
||||
gGJ7d6vD9A05h9CmAMiM277rFXEwtFG9JgatDYlETBRVdHRJHmkRECeh
|
||||
-----END PRIVATE KEY-----
|
||||
"""
|
||||
).strip()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def priv_ec_enc():
|
||||
return dedent(
|
||||
"""
|
||||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
MIHsMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAg2tWHDgK+VvQICCAAw
|
||||
DAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEP1nD6+tv5dFqdeEPh+FYPcEgZAq
|
||||
K7gUEvPm68aprE0jLURTIzB8VlJBpwZMYtWdBH2XBOIVjUms/rOaQvw7JDy3DaOq
|
||||
w7lpslo2twZj5rkyueXQkULyhMdg9nA2kIjZckApMPvKClRZtVmt9erMrhlstKYw
|
||||
IY2Nc2sYZeohwoifb+n2vMTg/rCVCCce40KDX5jdxQR7AwAHeL8shosqM4xpvtQ=
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
||||
"""
|
||||
).strip()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pub_ec():
|
||||
return dedent(
|
||||
"""
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEACXBqu2ndMLUS/Z0X/fKUGAgRUfe
|
||||
nYBie3erw/QNOYfQpgDIjNu+6xVxMLRRvSYGrQ2JREwUVXR0SR5pERAnoQ==
|
||||
-----END PUBLIC KEY-----
|
||||
"""
|
||||
).strip()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sig_ec():
|
||||
return dedent(
|
||||
"""
|
||||
MEYCIQDwWw2fvPO/ZtE+ezTS4voFoEHmg73ehTXwOfPwIygSJAIhAKnWM9PrXxVLTKE0JTogjz8HVXKn2cTD9ozlnnHWQzbS
|
||||
"""
|
||||
).strip()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sig_ec_sha512():
|
||||
return dedent(
|
||||
"""
|
||||
MEUCIQDu+3SngDQQhxnYGUyFhiiqYfVFKSWnNWfXXW8dmFe0FgIgXo5KByO0h6q8cyz6reQ4GTseWhn6Df+1UpACCCFiGIQ=
|
||||
"""
|
||||
).strip()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def priv_ed25519():
|
||||
return dedent(
|
||||
"""
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIO6BM6CnCIbOI4WvxFYys+QYjrFZLyrQYRxTqnYZLuEm
|
||||
-----END PRIVATE KEY-----
|
||||
"""
|
||||
).strip()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pub_ed25519():
|
||||
return dedent(
|
||||
"""
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MCowBQYDK2VwAyEAC5+5Ei58lozexyPknspZWwdONLxJFgKEHwmxpFsc+4k=
|
||||
-----END PUBLIC KEY-----
|
||||
"""
|
||||
).strip()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sig_ed25519():
|
||||
return dedent(
|
||||
"""
|
||||
Lo6RH3psCiop68Ryvb8b/Hf3Hkkr2CwBDBoIvuCU22n2rOz3XMJTfQL7FNqsQBhQparsy8zfq4XFf3K2YhQvBQ==
|
||||
"""
|
||||
).strip()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def priv_ed448():
|
||||
return dedent(
|
||||
"""
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MEcCAQAwBQYDK2VxBDsEOeHr/G4AyIHMuzQRY4rqX2Z52aE7QebFN1TZZUlHpt30
|
||||
taTKZ5B3zV+HEXUtBwPXQLl5Zz1+PKfNCQ==
|
||||
-----END PRIVATE KEY-----
|
||||
"""
|
||||
).strip()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pub_ed448():
|
||||
return dedent(
|
||||
"""
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MEMwBQYDK2VxAzoAaI9rB2S21l+PWK7KznH4O1G9sjsyzMeIRGolsN4/f9B+gKOh
|
||||
E+WnN/+tA0YoN+n8GluwOlss1cOA
|
||||
-----END PUBLIC KEY-----
|
||||
"""
|
||||
).strip()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sig_ed448():
|
||||
return dedent(
|
||||
"""
|
||||
oQj3viRXHfZdH655AF3Q9j18lHecXdRQf85QKVJo0FHz6ycM/cZ3ptEuMpi3MMYJDHv7MHwcTaQA6/5lRoVMARDaqmaBCHeer+j12KOu/uCK0iuSTvLFN7XmTIC2i0pX6RRE4pVLPo85ELHfE2tovy8A
|
||||
"""
|
||||
).strip()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def asymm(modules):
|
||||
return modules.asymmetric
|
||||
|
||||
|
||||
@pytest.mark.parametrize("signed_data", (False, True), indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
"algo",
|
||||
(
|
||||
"rsa",
|
||||
"ec",
|
||||
pytest.param("ed25519", marks=pytest.mark.skip_on_fips_enabled_platform),
|
||||
pytest.param("ed448", marks=pytest.mark.skip_on_fips_enabled_platform),
|
||||
),
|
||||
)
|
||||
def test_sign(algo, signed_data, request, asymm):
|
||||
privkey = request.getfixturevalue(f"priv_{algo}")
|
||||
filename = text = None
|
||||
if signed_data.startswith("/"):
|
||||
filename = signed_data
|
||||
data = Path(filename).read_bytes()
|
||||
else:
|
||||
text = signed_data
|
||||
data = text.encode()
|
||||
res = asymm.sign(privkey, filename=filename, text=text)
|
||||
pubkey = request.getfixturevalue(f"pub_{algo}")
|
||||
util.verify(pubkey, res, data)
|
||||
|
||||
|
||||
def test_sign_encrypted_privkey(priv_ec_enc, pub_ec, signed_data, asymm):
|
||||
res = asymm.sign(priv_ec_enc, text=signed_data, passphrase="hunter1")
|
||||
util.verify(pub_ec, res, signed_data)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("algo", ("rsa", "ec"))
|
||||
def test_sign_digest(algo, signed_data, request, asymm):
|
||||
privkey = request.getfixturevalue(f"priv_{algo}")
|
||||
res = asymm.sign(privkey, text=signed_data, digest="sha512")
|
||||
pubkey = request.getfixturevalue(f"pub_{algo}")
|
||||
with pytest.raises(util.InvalidSignature):
|
||||
util.verify(pubkey, res, signed_data)
|
||||
util.verify(pubkey, res, signed_data, digest="sha512")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("raw", (False, True))
|
||||
def test_sign_raw(raw, priv_ec, signed_data, asymm):
|
||||
res = asymm.sign(priv_ec, text=signed_data, raw=raw)
|
||||
assert isinstance(res, bytes) is raw
|
||||
assert isinstance(res, str) is not raw
|
||||
|
||||
|
||||
@pytest.mark.parametrize("raw", (False, True))
|
||||
def test_sign_path(raw, priv_ec, signed_data, tmp_path, asymm):
|
||||
out = tmp_path / "out"
|
||||
res = asymm.sign(priv_ec, text=signed_data, raw=raw, path=str(out))
|
||||
assert str(out) in res
|
||||
data = out.read_bytes()
|
||||
try:
|
||||
data.decode()
|
||||
unicode = True
|
||||
except UnicodeDecodeError:
|
||||
unicode = False
|
||||
assert raw is not unicode
|
||||
|
||||
|
||||
def test_sign_bytes(priv_ec, pub_ec, signed_data, asymm):
|
||||
res = asymm.sign(priv_ec, text=signed_data.encode())
|
||||
util.verify(pub_ec, res, signed_data.encode())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("signed_data", (False, True), indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
"algo",
|
||||
(
|
||||
"rsa",
|
||||
"ec",
|
||||
pytest.param("ed25519", marks=pytest.mark.skip_on_fips_enabled_platform),
|
||||
pytest.param("ed448", marks=pytest.mark.skip_on_fips_enabled_platform),
|
||||
),
|
||||
)
|
||||
def test_verify(algo, signed_data, request, asymm):
|
||||
pubkey = request.getfixturevalue(f"pub_{algo}")
|
||||
sig = request.getfixturevalue(f"sig_{algo}")
|
||||
filename = text = None
|
||||
if signed_data.startswith("/"):
|
||||
filename = signed_data
|
||||
else:
|
||||
text = signed_data
|
||||
res = asymm.verify(pubkey=pubkey, filename=filename, text=text, signature=sig)
|
||||
assert isinstance(res, dict)
|
||||
assert res["res"] is True
|
||||
assert "is valid" in res["message"]
|
||||
|
||||
|
||||
def test_verify_pub_sig_from_url(pub_ec, sig_ec, signed_data, state_tree, asymm):
|
||||
with pytest.helpers.temp_file("pub_ec.pem", pub_ec, state_tree):
|
||||
with pytest.helpers.temp_file("sig_ec", sig_ec, state_tree):
|
||||
res = asymm.verify(
|
||||
text=signed_data, pubkey="salt://pub_ec.pem", signature="salt://sig_ec"
|
||||
)
|
||||
assert res["res"] is True
|
||||
assert "is valid" in res["message"]
|
||||
|
||||
|
||||
def test_verify_pub_sig_from_file_url(
|
||||
pub_ec, sig_ec, signed_data, state_tree, tmp_path, asymm
|
||||
):
|
||||
sig = tmp_path / "sig"
|
||||
pub = tmp_path / "pub"
|
||||
sig.write_bytes(sig_ec.encode())
|
||||
pub.write_bytes(pub_ec.encode())
|
||||
res = asymm.verify(
|
||||
text=signed_data, pubkey=f"file://{pub}", signature=f"file://{sig}"
|
||||
)
|
||||
assert res["res"] is True
|
||||
assert "is valid" in res["message"]
|
||||
|
||||
|
||||
def test_verify_pub_from_url_notfound(pub_ec, sig_ec, signed_data, state_tree, asymm):
|
||||
with pytest.helpers.temp_file("sig_ec", sig_ec, state_tree):
|
||||
res = asymm.verify(
|
||||
text=signed_data, pubkey="salt://pub_ec.pem", signature="salt://sig_ec"
|
||||
)
|
||||
assert res["res"] is False
|
||||
assert "Failed fetching" in res["message"]
|
||||
|
||||
|
||||
def test_verify_sig_from_url_notfound(pub_ec, sig_ec, signed_data, state_tree, asymm):
|
||||
with pytest.helpers.temp_file("pub_ec.pem", pub_ec, state_tree):
|
||||
res = asymm.verify(
|
||||
text=signed_data, pubkey="salt://pub_ec.pem", signature="salt://sig_ec"
|
||||
)
|
||||
assert res["res"] is False
|
||||
assert "Failed fetching" in res["message"]
|
||||
|
||||
|
||||
def test_verify_bytes(pub_ec, sig_ec, signed_data, asymm):
|
||||
sig = base64.b64decode(sig_ec)
|
||||
res = asymm.verify(text=signed_data.encode(), pubkey=pub_ec, signature=sig)
|
||||
assert res["res"] is True
|
||||
|
||||
|
||||
def test_verify_fail_wrong_data(pub_ec, sig_ec, signed_data, asymm):
|
||||
signed_data += "!"
|
||||
res = asymm.verify(pubkey=pub_ec, signature=sig_ec, text=signed_data)
|
||||
assert res["res"] is False
|
||||
assert "Invalid signature" in res["message"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"algo",
|
||||
(
|
||||
"rsa",
|
||||
"ec",
|
||||
pytest.param("ed25519", marks=pytest.mark.skip_on_fips_enabled_platform),
|
||||
pytest.param("ed448", marks=pytest.mark.skip_on_fips_enabled_platform),
|
||||
),
|
||||
)
|
||||
def test_verify_fail_wrong_pubkey(algo, signed_data, request, modules, asymm):
|
||||
sig = request.getfixturevalue(f"sig_{algo}")
|
||||
pub = modules.x509.get_public_key(modules.x509.create_private_key(algo))
|
||||
res = asymm.verify(pubkey=pub, signature=sig, text=signed_data)
|
||||
assert res["res"] is False
|
||||
assert "Invalid signature" in res["message"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("raw", (False, True))
|
||||
@pytest.mark.parametrize(
|
||||
"algo",
|
||||
(
|
||||
"rsa",
|
||||
"ec",
|
||||
pytest.param("ed25519", marks=pytest.mark.skip_on_fips_enabled_platform),
|
||||
pytest.param("ed448", marks=pytest.mark.skip_on_fips_enabled_platform),
|
||||
),
|
||||
)
|
||||
def test_verify_signature_from_file(raw, algo, signed_data, request, asymm, tmp_path):
|
||||
pubkey = request.getfixturevalue(f"pub_{algo}")
|
||||
sig = request.getfixturevalue(f"sig_{algo}")
|
||||
sig_path = tmp_path / "sig"
|
||||
if raw:
|
||||
sig_data = base64.b64decode(sig)
|
||||
else:
|
||||
sig_data = sig.encode()
|
||||
sig_path.write_bytes(sig_data)
|
||||
|
||||
res = asymm.verify(pubkey=pubkey, text=signed_data, signature=str(sig_path))
|
||||
assert isinstance(res, dict)
|
||||
assert res["res"] is True
|
||||
assert "is valid" in res["message"]
|
||||
|
||||
|
||||
def test_verify_digest(pub_ec, signed_data, sig_ec_sha512, asymm):
|
||||
res = asymm.verify(
|
||||
pubkey=pub_ec, text=signed_data, signature=sig_ec_sha512, digest="sha512"
|
||||
)
|
||||
assert isinstance(res, dict)
|
||||
assert res["res"] is True
|
||||
assert "is valid" in res["message"]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pub_ec1():
|
||||
return dedent(
|
||||
"""
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEsxSmSTxtsNiCKtHru74H7L+62nF6
|
||||
qGgyw6gGkTBkh56GCPXfhLk7yR67aypZncncWMcJSTPYSo3jSVNEfxAHhw==
|
||||
-----END PUBLIC KEY-----
|
||||
"""
|
||||
).strip()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sig_ec1():
|
||||
return dedent(
|
||||
"""
|
||||
MEYCIQCgd+u3FHFrVCFxOgiUtGWeBnB38Vf9U8DkW/A2yqZhoQIhAIFFANHzHqjoTQcCazyCx8imEmchVCPssF9m5FRSnLxD
|
||||
"""
|
||||
).strip()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pub_ec2():
|
||||
return dedent(
|
||||
"""
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAErtBZ3qL5m97SzlSwOoxFzzG/1v5a
|
||||
sLzOIrXykh4yO8tDn4h6JMOe+P0HuoUbENxk4+f/1D9hTEI88rj70bi7Ig==
|
||||
-----END PUBLIC KEY-----
|
||||
"""
|
||||
).strip()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sig_ec2():
|
||||
return dedent(
|
||||
"""
|
||||
MEUCIQDRcivGIrzfFv0bZaLpP7cG3DOucRTIcAObez12H9dpuQIgHt56uSCHJqJK8J0EHLOjeunffAyM2Vllnv6zhZPKFjA=
|
||||
"""
|
||||
).strip()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pub_ec3():
|
||||
return dedent(
|
||||
"""
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/cuKGCA0Kj8YBEmBxKtj+jg4Hpy5
|
||||
OCN5s7cYUVq3Cl/dVObv3ZbBv7ttct4tWd25f4g46cpIjDwoUXP6IRwKYg==
|
||||
-----END PUBLIC KEY-----
|
||||
"""
|
||||
).strip()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sig_ec3():
|
||||
return dedent(
|
||||
"""
|
||||
MEQCIG5IPYRuQSykJYkp3wm9K4dxI12u/raQ1VSGoMP+iEtFAiB0f6NZh7QLlB+OazGUdrgdiQo/YXeQf6zrHOAYNQ0iOg==
|
||||
"""
|
||||
).strip()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sig_notasig():
|
||||
return "chaos is a ladder"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("signed_data", (False, True), indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
"sig,by,expected",
|
||||
[
|
||||
(["ec1"], ["ec1", "ec2"], True),
|
||||
(["ec1", "ec3"], ["ec1", "ec2"], True),
|
||||
(["ec1", "ec3"], ["ec2", "ec3"], True),
|
||||
(["ec1"], ["ec2", "ec3"], False),
|
||||
(["notasig"], ["ec1", "ec2", "ec3"], False),
|
||||
(["notasig", "ec2"], ["ec1", "ec2"], True),
|
||||
pytest.param(
|
||||
["ec2", "rsa", "ed25519", "ed448"],
|
||||
["ec1", "ec2"],
|
||||
True,
|
||||
marks=pytest.mark.skip_on_fips_enabled_platform,
|
||||
),
|
||||
pytest.param(
|
||||
["ec1", "rsa", "ed25519", "ed448"],
|
||||
["ec2", "ed448"],
|
||||
True,
|
||||
marks=pytest.mark.skip_on_fips_enabled_platform,
|
||||
),
|
||||
pytest.param(
|
||||
["ec1", "rsa", "ed25519", "ed448"],
|
||||
["ec2", "rsa"],
|
||||
True,
|
||||
marks=pytest.mark.skip_on_fips_enabled_platform,
|
||||
),
|
||||
pytest.param(
|
||||
["ec1", "rsa", "ed25519", "ed448"],
|
||||
["ec2", "ed25519"],
|
||||
True,
|
||||
marks=pytest.mark.skip_on_fips_enabled_platform,
|
||||
),
|
||||
pytest.param(
|
||||
["ec1", "rsa", "ed448"],
|
||||
["ec2", "ed25519"],
|
||||
False,
|
||||
marks=pytest.mark.skip_on_fips_enabled_platform,
|
||||
),
|
||||
pytest.param(
|
||||
["ec1", "rsa", "ed25519"],
|
||||
["ec2", "ed448"],
|
||||
False,
|
||||
marks=pytest.mark.skip_on_fips_enabled_platform,
|
||||
),
|
||||
pytest.param(
|
||||
["ec1", "ed25519", "ed448"],
|
||||
["ec2", "rsa"],
|
||||
False,
|
||||
marks=pytest.mark.skip_on_fips_enabled_platform,
|
||||
),
|
||||
pytest.param(
|
||||
["ec2", "rsa", "ed25519", "ed448"],
|
||||
["ec1"],
|
||||
False,
|
||||
marks=pytest.mark.skip_on_fips_enabled_platform,
|
||||
),
|
||||
pytest.param(
|
||||
["ec", "rsa", "ed25519", "ed448"],
|
||||
["ec1", "ec2", "ec3"],
|
||||
False,
|
||||
marks=pytest.mark.skip_on_fips_enabled_platform,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_verify_signed_by_any(signed_data, sig, by, expected, request, asymm):
|
||||
sigs = [request.getfixturevalue(f"sig_{key}") for key in sig]
|
||||
keys = [request.getfixturevalue(f"pub_{key}") for key in by]
|
||||
filename = text = None
|
||||
if signed_data.startswith("/"):
|
||||
filename = signed_data
|
||||
else:
|
||||
text = signed_data
|
||||
res = asymm.verify(
|
||||
filename=filename,
|
||||
text=text,
|
||||
signature=sigs,
|
||||
signed_by_any=keys,
|
||||
)
|
||||
assert res["res"] is expected
|
||||
|
||||
|
||||
def test_verify_signed_by_any_url(
|
||||
state_tree, signed_data, sig_ec, sig_rsa, pub_rsa, pub_ec, asymm
|
||||
):
|
||||
with pytest.helpers.temp_file("pub_rsa.pem", pub_rsa, state_tree):
|
||||
with pytest.helpers.temp_file("sig_rsa", sig_rsa, state_tree):
|
||||
res = asymm.verify(
|
||||
text=signed_data,
|
||||
signature=[sig_ec, "salt://sig_rsa"],
|
||||
signed_by_any=["salt://pub_rsa.pem", pub_ec],
|
||||
)
|
||||
assert res["res"] is True
|
||||
assert "All required keys have provided a signature" in res["message"]
|
||||
|
||||
|
||||
def test_verify_signed_by_any_sig_url_fail(
|
||||
state_tree, signed_data, sig_ec, sig_rsa, pub_rsa, pub_ec, asymm
|
||||
):
|
||||
with pytest.helpers.temp_file("pub_rsa.pem", pub_rsa, state_tree):
|
||||
res = asymm.verify(
|
||||
text=signed_data,
|
||||
signature=[sig_ec, "salt://sig_rsa"],
|
||||
signed_by_any=["salt://pub_rsa.pem", pub_ec],
|
||||
)
|
||||
assert res["res"] is True
|
||||
assert "All required keys have provided a signature" in res["message"]
|
||||
|
||||
|
||||
def test_verify_signed_by_any_pub_url_fail(
|
||||
state_tree, signed_data, sig_ec, sig_rsa, pub_rsa, pub_ec, asymm
|
||||
):
|
||||
with pytest.helpers.temp_file("sig_rsa", sig_rsa, state_tree):
|
||||
res = asymm.verify(
|
||||
text=signed_data,
|
||||
signature=[sig_ec, "salt://sig_rsa"],
|
||||
signed_by_any=["salt://pub_rsa.pem", pub_ec],
|
||||
)
|
||||
assert res["res"] is True
|
||||
assert "All required keys have provided a signature" in res["message"]
|
||||
|
||||
|
||||
def test_verify_sig_url_all_fail(state_tree, signed_data, pub_rsa, pub_ec, asymm):
|
||||
with pytest.raises(CommandExecutionError, match="Unable to locate .* signatures"):
|
||||
asymm.verify(
|
||||
text=signed_data,
|
||||
signature=["salt://sig_ec", "salt://sig_rsa"],
|
||||
signed_by_any=[pub_rsa, pub_ec],
|
||||
)
|
||||
|
||||
|
||||
def test_verify_pub_url_all_fail(
|
||||
state_tree, signed_data, sig_rsa, sig_ec, asymm, caplog
|
||||
):
|
||||
with caplog.at_level(logging.ERROR):
|
||||
res = asymm.verify(
|
||||
text=signed_data,
|
||||
signature=[sig_ec, sig_rsa],
|
||||
signed_by_any=["salt://pub_ec", "salt://pub_rsa"],
|
||||
)
|
||||
assert res["res"] is False
|
||||
for src in ("pub_ec", "pub_rsa"):
|
||||
assert f"Failed fetching 'salt://{src}'" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("signed_data", (False, True), indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
"sig,by,expected",
|
||||
[
|
||||
(["ec1"], ["ec1"], True),
|
||||
(["ec1", "ec2"], ["ec3"], False),
|
||||
(["ec1", "ec2"], ["ec1", "ec2"], True),
|
||||
(["ec1", "ec2"], ["ec2", "ec1"], True),
|
||||
(["ec1", "ec2"], ["ec2", "ec1", "ec3"], False),
|
||||
(["ec1", "ec2", "notasig"], ["ec2", "ec1"], True),
|
||||
pytest.param(
|
||||
["ec1", "rsa", "ed25519", "ed448"],
|
||||
["ed448", "ec2"],
|
||||
False,
|
||||
marks=pytest.mark.skip_on_fips_enabled_platform,
|
||||
),
|
||||
pytest.param(
|
||||
["ec1", "rsa", "ed25519", "ed448"],
|
||||
["ed25519", "ec2"],
|
||||
False,
|
||||
marks=pytest.mark.skip_on_fips_enabled_platform,
|
||||
),
|
||||
pytest.param(
|
||||
["ec1", "rsa", "ed25519", "ed448"],
|
||||
["rsa", "ec2"],
|
||||
False,
|
||||
marks=pytest.mark.skip_on_fips_enabled_platform,
|
||||
),
|
||||
pytest.param(
|
||||
["ec1", "rsa", "ed25519", "ed448"],
|
||||
["ec1", "ec2"],
|
||||
False,
|
||||
marks=pytest.mark.skip_on_fips_enabled_platform,
|
||||
),
|
||||
pytest.param(
|
||||
["ec1", "rsa", "ed25519", "ed448", "ec"],
|
||||
["rsa", "ed448", "ed25519", "ec1"],
|
||||
True,
|
||||
marks=pytest.mark.skip_on_fips_enabled_platform,
|
||||
),
|
||||
pytest.param(
|
||||
["ec1", "rsa", "ed25519", "ed448", "ec"],
|
||||
["rsa", "ed448", "ed25519", "ec2"],
|
||||
False,
|
||||
marks=pytest.mark.skip_on_fips_enabled_platform,
|
||||
),
|
||||
pytest.param(
|
||||
["ec1", "rsa", "ed25519", "ed448"],
|
||||
["ed448", "ec1", "rsa", "ed25519"],
|
||||
True,
|
||||
marks=pytest.mark.skip_on_fips_enabled_platform,
|
||||
),
|
||||
pytest.param(
|
||||
["ec1", "rsa", "ed25519", "ed448"],
|
||||
["ed25519", "rsa", "ec1"],
|
||||
True,
|
||||
marks=pytest.mark.skip_on_fips_enabled_platform,
|
||||
),
|
||||
pytest.param(
|
||||
["ec1", "rsa", "ed25519", "ed448", "notasig"],
|
||||
["ed25519", "rsa", "ec1"],
|
||||
True,
|
||||
marks=pytest.mark.skip_on_fips_enabled_platform,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_verify_signed_by_all(signed_data, sig, by, expected, request, asymm):
|
||||
sigs = [request.getfixturevalue(f"sig_{key}") for key in sig]
|
||||
keys = [request.getfixturevalue(f"pub_{key}") for key in by]
|
||||
filename = text = None
|
||||
if signed_data.startswith("/"):
|
||||
filename = signed_data
|
||||
else:
|
||||
text = signed_data
|
||||
res = asymm.verify(
|
||||
filename=filename,
|
||||
text=text,
|
||||
signature=sigs,
|
||||
signed_by_all=keys,
|
||||
)
|
||||
assert res["res"] is expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("signed_data", (False, True), indirect=True)
|
||||
@pytest.mark.parametrize(
|
||||
"sig,any_by,all_by,expected",
|
||||
[
|
||||
(["ec1", "ec2"], ["ec2", "ec3"], ["ec1"], True),
|
||||
(["ec1", "ec2"], ["ec", "ec3"], ["ec1", "ec2"], False),
|
||||
(["ec1", "ec2"], ["ec1", "ec3"], ["ec2", "ec3"], False),
|
||||
(["ec1", "ec2", "ec3"], ["ec1", "ec3"], ["ec2", "ec3"], True),
|
||||
pytest.param(
|
||||
["ec1", "ec2", "ec3", "rsa", "ed25519", "ed448"],
|
||||
["ec", "rsa"],
|
||||
["ed25519", "ed448", "ec2"],
|
||||
True,
|
||||
marks=pytest.mark.skip_on_fips_enabled_platform,
|
||||
),
|
||||
pytest.param(
|
||||
["ec1", "ec2", "ec3", "rsa", "ed25519", "ed448"],
|
||||
["ec"],
|
||||
["ed25519", "ed448", "ec2", "rsa"],
|
||||
False,
|
||||
marks=pytest.mark.skip_on_fips_enabled_platform,
|
||||
),
|
||||
pytest.param(
|
||||
["ec1", "ec2", "ec3", "rsa", "ed25519", "ed448"],
|
||||
["ec", "ec2"],
|
||||
["ed25519", "ed448", "ec2", "rsa"],
|
||||
True,
|
||||
marks=pytest.mark.skip_on_fips_enabled_platform,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_verify_signed_by_any_and_all(
|
||||
signed_data, sig, any_by, all_by, expected, request, asymm
|
||||
):
|
||||
sigs = [request.getfixturevalue(f"sig_{key}") for key in sig]
|
||||
any_keys = [request.getfixturevalue(f"pub_{key}") for key in any_by]
|
||||
all_keys = [request.getfixturevalue(f"pub_{key}") for key in all_by]
|
||||
filename = text = None
|
||||
if signed_data.startswith("/"):
|
||||
filename = signed_data
|
||||
else:
|
||||
text = signed_data
|
||||
res = asymm.verify(
|
||||
filename=filename,
|
||||
text=text,
|
||||
signature=sigs,
|
||||
signed_by_any=any_keys,
|
||||
signed_by_all=all_keys,
|
||||
)
|
||||
assert res["res"] is expected
|
||||
|
||||
|
||||
def test_signed_by_any_notalist(pub_ec, sig_ec, signed_data, asymm):
|
||||
res = asymm.verify(text=signed_data, signature=sig_ec, signed_by_any=pub_ec)
|
||||
assert res["res"] is True
|
||||
|
||||
|
||||
def test_signed_by_all_notalist(pub_ec, sig_ec, signed_data, asymm):
|
||||
res = asymm.verify(text=signed_data, signature=sig_ec, signed_by_all=pub_ec)
|
||||
assert res["res"] is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"params,err",
|
||||
[
|
||||
(
|
||||
("text", "filename", "pubkey", "signature"),
|
||||
"`text` and `filename` arguments are mutually exclusive",
|
||||
),
|
||||
(
|
||||
("text", "pubkey"),
|
||||
"Missing `signature` parameter",
|
||||
),
|
||||
(
|
||||
("text", "signature"),
|
||||
r"Missing pubkey\(s\) to check against",
|
||||
),
|
||||
(
|
||||
("signature", "pubkey"),
|
||||
"Missing data to verify.*",
|
||||
),
|
||||
(
|
||||
("text", "pubkey", "signed_by_all", "signature"),
|
||||
r"Either specify pubkey \+ signature or signed_by_\(any\|all\)",
|
||||
),
|
||||
(
|
||||
("text", "pubkey", "signed_by_any", "signature"),
|
||||
r"Either specify pubkey \+ signature or signed_by_\(any\|all\)",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_verify_parameter_validation(
|
||||
params, err, signed_data, pub_ec, sig_ec, tmp_path, asymm
|
||||
):
|
||||
kwargs = {}
|
||||
if "text" in params:
|
||||
kwargs["text"] = sig_ec
|
||||
if "filename" in params:
|
||||
data = tmp_path / "data"
|
||||
data.write_text(signed_data)
|
||||
kwargs["filename"] = str(data)
|
||||
if "pubkey" in params:
|
||||
kwargs["pubkey"] = pub_ec
|
||||
if "signature" in params:
|
||||
kwargs["signature"] = sig_ec
|
||||
if "signed_by_any" in params:
|
||||
kwargs["signed_by_any"] = [pub_ec]
|
||||
if "signed_by_all" in params:
|
||||
kwargs["signed_by_all"] = [pub_ec]
|
||||
with pytest.raises(SaltInvocationError, match=err):
|
||||
asymm.verify(**kwargs)
|
||||
|
||||
|
||||
def test_verify_single_pubkey_no_signature_list(pub_ec, sig_ec, signed_data, asymm):
|
||||
with pytest.raises(
|
||||
SaltInvocationError,
|
||||
match=".*must be a string or bytes when verifying a single.*",
|
||||
):
|
||||
asymm.verify(text=signed_data, pubkey=pub_ec, signature=[sig_ec])
|
||||
|
||||
|
||||
def test_verify_missing_filename(pub_ec, sig_ec, tmp_path, asymm):
|
||||
with pytest.raises(CommandExecutionError, match="Path .*does not exist.*"):
|
||||
asymm.verify(
|
||||
filename=str(tmp_path / "missing"), pubkey=pub_ec, signature=sig_ec
|
||||
)
|
Loading…
Add table
Reference in a new issue