@idleware/ringi 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/target/dts/adapters/transport-git/index.d.ts +1 -1
- package/target/dts/adapters/transport-git/remote/admit.d.ts +2 -2
- package/target/dts/adapters/transport-git/remote/heartbeat.d.ts +2 -2
- package/target/dts/adapters/transport-git/remote/keys.d.ts +3 -3
- package/target/dts/adapters/transport-git/remote.d.ts +1 -1
- package/target/dts/adapters/transport-git/signing.d.ts +3 -3
- package/target/esm/adapters/transport-git/index.js +7 -3
- package/target/esm/adapters/transport-git/marks.js +3 -9
- package/target/esm/adapters/transport-git/remote/admit.js +8 -8
- package/target/esm/adapters/transport-git/remote/heartbeat.js +4 -4
- package/target/esm/adapters/transport-git/remote/keys.js +7 -32
- package/target/esm/adapters/transport-git/remote.js +1 -1
- package/target/esm/adapters/transport-git/repo.js +5 -2
- package/target/esm/adapters/transport-git/signing.js +12 -17
- package/target/esm/adapters/transport-git/transport.js +6 -6
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@ export declare class GitStore extends Marks implements Store {
|
|
|
9
9
|
home: string;
|
|
10
10
|
signing?: SigningContext;
|
|
11
11
|
}, contract: Record<string, string>, text: string): GitStore;
|
|
12
|
-
static rootFor(hubDir: string, d: Descriptor, home: string, signing
|
|
12
|
+
static rootFor(hubDir: string, d: Descriptor, home: string, signing: SigningContext): string;
|
|
13
13
|
logIds(room: StoredRoom): string[];
|
|
14
14
|
logHead(room: StoredRoom, id: string): string | null;
|
|
15
15
|
messages(room: StoredRoom, id: string, head: string): StoredMessage[];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Handle, Room } from './types.ts';
|
|
2
|
-
export declare const join: (H: Handle, R: Room, who: string
|
|
2
|
+
export declare const join: (H: Handle, R: Room, who: string) => {
|
|
3
3
|
publicKey: string;
|
|
4
4
|
fingerprint: string;
|
|
5
5
|
admitted: boolean;
|
|
6
6
|
};
|
|
7
|
-
export declare const roomAdmit: (H: Handle, R: Room, who: string, publicKey: string
|
|
7
|
+
export declare const roomAdmit: (H: Handle, R: Room, who: string, publicKey: string) => string;
|
|
@@ -3,5 +3,5 @@ export { checkHeartbeat, heartbeatObject } from './heartbeat-object.ts';
|
|
|
3
3
|
export { heartbeatBody } from './heartbeat-facts.ts';
|
|
4
4
|
export { HEARTBEAT_NAME, heartbeatName, heartbeatRef } from './heartbeat-name.ts';
|
|
5
5
|
export declare const verifyHeartbeat: (H: Handle, R: Room, ref: string, sha: string) => string | null;
|
|
6
|
-
type HeartbeatArgs = [H: Handle, R: Room, who: string, target: string, text: string
|
|
7
|
-
export declare const makeHeartbeat: (...[H, R, who, target, text
|
|
6
|
+
type HeartbeatArgs = [H: Handle, R: Room, who: string, target: string, text: string];
|
|
7
|
+
export declare const makeHeartbeat: (...[H, R, who, target, text]: HeartbeatArgs) => string;
|
|
@@ -3,11 +3,11 @@ export declare const parseKey: (value: unknown) => Key;
|
|
|
3
3
|
export declare const keyInput: (value: string) => Key;
|
|
4
4
|
export declare const rootKey: (H: Handle) => Key;
|
|
5
5
|
export declare const signerLine: (who: string, key: Key) => string;
|
|
6
|
-
export declare const
|
|
7
|
-
export declare const
|
|
6
|
+
export declare const signingContext: (H: Pick<Handle, 'name' | 'signing'>) => import("../../../core/signing/index.ts").SigningContext;
|
|
7
|
+
export declare const identityKey: (H: Handle, who: string, create?: boolean) => Key;
|
|
8
|
+
export declare const setupSigned: (H: Handle, url?: string) => {
|
|
8
9
|
root: string;
|
|
9
10
|
allowedSigners: string;
|
|
10
11
|
};
|
|
11
|
-
export declare const sign: (H: Handle, who: string, home: string) => string[];
|
|
12
12
|
export declare const rootRoster: (H: Handle) => string;
|
|
13
13
|
export declare const checkUrl: (url: string) => void;
|
|
@@ -2,7 +2,7 @@ export type { Handle, Room, Branch } from './remote/types.ts';
|
|
|
2
2
|
export { withVerification } from './remote/cache.ts';
|
|
3
3
|
export { gitEnv, gitTimeout, checkGitTimeout } from './remote/process.ts';
|
|
4
4
|
export { isRemote } from './remote/git.ts';
|
|
5
|
-
export { parseKey, setupSigned,
|
|
5
|
+
export { parseKey, setupSigned, rootRoster } from './remote/keys.ts';
|
|
6
6
|
export { verifyHub } from './remote/main.ts';
|
|
7
7
|
export { readCommits } from './remote/commits.ts';
|
|
8
8
|
export { branch, verifyRoom, verify } from './remote/branch.ts';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { Handle } from './remote/types.ts';
|
|
2
|
-
export declare const signBytes: (H: Handle, who: string, payload: Buffer | string
|
|
3
|
-
export declare const signCommit: (H: Handle, who: string, sha: string
|
|
4
|
-
export declare const signedTag: (H: Handle, who: string, tag: string, target: string
|
|
2
|
+
export declare const signBytes: (H: Handle, who: string, payload: Buffer | string) => string;
|
|
3
|
+
export declare const signCommit: (H: Handle, who: string, sha: string) => string;
|
|
4
|
+
export declare const signedTag: (H: Handle, who: string, tag: string, target: string) => string;
|
|
@@ -22,10 +22,13 @@ import { CONTROL_FILES } from './branches.js';
|
|
|
22
22
|
import { Marks } from './marks.js';
|
|
23
23
|
export { runGit } from './run.js';
|
|
24
24
|
import { TEXT_MAX, checkOutgoing } from '../../core/hub/protocol/index.js';
|
|
25
|
+
import { signingContext } from './remote/keys.js';
|
|
25
26
|
export class GitStore extends Marks {
|
|
26
27
|
type = 'git';
|
|
27
28
|
static create(hubDir, d, connection, contract, text) {
|
|
28
29
|
const { home, signing } = typeof connection === 'string' ? { home: connection, signing: undefined } : connection;
|
|
30
|
+
if (d.trust === 'signed')
|
|
31
|
+
signingContext({ name: d.name, signing });
|
|
29
32
|
const dir = path.join(hubDir, d.store.path, '.git');
|
|
30
33
|
// A signed hub's repository already exists: rootFor made it with the root key.
|
|
31
34
|
if (!fs.existsSync(dir)) {
|
|
@@ -43,10 +46,11 @@ export class GitStore extends Marks {
|
|
|
43
46
|
}
|
|
44
47
|
// A new signed hub: the root key is generated on this machine and pinned in the descriptor.
|
|
45
48
|
static rootFor(hubDir, d, home, signing) {
|
|
49
|
+
signingContext({ name: d.name, signing });
|
|
46
50
|
const dir = path.join(hubDir, d.store.path, '.git');
|
|
47
51
|
mkdirIn(home, path.dirname(dir));
|
|
48
52
|
init(dir, home);
|
|
49
|
-
return Remote.setupSigned({ name: d.name, dir, d, signing },
|
|
53
|
+
return Remote.setupSigned({ name: d.name, dir, d, signing }, d.store.remote).root;
|
|
50
54
|
}
|
|
51
55
|
logIds(room) { return [...new Set(this.live(room).map(e => e.author))]; }
|
|
52
56
|
logHead(room, id) { return this.live(room).filter(e => e.author === id).at(-1)?.sha ?? null; }
|
|
@@ -114,8 +118,8 @@ export class GitStore extends Marks {
|
|
|
114
118
|
.map(b => /^\d+ blob [0-9a-f]+ +(\d+)\t(.*)$/s.exec(b.toString('utf8'))).filter((x) => !!x)
|
|
115
119
|
.filter(([, , p]) => wanted.has(p)).map(([, n, p]) => [p, Number(n)]));
|
|
116
120
|
}
|
|
117
|
-
admit(room, who, key) { return Remote.roomAdmit(this.h, this.remote(room), who, key
|
|
118
|
-
joinKey(room, who) { return Remote.join(this.h, this.remote(room), who
|
|
121
|
+
admit(room, who, key) { return Remote.roomAdmit(this.h, this.remote(room), who, key); }
|
|
122
|
+
joinKey(room, who) { return Remote.join(this.h, this.remote(room), who); }
|
|
119
123
|
// A room's transport alarm is the room's, whichever author the operator forgets.
|
|
120
124
|
forgetLog(room, _id) { Remote.forget(this.h, this.remote(room)); }
|
|
121
125
|
rootRoster() { return Remote.rootRoster(this.h); }
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// one mutable tag `<agent>.heartbeat.<genesis12>` per agent and room, replaced on every beat.
|
|
3
3
|
import * as Remote from './remote.js';
|
|
4
4
|
import { signedTag } from './signing.js';
|
|
5
|
-
import { ZERO, frames,
|
|
5
|
+
import { ZERO, frames, runGitRaw } from './run.js';
|
|
6
6
|
import { signalTags, tagObject } from './tags.js';
|
|
7
7
|
import { Branches } from './branches.js';
|
|
8
8
|
import { fail, note } from '../../lib/index.js';
|
|
@@ -15,7 +15,7 @@ export class Marks extends Branches {
|
|
|
15
15
|
heartbeat(room, who, target, text) {
|
|
16
16
|
const ref = Remote.heartbeatRef(who, room.genesis), name = ref.slice('refs/tags/'.length);
|
|
17
17
|
const old = this.rev(`${ref}^{tag}`) ? this.git(['rev-parse', ref]).trim() : null;
|
|
18
|
-
const obj = this.trust === 'signed' ? Remote.makeHeartbeat(this.h, this.remote(room), who, target, text
|
|
18
|
+
const obj = this.trust === 'signed' ? Remote.makeHeartbeat(this.h, this.remote(room), who, target, text)
|
|
19
19
|
: this.git(['mktag'], { input: `object ${target}\ntype commit\ntag ${name}\ntagger ${who} <${who}@ringi> ${Math.floor(Date.now() / 1000)} +0000\n\n${text}` }).trim();
|
|
20
20
|
if (this.gitOk(['update-ref', ref, obj, old ?? ZERO]).status !== 0)
|
|
21
21
|
fail(`${name} moved meanwhile; the next beat writes it`);
|
|
@@ -45,13 +45,7 @@ export class Marks extends Branches {
|
|
|
45
45
|
Remote.push(this.h, ref);
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
this.git(['update-ref', ref, signedTag(this.h, who, tag, target, this.home), ZERO]);
|
|
50
|
-
else {
|
|
51
|
-
const sign = Remote.sign(this.h, who, this.home);
|
|
52
|
-
const key = sign[0]?.slice(2);
|
|
53
|
-
this.git([...(key ? ['-c', `user.signingkey=${key}`] : []), 'tag', key ? '-s' : '-a', '-F', '-', tag, target], { input: '', env: ident(who) });
|
|
54
|
-
}
|
|
48
|
+
this.git(['update-ref', ref, signedTag(this.h, who, tag, target), ZERO]);
|
|
55
49
|
Remote.push(this.h, ref);
|
|
56
50
|
}
|
|
57
51
|
// The tags named `<actor>.<signal>.<message12>`, with the objects they name; a heartbeat is no signal (heartbeats()).
|
|
@@ -3,13 +3,13 @@ import * as C from '../../../lib/index.js';
|
|
|
3
3
|
import * as P from '../../../core/hub/protocol/index.js';
|
|
4
4
|
import { branch, verifyRoom } from './branch.js';
|
|
5
5
|
import { git, id, ident, isRemote, locked, rev } from './git.js';
|
|
6
|
-
import { keyInput, identityKey,
|
|
6
|
+
import { keyInput, identityKey, signerLine } from './keys.js';
|
|
7
7
|
import { signCommit } from '../signing.js';
|
|
8
8
|
import { push } from './push.js';
|
|
9
9
|
import { CONTROL_FILES, ROOT } from './types.js';
|
|
10
|
-
export const join = (H, R, who
|
|
10
|
+
export const join = (H, R, who) => {
|
|
11
11
|
const room = verifyRoom(H, R);
|
|
12
|
-
const key = locked(H, () => identityKey(H, who,
|
|
12
|
+
const key = locked(H, () => identityKey(H, who, true));
|
|
13
13
|
const admitted = !!room.entries.find(e => e.principal === who && e.key.line === key.line);
|
|
14
14
|
return { publicKey: key.line, fingerprint: key.fp, admitted };
|
|
15
15
|
};
|
|
@@ -30,14 +30,14 @@ const treeWith = (H, base, control, patches) => {
|
|
|
30
30
|
return git(H, ['mktree'], { input: [...files].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([, line]) => line).join('\n') + '\n' }).trim();
|
|
31
31
|
};
|
|
32
32
|
// The root's control record admitting a member, on top of the room's branch: the key and the member go in together.
|
|
33
|
-
export const roomAdmit = (H, R, who, publicKey
|
|
33
|
+
export const roomAdmit = (H, R, who, publicKey) => {
|
|
34
34
|
const key = admissionKey(H, R, who, publicKey);
|
|
35
35
|
for (let attempt = 0; attempt < 8; attempt++) {
|
|
36
36
|
const tip = rev(H, R.ref) ?? C.fail('room branch is missing');
|
|
37
37
|
const room = branch(H, R, tip), head = room.control.at(-1);
|
|
38
38
|
if (room.entries.some(e => e.principal === who || e.key.fp === key.fp))
|
|
39
39
|
C.fail('principal or key is already admitted; key rotation is not implemented');
|
|
40
|
-
const { sha, members } = admissionCommit({ H, R, who,
|
|
40
|
+
const { sha, members } = admissionCommit({ H, R, who, tip, head, room, key });
|
|
41
41
|
if (git(H, ['update-ref', R.ref, sha, tip], { ok: true }).status !== 0)
|
|
42
42
|
continue;
|
|
43
43
|
if (!push(H, R.ref, { behind: true }))
|
|
@@ -48,14 +48,14 @@ export const roomAdmit = (H, R, who, publicKey, home) => {
|
|
|
48
48
|
}
|
|
49
49
|
return C.fail('room kept moving while admitting; retry');
|
|
50
50
|
};
|
|
51
|
-
const admissionCommit = ({ H, who,
|
|
51
|
+
const admissionCommit = ({ H, who, tip, head, room, key }) => {
|
|
52
52
|
const members = { ...room.members, [who]: { role: 'member', since: new Date().toISOString() } };
|
|
53
53
|
const roster = [...room.entries.map(e => signerLine(e.principal, e.key)), signerLine(who, key)].join('');
|
|
54
54
|
const tree = treeWith(H, tip, head, { 'members.json': JSON.stringify(members, null, 2) + '\n', allowed_signers: roster });
|
|
55
|
-
const sha = git(H, ['commit-tree',
|
|
55
|
+
const sha = git(H, ['commit-tree', tree, '-p', tip], {
|
|
56
56
|
input: P.compose(`admit ${who}`, [['To', '*'], ['Kind', 'room'], ['Key', key.fp]]), env: ident(ROOT),
|
|
57
57
|
}).trim();
|
|
58
|
-
return { sha: signCommit(H, ROOT, sha
|
|
58
|
+
return { sha: signCommit(H, ROOT, sha), members };
|
|
59
59
|
};
|
|
60
60
|
const admissionKey = (H, R, who, publicKey) => {
|
|
61
61
|
id(who);
|
|
@@ -24,9 +24,9 @@ export const verifyHeartbeat = (H, R, ref, sha) => {
|
|
|
24
24
|
return reason(e);
|
|
25
25
|
}
|
|
26
26
|
};
|
|
27
|
-
//
|
|
28
|
-
export const makeHeartbeat = (...[H, R, who, target, text
|
|
29
|
-
const createHeartbeat = ({ H, R, who, target, text
|
|
27
|
+
// Object construction has one named input; signing needs no custody path.
|
|
28
|
+
export const makeHeartbeat = (...[H, R, who, target, text]) => createHeartbeat({ H, R, who, target, text });
|
|
29
|
+
const createHeartbeat = ({ H, R, who, target, text }) => {
|
|
30
30
|
const ref = heartbeatRef(who, R.genesis);
|
|
31
31
|
if (!SHA.test(target))
|
|
32
32
|
C.fail('heartbeat target must be a full commit id');
|
|
@@ -35,7 +35,7 @@ const createHeartbeat = ({ H, R, who, target, text, home }) => {
|
|
|
35
35
|
const body = `Room: ${R.genesis}\n${text.trimEnd()}\n`;
|
|
36
36
|
heartbeatBody(body);
|
|
37
37
|
const raw = `object ${target}\ntype commit\ntag ${ref.slice(TAGS.length)}\ntagger ${who} <${who}@ringi> ${Math.floor(Date.now() / 1000)} +0000\n\n${body}`;
|
|
38
|
-
const signature = signBytes(H, who, raw
|
|
38
|
+
const signature = signBytes(H, who, raw);
|
|
39
39
|
const sha = git(H, ['mktag'], { input: raw + signature }).trim();
|
|
40
40
|
if (isRemote(H))
|
|
41
41
|
checkHeartbeat(H, R, ref, sha);
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
// keys: public keys as given and pinned, a signed hub's root and its signers, and which key signs for a principal.
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
4
|
import * as C from '../../../lib/index.js';
|
|
6
|
-
import {
|
|
7
|
-
import { command } from './process.js';
|
|
5
|
+
import { id, rev } from './git.js';
|
|
8
6
|
import { ROOT } from './types.js';
|
|
9
7
|
export const parseKey = (value) => {
|
|
10
8
|
const text = String(value || '').trim();
|
|
@@ -20,47 +18,24 @@ export const parseKey = (value) => {
|
|
|
20
18
|
export const keyInput = (value) => parseKey(fs.existsSync(value) && fs.statSync(value).isFile() ? fs.readFileSync(value, 'utf8') : value);
|
|
21
19
|
export const rootKey = (H) => parseKey(H.d.root || C.fail(`hub ${H.name} has no out-of-band root pin; use hub add --root <public-key|file>`));
|
|
22
20
|
export const signerLine = (who, key) => `${who} namespaces="git" ${key.line}\n`;
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
if (!create)
|
|
28
|
-
C.fail(`no private key for ${who} on this machine; ${who === ROOT ? 'root keys are never obtained from a hub' : 'join here to generate one, then ask the root to admit its public key'}`);
|
|
29
|
-
mkdir(path.dirname(file));
|
|
30
|
-
command('ssh-keygen', ['-q', '-t', 'ed25519', '-N', '', '-C', `${who}@ringi`, '-f', file]);
|
|
31
|
-
}
|
|
32
|
-
// Derive from the private key instead of trusting a mutable .pub sidecar.
|
|
33
|
-
const key = parseKey(command('ssh-keygen', ['-y', '-f', file]));
|
|
34
|
-
return { ...key, file };
|
|
21
|
+
export const signingContext = (H) => H.signing ?? C.fail(`hub ${H.name}: signed writes require an injected Signer`);
|
|
22
|
+
export const identityKey = (H, who, create = false) => {
|
|
23
|
+
const context = signingContext(H);
|
|
24
|
+
return parseKey(context.signer.publicIdentity(context.resolve(id(H.name), id(who), create)).key);
|
|
35
25
|
};
|
|
36
|
-
export const
|
|
37
|
-
? parseKey(H.signing.signer.publicIdentity(H.signing.resolve(id(H.name), id(who), create)).key)
|
|
38
|
-
: localKey(H, who, home, create);
|
|
39
|
-
export const setupSigned = (H, home, url) => {
|
|
26
|
+
export const setupSigned = (H, url) => {
|
|
40
27
|
id(H.name);
|
|
41
28
|
if (rev(H, 'refs/heads/main'))
|
|
42
29
|
C.fail('setupSigned only initializes an empty hub; it cannot replace an existing trust anchor');
|
|
43
|
-
const key = identityKey(H, ROOT,
|
|
30
|
+
const key = identityKey(H, ROOT, true);
|
|
44
31
|
H.d.trust = 'signed';
|
|
45
32
|
H.d.root = key.line;
|
|
46
|
-
config(H, 'gpg.format', 'ssh');
|
|
47
|
-
config(H, 'gpg.ssh.program', 'ssh-keygen');
|
|
48
33
|
if (url) {
|
|
49
34
|
checkUrl(url);
|
|
50
35
|
H.d.store.remote = url;
|
|
51
36
|
}
|
|
52
37
|
return { root: key.line, allowedSigners: signerLine(ROOT, key) };
|
|
53
38
|
};
|
|
54
|
-
export const sign = (H, who, home) => {
|
|
55
|
-
if (!isRemote(H))
|
|
56
|
-
return [];
|
|
57
|
-
const key = localKey(H, who, home);
|
|
58
|
-
if (who === ROOT && key.line !== rootKey(H).line)
|
|
59
|
-
C.fail('the local operator key does not match this hub\'s pinned root');
|
|
60
|
-
config(H, 'gpg.format', 'ssh');
|
|
61
|
-
config(H, 'gpg.ssh.program', 'ssh-keygen');
|
|
62
|
-
return [`-S${key.file}`];
|
|
63
|
-
};
|
|
64
39
|
export const rootRoster = (H) => signerLine(ROOT, rootKey(H));
|
|
65
40
|
const supportedUrl = (url) => {
|
|
66
41
|
const malformed = /[\r\n\0]/.test(url) || url.startsWith('-');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { withVerification } from './remote/cache.js';
|
|
2
2
|
export { gitEnv, gitTimeout, checkGitTimeout } from './remote/process.js';
|
|
3
3
|
export { isRemote } from './remote/git.js';
|
|
4
|
-
export { parseKey, setupSigned,
|
|
4
|
+
export { parseKey, setupSigned, rootRoster } from './remote/keys.js';
|
|
5
5
|
export { verifyHub } from './remote/main.js';
|
|
6
6
|
export { readCommits } from './remote/commits.js';
|
|
7
7
|
export { branch, verifyRoom, verify } from './remote/branch.js';
|
|
@@ -8,6 +8,7 @@ import { mkdirIn } from '../fs/index.js';
|
|
|
8
8
|
import * as Remote from './remote.js';
|
|
9
9
|
import { ZERO, ident, runGit, runGitOk } from './run.js';
|
|
10
10
|
import { signCommit } from './signing.js';
|
|
11
|
+
import { signingContext } from './remote/keys.js';
|
|
11
12
|
// The store is a repository whose work tree holds the contract (`main`) as it is now, so Git clients (SourceTree, IDEs)
|
|
12
13
|
// open `store/` without errors, on the contract, with every room's branch beside it; `git status` stays clean.
|
|
13
14
|
export const init = (dir, home) => runGit(['init', '-q', '-b', 'main', path.dirname(dir)], { where: home });
|
|
@@ -24,6 +25,8 @@ export class Repo {
|
|
|
24
25
|
this.trust = d.trust;
|
|
25
26
|
this.dir = path.join(hubDir, d.store.path, '.git');
|
|
26
27
|
this.h = { name: d.name, dir: this.dir, d, ...(d.trust === 'signed' && signing ? { signing } : {}) };
|
|
28
|
+
if (this.trust === 'signed')
|
|
29
|
+
signingContext(this.h);
|
|
27
30
|
if (!fs.existsSync(this.dir))
|
|
28
31
|
fail(`hub ${d.name}: no git store at ${this.dir}`);
|
|
29
32
|
this.openable();
|
|
@@ -82,8 +85,8 @@ export class Repo {
|
|
|
82
85
|
}
|
|
83
86
|
// Every commit is made here: on a signed hub, signed by its author's key on this machine.
|
|
84
87
|
commit(who, text, tree, parents) {
|
|
85
|
-
const sha = this.git(['commit-tree',
|
|
86
|
-
return signCommit(this.h, who, sha
|
|
88
|
+
const sha = this.git(['commit-tree', tree, ...parents.flatMap(p => ['-p', p])], { input: text, env: ident(who) }).trim();
|
|
89
|
+
return signCommit(this.h, who, sha);
|
|
87
90
|
}
|
|
88
91
|
advance(ref, sha, old) {
|
|
89
92
|
if (this.gitOk(['update-ref', ref, sha, old ?? ZERO]).status !== 0)
|
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
// GitSigningBridge: Signer signs the exact unsigned Git object. Tags append its SSH armor; commits fold it into
|
|
2
|
-
// gpgsig.
|
|
2
|
+
// gpgsig. All signing and public identity come from the injected Signer; custody stays outside the transport.
|
|
3
3
|
import { fail } from '../../lib/index.js';
|
|
4
4
|
import { git, id, ident, isRemote } from './remote/git.js';
|
|
5
|
-
import { identityKey, parseKey, rootKey,
|
|
6
|
-
import { command } from './remote/process.js';
|
|
5
|
+
import { identityKey, parseKey, rootKey, signingContext } from './remote/keys.js';
|
|
7
6
|
import { checkSignature } from './remote/signature.js';
|
|
8
|
-
export const signBytes = (H, who, payload
|
|
7
|
+
export const signBytes = (H, who, payload) => {
|
|
9
8
|
if (!isRemote(H))
|
|
10
9
|
return '';
|
|
11
|
-
|
|
12
|
-
const file = sign(H, who, home)[0].slice(2);
|
|
13
|
-
return command('ssh-keygen', ['-Y', 'sign', '-n', 'git', '-f', file], { input: payload });
|
|
14
|
-
}
|
|
15
|
-
const context = H.signing, key = context.resolve(id(H.name), id(who));
|
|
10
|
+
const context = signingContext(H), key = context.resolve(id(H.name), id(who));
|
|
16
11
|
const identity = context.signer.publicIdentity(key), publicKey = parseKey(identity.key);
|
|
17
12
|
if (who === 'operator' && publicKey.line !== rootKey(H).line)
|
|
18
13
|
fail('the local operator key does not match this hub\'s pinned root');
|
|
@@ -21,24 +16,24 @@ export const signBytes = (H, who, payload, home) => {
|
|
|
21
16
|
fail('the signer returned an incompatible signature identity or format');
|
|
22
17
|
return Buffer.from(signature.bytes).toString('utf8');
|
|
23
18
|
};
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (!H.signing || !isRemote(H))
|
|
19
|
+
export const signCommit = (H, who, sha) => {
|
|
20
|
+
if (!isRemote(H))
|
|
27
21
|
return sha;
|
|
28
22
|
const raw = git(H, ['cat-file', 'commit', sha], { raw: true }), end = raw.indexOf('\n\n');
|
|
29
23
|
if (end < 0)
|
|
30
24
|
fail('malformed unsigned commit');
|
|
31
|
-
const signature = signBytes(H, who, raw
|
|
25
|
+
const signature = signBytes(H, who, raw).trimEnd().replace(/\n/g, '\n ');
|
|
32
26
|
const body = Buffer.concat([raw.subarray(0, end), Buffer.from(`\ngpgsig ${signature}\n\n`), raw.subarray(end + 2)]);
|
|
33
27
|
const signed = git(H, ['hash-object', '-t', 'commit', '-w', '--stdin'], { input: body }).trim();
|
|
34
|
-
checkSignature(H, signed, who, identityKey(H, who
|
|
28
|
+
checkSignature(H, signed, who, identityKey(H, who));
|
|
35
29
|
return signed;
|
|
36
30
|
};
|
|
37
|
-
export const signedTag = (H, who, tag, target
|
|
31
|
+
export const signedTag = (H, who, tag, target) => {
|
|
38
32
|
const tagger = git(H, ['var', 'GIT_COMMITTER_IDENT'], { env: ident(who) }).trim();
|
|
39
33
|
const raw = `object ${target}\ntype commit\ntag ${tag}\ntagger ${tagger}\n\n`;
|
|
40
|
-
const signed = git(H, ['mktag'], { input: raw + signBytes(H, who, raw
|
|
41
|
-
|
|
34
|
+
const signed = git(H, ['mktag'], { input: raw + signBytes(H, who, raw) }).trim();
|
|
35
|
+
if (isRemote(H))
|
|
36
|
+
checkSignature(H, signed, who, identityKey(H, who), 'tag');
|
|
42
37
|
return signed;
|
|
43
38
|
};
|
|
44
39
|
//# sourceMappingURL=signing.js.map
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// transport: the git store as a store type: a hub's repository is `store/.git` in the hub's directory; a signed hub's
|
|
2
|
-
// root
|
|
3
|
-
import
|
|
2
|
+
// root identity comes from Signer and a published hub is replicated by bootstrap (remote/bootstrap.ts).
|
|
3
|
+
import { fail } from '../../lib/index.js';
|
|
4
4
|
import { GitStore } from './index.js';
|
|
5
5
|
import * as Remote from './remote.js';
|
|
6
6
|
const context = (d, opt) => d.trust === 'signed'
|
|
7
|
-
? typeof opt.signer === 'function' ? opt.signer() : opt.signer
|
|
7
|
+
? (typeof opt.signer === 'function' ? opt.signer() : opt.signer) ?? fail('signed hubs require an injected Signer')
|
|
8
8
|
: undefined;
|
|
9
|
-
// `home`: where this machine keeps its state
|
|
9
|
+
// `home`: where this machine keeps its state and scratch indexes; Signer owns key custody.
|
|
10
10
|
export const gitTransport = (home, opt = {}) => ({
|
|
11
11
|
// One branch per room with its messages and control records, signals as tags on them (hub-1, Layout).
|
|
12
12
|
layout: 'git/2',
|
|
@@ -15,13 +15,13 @@ export const gitTransport = (home, opt = {}) => ({
|
|
|
15
15
|
open: (at, d) => new GitStore(at, d, home, context(d, opt)),
|
|
16
16
|
create: (at, d, genesis) => {
|
|
17
17
|
const signing = context(d, opt);
|
|
18
|
-
if (
|
|
18
|
+
if (signing)
|
|
19
19
|
d.root = GitStore.rootFor(at, d, home, signing);
|
|
20
20
|
const g = genesis(d);
|
|
21
21
|
const store = GitStore.create(at, d, { home, signing }, g.files, g.text);
|
|
22
22
|
if (d.trust === 'signed')
|
|
23
23
|
d.genesis = store.contractHead();
|
|
24
|
-
const custody =
|
|
24
|
+
const custody = signing?.custody?.(d.name, 'operator') ?? null;
|
|
25
25
|
return { store, custody };
|
|
26
26
|
},
|
|
27
27
|
attach: (name, url, pins) => { Remote.hubAdd({ home, name, url, ...pins }); },
|