sshKey.cpp   [plain text]


/*
 * sshKey.cpp - Standalone SSH key parser and converter. Uses libcrypto for 
 *              representing and storing RSA and DSA keys and for 
 *              writing and reading BIGNUMS to/from memory.
 */
 
#include <stdlib.h>
#include <strings.h>
#include <stdio.h>
#include <unistd.h>
#include <CommonCrypto/CommonDigest.h>
#include <CommonCrypto/CommonCryptor.h>
#include <security_cdsa_utils/cuFileIo.h>
#include <security_cdsa_utils/cuEnc64.h>
#include <openssl/rsa.h>
#include <openssl/dsa.h>
#include <CoreFoundation/CoreFoundation.h>
#include <security_utilities/devrandom.h>
#include <ctype.h>

#define dprintf(s...)		printf(s)

static void usage(char **argv)
{
	printf("usage: %s [options]\n", argv[0]);
	printf("Options:\n");
	printf("  -i inFile\n");
	printf("  -o outFile\n");
	printf("  -v             -- private key input; default is public\n");
	printf("  -V             -- private key output; default is public\n");
	printf("  -d             -- DSA; default is RSA\n");
	printf("  -r             -- parse & print inFile\n");
	printf("  -f ssh1|ssh2   -- input format; default = ssh2\n");
	printf("  -F ssh1|ssh2   -- output format; default = ssh2\n");
	printf("  -p password\n");
	printf("  -P             -- no password; private keys in the clear\n");
	printf("  -c comment\n");
	exit(1);
}

static const char *authfile_id_string = "SSH PRIVATE KEY FILE FORMAT 1.1\n";

/* from openssh cipher.h */
#define SSH_CIPHER_NONE		0	/* no encryption */
#define SSH_CIPHER_IDEA		1	/* IDEA CFB */
#define SSH_CIPHER_DES		2	/* DES CBC */
#define SSH_CIPHER_3DES		3	/* 3DES CBC */
#define SSH_CIPHER_BROKEN_TSS	4	/* TRI's Simple Stream encryption CBC */
#define SSH_CIPHER_BROKEN_RC4	5	/* Alleged RC4 */
#define SSH_CIPHER_BLOWFISH	6
#define SSH_CIPHER_RESERVED	7

#define SSH2_RSA_HEADER		"ssh-rsa"
#define SSH2_DSA_HEADER		"ssh-dss"

#pragma mark --- commmon code --- 

static uint32_t readUint32(
	const unsigned char *&cp,		// IN/OUT
	unsigned &len)					// IN/OUT 
{
	uint32_t r = 0;
	
	for(unsigned dex=0; dex<sizeof(uint32_t); dex++) {
		r <<= 8;
		r |= *cp++;
	}
	len -= 4;
	return r;
}

static uint16_t readUint16(
	const unsigned char *&cp,		// IN/OUT
	unsigned &len)					// IN/OUT 
{
	uint16_t r = *cp++;
	r <<= 8;
	r |= *cp++;
	len -= 2;
	return r;
}

static void appendUint32(
	CFMutableDataRef cfOut,
	uint32_t ui)
{
	UInt8 buf[sizeof(uint32_t)];
	
	for(int dex=(sizeof(uint32_t) - 1); dex>=0; dex--) {
		buf[dex] = ui & 0xff;
		ui >>= 8;
	}
	CFDataAppendBytes(cfOut, buf, sizeof(uint32_t));
}

static void appendUint16(
	CFMutableDataRef cfOut,
	uint16_t ui)
{
	UInt8 buf[sizeof(uint16_t)];
	
	buf[1] = ui & 0xff;
	ui >>= 8;
	buf[0] = ui;
	CFDataAppendBytes(cfOut, buf, sizeof(uint16_t));
}

/* parse text as decimal, return BIGNUM */
static BIGNUM *parseDecimalBn(
	const unsigned char *cp,
	unsigned len)
{
	for(unsigned dex=0; dex<len; dex++) {
		char c = *cp;
		if((c < '0') || (c > '9')) {
			return NULL;
		}
	}
	char *str = (char *)malloc(len + 1);
	memmove(str, cp, len);
	str[len] = '\0';
	BIGNUM *bn = NULL;
	BN_dec2bn(&bn, str);
	free(str);
	return bn;
}

/* Read BIGNUM, OpenSSH-1 version */
static BIGNUM *readBigNum(
	const unsigned char *&cp,	// IN/OUT
	unsigned &remLen)			// IN/OUT
{
	if(remLen < sizeof(uint16_t)) {
		dprintf("readBigNum: short record(1)\n");
		return NULL;
	}
	uint16_t numBits = readUint16(cp, remLen);
	unsigned bytes = (numBits + 7) / 8;
	if(remLen < bytes) {
		dprintf("readBigNum: short record(2)\n");
		return NULL;
	}
	BIGNUM *bn = BN_bin2bn(cp, bytes, NULL);
	if(bn == NULL) {
		dprintf("readBigNum: BN_bin2bn error\n");
		return NULL;
	}
	cp += bytes;
	remLen -= bytes;
	return bn;
}
	
/* Write BIGNUM, OpenSSH-1 version */
static int appendBigNum(
	CFMutableDataRef cfOut, 
	const BIGNUM *bn)
{
	/* 16 bits of numbits */
	unsigned numBits = BN_num_bits(bn);
	appendUint16(cfOut, numBits);
	
	/* serialize the bytes */
	int numBytes = (numBits + 7) / 8;
	unsigned char outBytes[numBytes];	// gcc is so cool...
	int moved = BN_bn2bin(bn, outBytes);
	if(moved != numBytes) {
		dprintf("appendBigNum: BN_bn2bin() screwup\n");
		return -1;
	}
	CFDataAppendBytes(cfOut, (UInt8 *)outBytes, numBytes);
	return 0;
}

