As extension formats go, Apple clearly chose the most obscure and least documented one for their Safari browser. It’s based on the XAR (eXtensible ARchiver) format which is a dead project with barely existing and outdated format documentation (note how it suggests setting
XAR_HEADER_VERSION to zero even though current header version is one). But Apple went further and added signing support to the format without documenting it. Why bother if everybody can use Safari to create an extension package? And so for a long time your best choice to automate the build process was a complicated list of instructions relying on a patched version of the xar command line tool. A year ago somebody apparently added a much more convenient xar-js library to the list but I didn’t find out until I started writing this blog post.
Yesterday I was transitioning our build process from M2Crypto which has neither Python 3 support nor useful documentation to PyCrypto which has both. While at it, I had to spend an awful lot of time reimplementing step 3 of that complicated list of instructions, namely creating a valid signature. It wasn’t working no matter what I tried, and I ended up understanding the format a lot better than I ever wished for. As it turned out, this step was relying on a broken RSA signing implementation as offered by the deprecated
openssl rsautl command and M2Crypto, but which PyCrypto chose not to implement for obvious reasons. I solved the problem, but in the process I realized that the format is simpler than the unsupported command line tool with its quirks and bugs. And so I created our own XAR generator which no longer relied on that command line tool (not quite general-purpose code unfortunately).
So, what does a XAR file look like? It starts with a minimalistic binary header (all values in big-endian byte order):
4 bytes magic number (0x78617221) 2 bytes header size (28) 2 bytes header version (1) 8 bytes compressed ToC length 8 bytes uncompressed ToC length 4 bytes checksum algorithm (1 = SHA-1)
It is followed up by a deflate-compressed XML blob called Table of Contents (ToC), its size is listed in the header. This contains all the necessary metadata, including positions of the checksum, signature and the compressed file contents as archive offsets counting from the end of the ToC data. Here is what the ToC typically looks like:
<?xml version="1.0" encoding="UTF-8"?> <xar> <toc> <creation-time>2016-08-17T12:34:56Z</creation-time> <checksum style="sha1"> <offset>0</offset> <size>20</size> </checksum> <file id="1"> <name>myextension.safariextension</name> <type>directory</type> <mode>0755</mode> <file id="2"> <name>Info.plist</name> <type>file</type> <mode>0644</mode> <data> <length>1234</length> <offset>20</offset> <size>321</size> <encoding style="application/x-gzip"/> <extracted-checksum style="sha1">1234567890abcdef1234567890abcdef12345678</extracted-checksum> <archived-checksum style="sha1">87654321fedcba0987654321fedcba0987654321</archived-checksum> </data> </file> </file> </toc> </xar>
The fields are pretty self-explaining. The first 20 bytes after the ToC are a SHA-1 checksum of the compressed ToC contents. What follows at offset 20 are 321 bytes representing the contents of the file
myextension.safariextension/Info.plist. These are deflate-compressed again, the uncompressed size of the file is 1234 bytes. Note that while you can have any number of files directly inside the
<toc> tag, in practice Safari extensions always have only one directory named
anything.safariextension at the top level. All other files go inside that directory (meaning inside the corresponding
This simple picture gets somewhat more complicated once you add signatures to it. You get two more tags inside the
<signature-creation-time>493130096.0</signature-creation-time> <signature style="RSA"> <offset>20</offset> <size>256</size> <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#"> <X509Data> <X509Certificate>MIIFZDCCBEygAw...</X509Certificate> <X509Certificate>MIIFZDCCBEygAw...</X509Certificate> <X509Certificate>MIIFZDCCBEygAw...</X509Certificate> </X509Data> </KeyInfo> </signature>
Apparently, Apple developers weren’t happy with the ISO time format and went with a numeric timestamp for the
<signature-creation-time> tag. It represents the number of seconds since
2001-01-01T00:00:00.000Z so if you’ve got a Unix timestamp you need to subtract
978307200 from it.
I guess that the signature can be located anywhere in the file, the existing tools seem to always put it at offset 20 however, right after the checksum. Note that 256 bytes as signature size isn’t a constant but rather dependent on the key size (2048 bits here). Finally, you have to include the base64-encoded data of the entire certificate chain, this typically means three certificates: your own Apple developer certificate, Apple Worldwide Developer Relations Certification Authority and Apple Root CA.
Like with the checksum, you are signing the compressed ToC data. It’s a standard PKCS #1 v1.5 signature that any crypto tool can produce (important: hashing algorithm should be SHA-1 here as well). While I didn’t test OpenSSL, there the
openssl dgst -sign command should do the right thing. That’s the last piece of information necessary to produce a valid Safari extension build.
One question remains: with SHA-1 being retired for SSL certificates right now, when is Apple going to adjust their extension format? At least they are currently in good company: both Firefox and Chrome are still betting on SHA-1 for extension signing as well. SHA-1 collision attacks are quickly becoming practicable which makes it a very questionable choice for signing to say the least.
And a side-note: I really don’t see what the designers of the XAR format were thinking when they put the ToC at the beginning of the archive. In order to generate the ToC you need to compress all the files first since you have to know their offsets. But you cannot write the compressed file data to the archive because the ToC has to be written first and you don’t know in advance how large it turns out to be. The XAR command line tool solves this by creating lots of temporary files which is both awkward and slow. All this could have been avoided by putting the ToC to the very end of the file, after the actual content. As a bonus point, it would also allow adding files to the archive without having to create it from scratch.