imgtool: initial sanity test

An initial sanity test for imgtool is added, checks
different commands for key operations (keygen, getpriv,
getpub and getpubhash).

Also very basic test for sign / verify is added.

Some tests are disabled (marked as 'xfail') due to
the missing implementation.

Signed-off-by: Denis Mingulov <denis@mingulov.com>
diff --git a/scripts/tests/test_keys.py b/scripts/tests/test_keys.py
new file mode 100644
index 0000000..a812ba2
--- /dev/null
+++ b/scripts/tests/test_keys.py
@@ -0,0 +1,253 @@
+# SPDX-License-Identifier: Apache-2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+import subprocess
+from click.testing import CliRunner
+from imgtool import main as imgtool_main
+from imgtool.main import imgtool
+
+# all supported key types for 'keygen'
+KEY_TYPES = [*imgtool_main.keygens]
+KEY_ENCODINGS = [*imgtool_main.valid_encodings]
+PUB_HASH_ENCODINGS = [*imgtool_main.valid_hash_encodings]
+PVT_KEY_FORMATS = [*imgtool_main.valid_formats]
+
+OPENSSL_KEY_TYPES = {
+    "rsa-2048": "Private-Key: (2048 bit, 2 primes)",
+    "rsa-3072": "Private-Key: (3072 bit, 2 primes)",
+    "ecdsa-p256": "Private-Key: (256 bit)",
+    "ecdsa-p384": "Private-Key: (384 bit)",
+    "ed25519": "ED25519 Private-Key:",
+    "x25519": "X25519 Private-Key:",
+}
+
+GEN_KEY_EXT = ".key"
+GEN_ANOTHER_KEY_EXT = ".another.key"
+PUB_KEY_EXT = ".pub"
+PUB_KEY_HASH_EXT = ".pubhash"
+
+
+def tmp_name(tmp_path, key_type, suffix=""):
+    return tmp_path / (key_type + suffix)
+
+
+@pytest.fixture(scope="session")
+def tmp_path_persistent(tmp_path_factory):
+    return tmp_path_factory.mktemp("keys")
+
+
+@pytest.mark.parametrize("key_type", KEY_TYPES)
+def test_keygen(key_type, tmp_path_persistent):
+    """Generate keys by imgtool"""
+
+    runner = CliRunner()
+
+    gen_key = tmp_name(tmp_path_persistent, key_type, GEN_KEY_EXT)
+
+    assert not gen_key.exists()
+    result = runner.invoke(
+        imgtool, ["keygen", "--key", str(gen_key), "--type", key_type]
+    )
+    assert result.exit_code == 0
+    assert gen_key.exists()
+    assert gen_key.stat().st_size > 0
+
+    # another key
+    gen_key2 = tmp_name(tmp_path_persistent, key_type, GEN_ANOTHER_KEY_EXT)
+
+    assert str(gen_key2) != str(gen_key)
+
+    assert not gen_key2.exists()
+    result = runner.invoke(
+        imgtool, ["keygen", "--key", str(gen_key2), "--type", key_type]
+    )
+    assert result.exit_code == 0
+    assert gen_key2.exists()
+    assert gen_key2.stat().st_size > 0
+
+    # content must be different
+    assert gen_key.read_bytes() != gen_key2.read_bytes()
+
+
+@pytest.mark.parametrize("key_type", KEY_TYPES)
+def test_keygen_type(key_type, tmp_path_persistent):
+    """Check generated keys"""
+    assert key_type in OPENSSL_KEY_TYPES
+
+    gen_key = tmp_name(tmp_path_persistent, key_type, GEN_KEY_EXT)
+
+    result = subprocess.run(
+        ["openssl", "pkey", "-in", str(gen_key), "-check", "-noout", "-text"],
+        capture_output=True,
+        text=True,
+    )
+    assert result.returncode == 0
+    assert "Key is valid" in result.stdout
+    assert OPENSSL_KEY_TYPES[key_type] in result.stdout
+
+
+@pytest.mark.parametrize("key_type", KEY_TYPES)
+@pytest.mark.parametrize("format", PVT_KEY_FORMATS)
+def test_getpriv(key_type, format, tmp_path_persistent):
+    """Get private key"""
+    runner = CliRunner()
+
+    gen_key = tmp_name(tmp_path_persistent, key_type, GEN_KEY_EXT)
+
+    result = runner.invoke(
+        imgtool,
+        [
+            "getpriv",
+            "--key",
+            str(gen_key),
+            "--format",
+            format,
+        ],
+    )
+    assert result.exit_code == 0
+
+
+@pytest.mark.parametrize("key_type", KEY_TYPES)
+@pytest.mark.parametrize("encoding", KEY_ENCODINGS)
+def test_getpub(key_type, encoding, tmp_path_persistent):
+    """Get public key"""
+    runner = CliRunner()
+
+    gen_key = tmp_name(tmp_path_persistent, key_type, GEN_KEY_EXT)
+    pub_key = tmp_name(tmp_path_persistent, key_type, PUB_KEY_EXT
+                       + "." + encoding)
+
+    assert not pub_key.exists()
+    result = runner.invoke(
+        imgtool,
+        [
+            "getpub",
+            "--key",
+            str(gen_key),
+            "--output",
+            str(pub_key),
+            "--encoding",
+            encoding,
+        ],
+    )
+    assert result.exit_code == 0
+    assert pub_key.exists()
+    assert pub_key.stat().st_size > 0
+
+
+@pytest.mark.parametrize("key_type", KEY_TYPES)
+@pytest.mark.parametrize("encoding", PUB_HASH_ENCODINGS)
+def test_getpubhash(key_type, encoding, tmp_path_persistent):
+    """Get the hash of the public key"""
+    runner = CliRunner()
+
+    gen_key = tmp_name(tmp_path_persistent, key_type, GEN_KEY_EXT)
+    pub_key_hash = tmp_name(
+        tmp_path_persistent, key_type, PUB_KEY_HASH_EXT + "." + encoding
+    )
+
+    assert not pub_key_hash.exists()
+    result = runner.invoke(
+        imgtool,
+        [
+            "getpubhash",
+            "--key",
+            str(gen_key),
+            "--output",
+            str(pub_key_hash),
+            "--encoding",
+            encoding,
+        ],
+    )
+    assert result.exit_code == 0
+    assert pub_key_hash.exists()
+    assert pub_key_hash.stat().st_size > 0
+
+
+@pytest.mark.parametrize("key_type", KEY_TYPES)
+def test_sign_verify(key_type, tmp_path_persistent):
+    """Test basic sign and verify"""
+    runner = CliRunner()
+
+    gen_key = tmp_name(tmp_path_persistent, key_type, GEN_KEY_EXT)
+    wrong_key = tmp_name(tmp_path_persistent, key_type, GEN_ANOTHER_KEY_EXT)
+    image = tmp_name(tmp_path_persistent, "image", "bin")
+    image_signed = tmp_name(tmp_path_persistent, "image", "signed")
+
+    with image.open("wb") as f:
+        f.write(b"\x00" * 1024)
+
+    # not all required arguments are provided
+    result = runner.invoke(
+        imgtool,
+        [
+            "sign",
+            "--key",
+            str(gen_key),
+            str(image),
+            str(image_signed),
+        ],
+    )
+    assert result.exit_code != 0
+    assert not image_signed.exists()
+
+    result = runner.invoke(
+        imgtool,
+        [
+            "sign",
+            "--key",
+            str(gen_key),
+            "--align",
+            "16",
+            "--version",
+            "1.0.0",
+            "--header-size",
+            "0x400",
+            "--slot-size",
+            "0x10000",
+            "--pad-header",
+            str(image),
+            str(image_signed),
+        ],
+    )
+    assert result.exit_code == 0
+    assert image_signed.exists()
+    assert image_signed.stat().st_size > 0
+
+    # original key can be used to verify a signed image
+    result = runner.invoke(
+        imgtool,
+        [
+            "verify",
+            "--key",
+            str(gen_key),
+            str(image_signed),
+        ],
+    )
+    assert result.exit_code == 0
+
+    # 'another' key is not valid to verify a signed image
+    result = runner.invoke(
+        imgtool,
+        [
+            "verify",
+            "--key",
+            str(wrong_key),
+            str(image_signed),
+        ],
+    )
+    assert result.exit_code != 0
+    image_signed.unlink()