/* read BIGNUM, OpenSSH-2 mpint version */
static BIGNUM *readBigNum2(
	const unsigned char *&cp,	// IN/OUT
	unsigned &remLen)			// IN/OUT
{
	if(remLen < 4) {
		dprintf("readBigNum2: short record(1)\n");
		return NULL;
	}
	uint32_t bytes = readUint32(cp, remLen);
	if(remLen < bytes) {
		dprintf("readBigNum2: short record(2)\n");
		return NULL;
	}
	BIGNUM *bn = BN_bin2bn(cp, bytes, NULL);
	if(bn == NULL) {
		dprintf("readBigNum2: BN_bin2bn error\n");
		return NULL;
	}
	cp += bytes;
	remLen -= bytes;
	return bn;
}

/* write BIGNUM, OpenSSH v2 format (with a 4-byte byte count) */
static int appendBigNum2(
	CFMutableDataRef cfOut,
	const BIGNUM *bn)
{
	if(bn == NULL) {
		dprintf("appendBigNum2: NULL bn");
		return -1;
	}
	if (BN_is_zero(bn)) {
		appendUint32(cfOut, 0);
		return 0;
	}
	if(bn->neg) {
		dprintf("appendBigNum2: negative numbers not supported\n");
		return -1;
	}
	int numBytes = BN_num_bytes(bn);
	unsigned char buf[numBytes];
	int moved = BN_bn2bin(bn, buf);
	if(moved != numBytes) {
		dprintf("appendBigNum: BN_bn2bin() screwup\n");
		return -1;
	}
	bool appendZero = false;
	if(buf[0] & 0x80) {
		/* prepend leading zero to make it positive */
		appendZero = true;
		numBytes++;		// to encode the correct 4-byte length 
	}
	appendUint32(cfOut, (uint32_t)numBytes);
	if(appendZero) {
		UInt8 z = 0;
		CFDataAppendBytes(cfOut, &z, 1);
		numBytes--;		// to append the correct number of bytes
	}
	CFDataAppendBytes(cfOut, buf, numBytes);
	memset(buf, 0, numBytes);
	return 0;
}

/* Write BIGNUM, OpenSSH-1 decimal (public key) version */
static int appendBigNumDec(
	CFMutableDataRef cfOut, 
	const BIGNUM *bn)
{
	char *buf = BN_bn2dec(bn);
	if(buf == NULL) {
		dprintf("appendBigNumDec: BN_bn2dec() error");
		return -1;
	}
	CFDataAppendBytes(cfOut, (const UInt8 *)buf, strlen(buf));
	OPENSSL_free(buf);
	return 0;
}

/* write string, OpenSSH v2 format (with a 4-byte byte count) */
static void appendString(
	CFMutableDataRef cfOut,
	const char *str,
	unsigned strLen)
{
	appendUint32(cfOut, (uint32_t)strLen);
	CFDataAppendBytes(cfOut, (UInt8 *)str, strLen);
}

/* skip whitespace */
static void skipWhite(
	const unsigned char *&cp,
	unsigned &bytesLeft)
{
	while(bytesLeft != 0) {
		if(isspace((int)(*cp))) {
			cp++;
			bytesLeft--;
		}
		else {
			return;
		}
	}
}

/* find next whitespace or EOF - if EOF, rtn pointer points to one past EOF */
static const unsigned char *findNextWhite(
	const unsigned char *cp,
	unsigned &bytesLeft)
{
	while(bytesLeft != 0) {
		if(isspace((int)(*cp))) {
			return cp;
		}
		cp++;
		bytesLeft--;
	}
	return cp;
}


/* 
 * Calculate d mod{p-1,q-1}
 * Used when decoding OpenSSH-1 private RSA key.
 */
static int
rsa_generate_additional_parameters(RSA *rsa)
{
	BIGNUM *aux;
	BN_CTX *ctx;
	
	if((rsa->dmq1 = BN_new()) == NULL) {
		dprintf("rsa_generate_additional_parameters: BN_new failed");
		return -1;
	}
	if((rsa->dmp1 = BN_new()) == NULL) {
		dprintf("rsa_generate_additional_parameters: BN_new failed");
		return -1;
	}
	if ((aux = BN_new()) == NULL) {
		dprintf("rsa_generate_additional_parameters: BN_new failed");
		return -1;
	}
	if ((ctx = BN_CTX_new()) == NULL) {
		dprintf("rsa_generate_additional_parameters: BN_CTX_new failed");
		BN_clear_free(aux);
		return -1;
	} 

	BN_sub(aux, rsa->q, BN_value_one());
	BN_mod(rsa->dmq1, rsa->d, aux, ctx);

	BN_sub(aux, rsa->p, BN_value_one());
	BN_mod(rsa->dmp1, rsa->d, aux, ctx);

	BN_clear_free(aux);
	BN_CTX_free(ctx);
	return 0;
}

#pragma mark --- OpenSSH-1 crypto --- 

