/ OSX / libsecurity_keychain / lib / SecImportExportOpenSSH.cpp
SecImportExportOpenSSH.cpp
  1  /*
  2   * Copyright (c) 2006,2011-2014 Apple Inc. All Rights Reserved.
  3   * 
  4   * @APPLE_LICENSE_HEADER_START@
  5   * 
  6   * This file contains Original Code and/or Modifications of Original Code
  7   * as defined in and that are subject to the Apple Public Source License
  8   * Version 2.0 (the 'License'). You may not use this file except in
  9   * compliance with the License. Please obtain a copy of the License at
 10   * http://www.opensource.apple.com/apsl/ and read it before using this
 11   * file.
 12   * 
 13   * The Original Code and all software distributed under the License are
 14   * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 15   * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 16   * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 17   * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 18   * Please see the License for the specific language governing rights and
 19   * limitations under the License.
 20   * 
 21   * @APPLE_LICENSE_HEADER_END@
 22   */
 23  
 24  
 25  /*
 26   * opensshCoding.cpp - Encoding and decoding of OpenSSH format public keys.
 27   *
 28   */
 29  
 30  #include "SecImportExportOpenSSH.h"
 31  #include "SecImportExportUtils.h"
 32  #include "SecImportExportCrypto.h"
 33  #include <ctype.h>
 34  #include <CommonCrypto/CommonDigest.h>	/* for CC_MD5_DIGEST_LENGTH */
 35  #include <security_utilities/debugging.h>
 36  #include <security_cdsa_utils/cuCdsaUtils.h>
 37  
 38  #define SecSSHDbg(args...)	secinfo("openssh", ## args)
 39  
 40  #define SSHv2_PUB_KEY_NAME		"OpenSSHv2 Public Key"
 41  #define SSHv1_PUB_KEY_NAME		"OpenSSHv1 Public Key"
 42  #define SSHv1_PRIV_KEY_NAME		"OpenSSHv1 Private Key"
 43  
 44  #pragma mark --- Utility functions ---
 45  
 46  /* skip whitespace */
 47  static void skipWhite(
 48  	const unsigned char *&cp,
 49  	unsigned &bytesLeft)
 50  {
 51  	while(bytesLeft != 0) {
 52  		if(isspace((int)(*cp))) {
 53  			cp++;
 54  			bytesLeft--;
 55  		}
 56  		else {
 57  			return;
 58  		}
 59  	}
 60  }
 61  
 62  /* find next whitespace or EOF - if EOF, rtn pointer points to one past EOF */
 63  static const unsigned char *findNextWhite(
 64  	const unsigned char *cp,
 65  	unsigned &bytesLeft)
 66  {
 67  	while(bytesLeft != 0) {
 68  		if(isspace((int)(*cp))) {
 69  			return cp;
 70  		}
 71  		cp++;
 72  		bytesLeft--;
 73  	}
 74  	return cp;
 75  }
 76  
 77  /* obtain comment as the n'th whitespace-delimited field */
 78  static char *commentAsNthField(
 79  	const unsigned char *key,
 80  	unsigned keyLen,
 81  	unsigned n)
 82  
 83  {
 84  	unsigned dex;
 85  	
 86  	skipWhite(key, keyLen);
 87  	if(keyLen == 0) {
 88  		return NULL;
 89  	}
 90  	for(dex=0; dex<(n-1); dex++) {
 91  		key = findNextWhite(key, keyLen);
 92  		if(keyLen == 0) {
 93  			return NULL;
 94  		}
 95  		skipWhite(key, keyLen);
 96  		if(keyLen == 0) {
 97  			return NULL;
 98  		}
 99  	}
100  	
101  	/* cp points to start of nth field */
102  	char *rtnStr = (char *)malloc(keyLen + 1);
103  	memmove(rtnStr, key, keyLen);
104  	if(rtnStr[keyLen - 1] == '\n') {
105  		/* normal terminator - snip it off */
106  		rtnStr[keyLen - 1] = '\0';
107  	}
108  	else {
109  		rtnStr[keyLen] = '\0';
110  	}
111  	return rtnStr;
112  
113  }
114  
115  static uint32_t readUint32(
116  	const unsigned char *&cp,		// IN/OUT
117  	unsigned &len)					// IN/OUT 
118  {
119  	uint32_t r = 0;
120  	
121  	for(unsigned dex=0; dex<sizeof(uint32_t); dex++) {
122  		r <<= 8;
123  		r |= *cp++;
124  	}
125  	len -= 4;
126  	return r;
127  }
128  
129  static uint16_t readUint16(
130  	const unsigned char *&cp,		// IN/OUT
131  	unsigned &len)					// IN/OUT 
132  {
133  	uint16_t r = *cp++;
134  	r <<= 8;
135  	r |= *cp++;
136  	len -= 2;
137  	return r;
138  }
139  
140  /* Skip over an SSHv1 private key formatted bignum */
141  static void skipBigNum(
142  	const unsigned char *&cp,		// IN/OUT
143  	unsigned &len)					// IN/OUT 
144  {
145  	if(len < 2) {
146  		cp += len;
147  		len = 0;
148  		return;
149  	}
150  	uint16 numBits = readUint16(cp, len);
151  	unsigned numBytes = (numBits + 7) / 8;
152  	if(numBytes > len) {
153  		cp += len;
154  		len = 0;
155  		return;
156  	}
157  	cp += numBytes;
158  	len -= numBytes;
159  }
160  
161  static char *genPrintName(
162  	const char *header,		// e.g. SSHv2_PUB_KEY_NAME
163  	const char *comment)	// optional, from key 
164  {
165  	size_t totalLen = strlen(header) + 1;
166  	if(comment) {
167  		/* append ": <comment>" */
168  		totalLen += strlen(comment);
169  		totalLen += 2;
170  	}
171  	char *rtnStr = (char *)malloc(totalLen);
172  	if(comment) {
173  		snprintf(rtnStr, totalLen, "%s: %s", header, comment);
174  	}
175  	else {
176  		strcpy(rtnStr, header);
177  	}
178  	return rtnStr;
179  }
180  
181  #pragma mark --- Infer PrintName attribute from raw keys ---
182  
183  /* obtain comment from OpenSSHv2 public key */
184  static char *opensshV2PubComment(
185  	const unsigned char *key,
186  	unsigned keyLen)
187  {
188  	/*
189  	 * The format here is
190  	 * header
191  	 * <space>
192  	 * keyblob
193  	 * <space>
194  	 * optional comment
195  	 * \n
196  	 */
197  	char *comment = commentAsNthField(key, keyLen, 3);
198  	char *rtnStr = genPrintName(SSHv2_PUB_KEY_NAME, comment);
199  	if(comment) {
200  		free(comment);
201  	}
202  	return rtnStr;
203  }
204  
205  /* obtain comment from OpenSSHv1 public key */
206  static char *opensshV1PubComment(
207  	const unsigned char *key,
208  	unsigned keyLen)
209  {
210  	/*
211  	 * Format:
212  	 * numbits
213  	 * <space>
214  	 * e (bignum in decimal)
215  	 * <space>
216  	 * n (bignum in decimal) 
217  	 * <space>
218  	 * optional comment 
219  	 * \n
220  	 */
221  	char *comment = commentAsNthField(key, keyLen, 4);
222  	char *rtnStr = genPrintName(SSHv1_PUB_KEY_NAME, comment);
223  	if(comment) {
224  		free(comment);
225  	}
226  	return rtnStr;
227  }
228  
229  static const char *authfile_id_string = "SSH PRIVATE KEY FILE FORMAT 1.1\n";
230  
231  /* obtain comment from OpenSSHv1 private key, wrapped or clear */
232  static char *opensshV1PrivComment(
233  	const unsigned char *key,
234  	unsigned keyLen)
235  {
236  	/* 
237  	 * Format:
238  	 * "SSH PRIVATE KEY FILE FORMAT 1.1\n"
239  	 * 1 byte cipherSpec
240  	 * 4 byte spares
241  	 * 4 bytes numBits
242  	 * bignum n
243  	 * bignum e
244  	 * 4 byte comment length 
245  	 * comment
246  	 * private key components, possibly encrypted 
247  	 *
248  	 * A bignum is encoded like so:
249  	 * 2 bytes numBits
250  	 * (numBits + 7)/8 bytes of data
251  	 */
252  	/* length: ID string, NULL, Cipher, 4-byte spare */
253  	size_t len = strlen(authfile_id_string);
254  	if(keyLen < (len + 6)) {
255  		return NULL;
256  	}
257  	if(memcmp(authfile_id_string, key, len)) {
258  		return NULL;
259  	}
260  	key    += (len + 6);
261  	keyLen -= (len + 6);
262  	
263  	/* key points to numBits */
264  	if(keyLen < 4) {
265  		return NULL;
266  	}
267  	key += 4;
268  	keyLen -= 4;
269  	
270  	/* key points to n */
271  	skipBigNum(key, keyLen);
272  	if(keyLen == 0) {
273  		return NULL;
274  	}
275  	skipBigNum(key, keyLen);
276  	if(keyLen == 0) {
277  		return NULL;
278  	}
279  	
280  	char *comment = NULL;
281  	uint32 commentLen = readUint32(key, keyLen);
282  	if((commentLen != 0) && (commentLen <= keyLen)) {
283  		comment = (char *)malloc(commentLen + 1);
284  		memmove(comment, key, commentLen);
285  		comment[commentLen] = '\0';
286  	}
287  	
288  	char *rtnStr = genPrintName(SSHv1_PRIV_KEY_NAME, comment);
289  	if(comment) {
290  		free(comment);
291  	}
292  	return rtnStr;
293  }
294  
295  /* 
296   * Infer PrintName attribute from raw key's 'comment' field. 
297   * Returned string is mallocd and must be freed by caller. 
298   */
299  char *impExpOpensshInferPrintName(
300  	CFDataRef external, 
301  	SecExternalItemType externType, 
302  	SecExternalFormat externFormat)
303  {
304  	const unsigned char *key = (const unsigned char *)CFDataGetBytePtr(external);
305  	unsigned keyLen = (unsigned)CFDataGetLength(external);
306  	switch(externType) {
307  		case kSecItemTypePublicKey:
308  			switch(externFormat) {
309  				case kSecFormatSSH:
310  					return opensshV1PubComment(key, keyLen);
311  				case kSecFormatSSHv2:
312  					return opensshV2PubComment(key, keyLen);
313  				default:
314  					/* impossible, right? */
315  					break;
316  			}
317  			break;
318  		case kSecItemTypePrivateKey:
319  			switch(externFormat) {
320  				case kSecFormatSSH:
321  				case kSecFormatWrappedSSH:
322  					return opensshV1PrivComment(key, keyLen);
323  				default:
324  					break;
325  			}
326  			break;
327  		default:
328  			break;
329  	}
330  	return NULL;
331  }
332  
333  #pragma mark --- Infer DescriptiveData from PrintName ---
334  
335  /* 
336   * Infer DescriptiveData (i.e., comment) from a SecKeyRef's PrintName
337   * attribute.
338   */
339  void impExpOpensshInferDescData(
340  	SecKeyRef keyRef,
341  	CssmOwnedData &descData)
342  {
343  	OSStatus ortn;
344  	SecKeychainAttributeInfo attrInfo;
345  	SecKeychainAttrType	attrType = kSecKeyPrintName;
346  	attrInfo.count = 1;
347  	attrInfo.tag = &attrType;
348  	attrInfo.format = NULL;	
349  	SecKeychainAttributeList *attrList = NULL;
350  	
351  	ortn = SecKeychainItemCopyAttributesAndData(
352  		(SecKeychainItemRef)keyRef, 
353  		&attrInfo,
354  		NULL,			// itemClass
355  		&attrList, 
356  		NULL,			// don't need the data
357  		NULL);
358  	if(ortn) {
359  		SecSSHDbg("SecKeychainItemCopyAttributesAndData returned %ld", (unsigned long)ortn);
360  		return;
361  	}
362  	/* subsequent errors to errOut: */
363  	SecKeychainAttribute *attr = attrList->attr;
364  		
365  	/*
366  	 * On a previous import, we would have set this to something like 
367  	 * "OpenSSHv2 Public Key: comment".
368  	 * We want to strip off everything up to the actual comment.
369  	 */
370  	unsigned toStrip = 0;	
371  	
372  	/* min length of attribute value for this code to be meaningful */
373  	unsigned len = strlen(SSHv2_PUB_KEY_NAME) + 1;	
374  	char *printNameStr = NULL;
375  	if(len < attr->length) {
376  		printNameStr = (char *)malloc(attr->length + 1);
377  		memmove(printNameStr, attr->data, attr->length);
378  		printNameStr[attr->length] = '\0';
379  		if(strstr(printNameStr, SSHv2_PUB_KEY_NAME) == printNameStr) {
380  			toStrip = strlen(SSHv2_PUB_KEY_NAME);
381  		}
382  		else if(strstr(printNameStr, SSHv1_PUB_KEY_NAME) == printNameStr) {
383  			toStrip = strlen(SSHv1_PUB_KEY_NAME);
384  		}
385  		else if(strstr(printNameStr, SSHv1_PRIV_KEY_NAME) == printNameStr) {
386  			toStrip = strlen(SSHv1_PRIV_KEY_NAME);
387  		}
388  		if(toStrip) {
389  			/* only strip if we have ": " after toStrip bytes */
390  			if((printNameStr[toStrip] == ':') && (printNameStr[toStrip+1] == ' ')) {
391  				toStrip += 2;
392  			}
393  		}
394  	}
395  	if(printNameStr) {
396  		free(printNameStr);
397  	}
398  	len = attr->length;
399  
400  	unsigned char *attrVal;
401  
402  	if(len < toStrip) {
403  		SecSSHDbg("impExpOpensshInferDescData: string parse screwup");
404  		goto errOut;
405  	}
406  	if(len > toStrip) {
407  		/* Normal case of stripping off leading header */
408  		len -= toStrip;
409  	}
410  	else {
411  		/* 
412  		 * If equal, then the attr value *is* "OpenSSHv2 Public Key: " with 
413  		 * no comment. Not sure how that could happen, but let's be careful.
414  		 */
415  		toStrip = 0;
416  	}
417  	
418  	attrVal = ((unsigned char *)attr->data) + toStrip;
419  	descData.copy(attrVal, len);
420  errOut:
421  	SecKeychainItemFreeAttributesAndData(attrList, NULL);
422  	return;
423  }
424  
425  #pragma mark --- Derive SSHv1 wrap/unwrap key ---
426  
427  /* 
428   * Common code to derive a wrap/unwrap key for OpenSSHv1.
429   * Caller must CSSM_FreeKey when done.
430   */
431  static CSSM_RETURN openSSHv1DeriveKey(
432  	CSSM_CSP_HANDLE		cspHand,
433  	const SecKeyImportExportParameters	*keyParams,		// required 
434  	impExpVerifyPhrase  verifyPhrase,					// for secure passphrase
435  	CSSM_KEY_PTR		symKey)							// RETURNED
436  {
437  	CSSM_KEY					*passKey = NULL;
438  	CFDataRef					cfPhrase = NULL;
439  	CSSM_RETURN					crtn;
440  	OSStatus					ortn;
441  	CSSM_DATA					dummyLabel;
442  	uint32						keyAttr;
443  	CSSM_CC_HANDLE 				ccHand = 0;
444  	CSSM_ACCESS_CREDENTIALS		creds;
445  	CSSM_CRYPTO_DATA			seed;
446  	CSSM_DATA					nullParam = {0, NULL};
447  	
448  	memset(symKey, 0, sizeof(CSSM_KEY));
449  	
450  	/* passphrase or passkey? */
451  	ortn = impExpPassphraseCommon(keyParams, cspHand, SPF_Data, verifyPhrase,
452  		(CFTypeRef *)&cfPhrase, &passKey);
453  	if(ortn) {
454  		return ortn;
455  	}
456  	/* subsequent errors to errOut: */
457  
458  	memset(&seed, 0, sizeof(seed));
459  	if(cfPhrase != NULL) {
460  		/* TBD - caller-supplied empty passphrase means "export in the clear" */
461  		size_t len = CFDataGetLength(cfPhrase);
462  		seed.Param.Data = (uint8 *)malloc(len);
463  		seed.Param.Length = len;
464  		memmove(seed.Param.Data, CFDataGetBytePtr(cfPhrase), len);
465  		CFRelease(cfPhrase);
466  	}
467  
468  	memset(&creds, 0, sizeof(CSSM_ACCESS_CREDENTIALS));
469  	crtn = CSSM_CSP_CreateDeriveKeyContext(cspHand,
470  		CSSM_ALGID_OPENSSH1,
471  		CSSM_ALGID_OPENSSH1,
472  		CC_MD5_DIGEST_LENGTH * 8,
473  		&creds,
474  		passKey,		// BaseKey
475  		0,				// iterationCount
476  		NULL,			// salt
477  		&seed,
478  		&ccHand);
479  	if(crtn) {
480  		SecSSHDbg("openSSHv1DeriveKey CSSM_CSP_CreateDeriveKeyContext failure");
481  		goto errOut;
482  	}
483  	
484  	/* not extractable even for the short time this key lives */
485  	keyAttr = CSSM_KEYATTR_RETURN_REF | CSSM_KEYATTR_SENSITIVE;
486  	dummyLabel.Data = (uint8 *)"temp unwrap key";
487  	dummyLabel.Length = strlen((char *)dummyLabel.Data);
488  	
489  	crtn = CSSM_DeriveKey(ccHand,
490  		&nullParam,
491  		CSSM_KEYUSE_ANY,
492  		keyAttr,
493  		&dummyLabel,
494  		NULL,			// cred and acl
495  		symKey);
496  	if(crtn) {
497  		SecSSHDbg("openSSHv1DeriveKey CSSM_DeriveKey failure");
498  	}
499  errOut:
500  	if(ccHand != 0) {
501  		CSSM_DeleteContext(ccHand);
502  	}
503  	if(passKey != NULL) {
504  		CSSM_FreeKey(cspHand, NULL, passKey, CSSM_FALSE);
505  		free(passKey);
506  	}
507  	if(seed.Param.Data) {
508  		memset(seed.Param.Data, 0, seed.Param.Length);
509  		free(seed.Param.Data);
510  	}
511  	return crtn;
512  }
513  
514  #pragma mark -- OpenSSHv1 Wrap/Unwrap ---
515  
516  /*
517   * If cspHand is provided instead of importKeychain, the CSP 
518   * handle MUST be for the CSPDL, not for the raw CSP.
519   */
520  OSStatus impExpWrappedOpenSSHImport(
521  	CFDataRef							inData,
522  	SecKeychainRef						importKeychain, // optional
523  	CSSM_CSP_HANDLE						cspHand,		// required
524  	SecItemImportExportFlags			flags,
525  	const SecKeyImportExportParameters	*keyParams,		// optional 
526  	const char							*printName,
527  	CFMutableArrayRef					outArray)		// optional, append here 
528  {
529  	OSStatus				ortn;
530  	impExpKeyUnwrapParams   unwrapParams;
531  
532  	assert(cspHand != 0);
533  	if(keyParams == NULL) {
534  		return errSecParam;
535  	}
536  	memset(&unwrapParams, 0, sizeof(unwrapParams));
537  
538  	/* derive unwrapping key */
539  	CSSM_KEY unwrappingKey;
540  	
541  	ortn = openSSHv1DeriveKey(cspHand, keyParams, VP_Import, &unwrappingKey);
542  	if(ortn) {
543  		return ortn;
544  	}
545  	
546  	/* set up key to unwrap */
547  	CSSM_KEY				wrappedKey;
548  	CSSM_KEYHEADER			&hdr = wrappedKey.KeyHeader;
549  	memset(&wrappedKey, 0, sizeof(CSSM_KEY));
550  	hdr.HeaderVersion = CSSM_KEYHEADER_VERSION;
551  	/* CspId : don't care */
552  	hdr.BlobType = CSSM_KEYBLOB_WRAPPED;
553  	hdr.Format = CSSM_KEYBLOB_WRAPPED_FORMAT_OPENSSH1;
554  	hdr.AlgorithmId = CSSM_ALGID_RSA;		/* the oly algorithm supported in SSHv1 */
555  	hdr.KeyClass = CSSM_KEYCLASS_PRIVATE_KEY;
556  	/* LogicalKeySizeInBits : calculated by CSP during unwrap */
557  	hdr.KeyAttr = CSSM_KEYATTR_EXTRACTABLE;
558  	hdr.KeyUsage = CSSM_KEYUSE_ANY;
559  
560  	wrappedKey.KeyData.Data = (uint8 *)CFDataGetBytePtr(inData);
561  	wrappedKey.KeyData.Length = CFDataGetLength(inData);
562  	
563  	unwrapParams.unwrappingKey = &unwrappingKey;
564  	unwrapParams.encrAlg = CSSM_ALGID_OPENSSH1;
565  	
566  	/* GO */
567  	ortn =  impExpImportKeyCommon(&wrappedKey, importKeychain, cspHand,
568  		flags, keyParams, &unwrapParams, printName, outArray);
569  		
570  	if(unwrappingKey.KeyData.Data != NULL) {
571  		CSSM_FreeKey(cspHand, NULL, &unwrappingKey, CSSM_FALSE);
572  	}
573  	return ortn;
574  }
575  
576  OSStatus impExpWrappedOpenSSHExport(
577  	SecKeyRef							secKey,
578  	SecItemImportExportFlags			flags,		
579  	const SecKeyImportExportParameters	*keyParams,		// optional 
580  	const CssmData						&descData,
581  	CFMutableDataRef					outData)		// output appended here
582  {
583  	CSSM_CSP_HANDLE cspdlHand = 0;
584  	OSStatus ortn;
585  	bool releaseCspHand = false;
586  	CSSM_RETURN crtn;
587  	
588  	if(keyParams == NULL) {
589  		return errSecParam;
590  	}
591  
592  	/* we need a CSPDL handle - try to get it from the key */	
593  	ortn = SecKeyGetCSPHandle(secKey, &cspdlHand);
594  	if(ortn) {
595  		cspdlHand = cuCspStartup(CSSM_FALSE);
596  		if(cspdlHand == 0) {
597  			return CSSMERR_CSSM_ADDIN_LOAD_FAILED;
598  		}
599  		releaseCspHand = true;
600  	}
601  	/* subsequent errors to errOut: */
602  
603  	/* derive wrapping key */
604  	CSSM_KEY wrappingKey;
605  	crtn = openSSHv1DeriveKey(cspdlHand, keyParams, VP_Export, &wrappingKey);
606  	if(crtn) {
607  		goto errOut;
608  	}
609  	
610  	/* GO */
611  	CSSM_KEY wrappedKey;
612  	memset(&wrappedKey, 0, sizeof(CSSM_KEY));
613  	
614  	crtn = impExpExportKeyCommon(cspdlHand, secKey, &wrappingKey, &wrappedKey,
615  		CSSM_ALGID_OPENSSH1, CSSM_ALGMODE_NONE, CSSM_PADDING_NONE,
616  		CSSM_KEYBLOB_WRAPPED_FORMAT_OPENSSH1, CSSM_ATTRIBUTE_NONE, CSSM_KEYBLOB_RAW_FORMAT_NONE, 
617  		&descData,
618  		NULL);	// IV
619  	if(crtn) {
620  		goto errOut;
621  	}
622  	
623  	/* the wrappedKey's KeyData is out output */
624  	CFDataAppendBytes(outData, wrappedKey.KeyData.Data, wrappedKey.KeyData.Length);
625  	CSSM_FreeKey(cspdlHand, NULL, &wrappedKey, CSSM_FALSE);
626  	
627  errOut:
628  	if(releaseCspHand) {
629  		cuCspDetachUnload(cspdlHand, CSSM_FALSE);
630  	}
631  	return crtn;
632  
633  }