Ad

Our DNA is written in Swift
Jump

Apple’s ASN.1 OID Names

For the DTCertificateViewer component that I am presently working on I needed to have a list of all known OIDs. Those are the tags (Object Identifiers) that identify the meaning of information encoded in CER/DER/ASN.1 files.

For example OID 2.5.4.6 means “Country Name”. I was able to glean a few dozen such identifiers from looking at a variety of certificates, but I couldn’t find a complete – and localized – list of those names anywhere online. Also Apple has registered a boatload of their own OIDs like “1.2.840.113635.100.6.1.2” = “Apple Developer Certificate (Development)”.

Since I’m building this component for use on iOS and Mac it became clear that I needed to go straight to the horse’s mouth to get my strings. In this article I am going to explain how I got my list of OIDs, in all 30 languages that OS X is localized in.

The first step was to search for a system framework that might contain a strings file for these OIDs.

Finding the OID Strings

After several failed attempts I honed in on /System/Library/Frameworks where I did some searching.

cd /System/Library/Frameworks
find . -name "*OID*"
./Security.framework/Versions/A/Headers/SecCertificateOIDs.h
./SecurityFoundation.framework/Versions/A/Resources/ar.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/ca.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/cs.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/da.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/Dutch.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/el.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/English.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/fi.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/French.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/German.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/he.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/hr.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/hu.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/Italian.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/Japanese.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/ko.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/no.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/pl.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/pt.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/pt_PT.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/ro.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/ru.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/sk.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/Spanish.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/sv.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/th.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/tr.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/uk.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/zh_CN.lproj/OID.strings
./SecurityFoundation.framework/Versions/A/Resources/zh_TW.lproj/OID.strings

Strings files contained inside compiled apps or frameworks are typically converted into binary plist format for efficiency. The reason for this is that internally the string macros are getting the strings by key from an NSDictionary. So we just need to change the extension from .strings to .plist and double-click on such a file to view it.

OID.plist

Ok, we got the strings, but what weird keys are those? To understand this we have to look at how Certificates are encoded. The encoding for certificates is called ASN.1 and certificates are using the so called Distinguished Encoding Rules (DER) which are basically just a slightly more strict way of using ASN.1.

Deciphering the Keys

On an iOS app submission certificate I found this extension:

Apple Certificate Extension

All the OIDs beginning with 1.2.840.113635.100.6 group Apple’s custom extensions. Now if we view this certificate with an online ASN1 viewer we find this to be looking like this:

ASN1 Viewer

ASN1 employs a scheme called TLV (Tag-Length-Value), the Length 2+10 in this case means that there are 2 bytes for the header comprised of tag 6 and length 10, followed by the special encoding for an object identifier (tag value 6). Compare the hex dump on the right side with the keys in the strings file and you find that they exactly match.

For a moment there I marveled at the Apple engineer’s decision to use the entire TLV sequence as the key, as opposed to – say – the OID in dot notation string format. You can have NSData as a key in a dictionary, but you can only use NSString as key in property lists on disk. So what he did was to represent the OID data as a hex string and use this as a key.

Writing a Converter

The final step on this project was to convert the keys to the dot notation format and create a textual strings file for each of the 30 language-specific plists. Other people might be going to Ruby or some other scripting language but I prefer to be able to reuse my already existing Objective-C code. Since we are not going to be doing this type of conversion all the time the best approach is to create a one-off command line utility.

I’m including the utility with my DTCertificateViewer project should you purchase a license.

The steps are:

  1. create a Foundation-based command line utility
  2. link in DTFoundation for Mac to get the ASN.1 decoder
  3. implement handling of command line parameters to specify input and output file names
  4. load the plist at the input path, convert the hex keys to NSData, deserialize these into strings
  5. save the strings to the output file path

Since my original command line utility tutorial I learned that command line arguments are getting merged with the user defaults. So the initial setup of the utility is very simple. We retrieve the input and output values and if either one is missing we print out a usage info.

