Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changes the format of the signature to allow for later extensions. #18

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 37 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,43 @@ Install the extension and add the pattern and pubkey shown in the page from the

## As a developer

You need to add a comment at the top of the html file (right after the doctype if exists) that contains the detached PGP signature of the content of the `<html>` tag after it has been minified with [minimized](https://github.com/Swaagie/minimize) with a specific set of settings.
The signatures are specified in html `<meta>` tags with the name `signature` within the document `<head>`. There are two types of signatures available: one over the whole document, and one over a minimized version. The signature over the minimized version helps with cross compatiblility accross different browsers. The whole document signatures may be more secure, but only works with browsers that support `filteredResponseData` (Currently only Firefox). Both types of signatures can be included in a document.

The `content` of the `signature` `<meta` tag are name-value pairs separated by commas. The fields are:
- type - *required*: (`pgp` or `pgp_minimized`)
- version - *required* - ( major.minor.patch )
- signature - *required* - (the actual armored gpg signature)
- allowedMethods - *optional* - Space seperated list of (`filteredResponseData`, `outsideHTML`).
- Default: "`filteredResponseData outsideHTML`"

To sign the document, you must include all of content of each signature `<meta>` tag except for the signature itself. There can be no whitespace between the `signature=` and the next item or end of tag.

Example:
```
<meta name="signature" content="
type=pgp,
version=1.0.0,
allowedMethods=filteredResponseData,
signature=">
```

For the whole document signature (type `pgp`), all you need to do is sign the document and include the signature in the meta tag.

For the minimized signature (type `pgp_minimized`), you will need to minimize with [minimize](https://github.com/Swaagie/minimize), version 2.1.0, with a specific set of settings. You will then sign this minimized version and include the signature in the original document `<meta>` tag

As you can see, it's a bit involved, so we created a script that does all of this for you. All you need to do is make sure you have a comment at the top of the file that contains the special replace tag like in [example.html](example.html).
As you can see, it's a bit involved, so we created a script that does all of this for you. All you need to do is make sure you include specific placeholders as in [example.html](example.html).

And then just run, on a secure machine, preferably with a PGP key on a separate hardware token:

```
# Print the signed page to stdout
$ ./page-signer.js input.html
# find key id for signing key
$ gpg --list-keys

# Print the signed page to stdout (You will have a different keyid)
$ ./page-signer.js 9C43B88E input.html

# Print the signed page to a file (can be the same as the input file)
$ ./page-signer.js input.html output.html
$ ./page-signer.js 9C43B88E input.html output.html
```

It's important that all of the external resources to the page (JS and CSS, whether hosted on the same server, or not) will have [subresource integrity](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) correctly set. This way you only need to sign the html page, and the rest will be automatically validated by the browser, ensuring that all of the scripts and styles used in the page are indeed what you expect.
Expand Down Expand Up @@ -116,7 +141,13 @@ What makes matters even worse is that browsers don't return the html as delivere

Be aware that the minifier may have bugs that can cause a page to pass verification while being different! Unlikely, but possible, so watch out for minifier bugs.

Since the same signature needs to work on all browsers, we unfortunately have to minimise the html on Firefox too. This workaround will be removed once the aforementioned `filterResponseData` is implemented across browsers.
# Versions

All signature types must have a version and they must match the supported versions within this extension. A version matches if the major and minor numbers match, and the patch level on the page is less than or equal to the supported patch level. This allows backwards-compatible patches without invalidating previous signatures. Also, when there is new version, the older version will be supported for time.

Current Supported Versions:
- pgp: 1.0.0
- pgp_minimized: 1.0.0

# Potential attacks

Expand Down
21 changes: 18 additions & 3 deletions example.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
<!doctype html>
<!--!
%%%SIGNED_PAGES_PGP_SIGNATURE%%%
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="signature" content="
type=pgp,
version=1.0.0,
allowedMethods=filterResponseData,
signature=
%%%SIGNED_PAGES_PGP_SIGNATURE%%%
">
<meta name="signature" content="
type=pgpMinimized,
version=1.0.0,
allowedMethods=outsideHTML,
signature=
%%%SIGNED_PAGES_PGP_SIGNATURE_MIN%%%
">
<meta name="trustedPublicKey" content="
%%%SIGNED_PAGES_PUBLICKEY%%%
">
<meta name="trustedDNS" content="true">
<title>A signed page example!</title>
</head>
<body>
Expand Down
2 changes: 1 addition & 1 deletion extension/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"manifest_version": 2,
"name": "Signed Pages",
"description": "Verifies PGP signed pages for extra security against malicious or breached servers.",
"version": "0.4.0",
"version": "0.5.0",
"homepage_url": "https://github.com/tasn/webext-signed-pages",
"icons": {
"48": "images/icon.png",
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"name": "signed-pages",
"description": "Verifies PGP signed pages for extra security against malicious or breanched servers.",
"version": "0.4.0",
"version": "0.5.0",
"main": "index.js",
"scripts": {
"build": "webpack -w --display-error-details --progress --colors",
"build": "npm-install-version [email protected] && webpack -w --display-error-details --progress --colors",
"package": "webpack --colors && web-ext build -s extension",
"start": "web-ext run -s extension/"
},
Expand All @@ -15,13 +15,13 @@
"babel-loader": "^7.1.2",
"babel-preset-react": "^6.24.1",
"babel-runtime": "^6.26.0",
"npm-install-version": "^6.0.2",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"web-ext": "^2.2.2",
"webpack": "^3.10.0"
},
"dependencies": {
"minimize": "^2.1.0",
"openpgp": "^2.6.1",
"webextension-polyfill": "^0.2.1"
},
Expand Down
73 changes: 52 additions & 21 deletions page-signer.js
Original file line number Diff line number Diff line change
@@ -1,59 +1,90 @@
#!/usr/bin/env node
/* eslint node: true */
/* eslint-env node */
/* eslint-disable no-console */

const fs = require('fs');
const child_process = require('child_process');
const Minimize = require('minimize');
const Minimize = require('minimize@2.1.0');

function errorAbort(text) {
console.error(text);
process.exit(1);
}

function getSignature(content, callback) {
function getSignature(content, keyid, callback) {
const tmpfile = `/tmp/${process.pid}`;
fs.writeFileSync(tmpfile, content, 'utf-8');
const gpg = child_process.spawnSync('gpg', ['--armor', '--output', '-', '--detach-sign', tmpfile], {
const gpg = child_process.spawnSync('gpg', ['--armor', '--output', '-', '--local-user', keyid, '--detach-sign', tmpfile], {
stdio: [
0,
'pipe',
]
});

fs.unlink(tmpfile, () => {});
const signature = gpg.stdout.toString();
if (callback)
callback(signature);
return signature;
}

callback(gpg.stdout.toString());
function getPublicKey( keyid, callback) {
const gpg = child_process.spawnSync('gpg', ['--armor', '--output', '-', '--export', keyid], {
stdio: [
0,
'pipe',
]
});
const key = gpg.stdout.toString();
if (callback)
callback(key);
return key;
}

let args = process.argv.slice(2);

const keyid = args.shift();
const filename = args.shift();
const outfile = args.shift();

if (!filename) {
errorAbort(`Usage: ${process.argv[1]} <infile> [outfile]`);
if (!filename || !keyid) {
errorAbort(`Usage: ${process.argv[1]} <keyid> <infile> [outfile]`);
}

fs.readFile(filename, 'utf8', (err, data) => {
if (err) {
errorAbort(err);
}

const key = getPublicKey(keyid);

// replace public keys
var out = data.replace('%%%SIGNED_PAGES_PUBLICKEY%%%', key);

// Strip placeholders (and whitespace around them)
const signed_content = out.replace(/\s*%%%SIGNED_PAGES_PGP_SIGNATURE\w*%%%\s*/g, '');

console.log(signed_content);

// Signature of entire document (like pulled from filterResponseData)
var signature = getSignature(signed_content, keyid);
out = out.replace('%%%SIGNED_PAGES_PGP_SIGNATURE%%%', signature);

// Signature using minimize
// Minimize and strip the doctype
const content = new Minimize({ spare:true, conditionals: true, empty: true, quotes: true }).parse(data)
const min_content = new Minimize({ spare:true, conditionals: true, empty: true, quotes: true }).parse(signed_content)
.replace(/^\s*<!doctype[^>]*>/i, '');

getSignature(content, (signature) => {
const out = data.replace('%%%SIGNED_PAGES_PGP_SIGNATURE%%%', signature);

if (outfile) {
fs.writeFile(outfile, out, 'utf8', (writeErr) => {
if (writeErr) {
errorAbort(writeErr);
}
});
} else {
process.stdout.write(out);
}
});
signature = getSignature(min_content, keyid);
out = out.replace('%%%SIGNED_PAGES_PGP_SIGNATURE_MIN%%%', signature);

if (outfile) {
fs.writeFile(outfile, out, 'utf8', (writeErr) => {
if (writeErr) {
errorAbort(writeErr);
}
});
} else {
process.stdout.write(out);
}
});
Loading