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