X-Git-Url: https://git.saurik.com/apple/security.git/blobdiff_plain/5dd5f9ec28f304ca377c42fd7f711d6cf12b90e1..5c19dc3ae3bd8e40a9c028b0deddd50ff337692c:/OSX/libsecurity_keychain/Security/SecImportExportOpenSSH.cpp diff --git a/OSX/libsecurity_keychain/Security/SecImportExportOpenSSH.cpp b/OSX/libsecurity_keychain/Security/SecImportExportOpenSSH.cpp new file mode 100644 index 00000000..d066c49c --- /dev/null +++ b/OSX/libsecurity_keychain/Security/SecImportExportOpenSSH.cpp @@ -0,0 +1,633 @@ +/* + * Copyright (c) 2006,2011-2014 Apple 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. + * + */ + +#include "SecImportExportOpenSSH.h" +#include "SecImportExportUtils.h" +#include "SecImportExportCrypto.h" +#include +#include /* for CC_MD5_DIGEST_LENGTH */ +#include +#include + +#define SecSSHDbg(args...) secdebug("openssh", ## args) + +#define SSHv2_PUB_KEY_NAME "OpenSSHv2 Public Key" +#define SSHv1_PUB_KEY_NAME "OpenSSHv1 Public Key" +#define SSHv1_PRIV_KEY_NAME "OpenSSHv1 Private Key" + +#pragma mark --- Utility functions --- + +/* 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; +} + +/* obtain comment as the n'th whitespace-delimited field */ +static char *commentAsNthField( + const unsigned char *key, + unsigned keyLen, + unsigned n) + +{ + unsigned dex; + + skipWhite(key, keyLen); + if(keyLen == 0) { + return NULL; + } + for(dex=0; dex<(n-1); dex++) { + key = findNextWhite(key, keyLen); + if(keyLen == 0) { + return NULL; + } + skipWhite(key, keyLen); + if(keyLen == 0) { + return NULL; + } + } + + /* cp points to start of nth field */ + char *rtnStr = (char *)malloc(keyLen + 1); + memmove(rtnStr, key, keyLen); + if(rtnStr[keyLen - 1] == '\n') { + /* normal terminator - snip it off */ + rtnStr[keyLen - 1] = '\0'; + } + else { + rtnStr[keyLen] = '\0'; + } + return rtnStr; + +} + +static uint32_t readUint32( + const unsigned char *&cp, // IN/OUT + unsigned &len) // IN/OUT +{ + uint32_t r = 0; + + for(unsigned dex=0; dex len) { + cp += len; + len = 0; + return; + } + cp += numBytes; + len -= numBytes; +} + +static char *genPrintName( + const char *header, // e.g. SSHv2_PUB_KEY_NAME + const char *comment) // optional, from key +{ + size_t totalLen = strlen(header) + 1; + if(comment) { + /* append ": " */ + totalLen += strlen(comment); + totalLen += 2; + } + char *rtnStr = (char *)malloc(totalLen); + if(comment) { + snprintf(rtnStr, totalLen, "%s: %s", header, comment); + } + else { + strcpy(rtnStr, header); + } + return rtnStr; +} + +#pragma mark --- Infer PrintName attribute from raw keys --- + +/* obtain comment from OpenSSHv2 public key */ +static char *opensshV2PubComment( + const unsigned char *key, + unsigned keyLen) +{ + /* + * The format here is + * header + * + * keyblob + * + * optional comment + * \n + */ + char *comment = commentAsNthField(key, keyLen, 3); + char *rtnStr = genPrintName(SSHv2_PUB_KEY_NAME, comment); + if(comment) { + free(comment); + } + return rtnStr; +} + +/* obtain comment from OpenSSHv1 public key */ +static char *opensshV1PubComment( + const unsigned char *key, + unsigned keyLen) +{ + /* + * Format: + * numbits + * + * e (bignum in decimal) + * + * n (bignum in decimal) + * + * optional comment + * \n + */ + char *comment = commentAsNthField(key, keyLen, 4); + char *rtnStr = genPrintName(SSHv1_PUB_KEY_NAME, comment); + if(comment) { + free(comment); + } + return rtnStr; +} + +static const char *authfile_id_string = "SSH PRIVATE KEY FILE FORMAT 1.1\n"; + +/* obtain comment from OpenSSHv1 private key, wrapped or clear */ +static char *opensshV1PrivComment( + const unsigned char *key, + unsigned keyLen) +{ + /* + * Format: + * "SSH PRIVATE KEY FILE FORMAT 1.1\n" + * 1 byte cipherSpec + * 4 byte spares + * 4 bytes numBits + * bignum n + * bignum e + * 4 byte comment length + * comment + * private key components, possibly encrypted + * + * A bignum is encoded like so: + * 2 bytes numBits + * (numBits + 7)/8 bytes of data + */ + /* length: ID string, NULL, Cipher, 4-byte spare */ + size_t len = strlen(authfile_id_string); + if(keyLen < (len + 6)) { + return NULL; + } + if(memcmp(authfile_id_string, key, len)) { + return NULL; + } + key += (len + 6); + keyLen -= (len + 6); + + /* key points to numBits */ + if(keyLen < 4) { + return NULL; + } + key += 4; + keyLen -= 4; + + /* key points to n */ + skipBigNum(key, keyLen); + if(keyLen == 0) { + return NULL; + } + skipBigNum(key, keyLen); + if(keyLen == 0) { + return NULL; + } + + char *comment = NULL; + uint32 commentLen = readUint32(key, keyLen); + if((commentLen != 0) && (commentLen <= keyLen)) { + comment = (char *)malloc(commentLen + 1); + memmove(comment, key, commentLen); + comment[commentLen] = '\0'; + } + + char *rtnStr = genPrintName(SSHv1_PRIV_KEY_NAME, comment); + if(comment) { + free(comment); + } + return rtnStr; +} + +/* + * Infer PrintName attribute from raw key's 'comment' field. + * Returned string is mallocd and must be freed by caller. + */ +char *impExpOpensshInferPrintName( + CFDataRef external, + SecExternalItemType externType, + SecExternalFormat externFormat) +{ + const unsigned char *key = (const unsigned char *)CFDataGetBytePtr(external); + unsigned keyLen = (unsigned)CFDataGetLength(external); + switch(externType) { + case kSecItemTypePublicKey: + switch(externFormat) { + case kSecFormatSSH: + return opensshV1PubComment(key, keyLen); + case kSecFormatSSHv2: + return opensshV2PubComment(key, keyLen); + default: + /* impossible, right? */ + break; + } + break; + case kSecItemTypePrivateKey: + switch(externFormat) { + case kSecFormatSSH: + case kSecFormatWrappedSSH: + return opensshV1PrivComment(key, keyLen); + default: + break; + } + break; + default: + break; + } + return NULL; +} + +#pragma mark --- Infer DescriptiveData from PrintName --- + +/* + * Infer DescriptiveData (i.e., comment) from a SecKeyRef's PrintName + * attribute. + */ +void impExpOpensshInferDescData( + SecKeyRef keyRef, + CssmOwnedData &descData) +{ + OSStatus ortn; + SecKeychainAttributeInfo attrInfo; + SecKeychainAttrType attrType = kSecKeyPrintName; + attrInfo.count = 1; + attrInfo.tag = &attrType; + attrInfo.format = NULL; + SecKeychainAttributeList *attrList = NULL; + + ortn = SecKeychainItemCopyAttributesAndData( + (SecKeychainItemRef)keyRef, + &attrInfo, + NULL, // itemClass + &attrList, + NULL, // don't need the data + NULL); + if(ortn) { + SecSSHDbg("SecKeychainItemCopyAttributesAndData returned %ld", (unsigned long)ortn); + return; + } + /* subsequent errors to errOut: */ + SecKeychainAttribute *attr = attrList->attr; + + /* + * On a previous import, we would have set this to something like + * "OpenSSHv2 Public Key: comment". + * We want to strip off everything up to the actual comment. + */ + unsigned toStrip = 0; + + /* min length of attribute value for this code to be meaningful */ + unsigned len = strlen(SSHv2_PUB_KEY_NAME) + 1; + char *printNameStr = NULL; + if(len < attr->length) { + printNameStr = (char *)malloc(attr->length + 1); + memmove(printNameStr, attr->data, attr->length); + printNameStr[attr->length] = '\0'; + if(strstr(printNameStr, SSHv2_PUB_KEY_NAME) == printNameStr) { + toStrip = strlen(SSHv2_PUB_KEY_NAME); + } + else if(strstr(printNameStr, SSHv1_PUB_KEY_NAME) == printNameStr) { + toStrip = strlen(SSHv1_PUB_KEY_NAME); + } + else if(strstr(printNameStr, SSHv1_PRIV_KEY_NAME) == printNameStr) { + toStrip = strlen(SSHv1_PRIV_KEY_NAME); + } + if(toStrip) { + /* only strip if we have ": " after toStrip bytes */ + if((printNameStr[toStrip] == ':') && (printNameStr[toStrip+1] == ' ')) { + toStrip += 2; + } + } + } + if(printNameStr) { + free(printNameStr); + } + len = attr->length; + + unsigned char *attrVal; + + if(len < toStrip) { + SecSSHDbg("impExpOpensshInferDescData: string parse screwup"); + goto errOut; + } + if(len > toStrip) { + /* Normal case of stripping off leading header */ + len -= toStrip; + } + else { + /* + * If equal, then the attr value *is* "OpenSSHv2 Public Key: " with + * no comment. Not sure how that could happen, but let's be careful. + */ + toStrip = 0; + } + + attrVal = ((unsigned char *)attr->data) + toStrip; + descData.copy(attrVal, len); +errOut: + SecKeychainItemFreeAttributesAndData(attrList, NULL); + return; +} + +#pragma mark --- Derive SSHv1 wrap/unwrap key --- + +/* + * Common code to derive a wrap/unwrap key for OpenSSHv1. + * Caller must CSSM_FreeKey when done. + */ +static CSSM_RETURN openSSHv1DeriveKey( + CSSM_CSP_HANDLE cspHand, + const SecKeyImportExportParameters *keyParams, // required + impExpVerifyPhrase verifyPhrase, // for secure passphrase + CSSM_KEY_PTR symKey) // RETURNED +{ + CSSM_KEY *passKey = NULL; + CFDataRef cfPhrase = NULL; + CSSM_RETURN crtn; + OSStatus ortn; + CSSM_DATA dummyLabel; + uint32 keyAttr; + CSSM_CC_HANDLE ccHand = 0; + CSSM_ACCESS_CREDENTIALS creds; + CSSM_CRYPTO_DATA seed; + CSSM_DATA nullParam = {0, NULL}; + + memset(symKey, 0, sizeof(CSSM_KEY)); + + /* passphrase or passkey? */ + ortn = impExpPassphraseCommon(keyParams, cspHand, SPF_Data, verifyPhrase, + (CFTypeRef *)&cfPhrase, &passKey); + if(ortn) { + return ortn; + } + /* subsequent errors to errOut: */ + + memset(&seed, 0, sizeof(seed)); + if(cfPhrase != NULL) { + /* TBD - caller-supplied empty passphrase means "export in the clear" */ + size_t len = CFDataGetLength(cfPhrase); + seed.Param.Data = (uint8 *)malloc(len); + seed.Param.Length = len; + memmove(seed.Param.Data, CFDataGetBytePtr(cfPhrase), len); + CFRelease(cfPhrase); + } + + memset(&creds, 0, sizeof(CSSM_ACCESS_CREDENTIALS)); + crtn = CSSM_CSP_CreateDeriveKeyContext(cspHand, + CSSM_ALGID_OPENSSH1, + CSSM_ALGID_OPENSSH1, + CC_MD5_DIGEST_LENGTH * 8, + &creds, + passKey, // BaseKey + 0, // iterationCount + NULL, // salt + &seed, + &ccHand); + if(crtn) { + SecSSHDbg("openSSHv1DeriveKey CSSM_CSP_CreateDeriveKeyContext failure"); + goto errOut; + } + + /* not extractable even for the short time this key lives */ + keyAttr = CSSM_KEYATTR_RETURN_REF | CSSM_KEYATTR_SENSITIVE; + dummyLabel.Data = (uint8 *)"temp unwrap key"; + dummyLabel.Length = strlen((char *)dummyLabel.Data); + + crtn = CSSM_DeriveKey(ccHand, + &nullParam, + CSSM_KEYUSE_ANY, + keyAttr, + &dummyLabel, + NULL, // cred and acl + symKey); + if(crtn) { + SecSSHDbg("openSSHv1DeriveKey CSSM_DeriveKey failure"); + } +errOut: + if(ccHand != 0) { + CSSM_DeleteContext(ccHand); + } + if(passKey != NULL) { + CSSM_FreeKey(cspHand, NULL, passKey, CSSM_FALSE); + free(passKey); + } + if(seed.Param.Data) { + memset(seed.Param.Data, 0, seed.Param.Length); + free(seed.Param.Data); + } + return crtn; +} + +#pragma mark -- OpenSSHv1 Wrap/Unwrap --- + +/* + * If cspHand is provided instead of importKeychain, the CSP + * handle MUST be for the CSPDL, not for the raw CSP. + */ +OSStatus impExpWrappedOpenSSHImport( + CFDataRef inData, + SecKeychainRef importKeychain, // optional + CSSM_CSP_HANDLE cspHand, // required + SecItemImportExportFlags flags, + const SecKeyImportExportParameters *keyParams, // optional + const char *printName, + CFMutableArrayRef outArray) // optional, append here +{ + OSStatus ortn; + impExpKeyUnwrapParams unwrapParams; + + assert(cspHand != 0); + if(keyParams == NULL) { + return errSecParam; + } + memset(&unwrapParams, 0, sizeof(unwrapParams)); + + /* derive unwrapping key */ + CSSM_KEY unwrappingKey; + + ortn = openSSHv1DeriveKey(cspHand, keyParams, VP_Import, &unwrappingKey); + if(ortn) { + return ortn; + } + + /* set up key to unwrap */ + CSSM_KEY wrappedKey; + CSSM_KEYHEADER &hdr = wrappedKey.KeyHeader; + memset(&wrappedKey, 0, sizeof(CSSM_KEY)); + hdr.HeaderVersion = CSSM_KEYHEADER_VERSION; + /* CspId : don't care */ + hdr.BlobType = CSSM_KEYBLOB_WRAPPED; + hdr.Format = CSSM_KEYBLOB_WRAPPED_FORMAT_OPENSSH1; + hdr.AlgorithmId = CSSM_ALGID_RSA; /* the oly algorithm supported in SSHv1 */ + hdr.KeyClass = CSSM_KEYCLASS_PRIVATE_KEY; + /* LogicalKeySizeInBits : calculated by CSP during unwrap */ + hdr.KeyAttr = CSSM_KEYATTR_EXTRACTABLE; + hdr.KeyUsage = CSSM_KEYUSE_ANY; + + wrappedKey.KeyData.Data = (uint8 *)CFDataGetBytePtr(inData); + wrappedKey.KeyData.Length = CFDataGetLength(inData); + + unwrapParams.unwrappingKey = &unwrappingKey; + unwrapParams.encrAlg = CSSM_ALGID_OPENSSH1; + + /* GO */ + ortn = impExpImportKeyCommon(&wrappedKey, importKeychain, cspHand, + flags, keyParams, &unwrapParams, printName, outArray); + + if(unwrappingKey.KeyData.Data != NULL) { + CSSM_FreeKey(cspHand, NULL, &unwrappingKey, CSSM_FALSE); + } + return ortn; +} + +OSStatus impExpWrappedOpenSSHExport( + SecKeyRef secKey, + SecItemImportExportFlags flags, + const SecKeyImportExportParameters *keyParams, // optional + const CssmData &descData, + CFMutableDataRef outData) // output appended here +{ + CSSM_CSP_HANDLE cspdlHand = 0; + OSStatus ortn; + bool releaseCspHand = false; + CSSM_RETURN crtn; + + if(keyParams == NULL) { + return errSecParam; + } + + /* we need a CSPDL handle - try to get it from the key */ + ortn = SecKeyGetCSPHandle(secKey, &cspdlHand); + if(ortn) { + cspdlHand = cuCspStartup(CSSM_FALSE); + if(cspdlHand == 0) { + return CSSMERR_CSSM_ADDIN_LOAD_FAILED; + } + releaseCspHand = true; + } + /* subsequent errors to errOut: */ + + /* derive wrapping key */ + CSSM_KEY wrappingKey; + crtn = openSSHv1DeriveKey(cspdlHand, keyParams, VP_Export, &wrappingKey); + if(crtn) { + goto errOut; + } + + /* GO */ + CSSM_KEY wrappedKey; + memset(&wrappedKey, 0, sizeof(CSSM_KEY)); + + crtn = impExpExportKeyCommon(cspdlHand, secKey, &wrappingKey, &wrappedKey, + CSSM_ALGID_OPENSSH1, CSSM_ALGMODE_NONE, CSSM_PADDING_NONE, + CSSM_KEYBLOB_WRAPPED_FORMAT_OPENSSH1, CSSM_ATTRIBUTE_NONE, CSSM_KEYBLOB_RAW_FORMAT_NONE, + &descData, + NULL); // IV + if(crtn) { + goto errOut; + } + + /* the wrappedKey's KeyData is out output */ + CFDataAppendBytes(outData, wrappedKey.KeyData.Data, wrappedKey.KeyData.Length); + CSSM_FreeKey(cspdlHand, NULL, &wrappedKey, CSSM_FALSE); + +errOut: + if(releaseCspHand) { + cuCspDetachUnload(cspdlHand, CSSM_FALSE); + } + return crtn; + +}