static int ssh1DES3Crypt(
	unsigned char cipher,
	bool doEncrypt,
	const unsigned char *inText,
	unsigned inTextLen,
	const char *password,		// C string
	unsigned char *outText,		// data RETURNED here, caller mallocs
	unsigned *outTextLen)		// RETURNED
{
	switch(cipher) {
		case SSH_CIPHER_3DES:
			break;
		case SSH_CIPHER_NONE:
			/* cleartext RSA private key, e.g. host key. */
			memmove(outText, inText, inTextLen);
			*outTextLen = inTextLen;
			return 0;
		default:
			/* who knows how we're going to figure these out */
			printf("***Unsupported cipher (%u)\n", cipher);
			return -1;
	}
	
	/* key starts with MD5(password) */
	unsigned char pwdDigest[CC_MD5_DIGEST_LENGTH];
	CC_MD5(password, strlen(password), pwdDigest);
	
	/* three keys from that, like so: */
	unsigned char k1[kCCKeySizeDES];
	unsigned char k2[kCCKeySizeDES];
	unsigned char k3[kCCKeySizeDES];
	memmove(k1, pwdDigest, kCCKeySizeDES);
	memmove(k2, pwdDigest + kCCKeySizeDES, kCCKeySizeDES);
	memmove(k3, pwdDigest, kCCKeySizeDES);
	
	CCOperation op1_3;
	CCOperation op2;
	if(doEncrypt) {
		op1_3 = kCCEncrypt;
		op2   = kCCDecrypt;
	}
	else {
		op1_3 = kCCDecrypt;
		op2   = kCCEncrypt;
	}
	
	/* the openssh v1 pseudo triple DES. Each DES pass has its own CBC. */
	size_t moved = 0;

	CCCryptorStatus cstat = CCCrypt(op1_3, kCCAlgorithmDES, 
		0,						// no padding
		k1, kCCKeySizeDES, 
		NULL,		// IV
		inText, inTextLen,
		outText, inTextLen, &moved);
	if(cstat) {
		dprintf("***ssh1DES3Crypt: CCCrypt()(1) returned %u\n", (unsigned)cstat);
		return -1;
	}
	cstat = CCCrypt(op2, kCCAlgorithmDES, 
		0,						// no padding - SSH does that itself 
		k2, kCCKeySizeDES, 
		NULL,		// IV
		outText, moved,
		outText, inTextLen, &moved);
	if(cstat) {
		dprintf("***ssh1DES3Crypt: CCCrypt()(2) returned %u\n", (unsigned)cstat);
		return -1;
	}
	cstat = CCCrypt(op1_3, kCCAlgorithmDES, 
		0,						// no padding - SSH does that itself 
		k3, kCCKeySizeDES, 
		NULL,		// IV
		outText, moved,
		outText, inTextLen, &moved);
	if(cstat) {
		dprintf("***ssh1DES3Crypt: CCCrypt()(3) returned %u\n", (unsigned)cstat);
		return -1;
	}

	*outTextLen = moved;	
	return 0;
}
	
#pragma mark --- OpenSSH-1 decode --- 

/* Decode OpenSSH-1 RSA private key */
static int decodeSSH1RSAPrivKey(
	const unsigned char *key,
	unsigned keyLen,
	char *password,
	RSA *rsa,						// returned
	char **comment)					// returned
{
	const unsigned char *cp = key;		// running pointer
	unsigned remLen = keyLen;
	unsigned len = strlen(authfile_id_string);
	
	/* length: ID string, NULL, Cipher, 4-byte spare */
	if(remLen < (len + 6)) {
		dprintf("decodeSSH1RSAPrivKey: short record(1)\n");
		return -1;
	}
	
	/* ID string plus a NULL */
	if(memcmp(authfile_id_string, cp, len)) {
		dprintf("decodeSSH1RSAPrivKey: bad header\n");
		return -1;
	}
	cp += (len + 1);
	remLen -= (len + 1);
	
	/* cipher */
	unsigned char cipherSpec = *cp;
	switch(cipherSpec) {
		case SSH_CIPHER_NONE:
			if(password != NULL) {
				dprintf("decodeSSH1RSAPrivKey: Attempt to decrypt plaintext key\n");
				return -1;
			}
			break;
		case SSH_CIPHER_3DES:
			if(password == NULL) {
				dprintf("decodeSSH1RSAPrivKey: Encrypted key with no decryptKey\n");
				return -1;
			}
			break;
		default:
			/* I hope we don't see any other values here */
			dprintf("decodeOpenSSHv1PrivKey: unknown cipherSpec (%u)\n", cipherSpec);
				return -1;
	}
	
	/* skip cipher, spares */
	cp += 5;
	remLen -= 5;
	
	/*
	 * Clear text public key:
	 * uint32 bits
	 * bignum n
	 * bignum e
	 */
	if(remLen < sizeof(uint32_t)) {
		dprintf("decodeSSH1RSAPrivKey: bad len(1)\n");
		return -1;
	}
	/* skip over keybits */
	readUint32(cp, remLen);
	rsa->n = readBigNum(cp, remLen);
	if(rsa->n == NULL) {
		dprintf("decodeSSH1RSAPrivKey: error decoding n\n");
		return -1;
	}
	rsa->e = readBigNum(cp, remLen);
	if(rsa->e == NULL) {
		dprintf("decodeSSH1RSAPrivKey: error decoding e\n");
		return -1;
	}
	
	/* comment string: 4-byte length and the string w/o NULL */
	if(remLen < sizeof(uint32_t)) {
		dprintf("decodeSSH1RSAPrivKey: bad len(2)\n");
		return -1;
	}
	uint32_t commentLen = readUint32(cp, remLen);
	if(commentLen > remLen) {
		dprintf("decodeSSH1RSAPrivKey: bad len(3)\n");
		return -1;
	}
	*comment = (char *)malloc(commentLen + 1);
	memmove(*comment, cp, commentLen);
	(*comment)[commentLen] = '\0';
	cp += commentLen;
	remLen -= commentLen;
	
	/* everything that remains is ciphertext */
	unsigned char ptext[remLen];
	unsigned ptextLen = 0;
	if(ssh1DES3Crypt(cipherSpec, false, cp, remLen, password, ptext, &ptextLen)) {
		dprintf("decodeSSH1RSAPrivKey: decrypt error\n");
		return -1;
	}
	/* subsequent errors to errOut: */
	
	int ourRtn = 0;
	
	/* plaintext contents:
	
	[0-1]		-- random bytes
	[2-3]		-- copy of [01] for passphrase validity checking
	buffer_put_bignum(d)
	buffer_put_bignum(iqmp)
	buffer_put_bignum(q)
	buffer_put_bignum(p)
	pad to block size
	*/
	cp = ptext;
	remLen = ptextLen;
	if(remLen < 4) {
		dprintf("decodeSSH1RSAPrivKey: bad len(4)\n");
		ourRtn = -1;
		goto errOut;
	}
	if((cp[0] != cp[2]) || (cp[1] != cp[3])) {
		/* decrypt fail */
		dprintf("decodeSSH1RSAPrivKey: check byte error\n");
		ourRtn = -1;
		goto errOut;
	}
	cp += 4;
	remLen -= 4;
	
	/* remainder comprises private portion of RSA key */
	rsa->d = readBigNum(cp, remLen);
	if(rsa->d == NULL) {
		dprintf("decodeSSH1RSAPrivKey: error decoding d\n");
		return -1;
	}
	rsa->iqmp = readBigNum(cp, remLen);
	if(rsa->iqmp == NULL) {
		dprintf("decodeSSH1RSAPrivKey: error decoding iqmp\n");
		return -1;
	}
	rsa->q = readBigNum(cp, remLen);
	if(rsa->q == NULL) {
		dprintf("decodeSSH1RSAPrivKey: error decoding q\n");
		return -1;
	}
	rsa->p = readBigNum(cp, remLen);
	if(rsa->p == NULL) {
		dprintf("decodeSSH1RSAPrivKey: error decoding p\n");
		return -1;
	}

	/* calculate d mod{p-1,q-1} */
	ourRtn = rsa_generate_additional_parameters(rsa);

errOut:
	memset(ptext, 0, ptextLen);
	return ourRtn; 
}

