/* * 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 "SecImportExportOpenSSH.h" #include "SecImportExportUtils.h" #include "SecImportExportCrypto.h" #include #include /* for CC_MD5_DIGEST_LENGTH */ #include #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 { unsigned 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 */ unsigned 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 = 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", 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" */ unsigned 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 paramErr; } 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 paramErr; } /* 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; }