rpm 0.23.1

A pure rust library for building and parsing RPMs
Documentation
"""Tests for Signer, Verifier, and Package signature operations."""

import os
import shutil
import tempfile

import pytest

from rpm_rs import Package, Signer, SignatureInfo, Verifier

from conftest import (
    RPM_BASIC,
    RPM_SIGNED,
    RPM_MULTI_SIGNED,
    PUBLIC_KEY,
    PRIVATE_KEY,
    OTHER_PUBLIC_KEY,
    PROTECTED_PRIVATE_KEY,
    PROTECTED_PUBLIC_KEY,
    KEY_PASSPHRASE,
    KEYRING_PRIVATE,
)


class TestSignerConstruction:
    def test_from_bytes(self):
        signer = Signer(open(PRIVATE_KEY, "rb").read())
        assert signer is not None

    def test_from_file(self):
        signer = Signer.from_file(PRIVATE_KEY)
        assert signer is not None

    def test_invalid_key_raises(self):
        with pytest.raises(RuntimeError):
            Signer(b"not a key")

    def test_with_key_passphrase(self):
        signer = Signer.from_file(PROTECTED_PRIVATE_KEY)
        signer = signer.with_key_passphrase(KEY_PASSPHRASE)
        pkg = Package.open(RPM_BASIC)
        pkg.sign(signer)

    def test_with_signing_key(self):
        signer = Signer(open(KEYRING_PRIVATE, "rb").read())
        pkg = Package.open(RPM_SIGNED)
        sigs = pkg.signatures()
        if sigs and sigs[0].fingerprint:
            try:
                signer.with_signing_key(sigs[0].fingerprint)
            except RuntimeError:
                pass  # Key might not be in this keyring

    def test_with_signing_key_invalid_hex(self):
        signer = Signer(open(PRIVATE_KEY, "rb").read())
        with pytest.raises(ValueError, match="invalid hex"):
            signer.with_signing_key("not-hex!")


class TestVerifierConstruction:
    def test_from_bytes(self):
        verifier = Verifier(open(PUBLIC_KEY, "rb").read())
        assert verifier is not None

    def test_from_file(self):
        verifier = Verifier.from_file(PUBLIC_KEY)
        assert verifier is not None

    def test_invalid_key_raises(self):
        with pytest.raises(RuntimeError):
            Verifier(b"not a key")

    def test_load_from_asc_bytes(self):
        verifier = Verifier(open(PUBLIC_KEY, "rb").read())
        verifier.load_from_asc_bytes(open(OTHER_PUBLIC_KEY, "rb").read())

    def test_load_from_asc_file(self):
        verifier = Verifier(open(PUBLIC_KEY, "rb").read())
        verifier.load_from_asc_file(OTHER_PUBLIC_KEY)

    def test_with_key_invalid_hex(self):
        verifier = Verifier(open(PUBLIC_KEY, "rb").read())
        with pytest.raises(ValueError, match="invalid hex"):
            verifier.with_key("not-hex!")


class TestSignAndVerify:
    def test_sign_and_verify(self):
        pkg = Package.open(RPM_BASIC)
        signer = Signer.from_file(PRIVATE_KEY)
        pkg.sign(signer)
        verifier = Verifier.from_file(PUBLIC_KEY)
        pkg.verify_signature(verifier)

    def test_sign_with_timestamp(self):
        pkg = Package.open(RPM_BASIC)
        signer = Signer.from_file(PRIVATE_KEY)
        pkg.sign(signer, timestamp=1_600_000_000)
        verifier = Verifier.from_file(PUBLIC_KEY)
        pkg.verify_signature(verifier)

    def test_sign_with_passphrase(self):
        pkg = Package.open(RPM_BASIC)
        signer = Signer.from_file(PROTECTED_PRIVATE_KEY)
        signer = signer.with_key_passphrase(KEY_PASSPHRASE)
        pkg.sign(signer)
        verifier = Verifier.from_file(PROTECTED_PUBLIC_KEY)
        pkg.verify_signature(verifier)

    def test_verify_wrong_key_fails(self):
        pkg = Package.open(RPM_BASIC)
        signer = Signer.from_file(PRIVATE_KEY)
        pkg.sign(signer)
        verifier = Verifier.from_file(OTHER_PUBLIC_KEY)
        with pytest.raises(RuntimeError):
            pkg.verify_signature(verifier)

    def test_verify_pre_signed(self):
        pkg = Package.open(RPM_SIGNED)
        verifier = Verifier.from_file(PUBLIC_KEY)
        pkg.verify_signature(verifier)


class TestClearSignatures:
    def test_clear_signatures(self):
        pkg = Package.open(RPM_BASIC)
        signer = Signer.from_file(PRIVATE_KEY)
        verifier = Verifier.from_file(PUBLIC_KEY)

        pkg.sign(signer)
        pkg.verify_signature(verifier)

        pkg.clear_signatures()
        with pytest.raises(RuntimeError):
            pkg.verify_signature(verifier)