/* Decode OpenSSH-1 RSA public key */
static int decodeSSH1RSAPubKey(
	const unsigned char *key,
	unsigned keyLen,
	RSA *rsa,						// returned
	char **comment)					// returned
{
	const unsigned char *cp = key;		// running pointer
	unsigned remLen = keyLen;
	
	*comment = NULL;
	skipWhite(cp, remLen);
	
	/* 
	 * cp points to start of size_in_bits in ASCII decimal' we really don't care about 
	 * this field. Find next space.
	 */
	cp = findNextWhite(cp, remLen);
	if(remLen == 0) {
		dprintf("decodeSSH1RSAPubKey: short key (1)\n");
		return -1;
	}
	skipWhite(cp, remLen);
	if(remLen == 0) {
		dprintf("decodeSSH1RSAPubKey: short key (2)\n");
		return -1;
	}
	
	/*
	 * cp points to start of e
	 */
	const unsigned char *ep = findNextWhite(cp, remLen);
	if(remLen == 0) {
		dprintf("decodeSSH1RSAPubKey: short key (3)\n");
		return -1;
	}
	unsigned len = ep - cp;
	rsa->e = parseDecimalBn(cp, len);
	if(rsa->e == NULL) {
		return -1;
	}
	cp += len;
	remLen -= len;
	
	skipWhite(cp, remLen);
	if(remLen == 0) {
		dprintf("decodeSSH1RSAPubKey: short key (4)\n");
		return -1;
	}
	
	/* cp points to start of n */
	ep = findNextWhite(cp, remLen);
	len = ep - cp;
	rsa->n = parseDecimalBn(cp, len);
	if(rsa->n == NULL) {
		return -1;
	}
	cp += len;
	remLen -= len;
	skipWhite(cp, remLen);
	if(remLen == 0) {
		/* no comment; we're done */
		return 0;
	}

	ep = findNextWhite(cp, remLen);
	len = ep - cp;
	if(len == 0) {
		return 0;
	}
	*comment = (char *)malloc(len + 1);
	memmove(*comment, cp, len);
	if((*comment)[len - 1] == '\n') {
		/* normal case closes with a newline, not part of the comment */
		len--;
	}
	(*comment)[len] = '\0';
	return 0;

}

#pragma mark --- OpenSSH-1 encode --- 