int main(int argc, const char * argv[])
{
   @autoreleasepool
   {
      // command line arguments get merged into user defaults
      NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
      NSString *inputPath = [defaults objectForKey:@"input"];
      NSString *outputPath = [defaults objectForKey:@"output"];
 
      if (!inputPath || !outputPath)
      {
         printf("Usage: OUTStrings -input <inputfile> -output <outputfile>\n");
         return 1;
      }
 
      // work goes here
 
   }
   return 0;
}

The next step is to reconstitute the NSData for the keys from the hex representation. By adding DTFoundation we get the ability to decode the ASN.1 data from the keys and ability to deserialize the string representation.

So the initial work setup looked like this

NSDictionary *inDict = [NSDictionary dictionaryWithContentsOfFile:inputPath];
NSMutableDictionary *outDict = [NSMutableDictionary dictionary];
 
for (NSString *oneKey in inDict)
{
   NSData *keyData = [oneKey dataFromHexRepresentation];
   NSString *oidString = [DTASN1Serialization objectWithData:keyData];
 
   if (oidString)
   {
      outDict[oidString] = inDict[oneKey];
   }
   else
   {
      NSLog(@"Unable to decode '%@' with string '%@'",
         [NSString hexStringSerialNumberFromData:keyData], inDict[oneKey]);
   }
}
 
[outDict writeToFile:outputPath atomically:YES];

So much for the theory.

Fixing Apple’s Bugs

In practice it turns out that Apple has several malformed entries in this list. I found one instance where there was one extra byte after the key which threw off my parser. And I also found several instances where the length field was longer than the actual data.

Filed as rdar://13436964 and on Open Radar.

// too many V bytes
06 05 2B 24 03 04 02 01 = 'ISO9796-2 with RED'
06 06 2A 86 48 CE 38 03 01 = 'Countersignature'
06 06 2A 86 48 CE 38 03 02 = 'Attribute Certificate'
06 07 2B 0C 02 87 73 07 03 01 = 'DEC MD2 with RSA'
06 07 2B 0C 02 87 73 07 03 02 = 'DEC MD4 with RSA'
06 07 2B 0C 02 87 73 07 03 03 = 'DEC DEA-MAC'
06 07 2B 0C 02 87 73 07 02 01 = 'DEC MD2'
06 07 2B 0C 02 87 73 07 02 02 = 'DEC MD4'
06 0B 60 86 48 01 65 02 01 0C 00 01 00 00 = 'TSP1 Tag Set Zero'
06 0B 60 86 48 01 65 02 01 0C 00 01 00 01 = 'TSP1 Tag Set One'
06 0B 60 86 48 01 65 02 01 0C 00 01 00 02 = 'TSP1 Tag Set Two'
06 0B 60 86 48 01 65 02 01 0C 00 02 00 00 = 'TSP2 Tag Set Zero'
06 0B 60 86 48 01 65 02 01 0C 00 02 00 01 = 'TSP2 Tag Set One'
06 0B 60 86 48 01 65 02 01 0C 00 02 00 02 = 'TSP2 Tag Set Two'
06 0B 60 86 48 01 65 02 01 0C 00 03 00 01 = 'Kafka Tag Set Name 1'
06 0B 60 86 48 01 65 02 01 0C 00 03 00 02 = 'Kafka Tag Set Name 2'
06 0B 60 86 48 01 65 02 01 0C 00 03 00 03 = 'Kafka Tag Set Name 3'

// too little V bytes
06 08 2A 86 48 86 F7 0D 02 = 'RSADSI Digest Algorithm'
06 08 2A 86 48 86 F7 0D 03 = 'RSADSI Encryption Algorithm'
06 09 2A 86 48 86 F7 14 01 02 81 71 = 'Delivery Mechanism'
06 09 2A 86 48 CE 3D 01 02 03 = 'Characteristic-Two Basis'
06 09 60 86 48 01 65 02 01 0B = 'US Department of Defense Infosec'
06 0A 2A 86 48 CE 3D 01 02 03 01 = 'Null Basis'
06 0A 2A 86 48 CE 3D 01 02 03 02 = 'Trinomial Basis'
06 0A 2A 86 48 CE 3D 01 02 03 03 = 'Pentanomial Basis'
06 0B 2A 83 08 8C 1A 4B 3D 01 01 01 = 'Symmetric Encryption Algorithm'
06 0B 60 86 48 01 86 F8 37 01 02 08 81 02 = 'MD4 Packet'
06 0B 60 86 48 01 86 F8 37 01 02 08 81 05 = 'Novell Obfuscate-1'
06 0C 2A 83 08 8C 9A 4B 3D 01 01 01 01 = 'MISTY1-CBC'