class TestSignatureInfo:
    def test_signatures_after_sign(self):
        pkg = Package.open(RPM_BASIC)
        signer = Signer.from_file(PRIVATE_KEY)
        pkg.sign(signer)

        sigs = pkg.signatures()
        assert isinstance(sigs, list)
        assert len(sigs) > 0

        sig = sigs[0]
        assert isinstance(sig, SignatureInfo)
        assert sig.fingerprint is not None
        assert isinstance(sig.fingerprint, str)
        assert len(sig.fingerprint) > 16
        assert sig.algorithm is not None
        assert sig.hash_algorithm is not None

    def test_signature_created_timestamp(self):
        pkg = Package.open(RPM_BASIC)
        signer = Signer.from_file(PRIVATE_KEY)
        pkg.sign(signer, timestamp=1_600_000_000)

        sigs = pkg.signatures()
        assert len(sigs) > 0
        assert sigs[0].created == 1_600_000_000

    def test_signature_repr(self):
        pkg = Package.open(RPM_BASIC)
        signer = Signer.from_file(PRIVATE_KEY)
        pkg.sign(signer)
        sigs = pkg.signatures()
        assert "SignatureInfo" in repr(sigs[0])

    def test_unsigned_package_empty(self):
        pkg = Package.open(RPM_BASIC)
        sigs = pkg.signatures()
        assert sigs == []

    def test_multi_signed_package(self):
        pkg = Package.open(RPM_MULTI_SIGNED)
        sigs = pkg.signatures()
        assert len(sigs) > 1


class TestRawSignatures:
    def test_unsigned_package(self):
        pkg = Package.open(RPM_BASIC)
        sigs = pkg.raw_signatures()
        assert isinstance(sigs, list)
        assert len(sigs) == 0

    def test_signed_package(self):
        pkg = Package.open(RPM_SIGNED)
        sigs = pkg.raw_signatures()
        assert isinstance(sigs, list)
        assert len(sigs) > 0
        assert all(isinstance(s, bytes) for s in sigs)


class TestRemoteSigning:
    def test_header_bytes_returns_bytes(self):
        pkg = Package.open(RPM_BASIC)
        header = pkg.header_bytes()
        assert isinstance(header, bytes)
        assert len(header) > 0

    def test_signer_sign_returns_bytes(self):
        signer = Signer.from_file(PRIVATE_KEY)
        pkg = Package.open(RPM_BASIC)
        header = pkg.header_bytes()
        sig = signer.sign(header)
        assert isinstance(sig, bytes)
        assert len(sig) > 0

    def test_sign_and_apply_signature(self):
        signer = Signer.from_file(PRIVATE_KEY)
        verifier = Verifier.from_file(PUBLIC_KEY)
        pkg = Package.open(RPM_BASIC)

        header = pkg.header_bytes()
        sig = signer.sign(header)
        pkg.apply_signature(sig)

        pkg.verify_signature(verifier)

    def test_apply_signature_wrong_key_fails(self):
        signer = Signer.from_file(PRIVATE_KEY)
        pkg = Package.open(RPM_BASIC)

        header = pkg.header_bytes()
        sig = signer.sign(header)
        pkg.apply_signature(sig)

        verifier = Verifier.from_file(OTHER_PUBLIC_KEY)
        with pytest.raises(RuntimeError):
            pkg.verify_signature(verifier)


class TestEmptyVerifier:
    def test_empty_verifier(self):
        verifier = Verifier()
        assert verifier is not None

    def test_empty_verifier_check_digests(self):
        pkg = Package.open(RPM_BASIC)
        verifier = Verifier()
        report = pkg.check_signatures(verifier)
        assert report.digests.is_ok()

    def test_empty_verifier_load_from_asc_bytes(self):
        verifier = Verifier()
        verifier.load_from_asc_bytes(open(PUBLIC_KEY, "rb").read())
        pkg = Package.open(RPM_SIGNED)
        pkg.verify_signature(verifier)


class TestInPlace:
    def test_clear_signatures_in_place(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            path = os.path.join(tmpdir, "test.rpm")
            shutil.copy2(RPM_SIGNED, path)
            original_size = os.path.getsize(path)

            Package.clear_signatures_in_place(path)

            assert os.path.getsize(path) == original_size
            pkg = Package.open(path)
            assert len(pkg.raw_signatures()) == 0

    def test_apply_signature_in_place(self):
        signed_pkg = Package.open(RPM_SIGNED)
        sigs = signed_pkg.raw_signatures()
        assert len(sigs) > 0

        with tempfile.TemporaryDirectory() as tmpdir:
            path = os.path.join(tmpdir, "test.rpm")
            shutil.copy2(RPM_SIGNED, path)
            original_size = os.path.getsize(path)

            Package.clear_signatures_in_place(path)
            Package.apply_signature_in_place(path, sigs[0])

            assert os.path.getsize(path) == original_size
            pkg = Package.open(path)
            assert len(pkg.raw_signatures()) > 0

    def test_resign_in_place(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            path = os.path.join(tmpdir, "test.rpm")
            shutil.copy2(RPM_SIGNED, path)
            original_size = os.path.getsize(path)

            signer = Signer.from_file(PRIVATE_KEY)
            Package.resign_in_place(path, signer)

            assert os.path.getsize(path) == original_size
            pkg = Package.open(path)
            assert len(pkg.raw_signatures()) > 0