~apreiml/hare-ssh

97f4ffae5cf62e74208158f968f93c13ba585d40 — Armin Preiml 1 year, 6 months ago 85251f9 newrsa
sshsign tool
3 files changed, 247 insertions(+), 1 deletions(-)

M Makefile
A cmd/sshsign/main.ha
A cmd/sshsign/util.ha
M Makefile => Makefile +3 -1
@@ 17,7 17,7 @@ check:
clean:
	rm ssh ssh-agent sshkey sshsign

cmd: ssh ssh-agent sshkey 
cmd: ssh ssh-agent sshkey sshsign

ssh: cmd/ssh/main.ha
	hare build cmd/ssh/


@@ 28,6 28,8 @@ ssh-agent: cmd/ssh-agent/main.ha
sshkey: cmd/sshkey/main.ha
	hare build cmd/sshkey/

sshsign: cmd/sshsign/*.ha
	hare build cmd/sshsign/

install:
	mkdir -p $(DESTDIR)$(THIRDPARTYDIR)/net/ssh/agent

A cmd/sshsign/main.ha => cmd/sshsign/main.ha +184 -0
@@ 0,0 1,184 @@
use bufio;
use bytes;
use crypto::sha256;
use crypto::sha512;
use encoding::hex;
use encoding::pem;
use fmt;
use format::ssh;
use fs;
use getopt;
use hash;
use io;
use os;
use strings;

const magic: str = "SSHSIG";

export fn main() void = {
	const cmd = getopt::parse(os::args,
		"ssh signer\n\nSigns or verifies data read from stdin.",
		('s', "file", "Verify input using given signature file"),
		('n', "namespace", "namespace"),
		('p', "pubkey", "Check signing key against public key file"),
		('i', "privkey", "Private key file to sign input with"),
	);
	defer getopt::finish(&cmd);

	let sig: str = "";
	let namespace: str = "";
	let pubkeyfile: str = "";
	let privkeyfile: str = "";

	for (let i = 0z; i < len(cmd.opts); i += 1) {
		const opt = cmd.opts[i];
		switch (opt.0) {
		case 's' =>
			sig = opt.1;
		case 'n' =>
			namespace = opt.1;
		case 'p' =>
			pubkeyfile = opt.1;
		case 'i' =>
			privkeyfile = opt.1;
		};
	};

	if (sig != "") {
		let f = os::open(sig)!;
		match (verify(f, namespace, os::stdin)) {
		case let e: error =>
			fmt::fatalf(strerror(e));
		case void =>
			fmt::println("signature ok")!;
		};

		if (pubkeyfile != "") {
			abort("todo");
		};

		return;
	};


	let pf = os::open(privkeyfile)!;
	let privfile = ssh::decodesshprivate(pf)!;
	let key = ssh::decodeprivate(&privfile)!;
};

type invalidnamespace = !void;

type error = (ssh::error | invalidnamespace | io::error);

fn strerror(e: error) str = {
	match (e) {
	case let e: ssh::error =>
		return ssh::strerror(e);
	case invalidnamespace =>
		return "invalid namespace";
	};
};

fn verify(sigin: io::handle, namespace: str, msg: io::handle) (void | error) = {
	let sigf = decodesignature(sigin)?;
	let kbuf = bufio::fixed(sigf.pubkey, io::mode::READ);
	let k = ssh::decodepublic(&kbuf)?;

	if (sigf.namespace != namespace) {
		return invalidnamespace;
	};

	let s = makesigmessage(sigf.hashalgo, namespace, msg)!;

	let sigbuf = bufio::fixed(sigf.sig, io::mode::READ);
	ssh::verify(&sigbuf, k, s)?;
};

fn sign(sink: io::handle, namespace: str, msg: io::handle, key: *ssh::key) (void | error) = {
	return;
};

type sigfile = struct {
	pubkey: []u8,
	namespace: str,
	hashalgo: str,
	sig: []u8,
};

fn decodesignature(in: io::handle) (sigfile | io::error | ssh::error) = {
	const pem = pem::newdecoder(in);
	defer pem::finish(&pem);
	const dec = match (pem::next(&pem)?) {
	case io::EOF =>
		return ssh::invalid;
	case let dec: (str, pem::pemdecoder) =>
		if (dec.0 != "SSH SIGNATURE") {
			return ssh::invalid;
		};
		yield dec.1;
	};

	let magicbuf: [6]u8 = [0...];
	match (io::readall(&dec, magicbuf)?) {
	case size => void;
	case io::EOF =>
		return ssh::invalid;
	};
	if (!bytes::equal(magicbuf, strings::toutf8(magic))) {
		return ssh::invalid;
	};

	const sigversion = readu32(&dec)?;
	assert(sigversion == 1, "unsupported signature file version");

	let sig = sigfile { ... };
	sig.pubkey = readslice(&dec)?;
	sig.namespace = readstr(&dec)?;
	readslice(&dec)?; // reserved
	sig.hashalgo = readstr(&dec)?;
	sig.sig = readslice(&dec)?;

	return sig;
};

fn hash(in: io::handle, hashalgo: str) ([]u8 | io::error) = {
	let h = switch (hashalgo) {
	case "sha512" =>
		yield &sha512::sha512(): *hash::hash;
	case "sha256" =>
		yield &sha256::sha256(): *hash::hash;
	case =>
		abort("unsupported hash algorithm");
	};
	defer hash::close(h);

	io::copy(h, in)?;

	let sum: []u8 = alloc([0...], hash::sz(h));
	hash::sum(h, sum);

	return sum;
};

fn makesigmessage(
	hashalgo: str,
	namespace: str,
	src: io::handle
) ([]u8 | io::error) = {
	let buf = bufio::dynamic(io::mode::WRITE);

	io::write(&buf, strings::toutf8(magic))?;
	writestr(&buf, namespace)?;
	writestr(&buf, "")?;
	writestr(&buf, hashalgo)?;

	let h = hash(src, hashalgo)!;
	defer free(h);

	writeslice(&buf, h)?;

	let signature = bufio::dynamic(io::mode::WRITE);

	return bufio::buffer(&buf);
};


A cmd/sshsign/util.ha => cmd/sshsign/util.ha +60 -0
@@ 0,0 1,60 @@
use endian;
use io;
use strings;
use types;
use format::ssh;

fn readu32(src: io::handle) (u32 | ssh::error) = {
	let buf: [4]u8 = [0...];
	match (io::readall(src, buf)?) {
	case size => void;
	case io::EOF =>
		return ssh::invalid;
	};
	return endian::begetu32(buf);
};

fn readstr(src: io::handle) (str | ssh::error) = {
	match (strings::fromutf8(readslice(src)?)) {
	case let s: str =>
		return s;
	case =>
		return ssh::invalid;
	};
};

fn readslice(src: io::handle) ([]u8 | ssh::error) = {
	const length = readu32(src)?;
	if (length == 0) {
		return [];
	};

	let buf: []u8 = alloc([0...], length);
	match (io::readall(src, buf)) {
	case size => void;
	case io::EOF =>
		free(buf);
		return ssh::invalid;
	case let e: io::error =>
		free(buf);
		return e;
	};

	return buf;
};

fn writeu32(sink: io::handle, v: u32) (void | io::error) = {
	let buf: [4]u8 = [0...];
	endian::beputu32(buf, v);
	io::writeall(sink, buf)?;
};

fn writeslice(sink: io::handle, sl: []u8) (void | io::error) = {
	assert(len(sl) <= types::U32_MAX, "parameter exceeds maximum length");
	writeu32(sink, len(sl): u32)?;
	io::writeall(sink, sl)?;
};

fn writestr(sink: io::handle, s: str) (void | io::error) = {
	return writeslice(sink, strings::toutf8(s));
};