Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
13 changes: 13 additions & 0 deletions .github/workflows/index.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Fails when an accepted submission is missing from the signed index (security plan S1.4).
name: Index matches submissions
on:
push:
branches: [main]
pull_request:
permissions: { contents: read }
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: python3 Scripts/check-index.py --selftest && python3 Scripts/check-index.py
675 changes: 675 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion REVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ published*, not *safe*: this review is what stands behind the store.
7. **Name and icon** don't impersonate another app or V2Mac.
8. **Changelog** describes this version; for an update, read the diff since the listed version.

Then merge, `python3 Scripts/prepare-index.py --add <checked plugin.json>`, `swift Scripts/sign.swift sign index.json`, push.
Record it in `reviews/<id>-<version>.md`. Then merge, `python3 Scripts/prepare-index.py --add <checked plugin.json>`, `swift Scripts/sign.swift sign index.json`, push.
36 changes: 36 additions & 0 deletions Scripts/check-index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Fail when an accepted submission is missing from the signed index (security plan S1.4).

Usage: check-index.py [--selftest]
Every submissions/<id>.json on the branch is an accepted plugin: index.json must list a plugin from that
repo whose layers are assets of that tag's release, and index.json.sig must exist. Offline: it reads
only this repo. Whether the signature is valid is the helper's check (it pins key.pub).
"""
import glob, json, os, sys

ROOT = os.path.join(os.path.dirname(__file__), "..")

def missing(submissions, index):
"""The submissions (path -> {repo, tag}) that no plugin in the index comes from."""
out = []
for path, sub in sorted(submissions.items()):
repo, release = sub["repo"].rstrip("/"), f"{sub['repo'].rstrip('/')}/releases/download/{sub['tag']}/"
if not any((p.get("source") or {}).get("repo") == repo and p.get("layers") and
all(str(l.get("url", "")).startswith(release) for l in p["layers"]) for p in index["plugins"]):
out.append(f"{path}: {repo} @ {sub['tag']} is not in index.json")
return out

def selftest():
p = {"source": {"repo": "https://github.com/a/b"}, "layers": [{"url": "https://github.com/a/b/releases/download/v2/x.tar.gz"}]}
assert missing({"s.json": {"repo": "https://github.com/a/b", "tag": "v2"}}, {"plugins": [p]}) == []
for sub in [{"repo": "https://github.com/a/b", "tag": "v1"}, {"repo": "https://github.com/c/b", "tag": "v2"}]:
assert missing({"s.json": sub}, {"plugins": [p]}), sub
assert missing({"s.json": {"repo": "https://github.com/a/b", "tag": "v2"}}, {"plugins": [{**p, "layers": []}]})
print("selftest ok")

if __name__ == "__main__":
if sys.argv[1:] == ["--selftest"]: selftest(); sys.exit(0)
subs = {os.path.relpath(f, ROOT): json.load(open(f)) for f in glob.glob(os.path.join(ROOT, "submissions", "*.json"))}
found = missing(subs, json.load(open(os.path.join(ROOT, "index.json"))))
if not os.path.exists(os.path.join(ROOT, "index.json.sig")): found.append("index.json.sig is missing: sign the index")
print("\n".join(found) or f"all {len(subs)} submissions are in the signed index")
sys.exit(1 if found else 0)
177 changes: 149 additions & 28 deletions index.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,124 @@
{
"version": 5,
"expires": "2027-01-22T22:58:21Z",
"version": 7,
"expires": "2027-02-06T00:31:42Z",
"minApi": 1,
"plugins": [
{
"api": 1,
"id": "hello-gui",
"name": "Hello Window",
"version": "0.2.1",
"publisher": "V2Mac",
"icon": "icon.svg",
"display": "gui",
"launch": {
"cmd": [
"/bin/bash",
"-c",
"{ echo 'Hello from a V2Mac mini VM.'; echo \"Exit: $(cat /run/v2mac/exit.json 2>/dev/null)\"; echo \"Time zone: ${TZ:-unknown}\"; echo 'Shared folder:'; ls ~/Shared 2>&1; echo; echo 'Type here; paste with Cmd-V:'; } > /tmp/hello.txt; exec zenity --text-info --editable --title='Hello Window' --width=900 --height=600 --filename=/tmp/hello.txt"
]
},
"permissions": [
"clipboard",
"shared"
],
"changelog": "Built on the base of V2Mac Sandbox 1.0.0: downloads only what it adds.",
"securityNotes": "Asks for the clipboard and the Shared folder to show both work.",
"from": "ghcr.io/lordversa/v2mac-base@sha256:3d73538e1d418c438e2cbc2890d5f024b72887072811a1f48ceac1deb0388161",
"source": {
"repo": "https://github.com/LordVersA/v2mac-plugin-template",
"commit": "cf9a45d8dfd29115ffebee3494911d5e661f31e9"
},
"layers": [
{
"url": "https://github.com/LordVersA/v2mac-plugin-template/releases/download/hello-gui-v0.2.1/hello-gui-0.2.1.tar.gz",
"sha256": "0074cd72c98450229fe568d8eb2920d3114ed22f5a5502fc8bdfee1491b79885",
"size": 314
}
],
"iconSvg": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"128\" height=\"128\" viewBox=\"0 0 128 128\">\n <rect width=\"128\" height=\"128\" rx=\"26\" fill=\"#234858\"/>\n <rect x=\"24\" y=\"30\" width=\"80\" height=\"68\" rx=\"6\" fill=\"#d4ecdf\"/>\n <path d=\"M24 48h80\" stroke=\"#234858\" stroke-width=\"6\"/>\n</svg>\n"
},
{
"api": 2,
"id": "firefox",
"name": "Firefox ESR",
"version": "0.1.0",
"publisher": "V2Mac",
"description": "Mozilla's browser, from Debian. Every site sees only this mini VM's exit: its time zone, its city for location, no local addresses over WebRTC.",
"icon": "icon.svg",
"display": "gui",
"launch": {
"cmd": [
"/usr/local/bin/start.sh"
]
},
"permissions": [
"clipboard",
"shared",
"sound",
"microphone"
],
"changelog": "First release: Debian's Firefox ESR, updated by Debian, with telemetry off, fingerprinting protection on, WebRTC limited to the exit's address and location set to the exit's city.",
"securityNotes": "Clipboard for copy and paste, Shared for downloads and uploads, Sound and Microphone for video, music and calls; V2Mac asks before each is first used while it is open. Location is answered from the exit's coordinates (exit.json); WebRTC never offers local addresses.",
"packages": [
"firefox-esr",
"fonts-noto-color-emoji"
],
"from": "ghcr.io/v2macapp/v2mac-base@sha256:fbe31f5579158feecc00957f96d2a7fb79c771a5fe99e388cbc4ddd4ae57abfc",
"source": {
"repo": "https://github.com/v2macapp/v2mac-firefox",
"commit": "69cb5f4d18c21316174481198d91e12d2e024d52"
},
"layers": [
{
"url": "https://github.com/v2macapp/v2mac-firefox/releases/download/v0.1.0/firefox-0.1.0.tar.gz",
"sha256": "90672f6a913c60175f99194d611ae6a39120b282b00897184cb009305961a130",
"size": 1782
}
],
"iconSvg": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"128\" height=\"128\" viewBox=\"0 0 128 128\">\n <defs>\n <linearGradient id=\"g\" x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <stop offset=\"0\" stop-color=\"#ffb347\"/>\n <stop offset=\"1\" stop-color=\"#e8452c\"/>\n </linearGradient>\n </defs>\n <rect width=\"128\" height=\"128\" rx=\"26\" fill=\"#2b1d4f\"/>\n <circle cx=\"64\" cy=\"64\" r=\"40\" fill=\"url(#g)\"/>\n <path d=\"M30 64h68M64 24c-16 18-16 62 0 80M64 24c16 18 16 62 0 80\" fill=\"none\" stroke=\"#2b1d4f\" stroke-width=\"5\"/>\n</svg>\n"
},
{
"api": 2,
"id": "ssh",
"name": "SSH",
"version": "0.1.0",
"publisher": "V2Mac",
"description": "Connect to your servers through this mini VM's exit with the SSH keys on your Mac. The keys never enter the VM. OpenSSH comes from Debian.",
"icon": "icon.svg",
"display": "pty",
"packages": [
"openssh-client"
],
"launch": {
"cmd": [
"/usr/local/bin/start.sh"
]
},
"permissions": [
"ssh-agent"
],
"changelog": "First release: OpenSSH from Debian, updated by Debian, using your Mac's ssh-agent after V2Mac asks.",
"securityNotes": "ssh-agent lets this app ask your Mac to sign with your keys while it is open and after you allow it; the keys stay on the Mac. Agent forwarding to servers is turned off.",
"from": "ghcr.io/v2macapp/v2mac-base@sha256:fbe31f5579158feecc00957f96d2a7fb79c771a5fe99e388cbc4ddd4ae57abfc",
"source": {
"repo": "https://github.com/v2macapp/v2mac-ssh",
"commit": "6977cea077220e33a01f3012e546bae858f29ea5"
},
"layers": [
{
"url": "https://github.com/v2macapp/v2mac-ssh/releases/download/v0.1.0/ssh-0.1.0.tar.gz",
"sha256": "1501d45d9949d882642e218b91ea8727341202e3d1ac722cb6081986185d92cf",
"size": 653
}
],
"iconSvg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\"><rect width=\"64\" height=\"64\" rx=\"14\" fill=\"#1f2937\"/><path d=\"M15 21l12 11-12 11\" fill=\"none\" stroke=\"#34d399\" stroke-width=\"6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/><path d=\"M32 44h17\" stroke=\"#e5e7eb\" stroke-width=\"6\" stroke-linecap=\"round\"/></svg>\n"
},
{
"api": 2,
"id": "chromium",
"name": "Chromium",
"version": "0.2.0",
"version": "0.3.1",
"publisher": "V2Mac",
"description": "The open-source browser behind Google Chrome, from Debian. Every site sees only this mini VM's exit, in its time zone.",
"icon": "icon.svg",
Expand All @@ -23,56 +134,66 @@
"sound",
"microphone"
],
"changelog": "Sound works (PulseAudio through PipeWire) and the microphone is available. V2Mac asks you the first time it plays sound or opens the microphone.",
"securityNotes": "Clipboard for copy and paste, Shared for downloads and uploads, Sound and Microphone for video, music and calls; V2Mac asks before each is first used while it is open. Runs with --no-sandbox: the mini VM container allows no user namespaces or setuid, so Chromium's own sandbox can't start; the container and the mini VM are the isolation.",
"from": "ghcr.io/lordversa/v2mac-base@sha256:3d73538e1d418c438e2cbc2890d5f024b72887072811a1f48ceac1deb0388161",
"changelog": "Security Notes now say plainly that Chromium's own sandbox is off, and why.",
"securityNotes": "Browser sandbox off: Chromium runs with --no-sandbox, so one site that breaks into Chromium can reach the rest of this app. Chromium refuses its sandbox when run as root, and mini VM apps run as root (without capabilities) in their container; the container and the mini VM are the isolation, so other mini VMs and your Mac stay out of reach. Clipboard for copy and paste, Shared for downloads and uploads, Sound and Microphone for video, music and calls; V2Mac asks before each is first used while it is open.",
"packages": [
"chromium",
"fonts-noto-color-emoji"
],
"from": "ghcr.io/v2macapp/v2mac-base@sha256:fbe31f5579158feecc00957f96d2a7fb79c771a5fe99e388cbc4ddd4ae57abfc",
"source": {
"repo": "https://github.com/LordVersA/v2mac-chromium",
"commit": "103aebdddefc3c5f362827b28f76538cf009be51"
"repo": "https://github.com/v2macapp/v2mac-chromium",
"commit": "8a6f75f06e4f025427f8b4452c69ad2fd9af9bec"
},
"layers": [
{
"url": "https://github.com/LordVersA/v2mac-chromium/releases/download/v0.2.0/chromium-0.2.0.tar.gz",
"sha256": "66a466f739939590d42f89db831fa47c698d8c69789e7bd6859b4c7f0fbe59ea",
"size": 314651017
"url": "https://github.com/v2macapp/v2mac-chromium/releases/download/v0.3.1/chromium-0.3.1.tar.gz",
"sha256": "519c81941c14b1f7019385588ad5f3b852f96a3e2ccf0a4b5e89b7a1322d0640",
"size": 861
}
],
"iconSvg": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"128\" height=\"128\" viewBox=\"0 0 128 128\">\n <rect width=\"128\" height=\"128\" rx=\"26\" fill=\"#1f3a5f\"/>\n <circle cx=\"64\" cy=\"64\" r=\"38\" fill=\"none\" stroke=\"#9cc3f0\" stroke-width=\"10\"/>\n <circle cx=\"64\" cy=\"64\" r=\"14\" fill=\"#9cc3f0\"/>\n</svg>\n"
},
{
"api": 1,
"id": "hello-gui",
"name": "Hello Window",
"version": "0.2.1",
"api": 2,
"id": "file-viewer",
"name": "File Viewer",
"version": "0.2.0",
"publisher": "V2Mac",
"description": "Open a PDF or Office file you don't trust in a mini VM instead of on your Mac: drop it on the app or use Open With. Macros and links stay off, and the app sees only that file, read-only, with no network. With the optional Shared permission it can open files in Shared instead, and then a successful attack could read that folder and send it through the mini VM's exit.",
"icon": "icon.svg",
"display": "gui",
"packages": [
"evince",
"libreoffice-gtk3",
"libreoffice-writer",
"libreoffice-calc",
"libreoffice-impress",
"fonts-noto-core"
],
"launch": {
"cmd": [
"/bin/bash",
"-c",
"{ echo 'Hello from a V2Mac mini VM.'; echo \"Exit: $(cat /run/v2mac/exit.json 2>/dev/null)\"; echo \"Time zone: ${TZ:-unknown}\"; echo 'Shared folder:'; ls ~/Shared 2>&1; echo; echo 'Type here; paste with Cmd-V:'; } > /tmp/hello.txt; exec zenity --text-info --editable --title='Hello Window' --width=900 --height=600 --filename=/tmp/hello.txt"
"/usr/local/bin/start.sh"
]
},
"permissions": [
"clipboard",
"shared"
],
"changelog": "Built on the base of V2Mac Sandbox 1.0.0: downloads only what it adds.",
"securityNotes": "Asks for the clipboard and the Shared folder to show both work.",
"from": "ghcr.io/lordversa/v2mac-base@sha256:3d73538e1d418c438e2cbc2890d5f024b72887072811a1f48ceac1deb0388161",
"changelog": "Open a file by dropping it on the app or with Open With: the app gets only that file, read-only, and no network. Shared is now optional.",
"securityNotes": "Without Shared, a file you open is copied in read-only and the app has no network, so an exploit can read only that file and can't send it anywhere. With Shared, which is read-write, a successful exploit could change or read what is in this mini VM's Shared folder and send it through the exit. It can't reach the Mac's other files or its IP either way. The profile is kept in memory and is gone when the app closes.",
"from": "ghcr.io/v2macapp/v2mac-base@sha256:fbe31f5579158feecc00957f96d2a7fb79c771a5fe99e388cbc4ddd4ae57abfc",
"source": {
"repo": "https://github.com/LordVersA/v2mac-plugin-template",
"commit": "cf9a45d8dfd29115ffebee3494911d5e661f31e9"
"repo": "https://github.com/v2macapp/v2mac-file-viewer",
"commit": "52de12f2f5a92986b828629650323e6b439dfcc5"
},
"layers": [
{
"url": "https://github.com/LordVersA/v2mac-plugin-template/releases/download/hello-gui-v0.2.1/hello-gui-0.2.1.tar.gz",
"sha256": "0074cd72c98450229fe568d8eb2920d3114ed22f5a5502fc8bdfee1491b79885",
"size": 314
"url": "https://github.com/v2macapp/v2mac-file-viewer/releases/download/v0.2.0/file-viewer-0.2.0.tar.gz",
"sha256": "15233255ddb7b95c189b251e91542caf66d96b46b14e4f41cc6688c320f13377",
"size": 1718
}
],
"iconSvg": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"128\" height=\"128\" viewBox=\"0 0 128 128\">\n <rect width=\"128\" height=\"128\" rx=\"26\" fill=\"#234858\"/>\n <rect x=\"24\" y=\"30\" width=\"80\" height=\"68\" rx=\"6\" fill=\"#d4ecdf\"/>\n <path d=\"M24 48h80\" stroke=\"#234858\" stroke-width=\"6\"/>\n</svg>\n"
"iconSvg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 64 64\"><rect width=\"64\" height=\"64\" rx=\"14\" fill=\"#0f766e\"/><path d=\"M18 10h20l10 10v34H18z\" fill=\"#f8fafc\"/><path d=\"M38 10v10h10\" fill=\"#cbd5e1\"/><path d=\"M24 30h16M24 37h16M24 44h10\" stroke=\"#0f766e\" stroke-width=\"3\" stroke-linecap=\"round\"/></svg>\n"
}
],
"templates": [],
Expand Down
2 changes: 1 addition & 1 deletion index.json.sig
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ayoXq87qqc1rYbHdA7pqN6PTpO0Fte5gRXYgiW8Mg0HgKzRK6/Y+8ZSivN7oDPtkTFU0aXBjEw2gvEl+OTXRCQ==
6/Wl+9W8fbQMup5k3Bl5xtWAYHSZXgSKbTNWGAtluSr6wshB+ma/yqwRpIZLrqbJ9qOZhY6Ls+bzfpsPMEC8Cw==
16 changes: 16 additions & 0 deletions reviews/chromium-0.3.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Chromium 0.3.0 (recipe)

Checked 2026-11-04 by Claude for the maintainer; automatic check (`Scripts/check-submission.py`) passed: hashes, size, schema, api 2, base digest, permissions, icon.

Base `ghcr.io/v2macapp/v2mac-base@sha256:fbe31f55…` (V2Mac Sandbox 1.2.0, Debian trixie with `trixie-security`).

1. **Source**: v2macapp/v2mac-chromium @ `d27bb01` (tag v0.3.0), read at that commit.
2. **Containerfile**: `FROM ${BASE}`, copies `start.sh`, `chmod`. No downloads.
3. **Build**: release CI run 41390565081 (success, push of v0.3.0 at `d27bb01`); release made by github-actions[bot].
4. **Permissions**: clipboard, shared, sound, microphone, each explained in `securityNotes`. Recipe: `chromium`, `fonts-noto-color-emoji`; the layer holds only `/usr/local/bin/start.sh`, byte-identical to the repo. Debian test: S1.3 (0.2.0 → 0.3.0 upgrade in a VM).
5. **Launch**: `start.sh` turns off sway fullscreen, takes a `flock` on the profile, runs `chromium --no-sandbox` (stated in `securityNotes`). Nothing fetched at runtime.
6. **No network or VM setup.**
7. **Name and icon**: "Chromium", an abstract circle icon, not Google's logo.
8. **Changelog** matches the diff since 0.2.0: apt step moved to `packages`, api 1 → 2, new base. `start.sh` unchanged.

**Accepted.**
16 changes: 16 additions & 0 deletions reviews/chromium-0.3.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Chromium 0.3.1 (recipe)

Checked 2026-11-08 by Claude for the maintainer; automatic check (`Scripts/check-submission.py`) passed: hashes, size, schema, api 2, base digest, permissions, icon.

Base `ghcr.io/v2macapp/v2mac-base@sha256:fbe31f55…` (V2Mac Sandbox 1.2.0, unchanged since 0.3.0).

1. **Source**: v2macapp/v2mac-chromium @ `8a6f75f` (tag v0.3.1). Diff since 0.3.0: `securityNotes`, `changelog`, a comment in `start.sh`, README.
2. **Containerfile**: unchanged; `FROM ${BASE}`, copies `start.sh`. No downloads.
3. **Build**: release CI run 42429932131 (success, push of v0.3.1 at `8a6f75f`); release made by github-actions[bot].
4. **Permissions**: unchanged (clipboard, shared, sound, microphone). Recipe unchanged: `chromium`, `fonts-noto-color-emoji`; the layer holds only `/usr/local/bin/start.sh`, byte-identical to the repo.
5. **Launch**: unchanged; `chromium --no-sandbox`, now explained first in `securityNotes`.
6. **No network or VM setup.**
7. **Name and icon**: unchanged.
8. **Changelog**: describes this version (Security Notes wording only).

**Accepted.**
18 changes: 18 additions & 0 deletions reviews/file-viewer-0.1.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# File Viewer 0.1.0 (recipe)

Checked 2026-11-04 by Claude for the maintainer; automatic check (`Scripts/check-submission.py`) passed: hashes, size, schema, api 2, base digest, permissions, icon.

Base `ghcr.io/v2macapp/v2mac-base@sha256:fbe31f55…` (V2Mac Sandbox 1.2.0, Debian trixie with `trixie-security`).

1. **Source**: v2macapp/v2mac-file-viewer @ `76ad717` (tag v0.1.0).
2. **Containerfile**: `FROM ${BASE}`, copies `v2mac-lockdown.xcd` and `start.sh`. No downloads.
3. **Build**: release CI run 41390574975 (success, `76ad717`); release by github-actions[bot].
4. **Permissions**: `shared` only, how files get in; the read-write risk is spelled out in `description` and `securityNotes`. Recipe: evince, libreoffice-gtk3/-writer/-calc/-impress, fonts-noto-core; the layer holds only the `.xcd` and `start.sh`, both identical to the repo. Debian test: not recorded in this repo; `start.sh` warns when LibreOffice isn't 25.2.
5. **Launch**: `zenity` picker in Shared, then `evince` or `soffice --view` with the profile under `/tmp`. Nothing fetched.
6. **No network or VM setup.**
7. **Name and icon**: "File Viewer", a generic document icon. Not called "Safe".
8. **Changelog**: first release.

**Finding (not blocking):** the lockdown sets `Calc/Content/Update/Link` = 2, which in LibreOffice 25.2's `Calc.xcs` means *update on request*. Calc's *never* is 1; Writer's *never* is 2, which is set correctly. So "link updates locked off" holds for Writer but not for Calc. `DisableActiveContent` (finalized, true) still blocks external links, DDE and OLE in Calc, so this needs no revocation. Fix it in 0.1.1 with Calc `Link` = 1.

**Accepted**, with the 0.1.1 fix above.
Loading
Loading