Skip to content

Commit 7220b29

Browse files
committed
wip the PGP verification is happening in a sandbox!
1 parent 587a3a4 commit 7220b29

File tree

5 files changed

+176
-61
lines changed

5 files changed

+176
-61
lines changed

middleware.js

Lines changed: 14 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ const fp = require('fs').promises
33
const path = require('path')
44
const url = require('url')
55
const EventEmitter = require('events').EventEmitter
6-
const { E, indexPack, plugins, readObject, verify } = require('isomorphic-git')
7-
const { serveInfoRefs, serveReceivePack, parseReceivePackRequest } = require('isomorphic-git/dist/for-node/isomorphic-git/internal-apis.js')
6+
const { E, indexPack, plugins, readObject, verify, serveInfoRefs, serveReceivePack, parseReceivePackRequest } = require('isomorphic-git')
87
const { pgp } = require('@isomorphic-git/pgp-plugin')
98

109
let ee = new EventEmitter()
@@ -16,6 +15,7 @@ const chalk = require('chalk')
1615
const is = require('./identify-request.js')
1716
const parse = require('./parse-request.js')
1817
const { lookup, demote } = require('./lookup.js')
18+
const { sandbox } = require('./sandbox.js')
1919

2020
function pad (str) {
2121
return (str + ' ').slice(0, 7)
@@ -27,6 +27,8 @@ function abbr (oid) {
2727

2828
const sleep = ms => new Promise(cb => setTimeout(cb, ms))
2929

30+
const tick = () => new Promise(cb => process.nextTick(cb))
31+
3032
function log(req, res) {
3133
const color = res.statusCode > 399 ? chalk.red : chalk.green
3234
console.log(color(`[git-server] ${res.statusCode} ${pad(req.method)} ${req.url}`))
@@ -86,6 +88,8 @@ function factory (config) {
8688
filepath = `pack-${last20}.pack`
8789
await fp.rename(path.join(dir, oldfilepath), path.join(dir, filepath))
8890
}
91+
const core = gitdir + '-' + String(Math.random()).slice(2, 8)
92+
console.log('core', core)
8993
gitdir = path.join(__dirname, gitdir)
9094

9195
// send HTTP response headers
@@ -95,19 +99,18 @@ function factory (config) {
9599
// index packfile
96100
res.write(await serveReceivePack({ type: 'print', message: 'Indexing packfile...' }))
97101
console.log('Indexing packfile...')
98-
await sleep(1)
102+
await tick()
99103
let currentPhase = null
100104
const listener = async ({ phase, loaded, total, lengthComputable }) => {
101105
let np = phase !== currentPhase ? '\n' : '\r'
102106
currentPhase = phase
103107
res.write(await serveReceivePack({ type: 'print', message: `${np}${phase} ${loaded}/${total}` }))
104-
res.flush()
105108
}
106109
let oids
107110
try {
108111
ee.on(`${last20}:progress`, listener)
109112
oids = await indexPack({ fs, gitdir, dir, filepath, emitterPrefix: `${last20}:` })
110-
await sleep(1)
113+
await tick()
111114
res.write(await serveReceivePack({ type: 'print', message: '\nIndexing completed' }))
112115
res.write(await serveReceivePack({ type: 'unpack', unpack: 'ok' }))
113116
} catch (e) {
@@ -121,59 +124,18 @@ function factory (config) {
121124
} finally {
122125
ee.removeListener(`${last20}:progress`, listener)
123126
}
124-
await sleep(1)
127+
await tick()
125128

126129
// Move packfile and index into repo
127130
await fp.rename(path.join(dir, filepath), path.join(gitdir, 'objects', 'pack', filepath))
128131
await fp.rename(path.join(dir, filepath.replace(/\.pack$/, '.idx')), path.join(gitdir, 'objects', 'pack', filepath.replace(/\.pack$/, '.idx')))
129132
await fp.rmdir(path.join(dir))
130133

131-
// Verify objects (ideally we'd do this _before_ moving it into the repo... but I think we'd need a custom 'fs' implementation with overlays)
132-
res.write(await serveReceivePack({ type: 'print', message: '\nVerifying objects...\n' }))
133-
let i = 0
134-
135-
for (const oid of oids) {
136-
i++
137-
res.write(await serveReceivePack({ type: 'print', message: `\rVerifying object ${i}/${oids.length}` }))
138-
const { type, object } = await readObject({ gitdir, oid })
139-
if (type === 'commit' || type === 'tag') {
140-
const email = type === 'commit' ? object.author.email : object.tagger.email
141-
res.write(await serveReceivePack({ type: 'print', message: `\nVerifying ${type} ${abbr(oid)} by ${email}: ` }))
142-
let keys
143-
try {
144-
keys = await lookup(email)
145-
} catch (e) {
146-
res.write(await serveReceivePack({ type: 'print', message: `no keys found 👎\n` }))
147-
throw e
148-
}
149-
if (keys.length === 0) {
150-
res.write(await serveReceivePack({ type: 'print', message: `no keys found 👎\n` }))
151-
throw new Error(`\nSignature verification failed for ${type} ${abbr(oid)}. No PGP keys could be found for ${email}.\n`)
152-
}
153-
let ok = false
154-
for (const key of keys) {
155-
const result = await verify({ gitdir, ref: oid, publicKeys: key })
156-
if (result === false) {
157-
demote(email, key)
158-
} else {
159-
res.write(await serveReceivePack({ type: 'print', message: `signed with ${result[0]} 👍\n` }))
160-
ok = true
161-
break
162-
}
163-
}
164-
if (!ok) {
165-
res.write(await serveReceivePack({ type: 'print', message: `no keys matched 👎\n` }))
166-
throw new Error(`\nSignature verification failed for ${type} ${abbr(oid)}. It was not signed with a key publicly associated with the email address "${email}".
167-
168-
Learn how you can associate your GPG key with your email account using GitHub here:
169-
https://help.github.com/en/github/authenticating-to-github/adding-a-new-gpg-key-to-your-github-account
170-
`)
171-
}
172-
}
173-
// await sleep(1)
174-
}
175-
176-
res.write(await serveReceivePack({ type: 'print', message: `\nVerification complete` }))
134+
// Run pre-receive-hook
135+
res.write(await serveReceivePack({ type: 'print', message: '\nRunning pre-receive-hook\n' }))
136+
await tick()
137+
const script = fs.readFileSync('./pre-receive-hook.js', 'utf8')
138+
await sandbox({ core, dir, gitdir, res, oids, script })
177139

178140
// refs
179141
for (const update of updates) {
@@ -185,14 +147,6 @@ https://help.github.com/en/github/authenticating-to-github/adding-a-new-gpg-key-
185147
} catch (e) {
186148
if (e.message === 'Client is done') {
187149
res.statusCode = 200
188-
} else if (e.code && e.code === E.NoSignatureError) {
189-
res.write(await serveReceivePack({ type: 'print', message: `no signature 👎\n` }))
190-
res.write(await serveReceivePack({ type: 'error', message: e.message + `
191-
192-
This server's policy is to only accept GPG-signed commits.
193-
Learn how you can create a GPG key and configure git to sign commits here:
194-
https://help.github.com/en/github/authenticating-to-github/managing-commit-signature-verification
195-
` }))
196150
} else {
197151
res.write(await serveReceivePack({ type: 'error', message: e.message }))
198152
}

package-lock.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"micro-cors": "^0.1.1",
2323
"minimisted": "^2.0.0",
2424
"simple-get": "^3.1.0",
25-
"tree-kill": "^1.2.0"
25+
"tree-kill": "^1.2.0",
26+
"vm2": "^3.8.4"
2627
},
2728
"publishConfig": {
2829
"access": "public"

pre-receive-hook.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
(async () => {
2+
console.log('Wish me luck!')
3+
4+
function abbr (oid) {
5+
return oid.slice(0, 7)
6+
}
7+
8+
// Verify objects (ideally we'd do this _before_ moving it into the repo... but I think we'd need a custom 'fs' implementation with overlays)
9+
console.log('\nVerifying objects...\n')
10+
let i = 0
11+
12+
for (const oid of oids) {
13+
i++
14+
console.log(`\rVerifying object ${i}/${oids.length}`)
15+
const { type, object } = await git.readObject({ oid })
16+
if (type === 'commit' || type === 'tag') {
17+
const email = type === 'commit' ? object.author.email : object.tagger.email
18+
console.log(`\nVerifying ${type} ${abbr(oid)} by ${email}: `)
19+
let keys
20+
try {
21+
keys = await pgp.lookup(email)
22+
} catch (e) {
23+
console.fatal(`no keys found 👎\n`)
24+
return
25+
}
26+
if (keys.length === 0) {
27+
console.log(`no keys found 👎\n`)
28+
console.fatal(`\nSignature verification failed for ${type} ${abbr(oid)}. No PGP keys could be found for ${email}.\n`)
29+
return
30+
}
31+
let ok = false
32+
for (const key of keys) {
33+
let result
34+
try {
35+
result = await git.verify({ ref: oid, publicKeys: key })
36+
} catch (e) {
37+
if (e.code && e.code === git.E.NoSignatureError) {
38+
console.log(`no signature 👎\n`)
39+
console.fatal(e.message + `
40+
41+
This server's policy is to only accept GPG-signed commits.
42+
Learn how you can create a GPG key and configure git to sign commits here:
43+
https://help.github.com/en/github/authenticating-to-github/managing-commit-signature-verification
44+
`)
45+
return
46+
} else {
47+
console.fatal(e.message)
48+
return
49+
}
50+
}
51+
if (result === false) {
52+
pgp.demote(email, key)
53+
} else {
54+
console.log(`signed with ${result[0]} 👍\n`)
55+
ok = true
56+
break
57+
}
58+
}
59+
if (!ok) {
60+
console.log(`no keys matched 👎\n`)
61+
console.fatal(`\nSignature verification failed for ${type} ${abbr(oid)}. It was not signed with a key publicly associated with the email address "${email}".
62+
63+
Learn how you can associate your GPG key with your email account using GitHub here:
64+
https://help.github.com/en/github/authenticating-to-github/adding-a-new-gpg-key-to-your-github-account
65+
`)
66+
return
67+
}
68+
}
69+
}
70+
71+
console.log(`\nVerification complete`)
72+
done()
73+
})()

sandbox.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
const fs = require('fs')
2+
const EventEmitter = require('events').EventEmitter
3+
4+
const { VM, VMScript } = require('vm2');
5+
const git = require('isomorphic-git')
6+
const { pgp } = require('@isomorphic-git/pgp-plugin')
7+
8+
const { lookup, demote } = require('./lookup.js')
9+
10+
const curry = ({ core, dir, gitdir }) => fn => argObject => fn({ ...argObject, core, dir, gitdir })
11+
12+
const sandbox = ({ core, dir, gitdir, res, script, oids }) => {
13+
let ee = new EventEmitter()
14+
plugincore = git.cores.create(core)
15+
plugincore.set('emitter', ee)
16+
plugincore.set('fs', fs)
17+
plugincore.set('pgp', pgp)
18+
19+
const $ = curry({ core, dir, gitdir })
20+
const $git = {
21+
E: { ...git.E },
22+
eventEmitter: ee,
23+
expandOid: $(git.expandOid),
24+
expandRef: $(git.expandRef),
25+
findMergeBase: $(git.findMergeBase),
26+
getRemoteInfo: $(git.getRemoteInfo),
27+
hashBlob: $(git.hashBlob),
28+
isDescendent: $(git.isDescendent),
29+
listBranches: $(git.listBranches),
30+
listFiles: $(git.listFiles),
31+
listRemotes: $(git.listRemotes),
32+
listTags: $(git.listTags),
33+
log: $(git.log),
34+
readObject: $(git.readObject),
35+
resolveRef: $(git.resolveRef),
36+
serveReceivePack: $(git.serveReceivePack),
37+
verify: $(git.verify),
38+
walkBeta2: $(git.walkBeta2),
39+
}
40+
const $res = {
41+
write: res.write.bind(res)
42+
}
43+
44+
return new Promise((resolve, reject) => {
45+
const $console = {
46+
log: async (...args) => {
47+
res.write(await git.serveReceivePack({ type: 'print', message: args.join() }))
48+
},
49+
fatal: (...args) => {
50+
reject(new Error(args.join()))
51+
}
52+
}
53+
const vm = new VM({
54+
timeout: 10000,
55+
eval: false,
56+
wasm: false,
57+
sandbox: {
58+
git: $git,
59+
pgp: { lookup, demote },
60+
done: resolve,
61+
console: $console,
62+
oids
63+
}
64+
});
65+
try {
66+
script = new VMScript(script).compile();
67+
} catch (err) {
68+
reject(err);
69+
}
70+
try {
71+
vm.run(script);
72+
} catch (e) {
73+
reject(e);
74+
}
75+
})
76+
77+
}
78+
79+
module.exports = {
80+
sandbox
81+
}
82+
// console.log(runVM({ core: 'default', dir: '', gitdir: '', script: `String(Object.keys(git))` }))

0 commit comments

Comments
 (0)