Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

fuzz/src/lib.rs

//! **Gunnar's fuzz entry points.** One function per target in `fuzz_targets/`, each taking
//! the bytes a stranger chose and handing them to the parser Gunnar runs on them. Each
//! returns whether the input was ACCEPTED (it parsed, opened or verified), which
//! `tests/seeds.rs` uses to prove the committed `ok-*` seeds still reach the deep end of
//! their parser. A panic, an overflow or a hang here is the bug a fuzzer is looking for.
//!
//! The fixed keys below are throwaway test keys derived from constant seeds: nothing secret.

use gix_hash::Kind;

/// gunnar-server's pack-end scanner, compiled in from its own source file so the fuzz
/// build does not link the whole server (the scanner needs only `anyhow` and `flate2`).
mod storage;

/// A pushed pack's framing (`read_pushed_pack_spooling`'s scanner). The first byte picks
/// the chunk size (1..=256) so splits inside every header are explored.
pub fn pack_scan(data: &[u8]) -> bool {
    let Some((&step, pack)) = data.split_first() else {
        return false;
    };
    storage::scan(pack, 20, step as usize + 1)
}

/// Loose-object payloads: a commit, a tag (and its target) and a tree, as gunnar-store
/// decodes them, in both hash widths.
pub fn object_decode(data: &[u8]) -> bool {
    let mut accepted = false;
    for kind in [Kind::Sha1, Kind::Sha256] {
        accepted |= gunnar_store::object::commit(data, kind).is_ok();
        accepted |= gunnar_store::object::tag(data, kind).is_ok();
        let _ = gunnar_store::object::tag_target(data, kind);
        let mut entries = 0usize;
        let mut iter = gunnar_store::object::TreeIter::from_bytes(data, kind);
        let mut clean = true;
        for entry in &mut iter {
            match entry {
                Ok(_) => entries += 1,
                Err(_) => {
                    clean = false;
                    break;
                }
            }
        }
        accepted |= clean && entries > 0;
    }
    accepted
}

/// The throwaway keyring every envelope seed was sealed with.
pub fn fuzz_ring() -> gunnar_crypt::keyring::Keyring {
    use gunnar_crypt::keyring::{DataKey, IdKey, Keyring, ID_KEY_LEN, KEY_LEN};
    Keyring::new(
        IdKey::from_bytes([7u8; ID_KEY_LEN]),
        DataKey::from_bytes(1, [9u8; KEY_LEN]),
    )
}

/// An encrypted repository's blob envelope (v1 and v2), opened whole and streamed.
pub fn envelope_open(data: &[u8]) -> bool {
    let ring = fuzz_ring();
    let whole = gunnar_crypt::envelope::open(&ring, data);
    let mut out = Vec::new();
    let streamed = gunnar_crypt::envelope::open_stream(&ring, &mut &data[..], &mut out);
    if let (Ok(plain), Ok(n)) = (&whole, &streamed) {
        assert_eq!(
            *n as usize,
            plain.len(),
            "whole and streamed open disagree on the length"
        );
        assert_eq!(
            &out, plain,
            "whole and streamed open disagree on the plaintext"
        );
    }
    whole.is_ok()
}

/// An encrypted repository's text files (the keyring, the member list, a quantum-safe
/// identity) and the crypt door's exec line and answer.
pub fn crypt_files(data: &[u8]) -> bool {
    let mut accepted = false;
    if let Some(r) = gunnar_crypt::door::parse_request(data) {
        accepted |= r.is_ok();
    }
    if let Ok(text) = std::str::from_utf8(data) {
        accepted |= gunnar_crypt::keyring::Keyring::parse(text).is_ok();
        accepted |= gunnar_crypt::members::Members::parse(text).is_ok();
        accepted |= gunnar_crypt::pq::parse_identity_file(text).is_ok();
        accepted |= gunnar_crypt::door::Answer::parse(text).is_some();
    }
    accepted
}

/// The throwaway ed25519 key every signature seed was made with.
pub fn fuzz_key() -> ssh_key::PrivateKey {
    let pair = ssh_key::private::Ed25519Keypair::from_seed(&[3u8; 32]);
    ssh_key::PrivateKey::from(pair)
}

/// A commit's SSH signature (`gpgsig` header), checked as the policy gate checks a push.
pub fn commit_signature(data: &[u8]) -> bool {
    let key = fuzz_key();
    let allowed = [key.public_key().clone()];
    let _ = gunnar_policy::signature::split_signature(data);
    gunnar_policy::signature::check_commit_bytes(
        gix_hash::ObjectId::null(Kind::Sha1),
        data,
        &allowed,
        "git",
    )
    .is_verified()
}

/// The bytes a signed forge event's signature covers in the seeds.
pub const EVENT_BYTES: &[u8] = b"gunnar-fuzz signed event";

/// A forge event's `sshsig` (PEM) and an authorized-key line, as gunnar-core reads them.
pub fn sshsig_event(data: &[u8]) -> bool {
    let Ok(text) = std::str::from_utf8(data) else {
        return false;
    };
    for line in text.lines().take(8) {
        let _ = gunnar_core::auth::parse_public_key(line);
    }
    let key = fuzz_key();
    let public = key.public_key();
    let fingerprint = gunnar_core::sshsig::fingerprint(public);
    gunnar_core::sshsig::verify(
        public,
        gunnar_core::forge::FORGE_NAMESPACE,
        EVENT_BYTES,
        &fingerprint,
        text,
    )
    .is_ok()
}

/// receive-pack's opening: the pushing client's ref commands and capabilities (pkt-lines).
pub fn receive_preamble(data: &[u8]) -> bool {
    let mut input: &[u8] = data;
    matches!(
        gunnar_wire::receive_pack::ReceivePack::read_preamble(Kind::Sha1, &mut input),
        Ok(Some(_))
    )
}

/// upload-pack v2: the argument sections of `command=fetch` and `command=ls-refs`.
pub fn upload_request(data: &[u8]) -> bool {
    let lines: Vec<Vec<u8>> = data.split(|&b| b == b'\n').map(<[u8]>::to_vec).collect();
    let fetch = gunnar_wire::upload_pack::FetchRequest::parse(
        &lines,
        Kind::Sha1,
        gunnar_wire::caps::Offered::default(),
    )
    .is_ok();
    let ls_refs = gunnar_wire::upload_pack::LsRefsRequest::parse(&lines).is_ok();
    fetch || ls_refs
}