/* Encode OpenSSH-1 RSA private key */
static int encodeSSH1RSAPrivKey(
	RSA *rsa, 
	const char *password, 
	const char *comment, 
	unsigned char **outKey,		// mallocd and RETURNED 
	unsigned *outKeyLen) 		// RETURNED
{
	CFMutableDataRef cfOut = CFDataCreateMutable(NULL, 0);
	
	/* ID string including NULL */
	CFDataAppendBytes(cfOut, (const UInt8 *)authfile_id_string, strlen(authfile_id_string) + 1);
	
	/* one byte cipher */
	UInt8 cipherSpec = SSH_CIPHER_3DES;
	CFDataAppendBytes(cfOut, &cipherSpec, 1);
	
	/* spares */
	UInt8 spares[4] = {0};
	CFDataAppendBytes(cfOut, spares, 4);
	
	/*
	 * Clear text public key:
	 * uint32 bits
	 * bignum n
	 * bignum e
	 */
	uint32_t keybits = RSA_size(rsa) * 8;
	appendUint32(cfOut, keybits);
	appendBigNum(cfOut, rsa->n);
	appendBigNum(cfOut, rsa->e);

	/* comment string: 4-byte length and the string w/o NULL */
	if(comment) {
		uint32_t len = strlen(comment);
		appendUint32(cfOut, len);
		CFDataAppendBytes(cfOut, (const UInt8 *)comment, len);
	}
	
	/* 
	 * Remainder is encrypted, consisting of
	 *
	 * [0-1]		-- random bytes
	 * [2-3]		-- copy of [01] for passphrase validity checking
	 * buffer_put_bignum(d)
	 * buffer_put_bignum(iqmp)
	 * buffer_put_bignum(q)
	 * buffer_put_bignum(p)
	 * pad to block size
	 */
	CFMutableDataRef ptext = CFDataCreateMutable(NULL, 0);
	
	/* [0..3] check bytes */
	UInt8 checkBytes[4];
	DevRandomGenerator rng = DevRandomGenerator();
	rng.random(checkBytes, 2);
	checkBytes[2] = checkBytes[0];
	checkBytes[3] = checkBytes[1];
	CFDataAppendBytes(ptext, checkBytes, 4);
	
	/* d, iqmp, q, p */
	appendBigNum(ptext, rsa->d);
	appendBigNum(ptext, rsa->iqmp);
	appendBigNum(ptext, rsa->q);
	appendBigNum(ptext, rsa->p);
	
	/* encrypt it */
	unsigned ptextLen = CFDataGetLength(ptext);
	unsigned padding = 0;
	unsigned rem = ptextLen & 0x7;
	if(rem) {
		padding = 8 - rem;
	}
	UInt8 padByte = 0;
	for(unsigned dex=0; dex<padding; dex++) {
		CFDataAppendBytes(ptext, &padByte, 1);
	}
	ptextLen = CFDataGetLength(ptext);
	unsigned char ctext[ptextLen];
	unsigned ctextLen;
	int ourRtn = ssh1DES3Crypt(SSH_CIPHER_3DES, true, 
		(unsigned char *)CFDataGetBytePtr(ptext), ptextLen, 
		password,
		ctext, &ctextLen);
	if(ourRtn != 0) {
		goto errOut;
	}
	
	/* appended encrypted portion */
	CFDataAppendBytes(cfOut, ctext, ctextLen);
	*outKeyLen = (unsigned)CFDataGetLength(cfOut);
	*outKey = (unsigned char *)malloc(*outKeyLen);
	memmove(*outKey, CFDataGetBytePtr(cfOut), *outKeyLen);
errOut:
	CFRelease(cfOut);
	/* it would be proper to zero out ptext here, but we can't do that to a CFData */
	CFRelease(ptext);
	return ourRtn;
}

/* Encode OpenSSH-1 RSA public key */
static int encodeSSH1RSAPubKey(
	RSA *rsa, 
	const char *comment, 
	unsigned char **outKey,		// mallocd and RETURNED 
	unsigned *outKeyLen) 		// RETURNED
{
	CFMutableDataRef cfOut = CFDataCreateMutable(NULL, 0);
	
	/*
	 * Format is
	 * num_bits in decimal
	 * <space>
	 * e, bignum in decimal
	 * <space>
	 * n, bignum in decimal
	 * <space>
	 * optional comment
	 * newline
	 */
	int ourRtn = 0;
	unsigned numBits = BN_num_bits(rsa->n);
	char bitString[20];
	UInt8 c = ' ';

	snprintf(bitString, sizeof(bitString), "%u ", numBits);
	CFDataAppendBytes(cfOut, (const UInt8 *)bitString, strlen(bitString));
	if(appendBigNumDec(cfOut, rsa->e)) {
		ourRtn = -1;
		goto errOut;
	}
	CFDataAppendBytes(cfOut, &c, 1);
	if(appendBigNumDec(cfOut, rsa->n)) {
		ourRtn = -1;
		goto errOut;
	}
	if(comment != NULL) {
		CFDataAppendBytes(cfOut, &c, 1);
		CFDataAppendBytes(cfOut, (UInt8 *)comment, strlen(comment));
	}
	c = '\n';
	CFDataAppendBytes(cfOut, &c, 1);
	*outKeyLen = CFDataGetLength(cfOut);
	*outKey = (unsigned char *)malloc(*outKeyLen);
	memmove(*outKey, CFDataGetBytePtr(cfOut), *outKeyLen);
errOut:
	CFRelease(cfOut);
	return ourRtn;
}

#pragma mark --- OpenSSH-2 public key decode --- 

/* 
 * Decode components from an SSHv2 public key.
 * Also verifies the leading header, e.g. "ssh-rsa".
 * The returned decodedBlob is algorithm-specific.
 */
static int parseSSH2PubKey(
	const unsigned char *key,
	unsigned keyLen,
	const char *header,				// SSH2_RSA_HEADER, SSH2_DSA_HEADER
	unsigned char **decodedBlob,	// mallocd and RETURNED
	unsigned *decodedBlobLen,		// RETURNED
	char **comment)					// optionally mallocd and RETURNED, NULL terminated
{
	unsigned len = strlen(header);
	const unsigned char *endOfKey = key + keyLen;
	*decodedBlob = NULL;
	*comment = NULL;
	
	/* ID string plus at least one space */
	if(keyLen < (len + 1)) {
		dprintf("parseSSH2PubKey: short record(1)\n");
		return -1;
	}
	
	if(memcmp(header, key, len)) {
		dprintf("parseSSH2PubKey: bad header (1)\n");
		return -1;
	}
	key += len;
	if(*key++ != ' ') {
		dprintf("parseSSH2PubKey: bad header (2)\n");
		return -1;
	}
	keyLen -= (len + 1);

	/* key points to first whitespace after header */
	skipWhite(key, keyLen);
	if(keyLen == 0) {
		dprintf("parseSSH2PubKey: short key\n");
		return -1;
	}
	
	/* key is start of base64 blob */
	const unsigned char *encodedBlob = key;
	const unsigned char *endBlob = findNextWhite(key, keyLen);
	unsigned encodedBlobLen = endBlob - encodedBlob;
	
	/* decode base 64 */
	*decodedBlob = cuDec64(encodedBlob, encodedBlobLen, decodedBlobLen);
	if(*decodedBlob == NULL) {
		dprintf("parseSSH2PubKey: base64 decode error\n");
		return -1;
	}
	
	/* skip over the encoded blob and possible whitespace after it */
	key = endBlob;
	keyLen = endOfKey - endBlob;
	skipWhite(key, keyLen);
	if(keyLen == 0) {
		/* nothing remains, no comment, no error */
		return 0;
	}
	
	/* optional comment */
	*comment = (char *)malloc(keyLen + 1);
	memmove(*comment, key, keyLen);
	if((*comment)[keyLen - 1] == '\n') {
		/* normal case closes with a newline, not part of the comment */
		keyLen--;
	}
	(*comment)[keyLen] = '\0';
	return 0;
}
	
