├── .gitignore ├── README.md ├── bashrc ├── chrome ├── README.md ├── crx.bash ├── launch-chrome-extensions-on-startup │ ├── background.js │ ├── icon19.png │ ├── icon48.png │ └── manifest.json ├── master_preferences └── zip2crx.sh ├── firefox ├── README.md └── build-firefox-extension.sh ├── maxthon ├── README.md └── mxpack.py └── safari ├── .gitignore ├── README.md ├── build-safari-extension.sh └── xar /.gitignore: -------------------------------------------------------------------------------- 1 | # Never check in your certificates! 2 | firefox/codesigning.pem 3 | safari/certs 4 | 5 | # Ignore dotfiles. If you want to store certs in the same dir, prefix 6 | # the path with a dot and you won't accidentally add it to the source tree. 7 | .* 8 | !.gitignore 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repository contains tools to ease the development and deployment 2 | of browser extensions. Most tools are shell scripts (Bash) or functions, 3 | directly usable on Linux and Mac. 4 | 5 | If you're using Windows, you need to install [Cygwin](http://www.cygwin.com/) 6 | or [Gnu On Windows](https://github.com/bmatzelle/gow/wiki). 7 | 8 | Author: Rob Wu (https://robwu.nl/). 9 | Website: https://github.com/Rob--W/extension-dev-tools 10 | 11 | # Usage 12 | The documentation for each browser can be found in the separate subdirectories. 13 | 14 | The easiest way to integrate this set of tools in your environment is by adding 15 | the following line to your `.bashrc`: 16 | 17 | source path/to/extension-dev-tools/bashrc 18 | 19 | ## Chrome 20 | - See `chrome/README.md` for details. 21 | 22 | ## Firefox 23 | - Quick start: Create a file or symlink at `firefox/codesigning.pem` 24 | - See `firefox/README.md` for details. 25 | 26 | ## Safari 27 | - Quick start: Create a directory or symlink at `safari/certs` 28 | - See `safari/README.md` for details. 29 | 30 | ## Opera 31 | ## Internet Explorer 32 | 33 | ## Maxthon 34 | - See `maxthon/README.md` for details. 35 | -------------------------------------------------------------------------------- /bashrc: -------------------------------------------------------------------------------- 1 | # Link this file in your .bashrc 2 | # Example: Add the following line (without #) at the end of your .bashrc: 3 | # source path/to/this/bin/bashrc 4 | 5 | pushd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 6 | 7 | # Load crx* functions for Chrome extension development 8 | source "chrome/crx.bash" 9 | 10 | alias build-safari-extension="$PWD/safari/build-safari-extension.sh" 11 | alias build-firefox-extension="$PWD/firefox/build-firefox-extension.sh" 12 | alias mxpack="$PWD/maxthon/mxpack.py" 13 | 14 | popd >/dev/null 15 | -------------------------------------------------------------------------------- /chrome/README.md: -------------------------------------------------------------------------------- 1 | Copyright Rob Wu (https://robwu.nl/) 2 | Last modified: 16 August 2014 3 | 4 | ## Files 5 | 6 | - `crx.bash` - Bash source that defines functions prefixed with "crx". 7 | - `launch-chrome-extensions-on-startup/` - Extension that opens `chrome://extensions/` on startup 8 | - `master_preferences` - Put this file near your Chromium executable if you want to override the 9 | default profile settings. Some notable preferences: 10 | * `"distribution.suppress_first_run_bubble = true` disables the first run search bubble. 11 | * `extensions.ui.developer_mode = true` enables developer mode for extensions. 12 | * and others (prompt to download instead of saving automatically, less network traffic by 13 | disabling search suggestions, DNS prefetching, etc.) 14 | 15 | ## Bash commands 16 | ``` 17 | crx Create a Chrome extension in the current directory (if not existent). 18 | crx [name] Create a Chrome extension with a given name in the current directory, 19 | and changes current directory to the directory containing the new 20 | extension. 21 | 22 | crxshow Show whether profile directory for current instance exists. 23 | crxtest Starts Chrome, loading the Chrome extension from current path. 24 | After running crxtest once, the extension path will be remembered. 25 | If you want to reset this value, use __CRX_PWD= 26 | crxdel Deletes temporary profile. 27 | crxget [arg...] Extracts the CRX file or extension ID from the arguments and download 28 | the CRX file as [original name or extension ID].crx. 29 | After saving the file, a command that extracts the downloaded [name].crx 30 | file(s) to a directory called [name] is shown. 31 | ``` 32 | -------------------------------------------------------------------------------- /chrome/crx.bash: -------------------------------------------------------------------------------- 1 | # (c) 2013 - 2014 Rob Wu 2 | # Exports several functions to ease Chrome extension development 3 | # 4 | # crx - Bootstraps Chrome extension if not existent 5 | # crxshow - Show whether profile directory for current instance exists. 6 | # crxtest - Starts Chrome, loading the Chrome extension from current path or parent 7 | # crxdel - Deletes temporary profile 8 | # crxget - Download and optionally suggest to extract a CRX file from the CWS or elsewhere 9 | # 10 | # Global variables 11 | # __CRX_CHROMIUM_BIN - Name of Chromium executable 12 | # __CRX_EXTRA_EXTENSIONS - Comma-separated list of extensions to be loaded 13 | # __CRX_PROFILE - Path to temp profile dir 14 | # __CRX_PWD - Path to extension dir 15 | # 16 | 17 | 18 | # If chromium is not found within the path, but google-chrome is, use it. 19 | __CRX_CHROMIUM_BIN=chromium 20 | if ! type "chromium" >/dev/null 2>/dev/null; then 21 | if ! type "google-chrome" >/dev/null 2>/dev/null; then 22 | __CRX_CHROMIUM_BIN=google-chrome 23 | fi 24 | fi 25 | 26 | # Chrome extension that provides quick access to chrome://extensions/ 27 | __CRX_EXTRA_EXTENSIONS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/launch-chrome-extensions-on-startup" 28 | 29 | crx() { 30 | if [ $# -ne 0 ] ; then 31 | if [ -e "$*" ] ; then 32 | if [ ! -e "$*/manifest.json" ] ; then 33 | echo "$* already exists. Did not create crx files." 34 | return 35 | else 36 | echo "$*/manifest.json already found." 37 | fi 38 | else 39 | if [ "$*" == "${PWD##*/}" ] ; then 40 | echo "Did not create directory or crx files, because the current directory has the same name" 41 | return 42 | fi 43 | echo "Created directory $*" 44 | mkdir "$*" 45 | fi 46 | echo "Changed directory to $*" 47 | cd "$*" 48 | fi 49 | if [ -e manifest.json ] ; then 50 | echo "manifest.json already found" 51 | else 52 | # Note: manifest.json for Chrome does not require background.scripts, 53 | # but it is there for cross-browser compat with Firefox, which is 54 | # accepted without error in Chrome 121+ - https://crbug.com/40257904 55 | # browser_specific_settings is not required in Chrome, and ignored 56 | # without warning in Chrome 131+ - https://crbug.com/40196501 57 | # We include it because it is almost required in Firefox - although 58 | # Firefox may load it without when loaded from a directory, when 59 | # uploaded to AMO or loaded as a zip file, it is required. 60 | echo '{ 61 | "name": "Name ", 62 | "version": "1", 63 | "manifest_version": 3, 64 | "background": { 65 | "scripts": ["background.js"], 66 | "service_worker": "background.js" 67 | }, 68 | "content_scripts": [{ 69 | "run_at": "document_idle", 70 | "js": ["contentscript.js"], 71 | "matches": [""] 72 | }], 73 | "action": { 74 | "default_title": "" 75 | }, 76 | "permissions": [ 77 | "tabs" 78 | ], 79 | "host_permissions": [ 80 | "" 81 | ], 82 | "web_accessible_resources": [ 83 | ], 84 | "minimum_chrome_version": "88", 85 | "browser_specific_settings": { 86 | "gecko": { 87 | "id": "{'"$(uuidgen || echo '00000000-fa11-bacc-fa11-bacc00000000')"'}" 88 | } 89 | } 90 | }' > manifest.json 91 | touch background.js 92 | touch contentscript.js 93 | echo "Created manifest.json, background.js and contentscript.js" 94 | fi 95 | __CRX_PWD=${PWD} 96 | } 97 | __get_crx_profile_path() { 98 | if [ ! -d "${__CRX_PWD}" ] ; then 99 | local path=${PWD} 100 | while [[ -n "${path}" && ! -e "${path}/manifest.json" ]] ; do 101 | path=${path%/*} 102 | done 103 | if [ -z "${path}" ] ; then 104 | echo "manifest.json not found in current or parent directory!" 105 | return 1 106 | fi 107 | __CRX_PWD=${path} 108 | fi 109 | if [ -z "${__CRX_PROFILE}" ] ; then 110 | # /tmp/CRX.prof-ABCDEF-BASENAMENOSPACE 111 | __CRX_PROFILE=/tmp/CRX.prof-$(echo "${__CRX_PWD}" | md5sum | cut -c 1-6 )-$(basename "${__CRX_PWD// /}") 112 | fi 113 | } 114 | crxshow() { 115 | __get_crx_profile_path || return 116 | if [ -e "${__CRX_PROFILE}" ] ; then 117 | # Show command for launching Chrome manually 118 | echo "${__CRX_CHROMIUM_BIN} --user-data-dir=${__CRX_PROFILE}" 119 | else 120 | echo "${__CRX_PROFILE} not found" 121 | fi 122 | } 123 | crxtest() { 124 | __get_crx_profile_path || return 125 | local command="cd \"${__CRX_PWD}\" && ${__CRX_CHROMIUM_BIN} --user-data-dir=\"${__CRX_PROFILE}\" \ 126 | --load-extension=\"${__CRX_EXTRA_EXTENSIONS},.\" $(printf '%q ' "$@")" 127 | echo "( ${command} )" 128 | bash -c "${command}" 129 | } 130 | crxdel() { 131 | __get_crx_profile_path 132 | if [[ -d "${__CRX_PROFILE}" ]] ; then 133 | if [[ "${__CRX_PROFILE}" =~ "/tmp/" ]] ; then 134 | rm -r "${__CRX_PROFILE}" && echo "# Removed \"${__CRX_PROFILE}\"" 135 | else 136 | echo "# Run the following command" 137 | echo "# rm -r ${__CRX_PROFILE}" 138 | fi 139 | else 140 | echo "# \$__CRX_PROFILE is not a directory" 141 | fi 142 | __CRX_PWD= 143 | __CRX_PROFILE= 144 | } 145 | 146 | 147 | # Download and extract crx file from the CWS for a given URL 148 | crxget() { 149 | # Some OS-specific values. Doesn't really matter if we only want to inspect the source 150 | local arch= 151 | local os= 152 | # See https://github.com/Rob--W/crxviewer/blob/master/src/chrome-platform-info.js 153 | case "$(uname)" in 154 | Darwin) 155 | os=mac 156 | ;; 157 | *WIN*) 158 | os=win 159 | ;; 160 | # Nope, no android 161 | # Nope, no cros 162 | *BSD) 163 | os=openbsd 164 | ;; 165 | *) 166 | # Default to Linux 167 | os=Linux 168 | esac 169 | 170 | if [ "$(getconf LONG_BIT)" = "64" ] ; then 171 | arch="x86-64" 172 | else 173 | arch="x86-32" 174 | fi 175 | 176 | local nacl_arch="$arch" 177 | for arg in "$@" ; do 178 | local dl_url= 179 | local filename= 180 | local url_without_questionmark="${arg%%\?*}" 181 | if [[ "$url_without_questionmark" == *.crx ]] ; then 182 | dl_url="$arg" 183 | # Last part 184 | filename="${url_without_questionmark##*/}" 185 | else 186 | local cws_id="$(echo "$arg" | grep -oP '\b[a-p]{32}\b' )" 187 | if [ -n "$cws_id" ] ; then 188 | # Assume that we got a CWS URL 189 | # See https://github.com/Rob--W/crxviewer/blob/master/src/cws_pattern.js 190 | dl_url="https://clients2.google.com/service/update2/crx?response=redirect" 191 | dl_url+="&os=$os" 192 | dl_url+="&arch=$arch" 193 | dl_url+="&nacl_arch=$nacl_arch" 194 | dl_url+="&prod=chromiumcrx" 195 | dl_url+="&prodchannel=unknown" 196 | dl_url+="&prodversion=${CHROME_VERSION:-122.0.6261.69}" 197 | dl_url+="&acceptformat=crx2,crx3" 198 | dl_url+="&x=id%3D$cws_id" 199 | dl_url+="%26uc" 200 | filename="$cws_id.crx" 201 | fi 202 | fi 203 | 204 | if [ -n "$dl_url" ] ; then 205 | if [ -e "$filename" ] ; then 206 | echo "$filename already exists, skipping download of $dl_url" 207 | else 208 | echo "Downloading $filename" 209 | curl -L "$dl_url" -o "$filename" 210 | # Do not use wget because it hangs on 204 211 | #wget "$dl_url" -O "$filename" 212 | if [ ! -e "$filename" ] ; then 213 | # Taken down, behind login, etc. 214 | echo "Cannot download $dl_url" 215 | fi 216 | fi 217 | fi 218 | done 219 | 220 | [ -z "$CRXGET_QUIET" ] || return 221 | 222 | cat <<'HERE' 223 | # To extract all downloaded crx files, run the following commands: 224 | for f in *.crx ; do 225 | if [ -e "${f%.crx}" ] ; then 226 | echo "${f%.crx} already exists. Skipping extraction of $f" 227 | else 228 | unzip "$f" -d "${f%.crx}" 229 | rm -r "${f%.crx}/_metadata" 230 | fi 231 | done 232 | HERE 233 | } 234 | -------------------------------------------------------------------------------- /chrome/launch-chrome-extensions-on-startup/background.js: -------------------------------------------------------------------------------- 1 | /* globals chrome, console */ 2 | 'use strict'; 3 | 4 | // Not working when running chromium --load-extension=PWD 5 | chrome.runtime.onStartup.addListener(showChromeExtensions); 6 | 7 | chrome.runtime.onInstalled.addListener(function(details) { 8 | if (details.reason == 'install') 9 | showChromeExtensions(); 10 | }); 11 | chrome.action.onClicked.addListener(showChromeExtensions); 12 | 13 | var TAB_NOT_FOUND = -1; 14 | function reduceChromeExtensionsTabId(tabId, tab) { 15 | return tabId == TAB_NOT_FOUND && /^chrome:\/\/extensions/.test(tab.url) ? tab.id : tabId; 16 | } 17 | function showChromeExtensions() { 18 | chrome.tabs.query({}, function(tabs) { 19 | var tabId = tabs.reduce(reduceChromeExtensionsTabId, TAB_NOT_FOUND); 20 | if (tabId != TAB_NOT_FOUND) { 21 | chrome.tabs.update(tabId, { 22 | active: true 23 | }); 24 | return; 25 | } 26 | chrome.tabs.create({ 27 | url: 'chrome://extensions/' 28 | }); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /chrome/launch-chrome-extensions-on-startup/icon19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rob--W/extension-dev-tools/d2f53ae1a786c2d6fb82c4d42c12be6034cfa051/chrome/launch-chrome-extensions-on-startup/icon19.png -------------------------------------------------------------------------------- /chrome/launch-chrome-extensions-on-startup/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rob--W/extension-dev-tools/d2f53ae1a786c2d6fb82c4d42c12be6034cfa051/chrome/launch-chrome-extensions-on-startup/icon48.png -------------------------------------------------------------------------------- /chrome/launch-chrome-extensions-on-startup/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Open chrome://extensions/ on startup", 3 | "description": "Open chrome://extensions on start-up of Chrome / on click of button.", 4 | "version": "1", 5 | "manifest_version": 3, 6 | "minimum_chrome_version": "88", 7 | "background": { 8 | "service_worker": "background.js" 9 | }, 10 | "icons": { 11 | "48": "icon48.png" 12 | }, 13 | "action": { 14 | "default_icon": "icon19.png", 15 | "default_title": "Open chrome://extensions/" 16 | }, 17 | "permissions": ["tabs"] 18 | } 19 | -------------------------------------------------------------------------------- /chrome/master_preferences: -------------------------------------------------------------------------------- 1 | { 2 | "alternate_error_pages": { 3 | "enabled": false 4 | }, 5 | "autofill": { 6 | "enabled": false 7 | }, 8 | "browser": { 9 | "check_default_browser": false 10 | }, 11 | "dns_prefetching": { 12 | "enabled": false 13 | }, 14 | "distribution": { 15 | "suppress_first_run_bubble": true 16 | }, 17 | "download": { 18 | "default_directory": "/tmp/", 19 | "directory_upgrade": true, 20 | "prompt_for_download": true 21 | }, 22 | "enable_do_not_track": true, 23 | "extensions": { 24 | "theme": { 25 | "use_system": false 26 | }, 27 | "toolbarsize": -1, 28 | "ui": { 29 | "developer_mode": true 30 | } 31 | }, 32 | "first_run_tabs": [ 33 | "chrome://newtab/" 34 | ], 35 | "homepage_is_newtabpage": true, 36 | "plugins": { 37 | "plugins_list": [ { 38 | "enabled": false, 39 | "name": "Java(TM)" 40 | } ], 41 | "show_details": true 42 | }, 43 | "profile": { 44 | "password_manager_enabled": false 45 | }, 46 | "safebrowsing": { 47 | "enabled": false 48 | }, 49 | "savefile": { 50 | "default_directory": "/tmp", 51 | "type": 0 52 | }, 53 | "search": { 54 | "suggest_enabled": false 55 | }, 56 | "selectfile": { 57 | "last_directory": "/tmp" 58 | }, 59 | "sync": { 60 | "suppress_start": true 61 | }, 62 | "translate": { 63 | "enabled": false 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /chrome/zip2crx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | zipfile="$1" 4 | basename="${zipfile%%.zip}" 5 | if [ "${basename}" == "$1" ] ; then 6 | echo "Usage: file.zip" 7 | echo "Prepends the CRX header before file.zip using file.pem, outputs to file.crx" 8 | echo "If file.pem does not exist, then it is automatically created" 9 | exit 10 | fi 11 | 12 | pemfile="${basename}.pem" 13 | crxfile="${basename}.crx" 14 | if [ ! -e "${pemfile}" ] ; then 15 | 2>/dev/null openssl genrsa 2048 | openssl pkcs8 -topk8 -nocrypt -out "${pemfile}" 16 | fi 17 | 18 | signature=$(2>/dev/null openssl sha1 -sha1 -binary -sign "${pemfile}" < "${zipfile}") 19 | publickey=$(2>/dev/null openssl rsa -in "${pemfile}" -pubout -outform DER) 20 | 21 | appendinteger() { 22 | bit1=$((($1 ) & 0xFF )) 23 | bit2=$((($1 >> 8) & 0xFF )) 24 | bit3=$((($1 >> 16) & 0xFF )) 25 | bit4=$((($1 >> 24) & 0xFF )) 26 | printf "$(printf '\\x%x\\x%x\\x%x\\x%x' ${bit1} ${bit2} ${bit3} ${bit4} )" >> "${crxfile}" 27 | } 28 | 29 | # CRX header 30 | printf 'Cr24\x02\x00\x00\x00' > "${crxfile}" 31 | appendinteger ${#publickey} 32 | appendinteger ${#signature} 33 | printf '%s' "${publickey}${signature}" >> "${crxfile}" 34 | cat "${zipfile}" >> "${crxfile}" 35 | -------------------------------------------------------------------------------- /firefox/README.md: -------------------------------------------------------------------------------- 1 | ## build-firefox-extension.sh 2 | `build-firefox-extension.sh` is an enhanced version of "cfx xpi". It does the following: 3 | 4 | 1. Runs `cfx xpi` to pack the extension. 5 | 2. If the current directory contains `install.rdf`, `minVersion` and `maxVersion` 6 | are copied to the "install.rdf" file inside the XPI, and the original install.rdf 7 | is replaced with this new install.rdf inside the XPI file 8 | (the previous one is moved to `install.rdf.bak`). 9 | 3. If an environment variable "`XPIPEM`" is set, t 10 | The XPI file is signed using the `codesigning.pem`. You can set the pem path via the 11 | `XPIPEM` environment variable. If the pem file does not exist, the XPI is not signed. 12 | 13 | The PEM file should contain the following files: 14 | 15 | - Your private key. 16 | - Your certificate. 17 | - All other certificates in the certificate chain, up to the root certificate. 18 | The root certificate is optional, it will not be included in the final xpi file, 19 | because it should already be included by default in the browser. 20 | 21 | For more information, see 22 | 23 | - [Signing Firefox extensions with Python and M2Crypto] 24 | (https://adblockplus.org/blog/signing-firefox-extensions-with-python-and-m2crypto) (blog post by Wladimir Palant). 25 | - [Signing Firefox add ons with a StartSSL Object Code Signing certificate] 26 | (https://github.com/nmaier/xpisign.py/wiki/Signing-Firefox-add-ons-with-a-StartSSL-Object-Code-Signing-certificate) 27 | (wiki of xpisign.py) 28 | 29 | ### Generation of PEM file 30 | 1. Generate private key and certificate signing request (CSR). 31 | **Note: StartCom will *ignore* all fields (such as CN, E, O) except for the public key 32 | in the CSR. All certificate details (common name, email address, locality, etc.) are 33 | directly taken from your StartCom Identity card (created during your identity verification.)** 34 | 35 | openssl req -nodes -newkey rsa:2048 -keyout codesigning.key -out codesigning.csr 36 | 37 | # OPTIONAL: Add a pass phrase to your private key 38 | openssl rsa -in codesigning.key -des3 -out codesigning2.key 39 | # Replace the password-free key with the password-protected key: 40 | mv codesigning2.key codesigning.key 41 | 42 | # Avoid loss of private key by marking it read-only (without it, you cannot use your certificate for signing) 43 | chmod -w codesigning.key 44 | 45 | A 2048-bit RSA key is secure enough for the next decade in most cases. If you're dealing with assets of a very high 46 | value, consider using 3072 or 4096-bit keys (at the cost of runtime performance). 47 | See also: [Why not use larger cipher keys?](http://security.stackexchange.com/questions/25375/why-not-use-larger-cipher-keys) 48 | 49 | 2. Send CSR to a Certificate Authority to get a certificate (.crt file). 50 | 3. Get the CA bundle from your CA (in PEM format). For example (StartCom): 51 | 52 | wget https://www.startssl.com/certs/sub.class2.code.ca.pem 53 | 54 | 4. Concatenate all files to the final file: 55 | 56 | cat codesigning.crt sub.class2.code.ca.pem codesigning.key > codesigning.pem 57 | 58 | # OPTIONAL: Export to PFX file (not needed for Firefox code signing, but necessary for 59 | # signing DLLs, EXE, etc. using Microsoft's signtool.exe 60 | openssl pkcs12 -in codesigning.pem -export -out codesigning.pfx 61 | 62 | ## Example 63 | ``` 64 | # Sign add-on in /tmp/test/ using the default certificate and private key pair (if set). 65 | build-firefox-extension.sh /tmp/test 66 | # Sign add-on in a subdirectory dist/firefox/ using the cert and key in certs/codesigning.pem. 67 | XPIPEM=certs/codesigning.pem build-firefox-extension.sh dist/firefox 68 | ``` 69 | 70 | ## Troubleshooting 71 | After signing the file, the value of "Organization Name" or "Common Name" will be displayed at the Add-on install dialog. 72 | If you still see "Author not verified", then something went wrong. 73 | 74 | To debug your issue, look in the browser's Error console (Ctrl + Shift + J). Make sure that "JS -> Log" is enabled. 75 | The next section shows how I found and debugged my "Author not verified" issue. 76 | 77 | ### Signature Verification Error 78 | After getting a **code signing** (*not* SSL) certificate from [StartCom](https://www.startssl.com/), I signed 79 | my XPI. When I opened the extension in Firefox 25, I saw the following error in my console: 80 | 81 | > "Signature Verification Error: the signature on this .jar archive is invalid because the digital signature 82 | > (\*.RSA) file is not a valid signature of the signature instruction file (\*.SF)." 83 | 84 | After hours of debugging (browsing Firefox's source code, gdb and Wireshark), I found the culprit. **It turns 85 | out that there's nothing wrong with my XPI file.** When Firefox queried the ocsp.startssl.com for the revocation 86 | status of my certificate, "certStatus: unknown (2)" was returned by StartCom. Apparently, it takes a half day 87 | before the certificate status is sent to all OCSP servers. Take a look StartCom's forum: 88 | [Certificate OCSP Validation Failiure in Firefox](https://forum.startcom.org/viewtopic.php?f=15&t=2654). 89 | 90 | If you're experiencing the same issue, go to `about:config` and set `security.OCSP.enabled` to `0`. This will 91 | prevent Firefox from checking the certificate revocation status. Installation of signed extensions will also 92 | be faster because the installation is no longer delayed by the OCSP request. 93 | 94 | If you want to debug the OCSP request: 95 | 96 | 1. Start Wireshark, start the capture and apply the following filter: 97 | 98 | http.content_type contains "ocsp" and http.server contains "StartCom" 99 | 2. Start Firefox (in a new (temporary) profile to make sure that Firefox does not use a cached OCSP response: 100 | 101 | rm -r fftemp ; mkdir fftemp ; firefox -profile fftemp --no-remote /tmp/path/to/addon.xpi 102 | 3. *The installation dialog shows up, and it shows either "Author not verified" or "your name"* 103 | 4. Go back to Wireshark, and look at "Online Certificate Status Protocol" -> "responseBytes" -> "tbsResponseData" -> 104 | "responses" -> "SingleResponse" -> "certStatus". This field's value should be "good (0)". 105 | 106 | ## Dependencies 107 | - [Add-on SDK](https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/tutorials/installation.html) 108 | to package the add-on (using the [`cfx tool`](https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/cfx-tool.html)). 109 | - [7-zip](http://www.7-zip.org) for manipulating `install.rdf` (at step 2). 110 | - [xpisign](https://github.com/nmaier/xpisign.py/) to sign the XPI file. 111 | 112 | ## External links 113 | Much of this build script would be obsolete when the following bugs get fixed: 114 | 115 | - [Bug 884924 - package.json should support minVersion in targetApplication](https://bugzilla.mozilla.org/show_bug.cgi?id=884924) 116 | - [Bug 657494 - add XPI code-signing tools to 'cfx xpi --sign'](https://bugzilla.mozilla.org/show_bug.cgi?id=657494) 117 | -------------------------------------------------------------------------------- /firefox/build-firefox-extension.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2013 Rob Wu (https://robwu.nl/) 3 | # Last modified 30 dec 2013 4 | # 5 | # 1. Runs cfx xpi (on failure, exit immediately) 6 | # 2. Writes minVersion/maxVersion if update.rdf is found in the current directory. 7 | # 3. Signs the xpi file. 8 | # 9 | # Environment variables: 10 | # XPIPEM = Path to PEM file that contains the certificates and private key. 11 | # 12 | # Depends on: 13 | # - addon-sdk 14 | # - 7-Zip 15 | # - xpisign 16 | 17 | 18 | if [ $# == 0 ] ; then 19 | echo "Usage: $0 path/to/firefox-addon/" 20 | exit 1 21 | fi 22 | cd "$1" || exit 2 23 | 24 | # To get greadlink, use brew install coreutils 25 | [ "$(uname)" == "Darwin" ] && { shopt -s expand_aliases; alias readlink=greadlink; } 26 | 27 | curdir="$( cd "$( dirname "$( readlink -f "${BASH_SOURCE[0]}" )" )/" && pwd )" 28 | 29 | DEFAULT_XPIPEM="${curdir}/codesigning.pem" 30 | # If environment variable is not set, use default PEM file: 31 | [ -z "${XPIPEM}" ] && [ -f "${DEFAULT_XPIPEM}" ] && XPIPEM="${DEFAULT_XPIPEM}" 32 | 33 | echo "Building Firefox add-on" 34 | if type cfx > /dev/null 2>/dev/null ; then 35 | sdkout=$(cfx xpi) 36 | else 37 | # Addon SDK not activated yet. Assume that the add-on SDK is installed to /opt/addon-sdk 38 | sdkout=$(cd /opt/addon-sdk && source bin/activate && cd - && cfx xpi) 39 | fi 40 | 41 | if [ -z "${sdkout}" ] ; then 42 | echo "Failed to run 'cfx xpi'. Did you set up the environment using addon-sdk?" 43 | echo "See https://addons.mozilla.org/en-US/developers/docs/sdk/latest/dev-guide/tutorials/installation.html" 44 | exit 7 45 | fi 46 | 47 | if [[ "${sdkout}" =~ [a-z0-9_\-]+\.xpi ]] ; then 48 | XPI="${BASH_REMATCH[0]}" 49 | else 50 | echo "${sdkout}" 51 | echo "Cannot find xpi file name in output! Stopping now." 52 | exit 8 53 | fi 54 | 55 | 56 | # Copy minVersion / maxVersion if install.rdf was specified. 57 | if [ -f install.rdf ] ; then 58 | echo "Backing up previous version of install.rdf" 59 | mv install.rdf install.rdf.bak 60 | 61 | echo "Reading minVersion and maxVersion from install.rdf" 62 | minVersionPattern='\s*[^<]+<\/em:minVersion>' 63 | maxVersionPattern='\s*[^<]+<\/em:maxVersion>' 64 | minVersion=$(awk "/${minVersionPattern}/" install.rdf.bak) 65 | maxVersion=$(awk "/${maxVersionPattern}/" install.rdf.bak) 66 | 67 | echo "Getting install.rdf from xpi" 68 | 7z x "${XPI}" install.rdf > /dev/null 69 | 70 | echo "Updating minVersion and maxVersion in install.rdf" 71 | sed -E "s#${minVersionPattern}#${minVersion}#" -i install.rdf 72 | sed -E "s#${maxVersionPattern}#${maxVersion}#" -i install.rdf 73 | 74 | echo "Updating install.rdf in the xpi" 75 | 7z d "${XPI}" install.rdf > /dev/null 76 | 7z a "${XPI}" install.rdf > /dev/null 77 | 78 | echo "Done!" 79 | echo "Version $(grep -Pow '[0-9.]+(?=<\/em:version)' install.rdf)" 80 | echo "minVersion ${minVersion}" 81 | echo "maxVersion ${maxVersion}" 82 | else 83 | echo "install.rdf not found. Did not change minVersion / maxVersion." 84 | fi 85 | 86 | if [ ! -f "${XPIPEM}" ] ; then 87 | if [ -z "${XPIPEM}" ] ; then 88 | echo "XPI not signed because PEM file is not set (environment variable XPIPEM= )" 89 | else 90 | echo "XPI not signed because pem file not found (${XPIPEM})." 91 | fi 92 | exit 93 | fi 94 | 95 | 96 | # Sign it! 97 | echo "Going to sign ${XPI} using ${XPIPEM}..." 98 | mv {,unsigned-}"${XPI}" 99 | if xpisign -k "${XPIPEM}" "unsigned-${XPI}" "${XPI}" ; then 100 | rm "unsigned-${XPI}" 101 | else 102 | mv {unsigned-,}"${XPI}" 103 | echo "Failed to sign ${XPI}" 104 | exit 9 105 | fi 106 | -------------------------------------------------------------------------------- /maxthon/README.md: -------------------------------------------------------------------------------- 1 | ## Maxthon package tool 2 | 3 | This part of the repository contains a Python script to pack a directory into a 4 | mxaddon file. I've reverse-engineered the file format from MxPacker.exe from 5 | http://forum.maxthon.com/thread-801-1-1.html (version 1.0.7, Mar 13th, 2013). 6 | 7 | ## Usage 8 | 9 | ``` 10 | mxpack.py [input directory] [optional outputfile.mxaddon] 11 | ``` 12 | 13 | If the second argument is omitted, the output file will be the input directory 14 | concatenated with ".mxaddon". 15 | For example, `mxpack.py ~/Documents/myaddon/` creates `~/Documents/myaddon.mxaddon`. 16 | If the file already exists, it will be overwritten without prompt. 17 | -------------------------------------------------------------------------------- /maxthon/mxpack.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # (c) 2014 Rob Wu (https://robwu.nl) 3 | # Py2 and Py3-compatible 4 | # Generate a Maxthon package for a given directory. 5 | import os 6 | import struct 7 | import sys 8 | 9 | 10 | def i2b(n): 11 | """ 12 | Converts an integer to a 32-bit little-endian binary representation. 13 | """ 14 | return struct.pack(' 3: 160 | print("") 161 | print("Usage: %s [inputdir] [output file]" % sys.argv[0]) 162 | print("Overwrites [outputfile] with the packed content of [inputdir].") 163 | print("[output file] is optional and defaults to .maxaddon") 164 | print("") 165 | sys.exit(1) 166 | 167 | indir = os.path.abspath(sys.argv[1]) 168 | if len(sys.argv) == 3: 169 | outfile = sys.argv[2] 170 | else: 171 | outfile = indir + '.mxaddon' 172 | 173 | if not os.path.isdir(indir): 174 | print("Exiting early because %s is not a directory!" % indir) 175 | sys.exit(2) 176 | 177 | content = createMxPak1(indir) 178 | with open(outfile, 'wb') as f: 179 | f.write(content) 180 | print("Written %d bytes to %s" % (len(content), outfile)) 181 | -------------------------------------------------------------------------------- /safari/.gitignore: -------------------------------------------------------------------------------- 1 | certs/ 2 | 3 | # Patched xar from http://mackyle.github.com/xar/ 4 | xar-1.6.1.tar.gz 5 | -------------------------------------------------------------------------------- /safari/README.md: -------------------------------------------------------------------------------- 1 | Copyright Rob Wu (https://robwu.nl/) 2 | Last modified: 30 december 2013 3 | 4 | ## File structure 5 | * `update.plist` - to be placed on the server hosting the extension 6 | * `*.safariextz` - the packed and signed extension 7 | * `*.safariextension/` - The extension's files. 8 | 9 | ## Building (manually) 10 | To build the extension, go to Safari: 11 | 12 | 1. Open the Develop menu (can be shown by opening `Preferences > Advanced` and check "Show Develop menu in menu bar") 13 | 2. [Menu item] "Develop" 14 | 3. [Menu item] "Show Extension builder" 15 | 4. [Button] "Add extension" and select the `*.safariextension` directory. 16 | 5. [Button] "Build extension" 17 | 18 | 19 | ## Building (automated, Linux/Mac) 20 | 1. Get xar-1.6.1.tar.gz from http://mackyle.github.com/xar/. 21 | 2. Create the xar binary: 22 | 23 | tar xf xar-1.6.1.tar.gz 24 | cd xar-1.6.1 25 | ./configure --disable-shared 26 | make 27 | mv src/xar .. 28 | cd .. 29 | rm -r xar-1.6.1 30 | 31 | 3. Put all required files in the `certs/` directory: 32 | - Download certificates from https://www.apple.com/certificateauthority/ 33 | - `AppleWWDRCA.cer` 34 | 35 | wget https://developer.apple.com/certificationauthority/AppleWWDRCA.cer -OAppleWWDRCA.cer 36 | - `AppleIncRootCertificate.cer` 37 | 38 | wget https://www.apple.com/appleca/AppleIncRootCertificate.cer -OAppleIncRootCertificate.cer 39 | - `safari_extension.cer` 40 | Log in to https://developer.apple.com/account/safari/certificate/certificateList.action 41 | and download the certificate. 42 | - `key.pem` 43 | This was generated together with your CSR (see section "Certificate"). 44 | 45 | If you are trying to automate the build steps for an existing extension, follow the following steps instead. 46 | This is advised, because there is a chance that one of the certificates have been modified since its initial 47 | creation. 48 | 49 | cd certs 50 | path/to/xar -f path/to/name.safariextz --extract-certs . 51 | mv cert00 safari_extension.cer 52 | mv cert01 AppleWWDRCA.cer 53 | mv cert02 AppleIncRootCertificate.cer 54 | 55 | The private key's location is only known to you. If you've got a PFX or P12 file, then you can use the 56 | following command to extract the private key: 57 | 58 | openssl pkcs12 -in safari_extension.pfx -nodes | openssl rsa -out key.pem 59 | 60 | If you wish to keep the `certs` directory in a different location, edit the shell script discussed 61 | below, and adjust the `certdir` variable (defaults to the `certs` directory at the same level of the shell script). 62 | 63 | 4. Now run the build script using `./build-safari-extension.sh path/to/name.safariextension` 64 | 5. Optional: Create a symlink to the shell script in a directory within your `$PATH`. 65 | For example, if `~/bin/` is a directory listed in your `$PATH` environment variable, use 66 | `ln -s path/to/build-safari-extension.sh ~/bin/safariext-build`. 67 | After doing that, you can easily build Safari extensions using `safariext-build path/to/name.safariextension` 68 | without having to carry around the files and developer certificates. 69 | 6. If you wish to (temporarily) use a different certificate directory for a specific project, set the `CERTDIR` 70 | environment variable to override the certdir set in the build script. 71 | Example: 72 | 73 | CERTDIR=path/to/certs safariext-build path/to/name.safariextension 74 | 75 | 76 | ## Certificate 77 | In order to build (or even test) a Safari extension, you need a certificate from Apple. 78 | 79 | 1. Create an Apple ID if not already done. After registering, visit 80 | https://developer.apple.com/account/safari/certificate/certificateList.action 81 | 2. To get a certificate, you need to get a private key and a CSR (Certificate Signing Request) file. 82 | The following command creates `private_key.key` and `cer_sign_request.csr`: 83 | 84 | openssl req -nodes -newkey rsa:2048 -keyout private_key.key -out cer_sign_request.csr 85 | # You do NOT want to loose these files (esp. the private key): make it read-only. 86 | chmod -w private_key.key cer_sign_request.csr 87 | 88 | 3. Upload `cer_sign_request.csr` to Apple, and download the certificate (link at step 1). 89 | 4. *Optional*. Only necessary if you wish to use the Extension Builder GUI in Apple Safari. 90 | Install the certificate on your OS. 91 | My `.key` and `.csr` files were created on a different computer, so Safari did not accept 92 | my downloaded `safari_extension.cer` (because the corresponding private key was not found 93 | at the machine). 94 | I solved this by creating a `.pem` file from my `.key` file, and generated a `.pfx` file 95 | (private key + certificate) from the `.pem` and `.cer` file. Commands used: 96 | 97 | # Create pem file from key 98 | (echo '-----BEGIN CERTIFICATE-----'; base64 safari_extension.cer; echo '-----END CERTIFICATE-----') > safari_extension.crt 99 | # Create pfx file 100 | openssl pkcs12 -inkey private_key.key -in safari_extension.crt -export -out safari_extension.pfx 101 | 102 | The last command will prompt for a password. This password will be asked when you import the pfx file. 103 | 104 | Enter Export Password: 105 | Verifying - Enter Export Password: 106 | After creating the `.pfx` file, I copied it (securely!) to my Windows VM (which is running Safari). 107 | I installed the certificate (`.pfx`), and Safari finally recognized my certificate! 108 | 109 | ## Refreshing an expired certificate 110 | Extension certificates issued by Apple expire after 1 year. Update your certificate as follows: 111 | 112 | 1. Submit the CSR file to Apple (may be the same as the one created at step 2 at the "Certificate" section). 113 | 2. Download `safari_extension.cer` from Apple. 114 | 3. Repeat step 3 of section "Building (automated, Linux/Mac)". 115 | -------------------------------------------------------------------------------- /safari/build-safari-extension.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright 2013 Rob Wu (https://robwu.nl/) 3 | # Last modified 30 dec 2013 4 | # 5 | # Environment variables: 6 | # XARPATH = Path to patched xar executable 7 | # CERTDIR = Path to certificates and keys 8 | # 9 | # Requirements: certs/ directory as defined in README.md 10 | 11 | # To get greadlink, use brew install coreutils 12 | [ "$(uname)" == "Darwin" ] && { shopt -s expand_aliases; alias readlink=greadlink; } 13 | 14 | curdir="$( cd "$( dirname "$( readlink -f "${BASH_SOURCE[0]}" )" )/" && pwd )" 15 | certdir="${curdir}/certs" 16 | xar="${curdir}/xar" 17 | 18 | # Allow override through environment variables 19 | [ -n "$XARPATH" ] && xar="$XARPATH" 20 | [ -n "$CERTDIR" ] && certdir="$CERTDIR" 21 | 22 | if [ ! -x "${xar}" ] ; then 23 | echo "${xar} is not an executable!" 24 | exit 10 25 | fi 26 | 27 | if [ $# == 0 ] ; then 28 | echo "Usage: $0 path/to/name.safariextension/" 29 | exit 1 30 | fi 31 | # Resolve relative paths, get rid of trailing slashes, validate path 32 | safariextensiondir="$( readlink -f "$1" )" 33 | 34 | if [ -z "${safariextensiondir}" ] ; then 35 | echo "Error: Path not found: ${safariextensiondir}" 36 | exit 2 37 | fi 38 | 39 | if [ ! -d "${safariextensiondir}" ] ; then 40 | echo "Error: Path is not a directory: ${safariextensiondir}" 41 | exit 3 42 | fi 43 | 44 | # Last part of dir, eg. "name.safariextension" 45 | safaridirname="${safariextensiondir##*/}" 46 | # Name of extension, eg "name" 47 | extensionname="${safaridirname%.safariextension}" 48 | # Parent dir of the "name.safariextension" and "name.safariextz", eg. "/resolved/path/to" 49 | safaridistdir="${safariextensiondir%/*}" 50 | 51 | if [ "${extensionname}" == "${safaridirname}" ] ; then 52 | echo "Error: ${safaridirname} does not end with .safariextension!" 53 | exit 4 54 | fi 55 | 56 | # Check if all certificate requirements are satisfied... 57 | if [ ! -d "${certdir}" ] ; then 58 | echo "Error: Certificate dir not found: ${certdir}" 59 | exit 5 60 | fi 61 | cert_exists() { 62 | local cert=$1 63 | local message=$2 64 | if [ ! -f "${certdir}/${cert}" ] ; then 65 | echo "Error: Not found in certificate dir: ${cert}" 66 | echo "${message}" 67 | exit 5 68 | fi 69 | } 70 | 71 | # Check requirements, on failure, show hint on recovering. 72 | 73 | cert_exists "safari_extension.cer" "Download this file from https://developer.apple.com/account/safari/certificate/certificateList.action" 74 | cert_exists "key.pem" "This RSA private key was generated by yourself (and should match your CSR)" 75 | cert_exists "AppleIncRootCertificate.cer" "Download it from https://www.apple.com/certificateauthority/ 76 | wget https://www.apple.com/appleca/AppleIncRootCertificate.cer -OAppleIncRootCertificate.cer" 77 | cert_exists "AppleWWDRCA.cer" "Download it from https://www.apple.com/certificateauthority/ 78 | wget http://developer.apple.com/certificationauthority/AppleWWDRCA.cer -OAppleWWDRCA.cer" 79 | 80 | # Size of the signature 81 | sigsizefile="${certdir}/size.txt" 82 | # Create file if not existent 83 | [ ! -e "${sigsizefile}" ] && openssl dgst -sign "${certdir}/key.pem" -binary < "${certdir}/key.pem" | wc -c > "${sigsizefile}" 84 | sigsize="$(cat "${sigsizefile}" )" 85 | 86 | extzfile="${safaridistdir}/${extensionname}.safariextz" 87 | 88 | # Insert --verbose if you want to see the files being processed 89 | "${xar}" -czf "${extzfile}" \ 90 | --distribution \ 91 | --directory="${safaridistdir}" \ 92 | "${extensionname}.safariextension" 93 | 94 | "${xar}" --sign -f "${extzfile}" --digestinfo-to-sign tmp-digest.dat \ 95 | --sig-size "${sigsize}" \ 96 | --cert-loc "${certdir}/safari_extension.cer" \ 97 | --cert-loc "${certdir}/AppleWWDRCA.cer" \ 98 | --cert-loc "${certdir}/AppleIncRootCertificate.cer" 99 | 100 | openssl rsautl -sign -inkey "${certdir}/key.pem" -in tmp-digest.dat -out tmp-sig.dat 101 | 102 | "${xar}" --inject-sig tmp-sig.dat -f "${extzfile}" 103 | 104 | rm -f tmp-sig.dat tmp-digest.dat 105 | 106 | echo "Built ${extzfile}" 107 | -------------------------------------------------------------------------------- /safari/xar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rob--W/extension-dev-tools/d2f53ae1a786c2d6fb82c4d42c12be6034cfa051/safari/xar --------------------------------------------------------------------------------