iOS中用Objective-C实现PEM公钥转ssh-rsa公钥(仿ssh-keygen逻辑)
我明白你要实现的是终端命令ssh-keygen -i -f publicKey.pem -m PKCS8 -i > pubkey-ssh.pub的功能,在iOS应用里用Objective-C完成PKCS#8格式PEM公钥到OpenSSH ssh-rsa格式的转换。你的现有代码没成功,主要是因为没有按照OpenSSH的特定格式组装数据,而且解析PKCS#8公钥的方式也不对,下面给你完整的可行方案:
先理解转换的核心逻辑
终端的ssh-keygen命令做了这几件事:
- 读取PKCS#8格式的PEM公钥,解码成DER二进制数据
- 解析DER数据得到RSA公钥的模(n)和指数(e)
- 按照OpenSSH的规范组装二进制数据:
- 先写入4字节大端的字符串长度,再写入
ssh-rsa字符串本身 - 接着写入4字节大端的指数长度,再写入指数的二进制数据(去掉前导零)
- 最后写入4字节大端的模长度,再写入模的二进制数据(去掉前导零)
- 先写入4字节大端的字符串长度,再写入
- 把组装好的二进制数据做Base64编码,前面加上
ssh-rsa前缀,得到最终的公钥字符串
完整的Objective-C实现代码
首先确保你已经集成了OpenSSL库(可以用CocoaPods引入pod 'OpenSSL-Universal',或者手动编译集成),然后用下面的代码:
#import <openssl/rsa.h> #import <openssl/pem.h> #import <openssl/bio.h> #import <openssl/x509.h> - (NSString *)convertPKCS8PEMToSSHRSA:(NSString *)pkcs8PEM { // 1. 预处理PEM字符串:去掉头、尾和换行 NSString *cleanedPEM = [pkcs8PEM stringByReplacingOccurrencesOfString:@"-----BEGIN PUBLIC KEY-----" withString:@""]; cleanedPEM = [cleanedPEM stringByReplacingOccurrencesOfString:@"-----END PUBLIC KEY-----" withString:@""]; cleanedPEM = [cleanedPEM stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; // 2. Base64解码得到DER格式的PKCS#8公钥数据 NSData *derData = [[NSData alloc] initWithBase64EncodedString:cleanedPEM options:NSDataBase64DecodingIgnoreUnknownCharacters]; if (!derData) { NSLog(@"Invalid PEM data"); return nil; } // 3. 解析DER数据得到RSA公钥结构体 const unsigned char *derBytes = (const unsigned char *)derData.bytes; RSA *rsa = NULL; EVP_PKEY *pkey = d2i_PUBKEY(NULL, &derBytes, (long)derData.length); if (pkey) { if (EVP_PKEY_id(pkey) == EVP_PKEY_RSA) { rsa = EVP_PKEY_get1_RSA(pkey); } EVP_PKEY_free(pkey); } if (!rsa) { NSLog(@"Failed to parse RSA public key"); return nil; } // 4. 组装OpenSSH格式的二进制数据 NSMutableData *sshData = [NSMutableData data]; // 写入"ssh-rsa"字符串的长度和内容 NSString *sshHeader = @"ssh-rsa"; uint32_t headerLen = htonl((uint32_t)sshHeader.length); [sshData appendBytes:&headerLen length:sizeof(headerLen)]; [sshData appendData:[sshHeader dataUsingEncoding:NSASCIIStringEncoding]]; // 写入指数e的长度和内容(去掉前导零) BIGNUM *e = rsa->e; int eLen = BN_num_bytes(e); uint32_t eLenBigEndian = htonl((uint32_t)eLen); [sshData appendBytes:&eLenBigEndian length:sizeof(eLenBigEndian)]; unsigned char *eBytes = malloc(eLen); BN_bn2bin(e, eBytes); [sshData appendBytes:eBytes length:eLen]; free(eBytes); // 写入模n的长度和内容(去掉前导零) BIGNUM *n = rsa->n; int nLen = BN_num_bytes(n); uint32_t nLenBigEndian = htonl((uint32_t)nLen); [sshData appendBytes:&nLenBigEndian length:sizeof(nLenBigEndian)]; unsigned char *nBytes = malloc(nLen); BN_bn2bin(n, nBytes); [sshData appendBytes:nBytes length:nLen]; free(nBytes); // 5. Base64编码并加上前缀 NSString *base64Str = [sshData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength]; NSString *sshRsaKey = [NSString stringWithFormat:@"ssh-rsa %@", base64Str]; // 释放资源 RSA_free(rsa); return sshRsaKey; }
代码关键点说明
- PEM预处理:必须去掉PEM的头尾标记和多余的换行,才能正确Base64解码。
- 解析PKCS#8公钥:用
d2i_PUBKEY而不是d2i_RSAPublicKey,因为PKCS#8是通用的公钥容器,d2i_PUBKEY能正确解析它,之后再提取RSA结构体。 - OpenSSH格式组装:严格按照大端字节序写入长度字段,并且确保模和指数的二进制数据没有前导零(
BN_num_bytes和BN_bn2bin会自动处理)。 - 内存管理:所有OpenSSL分配的结构体(
EVP_PKEY、RSA)和动态内存(eBytes、nBytes)都要手动释放,避免内存泄漏。
测试方法
把你的PKCS#8格式PEM公钥传入这个方法,得到的字符串应该和ssh-keygen命令生成的完全一致。比如:
NSString *pkcs8PEM = @"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuqNpHztewgvIiPGUHb/tWozPQCJ5uEUepNZgbhh1376vO2PZcvTYo80MVppFLzCpvEVYOLPtg4/rztT9OkN/5U4AxjFB2oz85Y3Fb6mJCVN4cQ2x+IfhUYEfy3/+uum9jO9Kh0sEiAFt5Cvok3nT8YpjjmXgDiEmudMKhP0+uocuZcWwBTLeeDkgoMYlEl6p9wPntsz0cIoU3KZNXmWRNS1b8NPYDDjOHDYuF3N8ZCWS/9SzgN2qGY3MZCiWtX3VRWeGiUbWcmGAlz0xrVSXC4iZWsQ+CSqfw4KMvTfPJrm2C7azcsrGuYtELPEqc/42UPuQbuO6M3l+ZABjw1KP\n-----END PUBLIC KEY-----"; NSString *sshRsaKey = [self convertPKCS8PEMToSSHRSA:pkcs8PEM]; NSLog(@"%@", sshRsaKey);
输出应该就是你期望的ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC6qU2kfO17CC8iI8ZQdv+1ajM9AInm4RR6k1mBuGHXfvq87Y9ly/NihzQxWmkUvMKm8RVg4s+2Dj+vO1P06Q3/lTgDGMUHajPzljcVvqYkJU3hxDbH4h+FRgR/Lf/6q6b2M70qHSwSIAW3kK+iTedPximOOZeAOISa50wqE/Tr6hy5lxbAFMt54OSCgxiUSXql3C+e2zPRwihTcpk1eZZE1LVvw09gOOM4cNi4Xc3xkJZL/1LOA3aoZdjxkCIa1fdVFZ4aJRtZyoYAXPTGtVJcLiJlaxD4JKp/Dgoy9N88nuyYLtrNyuxq5i0Qs8Spx/jZQ+5Bu47ozeX5kAGPDUo9这样的格式。
内容的提问来源于stack exchange,提问作者Sandeep Kumar