static int decodeSSH2RSAPubKey(
	const unsigned char *key,
	unsigned keyLen,
	RSA *rsa,						// returned
	char **comment)					// returned
{
	/* 
	 * Verify header
	 * get base64-decoded blob plus optional comment 
	 */
	unsigned char *decodedBlob = NULL;
	unsigned decodedBlobLen = 0;
	if(parseSSH2PubKey(key, keyLen, SSH2_RSA_HEADER, &decodedBlob, &decodedBlobLen, comment)) {
		return -1;
	}
	/* subsequent errors to errOut: */
	
	/*
	 * The inner base64-decoded blob, consisting of
	 * ssh-rsa
	 * e
	 * n
	 */
	uint32_t decLen;
	unsigned len;
	int ourRtn = 0;
	
	key = decodedBlob;
	keyLen = decodedBlobLen;
	if(keyLen < 12) {
		/* three length fields at least */
		dprintf("decodeSSH2RSAPubKey: short record(2)\n");
		ourRtn = -1;
		goto errOut;
	}
	decLen = readUint32(key, keyLen);
	len = strlen(SSH2_RSA_HEADER);
	if(decLen != len) {
		dprintf("decodeSSH2RSAPubKey: bad header (2)\n");
		ourRtn = -1;
		goto errOut;
	}
	if(memcmp(SSH2_RSA_HEADER, key, len)) {
		dprintf("decodeSSH2RSAPubKey: bad header (1)\n");
		return -1;
	}
	key += len;
	keyLen -= len;
	
	rsa->e = readBigNum2(key, keyLen);
	if(rsa->e == NULL) {
		ourRtn = -1;
		goto errOut;
	}
	rsa->n = readBigNum2(key, keyLen);
	if(rsa->n == NULL) {
		ourRtn = -1;
		goto errOut;
	}

errOut:
	free(decodedBlob);
	return ourRtn;
}

static int decodeSSH2DSAPubKey(
	const unsigned char *key,
	unsigned keyLen,
	DSA *dsa,						// returned
	char **comment)					// returned
{
	/* 
	 * Verify header
	 * get base64-decoded blob plus optional comment 
	 */
	unsigned char *decodedBlob = NULL;
	unsigned decodedBlobLen = 0;
	if(parseSSH2PubKey(key, keyLen, SSH2_DSA_HEADER, &decodedBlob, &decodedBlobLen, comment)) {
		return -1;
	}
	/* subsequent errors to errOut: */

	/*
	 * The inner base64-decoded blob, consisting of
	 * ssh-dss
	 * p
	 * q
	 * g
	 * pub_key
	 */
	uint32_t decLen;
	int ourRtn = 0;
	unsigned len;
	
	key = decodedBlob;
	keyLen = decodedBlobLen;
	if(keyLen < 20) {
		/* five length fields at least */
		dprintf("decodeSSH2DSAPubKey: short record(2)\n");
		ourRtn = -1;
		goto errOut;
	}
	decLen = readUint32(key, keyLen);
	len = strlen(SSH2_DSA_HEADER);
	if(decLen != len) {
		dprintf("decodeSSH2DSAPubKey: bad header (2)\n");
		ourRtn = -1;
		goto errOut;
	}
	if(memcmp(SSH2_DSA_HEADER, key, len)) {
		dprintf("decodeSSH2DSAPubKey: bad header (1)\n");
		return -1;
	}
	key += len;
	keyLen -= len;
	
	dsa->p = readBigNum2(key, keyLen);
	if(dsa->p == NULL) {
		ourRtn = -1;
		goto errOut;
	}
	dsa->q = readBigNum2(key, keyLen);
	if(dsa->q == NULL) {
		ourRtn = -1;
		goto errOut;
	}
	dsa->g = readBigNum2(key, keyLen);
	if(dsa->g == NULL) {
		ourRtn = -1;
		goto errOut;
	}
	dsa->pub_key = readBigNum2(key, keyLen);
	if(dsa->pub_key == NULL) {
		ourRtn = -1;
		goto errOut;
	}

errOut:
	free(decodedBlob);
	return ourRtn;
}

#pragma mark --- OpenSSH-2 public key encode --- 

static int encodeSSH2RSAPubKey(
	RSA *rsa, 
	const char *comment, 
	unsigned char **outKey,		// mallocd and RETURNED 
	unsigned *outKeyLen) 		// RETURNED
{
	unsigned char *b64 = NULL;
	unsigned b64Len;
	UInt8 c;
	
	/* 
	 * First, the inner base64-encoded blob, consisting of
	 * ssh-rsa
	 * e
	 * n
	 */
	CFMutableDataRef cfOut = CFDataCreateMutable(NULL, 0);
	int ourRtn = 0;
	appendString(cfOut, SSH2_RSA_HEADER, strlen(SSH2_RSA_HEADER));
	ourRtn = appendBigNum2(cfOut, rsa->e);
	if(ourRtn) {
		goto errOut;
	}
	ourRtn = appendBigNum2(cfOut, rsa->n);
	if(ourRtn) {
		goto errOut;
	}
	
	/* base64 encode that */
	b64 = cuEnc64((unsigned char *)CFDataGetBytePtr(cfOut), CFDataGetLength(cfOut), &b64Len);
	
	/* cuEnc64 added newline and NULL, which we really don't want */
	b64Len -= 2;
	
	/* Now start over, dropping that base64 into a public blob. */
	CFDataSetLength(cfOut, 0);
	CFDataAppendBytes(cfOut, (UInt8 *)SSH2_RSA_HEADER, strlen(SSH2_RSA_HEADER));
	c = ' ';
	CFDataAppendBytes(cfOut, &c, 1);
	CFDataAppendBytes(cfOut, b64, b64Len);
	
	/* optional comment */
	if(comment) {
		CFDataAppendBytes(cfOut, &c, 1);
		CFDataAppendBytes(cfOut, (UInt8 *)comment, strlen(comment));
	}
	
	/* finish it with a newline */
	c = '\n';
	CFDataAppendBytes(cfOut, &c, 1);
	
	*outKeyLen = (unsigned)CFDataGetLength(cfOut);
	*outKey = (unsigned char *)malloc(*outKeyLen);
	memmove(*outKey, CFDataGetBytePtr(cfOut), *outKeyLen);
	
errOut:
	CFRelease(cfOut);
	if(b64) {
		free(b64);
	}
	return ourRtn;
}

