/* * Copyright (c) 2006 Apple Computer, Inc. All Rights Reserved. * * @APPLE_LICENSE_HEADER_START@ * * This file contains Original Code and/or Modifications of Original Code * as defined in and that are subject to the Apple Public Source License * Version 2.0 (the 'License'). You may not use this file except in * compliance with the License. Please obtain a copy of the License at * http://www.opensource.apple.com/apsl/ and read it before using this * file. * * The Original Code and all software distributed under the License are * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. * Please see the License for the specific language governing rights and * limitations under the License. * * @APPLE_LICENSE_HEADER_END@ */ /* * opensshCoding.cpp - Encoding and decoding of OpenSSH format public keys. * * Created 8/29/2006 by dmitch. */ #include "opensshCoding.h" #include #include #include #include #define SSH2_RSA_HEADER "ssh-rsa" #define SSH2_DSA_HEADER "ssh-dss" #ifndef NDEBUG #include #define dprintf(s...) printf(s) #else #define dprintf(...) #endif #pragma mark --- commmon code --- 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)); } /* 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; } /* write BIGNUM, OpenSSH v2 format (with a 4-byte byte count) */ static CSSM_RETURN appendBigNum2( CFMutableDataRef cfOut, const BIGNUM *bn) { if(bn == NULL) { dprintf("appendBigNum2: NULL bn"); return CSSMERR_CSP_INTERNAL_ERROR; } if (BN_is_zero(bn)) { appendUint32(cfOut, 0); return 0; } if(bn->neg) { dprintf("appendBigNum2: negative numbers not supported\n"); return CSSMERR_CSP_INTERNAL_ERROR; } 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 CSSMERR_CSP_INTERNAL_ERROR; } 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 CSSM_OK; } /* 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-1 decimal (public key) version */ static CSSM_RETURN appendBigNumDec( CFMutableDataRef cfOut, const BIGNUM *bn) { char *buf = BN_bn2dec(bn); if(buf == NULL) { dprintf("appendBigNumDec: BN_bn2dec() error"); return CSSMERR_CSP_INTERNAL_ERROR; } CFDataAppendBytes(cfOut, (const UInt8 *)buf, strlen(buf)); Free(buf); return CSSM_OK; } /* 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; } /* * Decode components from an SSHv2 public key. * Also verifies the leading header, e.g. "ssh-rsa". * The returned decodedBlob is algorithm-specific. */ static CSSM_RETURN 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 { unsigned len = strlen(header); *decodedBlob = NULL; /* ID string plus at least one space */ if(keyLen < (len + 1)) { dprintf("parseSSH2PubKey: short record(1)\n"); return CSSMERR_CSP_INVALID_KEY; } if(memcmp(header, key, len)) { dprintf("parseSSH2PubKey: bad header (1)\n"); return CSSMERR_CSP_INVALID_KEY; } key += len; if(*key++ != ' ') { dprintf("parseSSH2PubKey: bad header (2)\n"); return CSSMERR_CSP_INVALID_KEY; } keyLen -= (len + 1); /* key points to first whitespace after header */ skipWhite(key, keyLen); if(keyLen == 0) { dprintf("parseSSH2PubKey: short key\n"); return CSSMERR_CSP_INVALID_KEY; } /* 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 CSSMERR_CSP_INVALID_KEY; } /* skip remainder; it's comment */ return CSSM_OK; } #pragma mark -- RSA OpenSSHv1 --- CSSM_RETURN RSAPublicKeyEncodeOpenSSH1( RSA *rsa, const CssmData &descData, CssmOwnedData &encodedKey) { CFMutableDataRef cfOut = CFDataCreateMutable(NULL, 0); CSSM_RETURN ourRtn = CSSM_OK; /* * Format is * num_bits in decimal * * e, bignum in decimal * * n, bignum in decimal * * optional comment * newline */ 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(ourRtn = appendBigNumDec(cfOut, rsa->e)) { goto errOut; } CFDataAppendBytes(cfOut, &c, 1); if(ourRtn = appendBigNumDec(cfOut, rsa->n)) { goto errOut; } if(descData.Length) { /* optional comment */ CFDataAppendBytes(cfOut, &c, 1); CFDataAppendBytes(cfOut, (UInt8 *)descData.Data, descData.Length); } c = '\n'; CFDataAppendBytes(cfOut, &c, 1); encodedKey.copy(CFDataGetBytePtr(cfOut), CFDataGetLength(cfOut)); errOut: CFRelease(cfOut); return ourRtn; } CSSM_RETURN RSAPublicKeyDecodeOpenSSH1( RSA *rsa, void *p, size_t length) { const unsigned char *cp = (const unsigned char *)p; unsigned remLen = length; 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("RSAPublicKeyDecodeOpenSSH1: short key (1)\n"); return CSSMERR_CSP_INVALID_KEY; } skipWhite(cp, remLen); if(remLen == 0) { dprintf("RSAPublicKeyDecodeOpenSSH1: short key (2)\n"); return CSSMERR_CSP_INVALID_KEY; } /* * cp points to start of e */ const unsigned char *ep = findNextWhite(cp, remLen); if(remLen == 0) { dprintf("RSAPublicKeyDecodeOpenSSH1: short key (3)\n"); return CSSMERR_CSP_INVALID_KEY; } unsigned len = ep - cp; rsa->e = parseDecimalBn(cp, len); if(rsa->e == NULL) { return CSSMERR_CSP_INVALID_KEY; } cp += len; skipWhite(cp, remLen); if(remLen == 0) { dprintf("RSAPublicKeyDecodeOpenSSH1: 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 CSSMERR_CSP_INVALID_KEY; } /* remainder is comment, we ignore */ return CSSM_OK; } CSSM_RETURN RSAPrivateKeyEncodeOpenSSH1( RSA *rsa, const CssmData &descData, CssmOwnedData &encodedKey) { CFDataRef cfOut; CSSM_RETURN ourRtn; ourRtn = encodeOpenSSHv1PrivKey(rsa, descData.Data, descData.Length, NULL, &cfOut); if(ourRtn) { return ourRtn; } encodedKey.copy(CFDataGetBytePtr(cfOut), CFDataGetLength(cfOut)); CFRelease(cfOut); return CSSM_OK; } extern CSSM_RETURN RSAPrivateKeyDecodeOpenSSH1( RSA *openKey, void *p, size_t length) { return decodeOpenSSHv1PrivKey((const unsigned char *)p, length, openKey, NULL, NULL, NULL); } #pragma mark -- RSA OpenSSHv2 --- CSSM_RETURN RSAPublicKeyEncodeOpenSSH2( RSA *rsa, const CssmData &descData, CssmOwnedData &encodedKey) { 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); CSSM_RETURN ourRtn = CSSM_OK; appendString(cfOut, SSH2_RSA_HEADER, strlen(SSH2_RSA_HEADER)); if(ourRtn = appendBigNum2(cfOut, rsa->e)) { goto errOut; } if(ourRtn = appendBigNum2(cfOut, rsa->n)) { 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); if(descData.Length) { /* optional comment */ CFDataAppendBytes(cfOut, &c, 1); CFDataAppendBytes(cfOut, (UInt8 *)descData.Data, descData.Length); } /* finish it with a newline */ c = '\n'; CFDataAppendBytes(cfOut, &c, 1); encodedKey.copy(CFDataGetBytePtr(cfOut), CFDataGetLength(cfOut)); errOut: CFRelease(cfOut); if(b64) { free(b64); } return ourRtn; } CSSM_RETURN RSAPublicKeyDecodeOpenSSH2( RSA *rsa, void *p, size_t length) { const unsigned char *key = (const unsigned char *)p; unsigned keyLen = length; CSSM_RETURN ourRtn; /* * Verify header * get base64-decoded blob */ unsigned char *decodedBlob = NULL; unsigned decodedBlobLen = 0; if(ourRtn = parseSSH2PubKey(key, keyLen, SSH2_RSA_HEADER, &decodedBlob, &decodedBlobLen)) { return ourRtn; } /* subsequent errors to errOut: */ /* * The inner base64-decoded blob, consisting of * ssh-rsa * e * n */ uint32_t decLen; unsigned len; key = decodedBlob; keyLen = decodedBlobLen; if(keyLen < 12) { /* three length fields at least */ dprintf("RSAPublicKeyDecodeOpenSSH2: short record(2)\n"); ourRtn = -1; goto errOut; } decLen = readUint32(key, keyLen); len = strlen(SSH2_RSA_HEADER); if(decLen != len) { dprintf("RSAPublicKeyDecodeOpenSSH2: bad header (2)\n"); ourRtn = CSSMERR_CSP_INVALID_KEY; goto errOut; } if(memcmp(SSH2_RSA_HEADER, key, len)) { dprintf("RSAPublicKeyDecodeOpenSSH2: bad header (1)\n"); return CSSMERR_CSP_INVALID_KEY; } key += len; keyLen -= len; rsa->e = readBigNum2(key, keyLen); if(rsa->e == NULL) { ourRtn = CSSMERR_CSP_INVALID_KEY; goto errOut; } rsa->n = readBigNum2(key, keyLen); if(rsa->n == NULL) { ourRtn = CSSMERR_CSP_INVALID_KEY; goto errOut; } errOut: free(decodedBlob); return ourRtn; } #pragma mark -- DSA OpenSSHv2 --- CSSM_RETURN DSAPublicKeyEncodeOpenSSH2( DSA *dsa, const CssmData &descData, CssmOwnedData &encodedKey) { 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)); if(ourRtn = appendBigNum2(cfOut, dsa->p)) { goto errOut; } if(ourRtn = appendBigNum2(cfOut, dsa->q)) { goto errOut; } if(ourRtn = appendBigNum2(cfOut, dsa->g)) { goto errOut; } if(ourRtn = appendBigNum2(cfOut, dsa->pub_key)) { 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); if(descData.Length) { /* optional comment */ CFDataAppendBytes(cfOut, &c, 1); CFDataAppendBytes(cfOut, (UInt8 *)descData.Data, descData.Length); } /* finish it with a newline */ c = '\n'; CFDataAppendBytes(cfOut, &c, 1); encodedKey.copy(CFDataGetBytePtr(cfOut), CFDataGetLength(cfOut)); errOut: CFRelease(cfOut); if(b64) { free(b64); } return ourRtn; } CSSM_RETURN DSAPublicKeyDecodeOpenSSH2( DSA *dsa, void *p, size_t length) { const unsigned char *key = (const unsigned char *)p; unsigned keyLen = length; CSSM_RETURN ourRtn; /* * Verify header * get base64-decoded blob */ unsigned char *decodedBlob = NULL; unsigned decodedBlobLen = 0; if(ourRtn = parseSSH2PubKey(key, keyLen, SSH2_DSA_HEADER, &decodedBlob, &decodedBlobLen)) { return ourRtn; } /* subsequent errors to errOut: */ /* * The inner base64-decoded blob, consisting of * ssh-dss * p * q * g * pub_key */ uint32_t decLen; unsigned len; key = decodedBlob; keyLen = decodedBlobLen; if(keyLen < 20) { /* five length fields at least */ dprintf("DSAPublicKeyDecodeOpenSSH2: short record(2)\n"); ourRtn = CSSMERR_CSP_INVALID_KEY; goto errOut; } decLen = readUint32(key, keyLen); len = strlen(SSH2_DSA_HEADER); if(decLen != len) { dprintf("DSAPublicKeyDecodeOpenSSH2: bad header (2)\n"); ourRtn = CSSMERR_CSP_INVALID_KEY; goto errOut; } if(memcmp(SSH2_DSA_HEADER, key, len)) { dprintf("DSAPublicKeyDecodeOpenSSH2: bad header (1)\n"); return CSSMERR_CSP_INVALID_KEY; } key += len; keyLen -= len; dsa->p = readBigNum2(key, keyLen); if(dsa->p == NULL) { ourRtn = CSSMERR_CSP_INVALID_KEY; goto errOut; } dsa->q = readBigNum2(key, keyLen); if(dsa->q == NULL) { ourRtn = CSSMERR_CSP_INVALID_KEY; goto errOut; } dsa->g = readBigNum2(key, keyLen); if(dsa->g == NULL) { ourRtn = CSSMERR_CSP_INVALID_KEY; goto errOut; } dsa->pub_key = readBigNum2(key, keyLen); if(dsa->pub_key == NULL) { ourRtn = CSSMERR_CSP_INVALID_KEY; goto errOut; } errOut: free(decodedBlob); return ourRtn; }