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