1/*
2 * Copyright (c) 2006,2011-2012,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 "opensshCoding.h"
31#include <CoreFoundation/CFData.h>
32#include <openssl/bn.h>
33#include <openssl/crypto.h>
34#include <security_cdsa_utils/cuEnc64.h>
35
36#define SSH2_RSA_HEADER		"ssh-rsa"
37#define SSH2_DSA_HEADER		"ssh-dss"
38
39#ifndef	NDEBUG
40#include <stdio.h>
41#define dprintf(s...)		printf(s)
42#else
43#define dprintf(...)
44#endif
45
46#pragma mark --- commmon code ---
47
48uint32_t readUint32(
49	const unsigned char *&cp,		// IN/OUT
50	unsigned &len)					// IN/OUT
51{
52	uint32_t r = 0;
53
54	for(unsigned dex=0; dex<sizeof(uint32_t); dex++) {
55		r <<= 8;
56		r |= *cp++;
57	}
58	len -= 4;
59	return r;
60}
61
62void appendUint32(
63	CFMutableDataRef cfOut,
64	uint32_t ui)
65{
66	UInt8 buf[sizeof(uint32_t)];
67
68	for(int dex=(sizeof(uint32_t) - 1); dex>=0; dex--) {
69		buf[dex] = ui & 0xff;
70		ui >>= 8;
71	}
72	CFDataAppendBytes(cfOut, buf, sizeof(uint32_t));
73}
74
75
76/* parse text as decimal, return BIGNUM */
77static BIGNUM *parseDecimalBn(
78	const unsigned char *cp,
79	unsigned len)
80{
81	for(unsigned dex=0; dex<len; dex++) {
82		char c = *cp;
83		if((c < '0') || (c > '9')) {
84			return NULL;
85		}
86	}
87	char *str = (char *)malloc(len + 1);
88	memmove(str, cp, len);
89	str[len] = '\0';
90	BIGNUM *bn = NULL;
91	BN_dec2bn(&bn, str);
92	free(str);
93	return bn;
94}
95
96/* write BIGNUM, OpenSSH v2 format (with a 4-byte byte count) */
97static CSSM_RETURN appendBigNum2(
98	CFMutableDataRef cfOut,
99	const BIGNUM *bn)
100{
101	if(bn == NULL) {
102		dprintf("appendBigNum2: NULL bn");
103		return CSSMERR_CSP_INTERNAL_ERROR;
104	}
105	if (BN_is_zero(bn)) {
106		appendUint32(cfOut, 0);
107		return 0;
108	}
109	if(bn->neg) {
110		dprintf("appendBigNum2: negative numbers not supported\n");
111		return CSSMERR_CSP_INTERNAL_ERROR;
112	}
113	int numBytes = BN_num_bytes(bn);
114	unsigned char buf[numBytes];
115	int moved = BN_bn2bin(bn, buf);
116	if(moved != numBytes) {
117		dprintf("appendBigNum: BN_bn2bin() screwup\n");
118		return CSSMERR_CSP_INTERNAL_ERROR;
119	}
120	bool appendZero = false;
121	if(buf[0] & 0x80) {
122		/* prepend leading zero to make it positive */
123		appendZero = true;
124		numBytes++;		// to encode the correct 4-byte length
125	}
126	appendUint32(cfOut, (uint32_t)numBytes);
127	if(appendZero) {
128		UInt8 z = 0;
129		CFDataAppendBytes(cfOut, &z, 1);
130		numBytes--;		// to append the correct number of bytes
131	}
132	CFDataAppendBytes(cfOut, buf, numBytes);
133	memset(buf, 0, numBytes);
134	return CSSM_OK;
135}
136
137/* read BIGNUM, OpenSSH-2 mpint version */
138static BIGNUM *readBigNum2(
139	const unsigned char *&cp,	// IN/OUT
140	unsigned &remLen)			// IN/OUT
141{
142	if(remLen < 4) {
143		dprintf("readBigNum2: short record(1)\n");
144		return NULL;
145	}
146	uint32_t bytes = readUint32(cp, remLen);
147	if(remLen < bytes) {
148		dprintf("readBigNum2: short record(2)\n");
149		return NULL;
150	}
151	BIGNUM *bn = BN_bin2bn(cp, bytes, NULL);
152	if(bn == NULL) {
153		dprintf("readBigNum2: BN_bin2bn error\n");
154		return NULL;
155	}
156	cp += bytes;
157	remLen -= bytes;
158	return bn;
159}
160
161/* Write BIGNUM, OpenSSH-1 decimal (public key) version */
162static CSSM_RETURN appendBigNumDec(
163	CFMutableDataRef cfOut,
164	const BIGNUM *bn)
165{
166	char *buf = BN_bn2dec(bn);
167	if(buf == NULL) {
168		dprintf("appendBigNumDec: BN_bn2dec() error");
169		return CSSMERR_CSP_INTERNAL_ERROR;
170	}
171	CFDataAppendBytes(cfOut, (const UInt8 *)buf, strlen(buf));
172	Free(buf);
173	return CSSM_OK;
174}
175
176/* write string, OpenSSH v2 format (with a 4-byte byte count) */
177static void appendString(
178	CFMutableDataRef cfOut,
179	const char *str,
180	unsigned strLen)
181{
182	appendUint32(cfOut, (uint32_t)strLen);
183	CFDataAppendBytes(cfOut, (UInt8 *)str, strLen);
184}
185
186/* skip whitespace */
187static void skipWhite(
188	const unsigned char *&cp,
189	unsigned &bytesLeft)
190{
191	while(bytesLeft != 0) {
192		if(isspace((int)(*cp))) {
193			cp++;
194			bytesLeft--;
195		}
196		else {
197			return;
198		}
199	}
200}
201
202/* find next whitespace or EOF - if EOF, rtn pointer points to one past EOF */
203static const unsigned char *findNextWhite(
204	const unsigned char *cp,
205	unsigned &bytesLeft)
206{
207	while(bytesLeft != 0) {
208		if(isspace((int)(*cp))) {
209			return cp;
210		}
211		cp++;
212		bytesLeft--;
213	}
214	return cp;
215}
216
217
218/*
219 * Decode components from an SSHv2 public key.
220 * Also verifies the leading header, e.g. "ssh-rsa".
221 * The returned decodedBlob is algorithm-specific.
222 */
223static CSSM_RETURN parseSSH2PubKey(
224	const unsigned char *key,
225	unsigned keyLen,
226	const char *header,				// SSH2_RSA_HEADER, SSH2_DSA_HEADER
227	unsigned char **decodedBlob,	// mallocd and RETURNED
228	unsigned *decodedBlobLen)		// RETURNED
229{
230	size_t len = strlen(header);
231	*decodedBlob = NULL;
232
233	/* ID string plus at least one space */
234	if(keyLen < (len + 1)) {
235		dprintf("parseSSH2PubKey: short record(1)\n");
236		return CSSMERR_CSP_INVALID_KEY;
237	}
238
239	if(memcmp(header, key, len)) {
240		dprintf("parseSSH2PubKey: bad header (1)\n");
241		return CSSMERR_CSP_INVALID_KEY;
242	}
243	key += len;
244	if(*key++ != ' ') {
245		dprintf("parseSSH2PubKey: bad header (2)\n");
246		return CSSMERR_CSP_INVALID_KEY;
247	}
248	keyLen -= (len + 1);
249
250	/* key points to first whitespace after header */
251	skipWhite(key, keyLen);
252	if(keyLen == 0) {
253		dprintf("parseSSH2PubKey: short key\n");
254		return CSSMERR_CSP_INVALID_KEY;
255	}
256
257	/* key is start of base64 blob */
258	const unsigned char *encodedBlob = key;
259	const unsigned char *endBlob = findNextWhite(key, keyLen);
260	unsigned encodedBlobLen = (unsigned)(endBlob - encodedBlob);
261
262	/* decode base 64 */
263	*decodedBlob = cuDec64(encodedBlob, encodedBlobLen, decodedBlobLen);
264	if(*decodedBlob == NULL) {
265		dprintf("parseSSH2PubKey: base64 decode error\n");
266		return CSSMERR_CSP_INVALID_KEY;
267	}
268
269	/* skip remainder; it's comment */
270
271	return CSSM_OK;
272}
273
274
275#pragma mark -- RSA OpenSSHv1 ---
276
277CSSM_RETURN RSAPublicKeyEncodeOpenSSH1(
278	RSA 			*rsa,
279	const CssmData	&descData,
280	CssmOwnedData	&encodedKey)
281{
282	CFMutableDataRef cfOut = CFDataCreateMutable(NULL, 0);
283	CSSM_RETURN ourRtn = CSSM_OK;
284
285	/*
286	 * Format is
287	 * num_bits in decimal
288	 * <space>
289	 * e, bignum in decimal
290	 * <space>
291	 * n, bignum in decimal
292	 * <space>
293	 * optional comment
294	 * newline
295	 */
296	unsigned numBits = BN_num_bits(rsa->n);
297	char bitString[20];
298	UInt8 c = ' ';
299
300	snprintf(bitString, sizeof(bitString), "%u ", numBits);
301	CFDataAppendBytes(cfOut, (const UInt8 *)bitString, strlen(bitString));
302	if((ourRtn = appendBigNumDec(cfOut, rsa->e))) {
303		goto errOut;
304	}
305	CFDataAppendBytes(cfOut, &c, 1);
306	if((ourRtn = appendBigNumDec(cfOut, rsa->n))) {
307		goto errOut;
308	}
309
310	if(descData.Length) {
311		/* optional comment */
312		CFDataAppendBytes(cfOut, &c, 1);
313		CFDataAppendBytes(cfOut, (UInt8 *)descData.Data, descData.Length);
314	}
315
316	c = '\n';
317	CFDataAppendBytes(cfOut, &c, 1);
318	encodedKey.copy(CFDataGetBytePtr(cfOut), CFDataGetLength(cfOut));
319errOut:
320	CFRelease(cfOut);
321	return ourRtn;
322}
323
324CSSM_RETURN RSAPublicKeyDecodeOpenSSH1(
325	RSA 			*rsa,
326	void 			*p,
327	size_t			length)
328{
329	const unsigned char *cp = (const unsigned char *)p;
330	unsigned remLen = (unsigned)length;
331
332	skipWhite(cp, remLen);
333
334	/*
335	 * cp points to start of size_in_bits in ASCII decimal; we really don't care about
336	 * this field. Find next space.
337	 */
338	cp = findNextWhite(cp, remLen);
339	if(remLen == 0) {
340		dprintf("RSAPublicKeyDecodeOpenSSH1: short key (1)\n");
341		return CSSMERR_CSP_INVALID_KEY;
342	}
343	skipWhite(cp, remLen);
344	if(remLen == 0) {
345		dprintf("RSAPublicKeyDecodeOpenSSH1: short key (2)\n");
346		return CSSMERR_CSP_INVALID_KEY;
347	}
348
349	/*
350	 * cp points to start of e
351	 */
352	const unsigned char *ep = findNextWhite(cp, remLen);
353	if(remLen == 0) {
354		dprintf("RSAPublicKeyDecodeOpenSSH1: short key (3)\n");
355		return CSSMERR_CSP_INVALID_KEY;
356	}
357	unsigned len = (unsigned)(ep - cp);
358	rsa->e = parseDecimalBn(cp, len);
359	if(rsa->e == NULL) {
360		return CSSMERR_CSP_INVALID_KEY;
361	}
362	cp += len;
363
364	skipWhite(cp, remLen);
365	if(remLen == 0) {
366		dprintf("RSAPublicKeyDecodeOpenSSH1: short key (4)\n");
367		return -1;
368	}
369
370	/* cp points to start of n */
371	ep = findNextWhite(cp, remLen);
372	len = (unsigned)(ep - cp);
373	rsa->n = parseDecimalBn(cp, len);
374	if(rsa->n == NULL) {
375		return CSSMERR_CSP_INVALID_KEY;
376	}
377
378	/* remainder is comment, we ignore */
379	return CSSM_OK;
380
381}
382
383CSSM_RETURN RSAPrivateKeyEncodeOpenSSH1(
384	RSA 			*rsa,
385	const CssmData	&descData,
386	CssmOwnedData	&encodedKey)
387{
388	CFDataRef cfOut;
389	CSSM_RETURN ourRtn;
390
391	ourRtn = encodeOpenSSHv1PrivKey(rsa, descData.Data, (unsigned)descData.Length, NULL, &cfOut);
392	if(ourRtn) {
393		return ourRtn;
394	}
395	encodedKey.copy(CFDataGetBytePtr(cfOut), CFDataGetLength(cfOut));
396	CFRelease(cfOut);
397	return CSSM_OK;
398}
399
400extern CSSM_RETURN RSAPrivateKeyDecodeOpenSSH1(
401	RSA 			*openKey,
402	void 			*p,
403	size_t			length)
404{
405	return decodeOpenSSHv1PrivKey((const unsigned char *)p, (unsigned)length,
406		openKey, NULL, NULL, NULL);
407}
408
409#pragma mark -- RSA OpenSSHv2 ---
410
411CSSM_RETURN RSAPublicKeyEncodeOpenSSH2(
412	RSA 			*rsa,
413	const CssmData	&descData,
414	CssmOwnedData	&encodedKey)
415{
416	unsigned char *b64 = NULL;
417	unsigned b64Len;
418	UInt8 c;
419
420	/*
421	 * First, the inner base64-encoded blob, consisting of
422	 * ssh-rsa
423	 * e
424	 * n
425	 */
426	CFMutableDataRef cfOut = CFDataCreateMutable(NULL, 0);
427	CSSM_RETURN ourRtn = CSSM_OK;
428	appendString(cfOut, SSH2_RSA_HEADER, strlen(SSH2_RSA_HEADER));
429	if((ourRtn = appendBigNum2(cfOut, rsa->e))) {
430		goto errOut;
431	}
432	if((ourRtn = appendBigNum2(cfOut, rsa->n))) {
433		goto errOut;
434	}
435
436	/* base64 encode that */
437	b64 = cuEnc64((unsigned char *)CFDataGetBytePtr(cfOut), (unsigned)CFDataGetLength(cfOut), &b64Len);
438
439	/* cuEnc64 added newline and NULL, which we really don't want */
440	b64Len -= 2;
441
442	/* Now start over, dropping that base64 into a public blob. */
443	CFDataSetLength(cfOut, 0);
444	CFDataAppendBytes(cfOut, (UInt8 *)SSH2_RSA_HEADER, strlen(SSH2_RSA_HEADER));
445	c = ' ';
446	CFDataAppendBytes(cfOut, &c, 1);
447	CFDataAppendBytes(cfOut, b64, b64Len);
448
449	if(descData.Length) {
450		/* optional comment */
451		CFDataAppendBytes(cfOut, &c, 1);
452		CFDataAppendBytes(cfOut, (UInt8 *)descData.Data, descData.Length);
453	}
454
455	/* finish it with a newline */
456	c = '\n';
457	CFDataAppendBytes(cfOut, &c, 1);
458
459	encodedKey.copy(CFDataGetBytePtr(cfOut), CFDataGetLength(cfOut));
460errOut:
461	CFRelease(cfOut);
462	if(b64) {
463		free(b64);
464	}
465	return ourRtn;
466}
467
468CSSM_RETURN RSAPublicKeyDecodeOpenSSH2(
469	RSA 			*rsa,
470	void 			*p,
471	size_t			length)
472{
473	const unsigned char *key = (const unsigned char *)p;
474	unsigned keyLen = (unsigned)length;
475	CSSM_RETURN ourRtn;
476
477	/*
478	 * Verify header
479	 * get base64-decoded blob
480	 */
481	unsigned char *decodedBlob = NULL;
482	unsigned decodedBlobLen = 0;
483	if((ourRtn = parseSSH2PubKey(key, keyLen, SSH2_RSA_HEADER, &decodedBlob, &decodedBlobLen))) {
484		return ourRtn;
485	}
486	/* subsequent errors to errOut: */
487
488	/*
489	 * The inner base64-decoded blob, consisting of
490	 * ssh-rsa
491	 * e
492	 * n
493	 */
494	uint32_t decLen;
495	unsigned len;
496
497	key = decodedBlob;
498	keyLen = decodedBlobLen;
499	if(keyLen < 12) {
500		/* three length fields at least */
501		dprintf("RSAPublicKeyDecodeOpenSSH2: short record(2)\n");
502		ourRtn = -1;
503		goto errOut;
504	}
505	decLen = readUint32(key, keyLen);
506	len = strlen(SSH2_RSA_HEADER);
507	if(decLen != len) {
508		dprintf("RSAPublicKeyDecodeOpenSSH2: bad header (2)\n");
509		ourRtn = CSSMERR_CSP_INVALID_KEY;
510		goto errOut;
511	}
512	if(memcmp(SSH2_RSA_HEADER, key, len)) {
513		dprintf("RSAPublicKeyDecodeOpenSSH2: bad header (1)\n");
514		return CSSMERR_CSP_INVALID_KEY;
515	}
516	key += len;
517	keyLen -= len;
518
519	rsa->e = readBigNum2(key, keyLen);
520	if(rsa->e == NULL) {
521		ourRtn = CSSMERR_CSP_INVALID_KEY;
522		goto errOut;
523	}
524	rsa->n = readBigNum2(key, keyLen);
525	if(rsa->n == NULL) {
526		ourRtn = CSSMERR_CSP_INVALID_KEY;
527		goto errOut;
528	}
529
530errOut:
531	free(decodedBlob);
532	return ourRtn;
533}
534
535#pragma mark -- DSA OpenSSHv2 ---
536
537CSSM_RETURN DSAPublicKeyEncodeOpenSSH2(
538	DSA 			*dsa,
539	const CssmData	&descData,
540	CssmOwnedData	&encodedKey)
541{
542	unsigned char *b64 = NULL;
543	unsigned b64Len;
544	UInt8 c;
545
546	/*
547	 * First, the inner base64-encoded blob, consisting of
548	 * ssh-dss
549	 * p
550	 * q
551	 * g
552	 * pub_key
553	 */
554	CFMutableDataRef cfOut = CFDataCreateMutable(NULL, 0);
555	int ourRtn = 0;
556	appendString(cfOut, SSH2_DSA_HEADER, strlen(SSH2_DSA_HEADER));
557	if((ourRtn = appendBigNum2(cfOut, dsa->p))) {
558		goto errOut;
559	}
560	if((ourRtn = appendBigNum2(cfOut, dsa->q))) {
561		goto errOut;
562	}
563	if((ourRtn = appendBigNum2(cfOut, dsa->g))) {
564		goto errOut;
565	}
566	if((ourRtn = appendBigNum2(cfOut, dsa->pub_key))) {
567		goto errOut;
568	}
569
570	/* base64 encode that */
571	b64 = cuEnc64((unsigned char *)CFDataGetBytePtr(cfOut), (unsigned)CFDataGetLength(cfOut), &b64Len);
572
573	/* cuEnc64 added newline and NULL, which we really don't want */
574	b64Len -= 2;
575
576	/* Now start over, dropping that base64 into a public blob. */
577	CFDataSetLength(cfOut, 0);
578	CFDataAppendBytes(cfOut, (UInt8 *)SSH2_DSA_HEADER, strlen(SSH2_DSA_HEADER));
579	c = ' ';
580	CFDataAppendBytes(cfOut, &c, 1);
581	CFDataAppendBytes(cfOut, b64, b64Len);
582
583	if(descData.Length) {
584		/* optional comment */
585		CFDataAppendBytes(cfOut, &c, 1);
586		CFDataAppendBytes(cfOut, (UInt8 *)descData.Data, descData.Length);
587	}
588
589	/* finish it with a newline */
590	c = '\n';
591	CFDataAppendBytes(cfOut, &c, 1);
592
593	encodedKey.copy(CFDataGetBytePtr(cfOut), CFDataGetLength(cfOut));
594
595errOut:
596	CFRelease(cfOut);
597	if(b64) {
598		free(b64);
599	}
600	return ourRtn;
601}
602
603CSSM_RETURN DSAPublicKeyDecodeOpenSSH2(
604	DSA 			*dsa,
605	void 			*p,
606	size_t			length)
607{
608	const unsigned char *key = (const unsigned char *)p;
609	unsigned keyLen = (unsigned)length;
610	CSSM_RETURN ourRtn;
611
612	/*
613	 * Verify header
614	 * get base64-decoded blob
615	 */
616	unsigned char *decodedBlob = NULL;
617	unsigned decodedBlobLen = 0;
618	if((ourRtn = parseSSH2PubKey(key, keyLen, SSH2_DSA_HEADER, &decodedBlob, &decodedBlobLen))) {
619		return ourRtn;
620	}
621	/* subsequent errors to errOut: */
622
623	/*
624	 * The inner base64-decoded blob, consisting of
625	 * ssh-dss
626	 * p
627	 * q
628	 * g
629	 * pub_key
630	 */
631	uint32_t decLen;
632	unsigned len;
633
634	key = decodedBlob;
635	keyLen = decodedBlobLen;
636	if(keyLen < 20) {
637		/* five length fields at least */
638		dprintf("DSAPublicKeyDecodeOpenSSH2: short record(2)\n");
639		ourRtn = CSSMERR_CSP_INVALID_KEY;
640		goto errOut;
641	}
642	decLen = readUint32(key, keyLen);
643	len = strlen(SSH2_DSA_HEADER);
644	if(decLen != len) {
645		dprintf("DSAPublicKeyDecodeOpenSSH2: bad header (2)\n");
646		ourRtn = CSSMERR_CSP_INVALID_KEY;
647		goto errOut;
648	}
649	if(memcmp(SSH2_DSA_HEADER, key, len)) {
650		dprintf("DSAPublicKeyDecodeOpenSSH2: bad header (1)\n");
651		return CSSMERR_CSP_INVALID_KEY;
652	}
653	key += len;
654	keyLen -= len;
655
656	dsa->p = readBigNum2(key, keyLen);
657	if(dsa->p == NULL) {
658		ourRtn = CSSMERR_CSP_INVALID_KEY;
659		goto errOut;
660	}
661	dsa->q = readBigNum2(key, keyLen);
662	if(dsa->q == NULL) {
663		ourRtn = CSSMERR_CSP_INVALID_KEY;
664		goto errOut;
665	}
666	dsa->g = readBigNum2(key, keyLen);
667	if(dsa->g == NULL) {
668		ourRtn = CSSMERR_CSP_INVALID_KEY;
669		goto errOut;
670	}
671	dsa->pub_key = readBigNum2(key, keyLen);
672	if(dsa->pub_key == NULL) {
673		ourRtn = CSSMERR_CSP_INVALID_KEY;
674		goto errOut;
675	}
676
677errOut:
678	free(decodedBlob);
679	return ourRtn;
680
681}
682
683