Skip to content

Commit

Permalink
Support for signed releases
Browse files Browse the repository at this point in the history
  • Loading branch information
corrideat committed Jul 1, 2024
1 parent 46bb5b8 commit d3fa002
Show file tree
Hide file tree
Showing 18 changed files with 248 additions and 42 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ jobs:
- run: npm ci
- run: npm run build
env:
LC_ALL: C
TZ: UTC
BUILD_TYPE: release
NODE_ENV: production
SIGNATURE_MODE: mandatory
- run: |
cd build
cp index.html encrypt.html
Expand Down
12 changes: 12 additions & 0 deletions assets/openpgp_signing_key.asc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----

mDMEZoHxgBYJKwYBBAHaRw8BAQdA04cBaC9lu9ryYKQBzZmRYQ1CaPbzBJDD9aMW
qWSipIa0L0V4YWN0IFJlYWx0eSBMaW1pdGVkIChAZXhhY3QtcmVhbHR5L2Ntcy1l
cC1zZngpiJAEExYIADgWIQRBaISOgUxaACAX/DG4AVasY/venQUCZoHxgAIbAwUL
CQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRC4AVasY/venT29AP9u1O1EEaIFmASF
SWbvn/PN4skhKW1auBp5msUmiQKivwD+PPBoT1vBNDOTHtg85t5+exsmJuycFxJ1
xZ2++XTPBQCIdQQQFggAHRYhBHrN/yLHc7dHXX6Qp/GI7f2A93QcBQJmgrveAAoJ
EPGI7f2A93Qc8ekBAOS/oFHbGlN724MKKcvpUnaJeJPQJS+0IF7qlAIsN09HAQCI
MJlQgpEAEBrpvNcrKSfBEGE7RhNZ6y/hS8OrStGAAQ==
=P3T0
-----END PGP PUBLIC KEY BLOCK-----
107 changes: 99 additions & 8 deletions esbuild.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ const exactRealtyBuilderPlugin = (
mainFields: ['module', 'main'],
outdir: OUTDIR_CLIENT,
target: 'es2018',
format: 'cjs',
plugins,
write: false,
});
Expand All @@ -318,8 +319,10 @@ const exactRealtyBuilderPlugin = (

function requireFromString(src) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const exports = {};
const ctx = vm.createContext({
exports: {},
module: { exports },
exports,
btoa,
atob,
crypto: webcrypto,
Expand All @@ -340,13 +343,101 @@ const exactRealtyBuilderPlugin = (
).contents;
const cssText = findPath(clientBuild.outputFiles, cssBundlePath).contents;

await fs
.access(TARGET_DIR, fs.constants.W_OK)
.catch(() => fs.mkdir(TARGET_DIR, { recursive: true }));
await fs.writeFile(
join(TARGET_DIR, 'index.html'),
await requireFromString(text).default(scriptText, cssText),
);
const m = requireFromString(text);

const prepOutDir = async () => {
await fs
.access(TARGET_DIR, fs.constants.W_OK)
.catch(() => fs.mkdir(TARGET_DIR, { recursive: true }));
};

const obtainSignatureInformation = () => {
const result = childProcess.spawnSync('git', [
'tag',
'-l',
'--format=%(contents:body)',
'--points-at',
'HEAD',
'v*',
]);

if (result.status === 0) {
const lines = result.stdout.toString('utf-8').split(/\r\n|\r|\n/);
const marker = lines.lastIndexOf('::');
if (marker < 0) {
console.warn('No signature information found in git tags');
return;
}
const expectedDigest = lines[marker + 1]
.trim()
.replace(/^:/, '')
.toLowerCase();
const openPgpSignature = lines
.slice(marker + 2)
.map((s) => s.trim())
.filter((s) => s.startsWith(':'))
.map((s) => s.replace(/^:/, ''))
.join('\r\n');

return [expectedDigest, openPgpSignature];
} else {
console.warn('Error getting git tag information');
}
};

switch ((process.env.SIGNATURE_MODE || '').toLowerCase()) {
case '':
case 'unsigned': {
await prepOutDir();

await fs.writeFile(
join(TARGET_DIR, 'index.html'),
await m.default(scriptText, cssText),
);
break;
}
case 'presign': {
await prepOutDir();

await fs.writeFile(
join(TARGET_DIR, 'tbs'),
await m.tbsPayload(scriptText, cssText),
);
break;
}
case 'opportunistic':
case 'mandatory': {
// opportunistic or mandatory
const [expectedDigest, openPgpSignature] =
obtainSignatureInformation() || [];

const tbsPayload = await m.tbsPayload(scriptText, cssText);
const digest = Buffer.from(
await crypto.subtle.digest(
{ name: 'SHA-256' },
Buffer.from(tbsPayload),
),
)
.toString('hex')
.toLowerCase();

if (digest !== expectedDigest) {
console.error('Digest mismatch', { digest, expectedDigest });
if (process.env.SIGNATURE_MODE.toLowerCase() === 'mandatory') {
throw new Error('Digest mismatch');
}
}

await prepOutDir();
await fs.writeFile(
join(TARGET_DIR, 'index.html'),
await m.default(scriptText, cssText, openPgpSignature || ''),
);
break;
}
default:
throw new Error('Unsupported SIGNATURE_MODE');
}
})().catch((e) => {
console.dir(e);
process.exit(1);
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@exact-realty/cms-ep-sfx",
"version": "1.0.9",
"version": "1.0.10",
"description": "Secure File Sharing Utility",
"type": "module",
"main": "-",
Expand Down
3 changes: 3 additions & 0 deletions scripts/cloudflare-pages.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ printf 'fffa52c22d797b715a962e6c8d11ec7d79b90dd819b5bc51d62137ea4b22a340 OpenJD
tar -xzf 'OpenJDK21U-jdk_x64_linux_hotspot_21.0.3_9.tar.gz'
export JAVA_HOME="$(pwd)/jdk-21.0.3+9"
export PATH="$JAVA_HOME/bin:$PATH"
export LC_ALL="C"
export TZ="UTC"
export NODE_ENV="production"
export BUILD_TYPE="release"
export SIGNATURE_MODE="opportunistic"
npm run build
13 changes: 13 additions & 0 deletions scripts/new-release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/sh

set -e
export LC_ALL="C"
export TZ="UTC"
export BUILD_TYPE="release"
export NODE_ENV="production"

dir=$(dirname "${0}")

SIGNATURE_MODE="presign" npm run build
. "$dir/sign-release.sh"
SIGNATURE_MODE="mandatory" npm run build
19 changes: 19 additions & 0 deletions scripts/sign-release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/sh

set -e
export LC_ALL="C"
export TZ="UTC"

tbs='build/tbs'
user='4168848E814C5A002017FC31B80156AC63FBDE9D'
version=$(jq '--raw-output' '.version' 'package.json')

if git 'rev-parse' '--quiet' '--verify' 'refs/tags/v'"$version"; then
echo 'Release tag already exists' >&2
exit 1
fi

digest=$(openssl 'dgst' '-binary' '-sha256' "$tbs" | xxd '-p' '-c' '256')
signature=$(gpg2 '--armor' '--clear-sign' '--local-user' "$user" '--digest-algo' 'SHA256' '--output' '-' "$tbs" | sed "-n" '/^-----BEGIN PGP SIGNATURE-----/,$p' | sed "-e" "s/^/:/g")
printf '%s\n\n::\n:%s\n%s\n' "v$version" "$digest" "$signature" | git 'tag' '-s' "v$version" '-F' '-'
echo 'Created tag v'"$version"
8 changes: 7 additions & 1 deletion src/components/Header.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,13 @@
let mainScript$_: HTMLScriptElement | undefined;
let mainStylesheet$_: HTMLLinkElement | undefined;
let openPgpSignature$_: HTMLScriptElement | undefined;
export { mainScript$_ as mainScript$, mainStylesheet$_ as mainStylesheet$ };
export {
mainScript$_ as mainScript$,
mainStylesheet$_ as mainStylesheet$,
openPgpSignature$_ as openPgpSignature$,
};
</script>

<header class="header">
Expand All @@ -32,6 +37,7 @@
<OfflineDownload
mainScript$={mainScript$_}
mainStylesheet$={mainStylesheet$_}
openPgpSignature$={openPgpSignature$_}
/>
</div>
</header>
16 changes: 13 additions & 3 deletions src/components/OfflineDownload.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,25 @@
let mainScript$_: HTMLScriptElement | undefined;
let mainStylesheet$_: HTMLLinkElement | undefined;
let openPgpSignature$_: HTMLScriptElement | undefined;
const handleClick = () => {
downloadArchive(mainScript$_!, mainStylesheet$_!, 'encrypt.html');
downloadArchive(
mainScript$_!,
mainStylesheet$_!,
openPgpSignature$_!,
'encrypt.html',
);
};
export { mainScript$_ as mainScript$, mainStylesheet$_ as mainStylesheet$ };
export {
mainScript$_ as mainScript$,
mainStylesheet$_ as mainStylesheet$,
openPgpSignature$_ as openPgpSignature$,
};
</script>

{#if mainScript$_ && mainStylesheet$_}
{#if mainScript$_ && mainStylesheet$_ && openPgpSignature$_}
<button on:click={handleClick} class="offline-download-button"
>&#x2b07;&#xfe0e; Download for offline use</button
>
Expand Down
2 changes: 2 additions & 0 deletions src/lib/downloadArchive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import generateHtml from './generateHtml.js';
const downloadArchive_ = async (
mainScript$: HTMLScriptElement,
mainStylesheet$: HTMLLinkElement,
openPgpSignature$: HTMLScriptElement,
archiveName: string,
encryptedContent?: string[] | undefined,
hint?: string | undefined,
Expand All @@ -36,6 +37,7 @@ const downloadArchive_ = async (
const htmlDocument = await generateHtml(
scriptSrc,
styleSrc,
openPgpSignature$.text.trim(),
encryptedContent,
hint,
);
Expand Down
1 change: 1 addition & 0 deletions src/lib/elementIds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
export const MAIN_SCRIPT_SRC_ELEMENT_ID_ = 'MAIN_SCRIPT_SRC_ELEMENT__';
export const MAIN_SCRIPT_ELEMENT_ID_ = 'MAIN_SCRIPT_ELEMENT__';
export const MAIN_STYLESHEET_ELEMENT_ID_ = 'MAIN_STYLESHEET_ELEMENT__';
export const OPENPGP_SIGNATURE_ELEMENT_ID_ = 'OPENPGP_SIGNATURE_ELEMENT__';
export const MAIN_CONTENT_ELEMENT_ID_ = 'MAIN_CONTENT_ELEMENT__';
export const CMS_DATA_ELEMENT_ID_ = 'CMS_DATA_ELEMENT__';
export const CMS_FILENAME_ELEMENT_ID_ = 'CMS_FILENAME_ELEMENT__';
Expand Down
59 changes: 44 additions & 15 deletions src/lib/generateHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
ERROR_ELEMENT_ID_,
MAIN_SCRIPT_SRC_ELEMENT_ID_,
MAIN_STYLESHEET_ELEMENT_ID_,
OPENPGP_SIGNATURE_ELEMENT_ID_,
} from './elementIds.js';
import {
xmlEscape_ as xmlEscape,
Expand All @@ -48,27 +49,15 @@ const sriDigest = async (buf: AllowSharedBufferSource) => {
return 'sha384-' + bbtoa(digest);
};

const generateHtml_ = async (
export const tbsPayload_ = async (
mainScriptText: AllowSharedBufferSource,
cssText: AllowSharedBufferSource,
encryptedContent?: string[],
hint?: string,
) => {
const pkcs7MimeType = 'application/pkcs7-mime';
const startJsonEscapeSequece = '<![CDATA[><!--\r\n';
const endJsonEscapeSequece = '\r\n--><!]]>';

const mainScriptTextSriDigest = await sriDigest(mainScriptText);
const cssTextSriDigest = cssText ? await sriDigest(cssText) : '';

if (hint) {
hint = hint.replace(/<\//g, '<//');
}

return (
'<!DOCTYPE html>' +
'<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">' +
'<head>' +
'<script type="text/plain"></script>' +
'<meta charset="UTF-8"/>' +
'<meta name="viewport" content="width=device-width, initial-scale=1.0"/>' +
'<meta' +
Expand All @@ -89,7 +78,47 @@ const generateHtml_ = async (
'\r\n' +
`<script src="data:text/javascript;base64,${xmlEscapeAttr(loader.contentBase64)}" defer="defer" integrity="${xmlEscapeAttr(loader.sri)}" crossorigin="anonymous">` +
'</script>' +
'\r\n' +
`<script type="text/plain" id="${xmlEscapeAttr(OPENPGP_SIGNATURE_ELEMENT_ID_)}">\r\n`
);
};

const openPgpSignatureWrapper = (payload: string, signature: string) => {
const fiveDashes = String.prototype.repeat.call('-', 5);

return (
'<script type="text/plain">\r\n' +
`${fiveDashes}BEGIN PGP SIGNED MESSAGE${fiveDashes}\r\n` +
'Hash: SHA256\r\n\r\n' +
payload +
signature.split(/\r\n|\r|\n/).join('\r\n') +
'\r\n</script>\r\n'
);
};

const generateHtml_ = async (
mainScriptText: AllowSharedBufferSource,
cssText: AllowSharedBufferSource,
openPgpSignatureText?: string,
encryptedContent?: string[],
hint?: string,
) => {
const pkcs7MimeType = 'application/pkcs7-mime';
const startJsonEscapeSequece = '<![CDATA[><!--\r\n';
const endJsonEscapeSequece = '\r\n--><!]]>';

const tbsPayload = await tbsPayload_(mainScriptText, cssText);

if (hint) {
hint = hint.replace(/<\//g, '<//');
}

return (
'<!DOCTYPE html>' +
'<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">' +
'<head>' +
(openPgpSignatureText
? openPgpSignatureWrapper(tbsPayload, openPgpSignatureText)
: tbsPayload + '</script>') +
(Array.isArray(encryptedContent) && encryptedContent.length > 1
? `<script type="${xmlEscapeAttr(pkcs7MimeType)}" id="${xmlEscapeAttr(CMS_DATA_ELEMENT_ID_)}">` +
startJsonEscapeSequece +
Expand Down
4 changes: 3 additions & 1 deletion src/lib/isCI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
* limitations under the License.
*/

const isCI_ = Object.prototype.hasOwnProperty.call(window, '__CI__');
declare const __CI__: unknown;

const isCI_ = typeof __CI__ !== [] + [][0] && !!__CI__;

export default isCI_;
Loading

0 comments on commit d3fa002

Please sign in to comment.