The second octet should always be the number of remaining bytes, but for the above examples this is not.

I copied together the above list and there I found that even though the length octet seems to have an incorrect length all the octets appear to be significant. For example the 3 keys prefixed 06 0A 2A have an incorrect length of 10, even though there only 9 bytes. But the rightmost octet forms a sequence.

Because of this I deducted that the length value needed correcting because the data by itself was correct.

NSMutableData *keyData = [[oneKey dataFromHexRepresentation] mutableCopy];
 
// calculate the proper length
uint8 correctLength = [keyData length]-2;
 
uint8 *bytes = (uint8 *)[keyData bytes];
uint8 originalLength = bytes[1];
 
if (correctLength != originalLength)
{
   NSLog(@"Incorrect length: %@ = '%@'", 
      [NSString hexStringSerialNumberFromData:keyData], inDict[oneKey]);
   [keyData replaceBytesInRange:NSMakeRange(1, 1) withBytes:&correctLength];
}
 
NSString *oidString = [DTASN1Serialization objectWithData:keyData];

After this detour all that was remaining was to do was to output the dictionary, preferably sorted by key.

NSArray *sortedKeys = [[outDict allKeys] sortedArrayUsingComparator:^NSComparisonResult(NSString *key1, NSString *key2) {
	if ([key1 isEqualToString:key2])
	{
		return NSOrderedSame;
	}
 
	NSArray *key1Components = [key1 componentsSeparatedByString:@"."];
	NSArray *key2Components = [key2 componentsSeparatedByString:@"."];
 
	NSUInteger minComponents = MIN([key1Components count], [key2Components count]);
 
	for (int i=0; i<minComponents; i++)
	{
		NSInteger key1Value = [key1Components[i] integerValue];
		NSInteger key2Value = [key2Components[i] integerValue];
 
		if (key1Value<key2Value)
		{
			return NSOrderedAscending;
		}
		else if (key1Value>key2Value)
		{
			return NSOrderedDescending;
		}
	}
 
	// at this point the shorter wins
 
	if ([key1Components count] < [key2Components count])
	{
		return NSOrderedAscending;
	}
	else if ([key1Components count] > [key2Components count])
	{
		return NSOrderedDescending;
	}
 
	return NSOrderedSame;
}];
 
NSMutableString *tmpString = [NSMutableString string];
 
for (NSString *oneKey in sortedKeys)
{
	[tmpString appendFormat:@"\"%@\" = \"%@\";\n", oneKey, outDict[oneKey]];
}
 
NSError *error;
if (![tmpString writeToFile:outputPath atomically:YES encoding:NSUTF8StringEncoding error:&error])
{
	printf("Unable to write to output path, %s\n", [[error localizedDescription] UTF8String]);
	return 1;
}

As an extra bonus I’m also sorting by the value of the individual portions of the OIDs, not just by string characters.

Conclusion

Not only do I have translated versions of all 1730 OIDs that Apple has specified names for, I also was able to identify 29 errors in the strings files which you probably would only notice if you are looking for them. Encountering these values helped me to make my ASN1 parser even more robust, so that it no longer crashes if there are extra bytes.

Unrelated the the above mentioned bug report about the incorrect keys I also filed a second Radar about the OIDs missing that are used for Apple Developer ID certificates as rdar://13438182 and Open Radar.

In summary we can call this a quite productive outcome.


Categories: Recipes

Leave a Comment