static int encodeSSH2DSAPubKey(
	DSA *dsa, 
	const char *comment, 
	unsigned char **outKey,		// mallocd and RETURNED 
	unsigned *outKeyLen) 		// RETURNED
{
	unsigned char *b64 = NULL;
	unsigned b64Len;
	UInt8 c;
	
	/* 
	 * First, the inner base64-encoded blob, consisting of
	 * ssh-dss
	 * p
	 * q
	 * g
	 * pub_key
	 */
	CFMutableDataRef cfOut = CFDataCreateMutable(NULL, 0);
	int ourRtn = 0;
	appendString(cfOut, SSH2_DSA_HEADER, strlen(SSH2_DSA_HEADER));
	ourRtn = appendBigNum2(cfOut, dsa->p);
	if(ourRtn) {
		goto errOut;
	}
	ourRtn = appendBigNum2(cfOut, dsa->q);
	if(ourRtn) {
		goto errOut;
	}
	ourRtn = appendBigNum2(cfOut, dsa->g);
	if(ourRtn) {
		goto errOut;
	}
	ourRtn = appendBigNum2(cfOut, dsa->pub_key);
	if(ourRtn) {
		goto errOut;
	}
	
	/* base64 encode that */
	b64 = cuEnc64((unsigned char *)CFDataGetBytePtr(cfOut), CFDataGetLength(cfOut), &b64Len);
	
	/* cuEnc64 added newline and NULL, which we really don't want */
	b64Len -= 2;
	
	/* Now start over, dropping that base64 into a public blob. */
	CFDataSetLength(cfOut, 0);
	CFDataAppendBytes(cfOut, (UInt8 *)SSH2_DSA_HEADER, strlen(SSH2_DSA_HEADER));
	c = ' ';
	CFDataAppendBytes(cfOut, &c, 1);
	CFDataAppendBytes(cfOut, b64, b64Len);
	
	/* optional comment */
	if(comment) {
		CFDataAppendBytes(cfOut, &c, 1);
		CFDataAppendBytes(cfOut, (UInt8 *)comment, strlen(comment));
	}
	
	/* finish it with a newline */
	c = '\n';
	CFDataAppendBytes(cfOut, &c, 1);
	
	*outKeyLen = (unsigned)CFDataGetLength(cfOut);
	*outKey = (unsigned char *)malloc(*outKeyLen);
	memmove(*outKey, CFDataGetBytePtr(cfOut), *outKeyLen);
	
errOut:
	CFRelease(cfOut);
	if(b64) {
		free(b64);
	}
	return ourRtn;
}


#pragma mark --- print RSA/DSA keys --- 

static void printBNLong(
	BN_ULONG bnl)
{
	/* for now assume it's 32 bits */
	unsigned i = bnl >> 24;
	printf("%02X ", i);
	i = (bnl >> 16) & 0xff;
	printf("%02X ", i);
	i = (bnl >> 8) & 0xff;
	printf("%02X ", i);
	i = bnl & 0xff;
	printf("%02X ", i);
}

static void printBN(
	const char *label,
	BIGNUM *bn)
{
	printf("%s: %d bits: bn->top %d: ", label, BN_num_bits(bn), bn->top);
	for(int dex=bn->top-1; dex>=0; dex--) {
		printBNLong(bn->d[dex]);
	}
	printf("\n");
}
static void printRSA(
	RSA *rsa)
{
	if(rsa->n) {
		printBN("   n", rsa->n);
	}
	if(rsa->e) {
		printBN("   e", rsa->e);
	}
	if(rsa->d) {
		printBN("   d", rsa->d);
	}
	if(rsa->p) {
		printBN("   p", rsa->p);
	}
	if(rsa->q) {
		printBN("   q", rsa->q);
	}
	if(rsa->dmp1) {
		printBN("dmp1", rsa->dmp1);
	}
	if(rsa->dmq1) {
		printBN("dmq1", rsa->dmq1);
	}
	if(rsa->iqmp) {
		printBN("iqmp", rsa->iqmp);
	}
}

/* only public keys here */
static void printDSA(
	DSA *dsa)
{
	if(dsa->p) {
		printBN("   p", dsa->p);
	}
	if(dsa->q) {
		printBN("   q", dsa->q);
	}
	if(dsa->g) {
		printBN("   g", dsa->g);
	}
	if(dsa->pub_key) {
		printBN(" pub", dsa->pub_key);
	}
}

/* parse format string, returns nonzero on error */
static int parseFormat(
	const char *formatStr,
	bool *isSSH1)
{
	if(!strcmp(formatStr, "ssh1")) {
		*isSSH1 = true;
		return 0;
	}
	else if(!strcmp(formatStr, "ssh2")) {
		*isSSH1 = false;
		return 0;
	}
	else {
		return -1;
	}
}

