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...)	secdebug("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 */
47static 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 */
63static 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 */
78static 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
115static 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
129static 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 */
141static 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
161static 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 */
184static 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 */
206static 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
229static const char *authfile_id_string = "SSH PRIVATE KEY FILE FORMAT 1.1\n";
230
231/* obtain comment from OpenSSHv1 private key, wrapped or clear */
232static 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 */
299char *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 */
339void 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);
420errOut:
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 */
431static 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	}
499errOut:
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 */
520OSStatus 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
576OSStatus 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
627errOut:
628	if(releaseCspHand) {
629		cuCspDetachUnload(cspdlHand, CSSM_FALSE);
630	}
631	return crtn;
632
633}
634