可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
In Swift, I created a SecKeyRef
object by calling SecTrustCopyPublicKey
on some raw X509 certificate data. This is what this SecKeyRef
object looks like.
Optional(<SecKeyRef algorithm id: 1,
key type: RSAPublicKey,
version: 3, block size: 2048 bits,
exponent: {hex: 10001, decimal: 65537},
modulus: <omitted a bunch of hex data>,
addr: 0xsomeaddresshere>)
Basically, this SecKeyRef
object holds a whole bunch of information about the public key, but there seems to be no way to actually convert this SecKeyRef
into a string, NSData
, or anything else (this is my goal, is just to get a base64 public key).
However, I have a function that I can give a modulus
and an exponent
, and it will just calculate what the public key is. I've tested it by passing in the data that's logged from the above SecKeyRef
.
But somehow I can't access those properties from the SecKeyRef
object (I can only see the whole object in the console; for example, I cannot do SecKeyRef.modulus
or anything of the sort, it seems).
My question: how can I access SecKeyRef.modulus
, or alternatively, convert this SecKeyRef
into NSData
or something similar? Thanks
Edit
(for more information)
I am creating my SecKeyRef
dynamically, through this function I have:
func bytesToPublicKey(certData: NSData) -> SecKeyRef? {
guard let certRef = SecCertificateCreateWithData(nil, certData) else { return nil }
var secTrust: SecTrustRef?
let secTrustStatus = SecTrustCreateWithCertificates(certRef, nil, &secTrust)
if secTrustStatus != errSecSuccess { return nil }
var resultType: SecTrustResultType = UInt32(0) // result will be ignored.
let evaluateStatus = SecTrustEvaluate(secTrust!, &resultType)
if evaluateStatus != errSecSuccess { return nil }
let publicKeyRef = SecTrustCopyPublicKey(secTrust!)
return publicKeyRef
}
What that does is takes the raw byte stream from a certificate (which can be broadcasted from, say, a piece of hardware using PKI), and then turns that into a SecKeyRef
.
Edit 2
(comments on existing answers as of 7 January 2015)
This does not work:
let mirror = Mirror(reflecting: mySecKeyObject)
for case let (label?, value) in mirror.children {
print (label, value)
}
This results in this output in the console:
Some <Raw SecKeyRef object>
Not sure what the string "Some" means.
Additionally, mirror.descendant("exponent")
(or "modulus") results in nil
, even though when printing the raw object in the console, I can clearly see that those properties exist, and that they are in fact populated.
Also, if at all possible, I would like to avoid having to save to the keychain, reading as NSData
, and then deleting from the keychain. As stated in the bounty description, if this is the only way possible, please cite an authoritative reference. Thank you for all answers provided so far.
回答1:
I've been down the same path trying to do SSL Public Key Pinning. The API's are pretty much non-existent, and the solution I found was to put it in the Keychain which you can then retrieve as NSData (which can then be Base64 Encoded). It's horrible but the only thing I could find after a day or so of research (without resorting to bundling OpenSSL with my app).
I ported some of my code over to Swift, but I haven't tested it very much so I'm not 100% sure that it works: https://gist.github.com/chedabob/64a4cdc4a1194d815814
It's based off this Obj-C code (which I'm confident works as it's in a production app): https://gist.github.com/chedabob/49eed109a3dfcad4bd41
回答2:
It is indeed possible to extract modulus and exponent using neither keychains nor private API.
There is the (public but undocumented) function SecKeyCopyAttributes
which extracts a CFDictionary
from a SecKey
.
A useful source for attribute keys is SecItemConstants.c
Inspecting the content of this dictionary, we find an entry "v_Data" : <binary>
. Its content is DER-encoded ASN for
SEQUENCE {
modulus INTEGER,
publicExponent INTEGER
}
Be aware that integers are padded with a zero byte if they are positive and have a leading 1-bit (so as not to confuse them with a two-complement negative number), so you may find one byte more than you expect. If that happens, just cut it away.
You can implement a parser for this format or, knowing your key size, hard-code the extraction. For 2048 bit keys (and 3-byte exponent), the format turns out to be:
30|82010(a|0) # Sequence of length 0x010(a|0)
02|82010(1|0) # Integer of length 0x010(1|0)
(00)?<modulus>
02|03 # Integer of length 0x03
<exponent>
For a total of 10 + 1? + 256 + 3 = 269 or 270 bytes.
import Foundation
extension String: Error {}
func parsePublicSecKey(publicKey: SecKey) -> (mod: Data, exp: Data) {
let pubAttributes = SecKeyCopyAttributes(publicKey) as! [String: Any]
// Check that this is really an RSA key
guard Int(pubAttributes[kSecAttrKeyType as String] as! String)
== Int(kSecAttrKeyTypeRSA as String) else {
throw "Tried to parse non-RSA key as RSA key"
}
// Check that this is really a public key
guard Int(pubAttributes[kSecAttrKeyClass as String] as! String)
== Int(kSecAttrKeyClassPublic as String)
else {
throw "Tried to parse non-public key as public key"
}
let keySize = pubAttributes[kSecAttrKeySizeInBits as String] as! Int
// Extract values
let pubData = pubAttributes[kSecValueData as String] as! Data
var modulus = pubData.subdata(in: 8..<(pubData.count - 5))
let exponent = pubData.subdata(in: (pubData.count - 3)..<pubData.count)
if modulus.count > keySize / 8 { // --> 257 bytes
modulus.removeFirst(1)
}
return (mod: modulus, exp: exponent)
}
(I ended up writing a full ASN parser, so this code is not tested, beware!)
Note that you can extract details of private keys in very much the same way. Using DER terminology, this is the format of v_Data
:
PrivateKey ::= SEQUENCE {
version INTEGER,
modulus INTEGER, -- n
publicExponent INTEGER, -- e
privateExponent INTEGER, -- d
prime1 INTEGER, -- p
prime2 INTEGER, -- q
exponent1 INTEGER, -- d mod (p-1) (dmp1)
exponent2 INTEGER, -- d mod (q-1) (dmq1)
coefficient INTEGER, -- (inverse of q) mod p (coeff)
otherPrimeInfos OtherPrimeInfos OPTIONAL
}
Parsing this by hand is probably ill-advised since any of the integers may have been padded.
Nota bene: The format of the public key is different if the key has been generated on macOS; the structure given above is wrapped like so:
SEQUENCE {
id OBJECTID,
PublicKey BITSTRING
}
The bit-string is DER-encoded ASN of the form above.
回答3:
Update The answer below might result in your app being rejected due to usage of non-public API's.
The answer lies in the SecRSAKey.h file from Apple's opensource website (Security is part of the code that Apple opensourced). The file is not big, and among other stuff it declares the following two important functions:
CFDataRef SecKeyCopyModulus(SecKeyRef rsaPublicKey);
CFDataRef SecKeyCopyExponent(SecKeyRef rsaPublicKey);
You can add those functions to your bridging header to be able to call them from Swift, also while doing this you can switch from CFDataRef
to NSData*
as the two types toll-free bridged:
NSData* SecKeyCopyModulus(SecKeyRef rsaPublicKey);
NSData* SecKeyCopyExponent(SecKeyRef rsaPublicKey);
Demo Swift usage:
let key = bytesToPublicKey(keyData)
let modulus = SecKeyCopyModulus(key)
let exponent = SecKeyCopyExponent(key)
print(modulus, exponent)
This is a private API though, and there might be a chance it will no longer be available at some point, however I looked over the versions of Security
made public (http://www.opensource.apple.com/source/Security), and looks like the two functions are present in all of them. More, since Security
is a critical component of the OS, it's unlikely Apple will do major changes over it.
Tested on iOS 8.1, iOS 9.2, and OSX 10.10.5, and the code works on all three platforms.
回答4:
SecKeyRef
is a struct so there is a chance that it can be reflected with Mirror()
to retrieve the wanted values.
struct myStruct {
let firstString = "FirstValue"
let secondString = "SecondValue"}
let testStruct = myStruct()
let mirror = Mirror(reflecting: testStruct)
for case let (label?, value) in mirror.children {
print (label, value)
}
/**
Prints:
firstString FirstValue
secondString SecondValue
*/
回答5:
I've found a single Obj-c re-implementation of the ASN.1 parser in an abandoned project, that appears to work. Problem is, it uses a great deal of pointer tricks that I don't know how to translate into Swift (not even sure some of it is possible). It should be possible to create a swift friendly wrapper around it, since the only input it takes is the NSData.
Everything on the net is using the store and retrieve in the Keychain trick to get to the pub key data, even really popular libs like TrustKit. I found reference in the Apple docs on SecKeyRef to the root cause (I think):
A SecKeyRef object for a key that is stored in a keychain can be
safely cast to a SecKeychainItemRef for manipulation as a keychain
item. On the other hand, if the SecKeyRef is not stored in a keychain,
casting the object to a SecKeychainItemRef and passing it to Keychain
Services functions returns errors.
Since SecCertificateCopyValues
isn't available on iOS at this time, you're limited to either parsing the certificate data, or doing the Keychain Item shuffle.
回答6:
I found how to get data for a SecKey
.
let publicKey: SecKey = ...
let data = SecKeyCopyExternalRepresentation(publicKey, nil)
This seems to work well and I have been able to successfully compare public keys.
This is in Swift 3 (Xcode 8 beta 3)
回答7:
Did you think about using SecCertificateCopyData()
? The resulting CFData
is toll-Free bridged, I think.
Refer to https://developer.apple.com/library/ios/documentation/Security/Reference/certifkeytrustservices/ to see the relevant documentation of the API.
回答8:
From How do I encode an unmanaged<SecKey> to base64 to send to another server? :
func convertSecKeyToBase64(inputKey: SecKey) ->String? {
// Add to keychain
let tempTag = "net.example." + NSUUID().UUIDString
let addParameters :[String:AnyObject] = [
String(kSecClass): kSecClassKey,
String(kSecAttrApplicationTag): tempTag,
String(kSecAttrKeyType): kSecAttrKeyTypeRSA,
String(kSecValueRef): inputKey,
String(kSecReturnData):kCFBooleanTrue
]
var result: String?
var keyPtr: AnyObject?
if (SecItemAdd(addParameters, &keyPtr) == noErr) {
let data = keyPtr! as! NSData
result = data.base64EncodedStringWithOptions(NSDataBase64EncodingOptions(rawValue: 0))
}
// Remove from Keychain:
SecItemDelete(addParameters)
return result
}
But if you want to avoid adding to keychain, you can use Mirror:
let mirrorKey = Mirror(reflecting: secKey)
let exponent = mirrorKey.descendant("exponent")
let modulus = mirrorKey.descendant("modulus");
[edit: Mirror not working according to Josh]
回答9:
I wrote this one base on some other's answer in stackoverflow. Currently I am using it in my production but I am happy to use another solution that doesn't require to write into keychain.
- (NSData *)getPublicKeyBitsFromKey:(SecKeyRef)givenKey host:(NSString*)host {
NSString *tag = [NSString stringWithFormat:@"%@.%@",[[NSBundle mainBundle] bundleIdentifier], host];
const char* publicKeyIdentifier = [tag cStringUsingEncoding:NSUTF8StringEncoding];
NSData *publicTag = [[NSData alloc] initWithBytes:publicKeyIdentifier length:strlen(publicKeyIdentifier) * sizeof(char)];
OSStatus sanityCheck = noErr;
// NSData * publicKeyBits = nil;
CFTypeRef publicKeyBits;
NSMutableDictionary * queryPublicKey = [[NSMutableDictionary alloc] init];
// Set the public key query dictionary.
[queryPublicKey setObject:(id)kSecClassKey forKey:(id)kSecClass];
[queryPublicKey setObject:publicTag forKey:(id)kSecAttrApplicationTag];
[queryPublicKey setObject:(id)kSecAttrKeyTypeRSA forKey:(id)kSecAttrKeyType];
[queryPublicKey setObject:[NSNumber numberWithBool:YES] forKey:(id)kSecReturnData];
[queryPublicKey setObject:(__bridge id)givenKey forKey:(__bridge id)kSecValueRef];
// Get the key bits.
NSData *data = nil;
sanityCheck = SecItemCopyMatching((CFDictionaryRef)queryPublicKey, &publicKeyBits);
if (sanityCheck == errSecSuccess) {
data = CFBridgingRelease(publicKeyBits);
//I don't want to leak this information
(void)SecItemDelete((__bridge CFDictionaryRef) queryPublicKey);
}else {
sanityCheck = SecItemAdd((CFDictionaryRef)queryPublicKey, &publicKeyBits);
if (sanityCheck == errSecSuccess)
{
data = CFBridgingRelease(publicKeyBits);
(void)SecItemDelete((__bridge CFDictionaryRef) queryPublicKey);
}
}
return data;
}