rpm 0.23.3

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

import os
import shutil
import tempfile
from pathlib import Path

import pytest

from rpm_rs import Package, Signer, SignatureInfo, SignatureVersion, 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_from_file_pathlike(self):
        signer = Signer.from_file(Path(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_from_file_pathlike(self):
        verifier = Verifier.from_file(Path(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
        assert sig.version == SignatureVersion.V6

    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
        for sig in sigs:
            assert sig.version == SignatureVersion.V6


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