Skip to content

Commit

Permalink
Can install SSL certificate to a simulator
Browse files Browse the repository at this point in the history
* Parses subject, fingerprint and der data from a PEM file using OpenSSL and adds it to TrustStore certificate to SQLite DB in tsettings

* Exposed methods in index.js as 'installSSLCert' and 'uninstallSSLCert' that install/uninstall a certificate to a Simulator with given udid

* Added test server that creates a dummy SSL test server
  • Loading branch information
dpgraham authored Nov 23, 2016
1 parent aa88dd7 commit d04f7ee
Show file tree
Hide file tree
Showing 20 changed files with 533 additions and 111 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ node_modules
build
*.log
.DS_Store
test/assets/certificate-test-server/random-pem.pem
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
[![Build Status](https://api.travis-ci.org/appium/appium-ios-simulator.png?branch=master)](https://travis-ci.org/appium/appium-ios-simulator)
[![Coverage Status](https://coveralls.io/repos/appium/appium-ios-simulator/badge.svg?branch=master)](https://coveralls.io/r/appium/appium-ios-simulator?branch=master)

Appium API for dealing with iOS simulators. Allows the user to find locations of directories and applications, gives access to settings in order to read from and write to simualtor plists, and allows control over starting and stopping simulators.
Appium API for dealing with iOS simulators. Allows the user to find locations of directories and applications, gives access to settings in order to read from and write to simulator plists, and allows control over starting and stopping simulators.


### Usage
Expand Down
4 changes: 2 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// transpile:main

import { getSimulator, getDeviceString } from './lib/simulator';
import { killAllSimulators, endAllSimulatorDaemons, simExists } from './lib/utils';
import { killAllSimulators, endAllSimulatorDaemons, simExists, installSSLCert, uninstallSSLCert } from './lib/utils';

export { getSimulator, getDeviceString, killAllSimulators, endAllSimulatorDaemons, simExists };
export { getSimulator, getDeviceString, killAllSimulators, endAllSimulatorDaemons, simExists, installSSLCert, uninstallSSLCert };
145 changes: 145 additions & 0 deletions lib/certificate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import crypto from 'crypto';
import B from 'bluebird';
import path from 'path';

const sqlite3 = B.promisifyAll(require('sqlite3'));
const openssl = B.promisify(require('openssl-wrapper').exec);

let tset = `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\">
<array/>
</plist>`;

/**
* Library for programatically adding certificates
*/
class Certificate {

constructor (pemFilename) {
this.pemFilename = pemFilename;
}

/**
* Add a certificate to the TrustStore
*/
async add (dir) {
let data = await this.getDerData(this.pemFilename);
let subject = await this.getSubject(this.pemFilename);
let fingerprint = await this.getFingerPrint(this.data);

let trustStore = new TrustStore(dir);
return trustStore.addRecord(fingerprint, tset, subject, data);
}

/**
* Checks if keychain at given directory has this certificate
*/
async has (dir) {
let subject = await this.getSubject(this.pemFilename);

let trustStore = new TrustStore(dir);
let records = await trustStore.getRecords(subject);
return records.length > 0;
}

/**
* Remove certificate from the TrustStore
*/
async remove (dir) {
let subject = await this.getSubject(this.pemFilename);
let trustStore = new TrustStore(dir);
return trustStore.removeRecord(subject);
}

/**
* Translate PEM file to DER buffer
*/
async getDerData () {
if (this.data) {
return this.data;
}

// Convert 'pem' file to 'der'
this.data = await openssl('x509', {
outform: 'der',
in: this.pemFilename
});

return this.data;
}

/**
* Get SHA1 fingerprint from der data before
*/
async getFingerPrint () {
if (this.fingerprint) {
return this.fingerprint;
}

let data = await this.getDerData();
let shasum = crypto.createHash('sha1');
shasum.update(data);
this.fingerprint = shasum.digest();
return this.fingerprint;
}

/**
* Parse the subject from the der data
*/
async getSubject () {
if (this.subject) {
return this.subject;
}

// Convert 'pem' file to 'der'
let subject = await openssl('x509', {
noout: true,
subject: true,
in: this.pemFilename
});
let subRegex = /^subject[\w\W]*\/CN=([\w\W]*)(\n)?/;
this.subject = subject.toString().match(subRegex)[1];
return this.subject;
}

}

/**
* Interface for adding and removing records to TrustStore.sqlite3 databases that Keychains use
*/
class TrustStore {
constructor (sharedResourceDir) {
this.sqliteDBPath = path.resolve(sharedResourceDir, 'Library/Keychains/TrustStore.sqlite3');
this.db = new sqlite3.Database(this.sqliteDBPath);
}

/**
* Add record to tsettings
*/
async addRecord (sha1, tset, subj, data) {
let existingRecords = await this.getRecords(subj);
if (existingRecords.length > 0) {
return await this.db.runAsync(`UPDATE tsettings SET sha1=?, tset=?, data=? WHERE subj=?`, [sha1, tset, data, subj]);
} else {
return await this.db.runAsync(`INSERT INTO tsettings (sha1, subj, tset, data) VALUES (?, ?, ?, ?)`, [sha1, subj, tset, data]);
}
}

/**
* Remove record from tsettings
*/
async removeRecord (subj) {
return this.db.runAsync(`DELETE FROM tsettings WHERE subj = ?`, [subj]);
}

/**
* Get a record from tsettings
*/
async getRecords (subj) {
return this.db.allAsync(`SELECT * FROM tsettings WHERE subj = ?`, [subj]);
}
}

export default Certificate;
export { Certificate, TrustStore };
3 changes: 3 additions & 0 deletions lib/tail-until.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ async function tailUntil (filePath, until, timeout = 5000) {

return new B(async (resolve, reject) => {
let started = proc.start(startDetector);

/* eslint-disable promise/prefer-await-to-then */
let timedout = B.delay(timeout).then(() => {
return reject(`tailing file ${filePath} failed after ${timeout}ms`);
});
/* eslint-enable */

await B.race([started, timedout]);

Expand Down
24 changes: 22 additions & 2 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { waitForCondition } from 'asyncbox';
import { getVersion } from 'appium-xcode';
import { getDevices } from 'node-simctl';
import { fs } from 'appium-support';

import { Certificate } from './certificate';
import path from 'path';
import Simulator from './simulator-xcode-6';

const OSASCRIPT_TIMEOUT = 10000;

Expand Down Expand Up @@ -132,4 +134,22 @@ async function safeRimRaf (delPath, tryNum = 0) {
}
}

export { killAllSimulators, endAllSimulatorDaemons, safeRimRaf, simExists };
async function installSSLCert (pemText, udid) {
let tempFileName = path.resolve(`${__dirname}/temp-ssl-cert.pem`);
let pathToKeychain = path.resolve(new Simulator(udid).getDir());
await fs.writeFile(tempFileName, pemText);
let certificate = new Certificate(tempFileName);
await certificate.add(pathToKeychain);
return await fs.unlink(tempFileName);
}

async function uninstallSSLCert (pemText, udid) {
let tempFileName = path.resolve(`${__dirname}/temp-ssl-cert.pem`);
let pathToKeychain = path.resolve(new Simulator(udid).getDir());
await fs.writeFile(tempFileName, pemText);
let certificate = new Certificate(tempFileName);
await certificate.remove(pathToKeychain);
return await fs.unlink(tempFileName);
}

export { killAllSimulators, endAllSimulatorDaemons, safeRimRaf, simExists, installSSLCert, uninstallSSLCert };
14 changes: 12 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@
"bluebird": "^2.9.34",
"lodash": "^4.2.1",
"node-simctl": "^3.4.3",
"openssl-wrapper": "^0.3.4",
"semver-compare": "^1.0.0",
"source-map-support": "^0.4.0",
"sqlite3": "^3.1.8",
"teen_process": "^1.3.0"
},
"scripts": {
Expand All @@ -52,14 +54,22 @@
"babel-eslint": "^6.1.0",
"chai": "^3.2.0",
"chai-as-promised": "^5.1.0",
"colors": "^1.1.2",
"eslint": "^2.13.1",
"eslint-config-appium": "0.0.6",
"eslint-config-appium": "0.1.0",
"eslint-plugin-babel": "^3.3.0",
"eslint-plugin-import": "^1.9.2",
"eslint-plugin-mocha": "^3.0.0",
"eslint-plugin-promise": "^3.4.0",
"express": "^4.14.0",
"fs-extra": "^1.0.0",
"gulp": "^3.8.11",
"https": "^1.0.0",
"inquirer": "^1.2.3",
"pem": "^1.8.3",
"pre-commit": "^1.1.3",
"sample-apps": "^2.0.2",
"sinon": "^1.15.4"
"sinon": "^1.15.4",
"uuid": "^2.0.3"
}
}
Binary file not shown.
Binary file not shown.
Binary file added test/assets/Library/certificates/test-data.txt
Binary file not shown.
1 change: 1 addition & 0 deletions test/assets/Library/certificates/test-fingerprint.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
��n��\8�ys2�/ w%�
1 change: 1 addition & 0 deletions test/assets/Library/certificates/test-subj.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Charles Proxy CA (15 Nov 2016, Daniels-MacBook-Pro.local)/OU=https://charlesproxy.com/ssl/O=XK72 Ltd/L=Auckland/ST=Auckland/C=NZ
105 changes: 105 additions & 0 deletions test/assets/certificate-test-server/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/* eslint-disable no-console */
import https from 'https';
import inquirer from 'inquirer';

import { installSSLCert, uninstallSSLCert } from '../../../lib/utils';
import { getDevices } from 'node-simctl';
import B from 'bluebird';

/* jshint ignore:start */ /* eslint-disable no-unused-vars */
import colors from 'colors';
/* eslint-enable */ /* jshint ignore:end */

const pem = B.promisifyAll(require('pem'));

(async () => {

// Create an HTTPS server with a randomly generated certificate
let key = await pem.createPrivateKeyAsync();
let keys = await pem.createCertificateAsync({days:1, selfSigned: true, serviceKey: key.key});

let server = https.createServer({key: keys.serviceKey, cert: keys.certificate}, function (req, res) {
res.end('If you are seeing this the certificate has been installed');
}).listen(9758);

console.log('Make sure you have at least one IOS Simulator running'.yellow);
let devices = await getDevices();

// Get currently booted devices
let bootedDevices = [];
for (let osName in devices) {
let os = devices[osName];
for (let deviceName in os) {
let device = os[deviceName];
if (device.state === 'Booted') {
bootedDevices.push(device);
}
}
}

if (bootedDevices.length === 0) {
return console.log('You must have at least one IOS Simulator running to do this test'.red);
}

// Get info for first device
let bootedDevice = bootedDevices[0];
let udid = bootedDevice.udid;
let deviceName = bootedDevice.name;

console.log('HTTPS server is running at localhost:9758 and has created a new certificate at "random-pem.pem"'.yellow);
console.log(`Navigate to https://localhost:9758 in '${deviceName} Simulator'`.yellow);
console.log('DO NOT PUSH THE CONTINUE BUTTON. PUSH CANCEL.'.red);

// Call this if the user answers 'No' to any prompts
async function done () {
await uninstallSSLCert(keys.certificate, udid);
server.close();
console.log('Incomplete/failed test'.red);
}

let result = await inquirer.prompt([{
type: 'confirm',
name: 'confirmOpenSite',
message: `Is https//localhost:9758 on '${deviceName} Simulator' unaccessible?`,
}]);

console.log('Certificate', keys.certificate, udid);

await installSSLCert(keys.certificate, udid);

if (!result.confirmOpenSite) return done();

// Apply certificate to Simulator
console.log('Installing certificate'.yellow);
console.log(`Certificate installed to '${deviceName} ${udid}'. Navigate back to https://localhost:9758.`.yellow);

result = await inquirer.prompt([{
type: 'confirm',
name: 'confirmOpenedSite',
message: 'Now is https://localhost:9758 accessible?',
}]);

if (!result.confirmOpenedSite) {
return done();
}

// Uninstall cert
console.log(`Uninstalling SSL cert`.yellow);
await uninstallSSLCert(keys.certificate, udid);
console.log(`SSL cert removed.`.yellow);
console.log(`Close the simulator, re-open it and then navigate back to https://localhost:9758`.yellow);

result = await inquirer.prompt([{
type: 'confirm',
name: 'confirmUninstallCert',
message: `Is https://localhost:9758 unaccessible?`,
}]);

if (result.confirmUninstallCert) {
console.log('Test passed'.green);
}

return server.close();
})();

/* eslint-enable */
Loading

0 comments on commit d04f7ee

Please sign in to comment.