1/*
2 * Copyright (c) 2004,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 * SecImportExportPem.cpp - private PEM routines for SecImportExport
24 */
25
26#include "SecImportExportPem.h"
27#include "SecExternalRep.h"
28#include "SecImportExportUtils.h"
29#include <security_cdsa_utils/cuEnc64.h>
30#include <security_cdsa_utils/cuPem.h>
31#include <stdlib.h>
32#include <string.h>
33#include <ctype.h>
34
35/*
36 * Text parsing routines.
37 *
38 * Search incoming text for specified string. Does not assume inText is
39 * NULL terminated. Returns pointer to start of found string in inText.
40 */
41static const char *findStr(
42	const char *inText,
43	unsigned inTextLen,
44	const char *str)				// NULL terminated - search for this
45{
46	/* probably not the hottest string search algorithm... */
47	const char *cp;
48	unsigned srchStrLen = (unsigned)strlen(str);
49	char c = str[0];
50
51	/* last char * we can search in inText for start of str */
52	const char *endCp = inText + inTextLen - srchStrLen;
53
54	for(cp=inText; cp<=endCp; cp++) {
55		if(*cp == c) {
56			if(!memcmp(cp, str, srchStrLen)) {
57				return cp;
58			}
59		}
60	}
61	return NULL;
62}
63
64/*
65 * Obtain one line from current text. Returns a mallocd, NULL-terminated string
66 * which caller must free(). Also returns the number of chars consumed including
67 * the returned chars PLUS EOL terminators (\n and/or \r).
68 *
69 * ALWAYS returns a mallocd string if there is ANY data remaining per the
70 * incoming inTextLen. Returns NULL if inTextLen is zero.
71 */
72static const char *getLine(
73	const char *inText,
74	unsigned inTextLen,			// RETURNED
75	unsigned *consumed)			// RETURNED
76
77{
78	*consumed = 0;
79	const char *cp = inText;
80	const char *newline = NULL;		// if we found a newline, this points to the first one
81
82	while(inTextLen) {
83		char c = *cp;
84		if((c == '\r') || (c == '\n')) {
85			if(newline == NULL) {
86				/* first newline */
87				newline = cp;
88			}
89		}
90		else if(newline != NULL) {
91			/* non newline after newline, done */
92			break;
93		}
94		(*consumed)++;
95		inTextLen--;
96		cp++;
97	}
98	unsigned linelen;
99	if(newline) {
100		linelen = (unsigned)(newline - inText);
101	}
102	else {
103		linelen = *consumed;
104	}
105	char *rtn = (char *)malloc(linelen + 1);
106	memmove(rtn, inText, linelen);
107	rtn[linelen] = 0;
108	return rtn;
109}
110
111/*
112 * Table to facilitate conversion of known PEM header strings to
113 * the things we know about.
114 */
115typedef struct {
116	const char			*pemStr;			// e.g. PEM_STRING_X509, "CERTIFICATE"
117	SecExternalItemType itemType;
118	SecExternalFormat   format;
119	CSSM_ALGORITHMS		keyAlg;
120} PemHeader;
121
122#define NOALG   CSSM_ALGID_NONE
123
124static const PemHeader PemHeaders[] =
125{
126	/* from openssl/pem.h standard header */
127	{ PEM_STRING_X509_OLD, kSecItemTypeCertificate, kSecFormatX509Cert, NOALG},
128	{ PEM_STRING_X509,  kSecItemTypeCertificate, kSecFormatX509Cert, NOALG },
129	{ PEM_STRING_EVP_PKEY, kSecItemTypePrivateKey, kSecFormatOpenSSL, NOALG},
130	{ PEM_STRING_PUBLIC, kSecItemTypePublicKey, kSecFormatOpenSSL, NOALG },
131	{ PEM_STRING_RSA, kSecItemTypePrivateKey, kSecFormatOpenSSL, CSSM_ALGID_RSA },
132	{ PEM_STRING_RSA_PUBLIC, kSecItemTypePublicKey, kSecFormatOpenSSL, CSSM_ALGID_RSA },
133	{ PEM_STRING_DSA, kSecItemTypePrivateKey, kSecFormatOpenSSL, CSSM_ALGID_DSA },
134	{ PEM_STRING_DSA_PUBLIC, kSecItemTypePublicKey, kSecFormatOpenSSL, CSSM_ALGID_DSA },
135	{ PEM_STRING_PKCS7, kSecItemTypeAggregate, kSecFormatPKCS7, NOALG },
136	{ PEM_STRING_PKCS8, kSecItemTypePrivateKey, kSecFormatWrappedPKCS8, NOALG },
137	{ PEM_STRING_PKCS8INF, kSecItemTypePrivateKey, kSecFormatUnknown, NOALG },
138	/* we define these  */
139	{ PEM_STRING_DH_PUBLIC, kSecItemTypePublicKey, kSecFormatOpenSSL, CSSM_ALGID_DH },
140	{ PEM_STRING_DH_PRIVATE, kSecItemTypePrivateKey, kSecFormatOpenSSL, CSSM_ALGID_DH },
141	{ PEM_STRING_PKCS12, kSecItemTypeAggregate, kSecFormatPKCS12, NOALG },
142	{ PEM_STRING_SESSION, kSecItemTypeSessionKey, kSecFormatRawKey, NOALG },
143	{ PEM_STRING_ECDSA_PUBLIC, kSecItemTypePublicKey, kSecFormatOpenSSL, CSSM_ALGID_ECDSA },
144	{ PEM_STRING_ECDSA_PRIVATE, kSecItemTypePrivateKey, kSecFormatOpenSSL, CSSM_ALGID_ECDSA }
145};
146#define NUM_PEM_HEADERS (sizeof(PemHeaders) / sizeof(PemHeader))
147
148/*
149 * PEM decode incoming data which we've previously determined to contain
150 * exactly one reasonably well formed PEM blob (it has no more than one
151 * START and END line - though it may have none - and is all ASCII).
152 *
153 * Returned SecImportRep may or may not have a known type and format and
154 * (if it is a key) algorithm.
155 */
156static OSStatus impExpImportSinglePEM(
157	const char			*currCp,
158	unsigned			lenToGo,
159	CFMutableArrayRef	importReps)		// output appended here
160{
161	unsigned consumed;
162	const char *currLine = NULL;		// mallocd by getLine()
163	const char *lastCp = currCp;
164	CFMutableArrayRef pemParamLines = NULL;
165	OSStatus ortn = errSecSuccess;
166	CFDataRef cdata = NULL;
167	Security::KeychainCore::SecImportRep *rep = NULL;
168	const char *start64;
169	unsigned base64Len;
170	const char *end64;
171	unsigned char *decData;
172	unsigned decDataLen;
173
174	/* we try to glean these from the header, but it's not fatal if we can not */
175	SecExternalFormat format = kSecFormatUnknown;
176	SecExternalItemType itemType = kSecItemTypeUnknown;
177	CSSM_ALGORITHMS keyAlg = CSSM_ALGID_NONE;
178
179	/* search to START line, parse it to get type/format/alg */
180	const char *startLine = findStr(currCp, lenToGo, "-----BEGIN");
181	if(startLine != NULL) {
182		/* possibly skip over leading garbage */
183		consumed = (unsigned)(startLine - currCp);
184		lenToGo -= consumed;
185		currCp = startLine;
186
187		/* get C string of START line */
188		currLine = getLine(startLine, lenToGo, &consumed);
189		if(currLine == NULL) {
190			/* somehow got here with no data */
191			assert(lenToGo == 0);
192			SecImpInferDbg("impExpImportSinglePEM empty data");
193			ortn = errSecUnsupportedFormat;
194			goto errOut;
195		}
196		assert(consumed <= lenToGo);
197		currCp  += consumed;
198		lenToGo -= consumed;
199
200		/*
201		 * Search currLine for known PEM header strings.
202		 * It is not an error if we don't recognize this
203		 * header.
204		 */
205		for(unsigned dex=0; dex<NUM_PEM_HEADERS; dex++) {
206			const PemHeader *ph = &PemHeaders[dex];
207			if(!strstr(currLine, ph->pemStr)) {
208				continue;
209			}
210			/* found one! */
211			format   = ph->format;
212			itemType = ph->itemType;
213			keyAlg   = ph->keyAlg;
214			break;
215		}
216
217		free((void *)currLine);
218	}
219
220	/*
221	 * Skip empty lines. Save all lines containing ':' (used by openssl
222	 * to specify key wrapping parameters). These will be saved in
223	 * outgoing SecImportReps' pemParamLines.
224	 */
225	for( ; ; ) {
226		currLine = getLine(currCp, lenToGo, &consumed);
227		if(currLine == NULL || currCp == lastCp) {
228			/* out of data (unable to advance to next line) */
229			SecImpInferDbg("impExpImportSinglePEM out of data");
230			if (currLine) free((void *)currLine);
231			ortn = errSecUnsupportedFormat;
232			goto errOut;
233		}
234		lastCp = currCp;
235
236		bool skipThis = false;
237		unsigned lineLen = (unsigned)strlen(currLine);
238		if(lineLen == 0) {
239			/* empty line */
240			skipThis = true;
241		}
242		if(strchr(currLine, ':')) {
243			/*
244			 * Save this PEM header info. Used for traditional openssl
245			 * wrapped keys to indicate IV.
246			 */
247			SecImpInferDbg("import PEM: param line %s", currLine);
248			CFStringRef cfStr = CFStringCreateWithCString(NULL, currLine,
249				kCFStringEncodingASCII);
250			if(pemParamLines == NULL) {
251				/* first param line */
252				pemParamLines = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
253
254				/*
255				 * If it says "ENCRYPTED" and this is a private key,
256				 * flag the fact that it's wrapped in openssl format
257				 */
258				if(strstr(currLine, "ENCRYPTED")) {
259					if((format == kSecFormatOpenSSL) &&
260					   (itemType == kSecItemTypePrivateKey)) {
261						format = kSecFormatWrappedOpenSSL;
262					}
263				}
264			}
265			CFArrayAppendValue(pemParamLines, cfStr);
266			CFRelease(cfStr);		// array owns it
267			skipThis = true;
268		}
269		free((void *)currLine);
270		if(!skipThis) {
271			/* looks like good stuff; process */
272			break;
273		}
274		/* skip this line */
275		assert(consumed <= lenToGo);
276		currCp  += consumed;
277		lenToGo -= consumed;
278	}
279	if(lenToGo <= 2) {
280		SecImpInferDbg("impExpImportSinglePEM no valid base64 data");
281		ortn = errSecUnsupportedFormat;
282		goto errOut;
283	}
284
285	/*
286	 * currCP points to start of base64 data - mark it and search for end line.
287	 * We skip everything after the end line.
288	 */
289	start64 = currCp;
290	base64Len = lenToGo;			// if no END
291	end64 = findStr(currCp, lenToGo, "-----END");
292	if(end64 != NULL) {
293		if(end64 == start64) {
294			/* Empty, nothing between START and END */
295			SecImpInferDbg("impExpImportSinglePEM no base64 between terminators");
296			ortn = errSecUnsupportedFormat;
297			goto errOut;
298		}
299		base64Len = (unsigned)(end64 - start64);
300	}
301	/* else no END, no reason to complain about that as long as base64 decode works OK */
302
303	/* Base 64 decode */
304	decData = cuDec64((const unsigned char *)start64, base64Len, &decDataLen);
305	if(decData == NULL) {
306		SecImpInferDbg("impExpImportSinglePEM bad base64 data");
307		ortn = errSecUnsupportedFormat;
308		goto errOut;
309	}
310
311	cdata = CFDataCreate(NULL, decData, decDataLen);
312	free((void *)decData);
313	rep = new Security::KeychainCore::SecImportRep(cdata, itemType, format, keyAlg,
314		pemParamLines);
315	CFArrayAppendValue(importReps, rep);
316	CFRelease(cdata);		// SecImportRep holds ref
317	return errSecSuccess;
318
319errOut:
320	if(pemParamLines != NULL) {
321		CFRelease(pemParamLines);
322	}
323	return ortn;
324}
325
326/*
327 * PEM decode incoming data, appending SecImportRep's to specified array.
328 * Returned SecImportReps may or may not have a known type and format and
329 * (if they are keys) algorithm.
330 */
331OSStatus impExpParsePemToImportRefs(
332	CFDataRef			importedData,
333	CFMutableArrayRef	importReps,		// output appended here
334	bool				*isPem)			// true means we think it was PEM regardless of
335										// final return code
336{
337	/*
338	 * First task: is this PEM or at least base64 encoded?
339	 */
340	const char *currCp = (const char *)CFDataGetBytePtr(importedData);
341	const char *cp = currCp;
342	unsigned lenToGo = (unsigned)CFDataGetLength(importedData);
343	OSStatus ortn;
344
345	*isPem = false;
346	unsigned dex;
347	bool allBlanks = true;
348
349	for(dex=0; dex<lenToGo; dex++, cp++) {
350		if (!isspace(*cp)) {
351			// it's not a space.  Is it a non-ascii character?
352			if (!isascii(*cp)) {
353				return errSecSuccess;
354			}
355
356			// is it a control character?
357			if (iscntrl(*cp))
358			{
359				return errSecSuccess;
360			}
361
362			// no, mark that an acceptable character was encountered and keep going
363			allBlanks = false;
364		}
365	}
366
367	if (allBlanks)
368	{
369		return errSecSuccess;
370	}
371
372	/* search for START line */
373	const char *startLine = findStr(currCp, lenToGo, "-----BEGIN");
374	if(startLine == NULL) {
375		/* Assume one item, raw base64 */
376		SecImpInferDbg("impExpParsePemToImportRefs no PEM headers, assuming raw base64");
377		ortn = impExpImportSinglePEM(currCp, lenToGo, importReps);
378		if(ortn == errSecSuccess) {
379			*isPem = true;
380		}
381		return ortn;
382	}
383
384	/* break up input into chunks between START and END lines */
385	ortn = errSecSuccess;
386	bool gotSomePem = false;
387	do {
388		/* get to beginning of START line */
389		startLine = findStr(currCp, lenToGo, "-----BEGIN");
390		if(startLine == NULL) {
391			break;
392		}
393		unsigned consumed = (unsigned)(startLine - currCp);
394		assert(consumed <= lenToGo);
395		lenToGo -= consumed;
396		currCp  += consumed;
397
398		/* get to beginning of END line */
399		const char *endLine = findStr(currCp+10, lenToGo, "-----END");
400		unsigned toDecode = lenToGo;
401		if(endLine) {
402			consumed = (unsigned)(endLine - startLine);
403			assert(consumed <= lenToGo);
404			currCp  += consumed;
405			lenToGo -= consumed;
406
407			/* find end of END line */
408			const char *tmpLine = getLine(endLine, lenToGo, &consumed);
409			assert((tmpLine != NULL) && (tmpLine[0] != 0));
410			/* don't decode the terminators */
411			toDecode = (unsigned)(endLine - startLine + strlen(tmpLine));
412			free((void *)tmpLine);
413
414			/* skip past END line and newlines */
415			assert(consumed <= lenToGo);
416			currCp  += consumed;
417			lenToGo -= consumed;
418		}
419		else {
420			/* no END line, we'll allow that - decode to end of file */
421			lenToGo = 0;
422		}
423
424		ortn = impExpImportSinglePEM(startLine, toDecode, importReps);
425		if(ortn) {
426			break;
427		}
428		gotSomePem = true;
429	} while(lenToGo != 0);
430	if(ortn == errSecSuccess) {
431		if(gotSomePem) {
432			*isPem = true;
433		}
434		else {
435			SecImpInferDbg("impExpParsePemToImportRefs empty at EOF, no PEM found");
436			ortn = kSecFormatUnknown;
437		}
438	}
439	return ortn;
440}
441
442
443/*
444 * PEM encode a single SecExportRep's data, appending to a CFData.
445 */
446OSStatus impExpPemEncodeExportRep(
447	CFDataRef			derData,
448	const char			*pemHeader,
449	CFArrayRef			pemParamLines,  // optional
450	CFMutableDataRef	outData)
451{
452	unsigned char *enc;
453	unsigned encLen;
454
455	char headerLine[200];
456	if(strlen(pemHeader) > 150) {
457		return errSecParam;
458	}
459
460	/* First base64 encode */
461	enc = cuEnc64WithLines(CFDataGetBytePtr(derData), (unsigned)CFDataGetLength(derData),
462		64, &encLen);
463	if(enc == NULL) {
464		/* malloc error is actually the only known failure */
465		SecImpExpDbg("impExpPemEncodeExportRep: cuEnc64WithLines failure");
466		return errSecAllocate;
467	}
468
469	/* strip off trailing NULL */
470	if((encLen != 0) && (enc[encLen - 1] == '\0')) {
471		encLen--;
472	}
473	sprintf(headerLine, "-----BEGIN %s-----\n", pemHeader);
474	CFDataAppendBytes(outData, (const UInt8 *)headerLine, strlen(headerLine));
475
476	/* optional PEM parameters lines (currently used for openssl wrap format only) */
477	if(pemParamLines != NULL) {
478		CFIndex numLines = CFArrayGetCount(pemParamLines);
479		for(CFIndex dex=0; dex<numLines; dex++) {
480			CFStringRef cfStr =
481				(CFStringRef)CFArrayGetValueAtIndex(pemParamLines, dex);
482			char cStr[512];
483			UInt8 nl = '\n';
484			if(!CFStringGetCString(cfStr, cStr, sizeof(cStr),
485					kCFStringEncodingASCII)) {
486				/*
487				 * Should never happen; this module created this CFString
488				 * from a C string with ASCII encoding. Keep going, though
489				 * this is probably fatal to the exported representation.
490				 */
491				SecImpExpDbg("impExpPemEncodeExportRep: pemParamLine screwup");
492				continue;
493			}
494			CFDataAppendBytes(outData, (const UInt8 *)cStr, strlen(cStr));
495			CFDataAppendBytes(outData, &nl, 1);
496		}
497	}
498	CFDataAppendBytes(outData, enc, encLen);
499	sprintf(headerLine, "-----END %s-----\n", pemHeader);
500	CFDataAppendBytes(outData, (const UInt8 *)headerLine, strlen(headerLine));
501	free((void *)enc);
502	return errSecSuccess;
503}
504
505