X-Git-Url: https://git.saurik.com/apple/security.git/blobdiff_plain/80e2389990082500d76eb566d4946be3e786c3ef..d8f41ccd20de16f8ebe2ccc84d47bf1cb2b26bbb:/SecurityTests/cspxutils/sshKey/sshKey.cpp?ds=sidebyside diff --git a/SecurityTests/cspxutils/sshKey/sshKey.cpp b/SecurityTests/cspxutils/sshKey/sshKey.cpp new file mode 100644 index 00000000..6b181224 --- /dev/null +++ b/SecurityTests/cspxutils/sshKey/sshKey.cpp @@ -0,0 +1,1461 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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=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 '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 + * e, bignum in decimal + * + * n, bignum in decimal + * + * 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; +}