#pragma mark --- main --- 

/* parse format string */
int main(int argc, char **argv)
{
	char *inFile = NULL;
	char *outFile = NULL;
	bool privKeyIn = false;
	bool privKeyOut = false;
	char *password = NULL;
	char *comment = NULL;
	bool doPrint = false;
	bool isDSA = false;
	bool inputSSH1 = false;
	bool outputSSH1 = false;
	bool clearPrivKeys = false;
	
	int ourRtn = 0;
	
	extern char *optarg;
	int arg;
	while ((arg = getopt(argc, argv, "i:o:vVdrf:F:p:Pc:h")) != -1) {
		switch (arg) {
			case 'i':
				inFile = optarg;
				break;
			case 'o':
				outFile = optarg;
				break;
			case 'v':
				privKeyIn = true;
				break;
			case 'V':
				privKeyOut = true;
				break;
			case 'd':
				isDSA = true;
				break;
			case 'r':
				doPrint = true;
				break;
			case 'f':
				if(parseFormat(optarg, &inputSSH1)) {
					usage(argv);
				}
				break;
			case 'F':
				if(parseFormat(optarg, &outputSSH1)) {
					usage(argv);
				}
				break;
			case 'p':
				password = optarg;
				break;
			case 'P':
				clearPrivKeys = true;
				break;
			case 'c':
				comment = optarg;
				break;
			case 'h':
			default:
				usage(argv);
		}
	}
	
	if(inFile == NULL) {
		printf("***You must specify an input file.\n");
		usage(argv);
	}
	if((privKeyIn && !inputSSH1) || (privKeyOut && !outputSSH1)) {
		printf("***Private keys in SSH2 format are handled elsewhere - Wrapped OpenSSL.\n");
		exit(1);
	}
	if((privKeyIn || privKeyOut) && (password == NULL) & !clearPrivKeys) {
		printf("***Private key handling requires a password or the -P option.\n");
		usage(argv);
	}
	unsigned char *inKey = NULL;
	unsigned inKeyLen = 0;
	if(readFile(inFile, &inKey, &inKeyLen)) {
		printf("Error reading %s. Aborting.\n", inFile);
		exit(1);
	}
	
	RSA *rsa = NULL;
	DSA *dsa = NULL;
	
	/* parse incoming key */
	if(isDSA) {
		if(inputSSH1) {
			printf("***SSHv1 did not support DSA keys.\n");
			exit(1);
		}
		/* already verified that this is not SSH2 & priv (Wrapped OpenSSL) */
		dsa = DSA_new();
		if(decodeSSH2DSAPubKey(inKey, inKeyLen, dsa, &comment)) {
			printf("***Error decoding SSH2 DSA public key.\n");
			exit(1);
		}
	}
	else {
		rsa = RSA_new();
		if(privKeyIn) {
			/* already verified that this is SSH1 (SSH2 is Wrapped OpenSSL) */
			if(decodeSSH1RSAPrivKey(inKey, inKeyLen, password, rsa, &comment)) {
				printf("***Error decoding SSH1 RSA Private key.\n");
				exit(1);
			}
		}
		else {
			if(inputSSH1) {
				if(decodeSSH1RSAPubKey(inKey, inKeyLen, rsa, &comment)) {
					printf("***Error decoding SSH1 RSA Public key.\n");
					exit(1);
				}
			}
			else {
				if(decodeSSH2RSAPubKey(inKey, inKeyLen, rsa, &comment)) {
					printf("***Error decoding SSH2 RSA Public key.\n");
					exit(1);
				}
			}
		}
	}

	/* optionally display the key */
	if(doPrint) {
		if(isDSA) {
			printf("DSA key:\n");
			printDSA(dsa);
			printf("Comment: %s\n", comment);
		}
		else {
			printf("RSA key:\n");
			printRSA(rsa);
			printf("Comment: %s\n", comment);
		}
	}
	
	/* optionally convert to (optionally different) output format */
	
	if(outFile) {
		unsigned char *outKey = NULL;
		unsigned outKeyLen = 0;
		
		if(isDSA) {
			if(outputSSH1 || privKeyOut) {
				printf("***DSA: Only public SSHv2 keys allowed.\n");
				exit(1);
			}
			if(encodeSSH2DSAPubKey(dsa, comment, &outKey, &outKeyLen)) {
				printf("***Error encoding DSA public key.\n");
				exit(1);
			}
		}
		else {
			if(privKeyOut) {
				/* already verified that this is SSH1 (SSH2 is Wrapped OpenSSL) */
				if(encodeSSH1RSAPrivKey(rsa, password, comment, &outKey, &outKeyLen)) {
					printf("***Error encoding RSA private key.\n");
					exit(1);
				}
			}
			else {
				if(outputSSH1) {
					if(encodeSSH1RSAPubKey(rsa, comment, &outKey, &outKeyLen)) {
						printf("***Error encoding RSA public key.\n");
						exit(1);
					}
				}
				else {
					if(encodeSSH2RSAPubKey(rsa, comment, &outKey, &outKeyLen)) {
						printf("***Error encoding RSA public key.\n");
						exit(1);
					}
				}
			}	/* RSA public */
		}	/* RSA */
		
		if(writeFile(outFile, outKey, outKeyLen)) {
			printf("***Error writing to %s.\n", outFile);
			ourRtn = -1;
		}
		else {
			printf("...wrote %u bytes to %s.\n", outKeyLen, outFile);
		}
		free(outKey);
	}
	else if(!doPrint) {
		printf("...parsed a key but you didn't ask me to do anything with it.\n");
	}
	if(rsa) {
		RSA_free(rsa);
	}
	if(dsa) {
		DSA_free(dsa);
	}
	
	return 0;
}