apostate 0.1.0__tar.gz
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.
- apostate-0.1.0/PKG-INFO +355 -0
- apostate-0.1.0/README.md +341 -0
- apostate-0.1.0/apostate/__init__.py +112 -0
- apostate-0.1.0/apostate/__main__.py +3 -0
- apostate-0.1.0/apostate/assets/catalogue.json +1 -0
- apostate-0.1.0/apostate/assets/profile.schema.json +875 -0
- apostate-0.1.0/apostate/assets/release-manifest.json +1 -0
- apostate-0.1.0/apostate/binary.py +1139 -0
- apostate-0.1.0/apostate/cli.py +107 -0
- apostate-0.1.0/apostate/config.py +357 -0
- apostate-0.1.0/apostate/errors.py +72 -0
- apostate-0.1.0/apostate/geoip.py +227 -0
- apostate-0.1.0/apostate/launch.py +989 -0
- apostate-0.1.0/apostate/profile_validation.py +168 -0
- apostate-0.1.0/apostate/resolver.py +515 -0
- apostate-0.1.0/apostate/widevine.py +322 -0
- apostate-0.1.0/apostate.egg-info/PKG-INFO +355 -0
- apostate-0.1.0/apostate.egg-info/SOURCES.txt +37 -0
- apostate-0.1.0/apostate.egg-info/dependency_links.txt +1 -0
- apostate-0.1.0/apostate.egg-info/entry_points.txt +2 -0
- apostate-0.1.0/apostate.egg-info/requires.txt +4 -0
- apostate-0.1.0/apostate.egg-info/top_level.txt +1 -0
- apostate-0.1.0/pyproject.toml +43 -0
- apostate-0.1.0/setup.cfg +4 -0
- apostate-0.1.0/tests/test_package.py +1153 -0
apostate-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: apostate
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Patchright-compatible Python interface for the Apostate native Chromium build
|
|
5
|
+
Author: Apostate contributors
|
|
6
|
+
License-Expression: GPL-3.0-only
|
|
7
|
+
Project-URL: Homepage, https://github.com/heretic-tech/apostate
|
|
8
|
+
Project-URL: Source, https://github.com/heretic-tech/apostate
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: patchright>=1.49
|
|
12
|
+
Provides-Extra: playwright
|
|
13
|
+
Requires-Dist: playwright>=1.40; extra == "playwright"
|
|
14
|
+
|
|
15
|
+
# apostate
|
|
16
|
+
|
|
17
|
+
Python interface to the Apostate anti-detect Chromium build. A real browser
|
|
18
|
+
binary whose fingerprint is modified in C++ at the source level, driven through
|
|
19
|
+
the Playwright API you already use.
|
|
20
|
+
|
|
21
|
+
Free and open source. No licence key, no account, no telemetry, no paid tier.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
pip install apostate
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
That is the whole install. Patchright comes with the package and is the default
|
|
30
|
+
driver; it is a required dependency because the launcher drives it. Playwright
|
|
31
|
+
works too — `pip install playwright`, then `driver="playwright"` — and a launch
|
|
32
|
+
falls back to it if Patchright is somehow missing.
|
|
33
|
+
|
|
34
|
+
The division of labour is that the browser handles what a page can observe about
|
|
35
|
+
the browser, and the driver's remaining job is to avoid *creating* artifacts of
|
|
36
|
+
its own — main-world `addInitScript` and `exposeFunction` bindings,
|
|
37
|
+
`Runtime.addBinding`, evaluation-script names visible in stack traces, and its
|
|
38
|
+
automation argv. Patchright is the hardened fork of that family, which is why it
|
|
39
|
+
is the default.
|
|
40
|
+
|
|
41
|
+
Be aware of what has and has not been measured here. The classic sentinels
|
|
42
|
+
(`$cdc_`, `__webdriver_evaluate`, `__playwright` and eight others) were **absent
|
|
43
|
+
under every driver tested**, and the `window` key set was byte-identical between
|
|
44
|
+
a bare launch and a driven page — so the folklore checks are not what
|
|
45
|
+
distinguishes these drivers. Patchright's advantage over Playwright has not been
|
|
46
|
+
measured on this project, and the default reflects the fork's intent rather than
|
|
47
|
+
a result. Treat any claim otherwise, including ours, as unverified.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from apostate import driver_info
|
|
51
|
+
print(driver_info())
|
|
52
|
+
# {'preference_order': ['patchright', 'playwright'],
|
|
53
|
+
# 'installed': ['patchright'], 'selected': 'patchright',
|
|
54
|
+
# 'recommended': 'patchright'}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Pass `driver="playwright"` to force one, and read `browser.apostate_driver_name`
|
|
58
|
+
to see what was used.
|
|
59
|
+
|
|
60
|
+
The browser is not bundled. On first use the package downloads the ~150 MB
|
|
61
|
+
archive for your platform from the GitHub release, checks its SHA-256 against a
|
|
62
|
+
manifest shipped inside the package, extracts it, and reuses it afterwards. You
|
|
63
|
+
do not need `playwright install` — Apostate supplies its own browser.
|
|
64
|
+
|
|
65
|
+
Or fetch it ahead of time:
|
|
66
|
+
|
|
67
|
+
```sh
|
|
68
|
+
python -m apostate install
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Already have the browser
|
|
72
|
+
|
|
73
|
+
Four sources, in this order, and the first one that answers wins:
|
|
74
|
+
|
|
75
|
+
| Where a launch looks | How |
|
|
76
|
+
|---|---|
|
|
77
|
+
| the path you name | `launch(binary_path="/path/to/chrome")` |
|
|
78
|
+
| `APOSTATE_BINARY` | `export APOSTATE_BINARY=/path/to/chrome` |
|
|
79
|
+
| this package's own install | whatever `python -m apostate install` wrote |
|
|
80
|
+
| well-known locations | macOS `/Applications` and `~/Applications` for a `Chromium.app` or `Apostate.app`; Linux `~/.cache/apostate` and `/opt/apostate` for a `chrome`; Windows `%LOCALAPPDATA%\apostate` for a `chrome.exe` — each directory and one level below it |
|
|
81
|
+
|
|
82
|
+
The first two are you naming a file and are taken at your word. The last two
|
|
83
|
+
are searches, and **a stock Chrome or Chromium is never adopted.** The
|
|
84
|
+
executable is named `chrome` and the bundle `Chromium.app` exactly as upstream
|
|
85
|
+
names them, and Chromium 152.0.7977.83 exists upstream too, so the file alone
|
|
86
|
+
proves nothing. What is checked is the payload staged beside it —
|
|
87
|
+
`build/MANIFEST.lock`, which carries this build's patch-series digests, or
|
|
88
|
+
`resources/profiles/` — and the version, and both are required. Marker first,
|
|
89
|
+
then version: nothing is executed until a file only an Apostate payload carries
|
|
90
|
+
has already vouched for the tree, because a stock Chrome started with these
|
|
91
|
+
switches is a session with no protection at all and nothing to say so.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from apostate import discovery_report
|
|
95
|
+
print(discovery_report())
|
|
96
|
+
# {'order': ['argument', 'environment', 'cache', 'well-known'],
|
|
97
|
+
# 'searched': ['/opt/apostate'],
|
|
98
|
+
# 'found': {'executable': '/opt/apostate/apostate-152.0.7977.83-linux-x64/chrome',
|
|
99
|
+
# 'source': 'well-known', 'chromium_version': '152.0.7977.83',
|
|
100
|
+
# 'payload_root': '/opt/apostate/apostate-152.0.7977.83-linux-x64'},
|
|
101
|
+
# 'rejected': [{'path': '/opt/chromium/chrome',
|
|
102
|
+
# 'reason': 'no Apostate payload beside it (build/MANIFEST.lock or resources/profiles/catalogue.json)'}]}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`python -m apostate info` prints the same thing as JSON, alongside the manifest
|
|
106
|
+
state, as `executable`, `executable_source` and `discovery`.
|
|
107
|
+
|
|
108
|
+
Supported hosts: `macos-arm64`, `linux-x64`, `linux-arm64`, `windows-x64`.
|
|
109
|
+
|
|
110
|
+
## Launch
|
|
111
|
+
|
|
112
|
+
`launch()` returns a Playwright `Browser`. An existing Playwright script works
|
|
113
|
+
with only the import changed.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from apostate import launch
|
|
117
|
+
|
|
118
|
+
browser = launch()
|
|
119
|
+
page = browser.new_page()
|
|
120
|
+
page.goto("https://example.com")
|
|
121
|
+
print(page.title())
|
|
122
|
+
browser.close()
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
With no arguments the browser draws a fresh fingerprint seed and composes a
|
|
126
|
+
coherent identity — GPU, CPU count, memory, screen, fonts, locale and timezone
|
|
127
|
+
all agree with each other. Every launch is a different device.
|
|
128
|
+
|
|
129
|
+
### A stable identity
|
|
130
|
+
|
|
131
|
+
A fresh seed every launch means a site you revisit sees a different device each
|
|
132
|
+
time. Pass a seed to get the same one back:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
browser = launch(fingerprint=42)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Same seed, same fingerprint, on every launch and on every machine — a seed
|
|
139
|
+
travels as a string, where a profile directory has to be copied.
|
|
140
|
+
|
|
141
|
+
It is not the only thing that pins a device, and this changed: a persistent
|
|
142
|
+
`user_data_dir` now keeps one too. The first launch against a directory mints
|
|
143
|
+
an identity into `DIR/apostate/identity` and every launch after reads it back,
|
|
144
|
+
because that directory already holds cookies and logged-in sessions, and one
|
|
145
|
+
account whose hardware changes between visits is a worse story than any single
|
|
146
|
+
fingerprint value. Three lifetimes:
|
|
147
|
+
|
|
148
|
+
| You launch with | The identity is | It lasts |
|
|
149
|
+
|---|---|---|
|
|
150
|
+
| nothing | drawn fresh from OS entropy | this launch only, recorded nowhere |
|
|
151
|
+
| `user_data_dir=DIR` | bound to `DIR` | until you delete `DIR`; it survives renaming and moving it |
|
|
152
|
+
| `fingerprint=SEED` | the one that seed selects | forever, on any host |
|
|
153
|
+
|
|
154
|
+
If you wanted a fresh machine every run and have been reusing one directory out
|
|
155
|
+
of habit, you now have to say so — drop `user_data_dir`, give each run its own,
|
|
156
|
+
or delete `DIR/apostate/identity` between runs.
|
|
157
|
+
|
|
158
|
+
Viewport geometry is handled for you: the drivers' default viewports report
|
|
159
|
+
impossible values (Playwright: `screen == inner == avail` with
|
|
160
|
+
`devicePixelRatio` flattened to 1; Puppeteer: an inner viewport *larger* than
|
|
161
|
+
its own window), so Apostate lets the real window size through and the composed
|
|
162
|
+
profile's geometry survives. Pass a viewport explicitly and yours wins.
|
|
163
|
+
|
|
164
|
+
One case Apostate cannot fix: `fingerprint="host"` under `headless=True` has no
|
|
165
|
+
display to inherit, so headless Chrome reports its synthetic 800x600 with
|
|
166
|
+
`availHeight == height`. No real desktop looks like that. Use a seed — any
|
|
167
|
+
composed profile supplies coherent geometry, measured at screen 1710x1112
|
|
168
|
+
against avail 1710x1079 with `devicePixelRatio` 2 — or run headful.
|
|
169
|
+
|
|
170
|
+
### Other options
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
browser = launch(
|
|
174
|
+
fingerprint=42,
|
|
175
|
+
fingerprint_platform="windows", # present as a Windows desktop
|
|
176
|
+
proxy="http://user:pass@host:8080",
|
|
177
|
+
headless=False,
|
|
178
|
+
args=["--fingerprint-hardware-concurrency=8"],
|
|
179
|
+
)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
| Option | Default | Meaning |
|
|
183
|
+
|---|---|---|
|
|
184
|
+
| `fingerprint` | a fresh random seed | Seed for the whole identity. `"host"` (also `"off"`, `"false"`, `"0"`, `"disable"`, `"disabled"`) inherits the real machine and composes nothing. |
|
|
185
|
+
| `fingerprint_platform` | host's own OS on macOS and Windows; `"windows"` on Linux | `windows`, `macos` or `linux`. Selects the GPU cluster as well as the OS identity. Needs that platform's fonts installed — see below. Cannot be combined with host inheritance. |
|
|
186
|
+
| `locale`, `timezone` | GeoIP of the effective egress, else the host's own | Override just these. Never drawn from the seed: a timezone the seed chose cannot correlate with the exit IP, so the precedence is this override, then GeoIP, then the host. The resolved locale is also written into the browser process environment (`LANGUAGE`, `LC_ALL`, `LC_MESSAGES`, `LANG`), which is what moves `Intl` formatting rather than only the language list; a composed launch never inherits those from your shell, so your own locale cannot leak into a synthetic identity. Host inheritance is the one exception and does inherit them. Takes effect once the artifact ships the full locale pak set — see docs/FINGERPRINTS.md section 8. |
|
|
187
|
+
| `geoip` | `True` | Best-effort: derive locale and timezone from the effective egress — the proxy exit when one is configured, the direct IP otherwise. A failed or partial lookup never invents one and no longer raises from `launch()` — it warns into the plan's `diagnostics["warnings"]`, sends no override for the fields it could not answer for, and the launch serves the host's own for those. Behind a proxy that is the host's and not the exit's. Pass `locale` and `timezone` explicitly when geo-matching has to be guaranteed. `resolve_geoip()` called directly still raises. |
|
|
188
|
+
| `proxy` | none | `http://`, `https://`, `socks5://`; credentials are kept out of the command line. |
|
|
189
|
+
| `headless` | `True` | |
|
|
190
|
+
| `user_data_dir` | off-the-record | Persist cookies and storage. |
|
|
191
|
+
| `args` | none | Extra switches passed to the browser. |
|
|
192
|
+
|
|
193
|
+
### Headless Linux servers
|
|
194
|
+
|
|
195
|
+
The usual deployment, and the one this is built for: a Linux server with no
|
|
196
|
+
graphics device. Nothing extra is needed and no GPU is required. `headless` is
|
|
197
|
+
already the default, and the claimed operating system rather than the host's
|
|
198
|
+
graphics stack selects the GPU identity, so a GPU-less server presents the
|
|
199
|
+
capability cluster and renderer string of the OS it claims.
|
|
200
|
+
|
|
201
|
+
**On a Linux host the default claimed OS is Windows, so install the Windows
|
|
202
|
+
fonts.** It is the one setup step here and the most common cause of a block: a
|
|
203
|
+
Windows persona missing Windows faces is measurable in text metrics. The
|
|
204
|
+
families and where to copy them from are in
|
|
205
|
+
[docs/FONTS.md](https://github.com/heretic-tech/apostate/blob/main/docs/FONTS.md).
|
|
206
|
+
Passing `fingerprint_platform="linux"` composes the host's own OS instead and
|
|
207
|
+
needs nothing installed. Windows-on-Linux is the default because it is the least
|
|
208
|
+
bad cross-OS pairing, not because it is free; `fingerprint_platform="macos"` on
|
|
209
|
+
a Linux host is the riskier one, at 184 core families.
|
|
210
|
+
|
|
211
|
+
For the most aggressive targets, the suites that score behaviour as well as the
|
|
212
|
+
fingerprint, run headed on a virtual display instead. Still no GPU: the display
|
|
213
|
+
is an X server with nothing behind it.
|
|
214
|
+
|
|
215
|
+
```sh
|
|
216
|
+
sudo apt install xvfb
|
|
217
|
+
Xvfb :99 -screen 0 1920x1080x24 &
|
|
218
|
+
export DISPLAY=:99
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
browser = launch(
|
|
223
|
+
fingerprint=42,
|
|
224
|
+
headless=False,
|
|
225
|
+
proxy="http://user:pass@residential-host:8080",
|
|
226
|
+
)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`DISPLAY` reaches the browser through the environment. The launcher sets no
|
|
230
|
+
environment of its own, so the driver hands the browser yours, and exporting
|
|
231
|
+
`DISPLAY` before the script runs is the whole of the wiring.
|
|
232
|
+
|
|
233
|
+
What a page can still tell on such a host is render timing and per-pixel output,
|
|
234
|
+
which come from the rasteriser rather than from the identity. The measured
|
|
235
|
+
numbers, the exact extension names a software backend does not currently serve,
|
|
236
|
+
and the status of the served GPU identity — new in this release, and on that
|
|
237
|
+
page's list of behaviours written but not yet run — are in
|
|
238
|
+
[docs/LIMITATIONS.md](https://github.com/heretic-tech/apostate/blob/main/docs/LIMITATIONS.md).
|
|
239
|
+
|
|
240
|
+
### Fonts for a cross-platform persona
|
|
241
|
+
|
|
242
|
+
Asking for a platform other than the one you are running on means that
|
|
243
|
+
platform's fonts have to be on the host. Apple and Microsoft fonts cannot be
|
|
244
|
+
redistributed, so you supply them; a persona without its fonts is a common
|
|
245
|
+
cause of blocks. Install them into the normal OS font directories (on Linux,
|
|
246
|
+
run `fc-cache -f` afterwards).
|
|
247
|
+
[docs/FONTS.md](https://github.com/heretic-tech/apostate/blob/main/docs/FONTS.md)
|
|
248
|
+
has the per-persona family lists, source directories and a verification
|
|
249
|
+
command.
|
|
250
|
+
|
|
251
|
+
The browser assumes you have done this and does not check. What it will not do
|
|
252
|
+
is claim a face that is absent: the font list a page sees is filtered down from
|
|
253
|
+
what the host actually has, never added to.
|
|
254
|
+
|
|
255
|
+
Also available: `launch_context()`, `launch_persistent_context()`, and
|
|
256
|
+
`launch_async()` / `launch_context_async()` /
|
|
257
|
+
`launch_persistent_context_async()` for the async API.
|
|
258
|
+
|
|
259
|
+
### Checking what you got
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
browser = launch(fingerprint=42)
|
|
263
|
+
page = browser.new_page()
|
|
264
|
+
print(page.evaluate("navigator.hardwareConcurrency"))
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Or ask the browser directly, without launching a session:
|
|
268
|
+
|
|
269
|
+
```sh
|
|
270
|
+
python -m apostate run -- --fingerprint=42 --fingerprint-explain
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Command line
|
|
274
|
+
|
|
275
|
+
```sh
|
|
276
|
+
python -m apostate install # download, verify and extract
|
|
277
|
+
python -m apostate path # print the executable path
|
|
278
|
+
python -m apostate info # install and manifest state as JSON
|
|
279
|
+
python -m apostate run -- --version
|
|
280
|
+
python -m apostate clear # delete the cache
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## DRM (Widevine)
|
|
284
|
+
|
|
285
|
+
Chromium fetches the Widevine CDM from Google at runtime into the profile
|
|
286
|
+
directory, and it cannot be redistributed, so Apostate does not ship it. A
|
|
287
|
+
default `launch()` uses a throwaway profile, so there is no CDM and
|
|
288
|
+
`navigator.requestMediaKeySystemAccess("com.widevine.alpha", ...)` rejects with
|
|
289
|
+
`NotSupportedError` — which a site can read in a single call.
|
|
290
|
+
|
|
291
|
+
If you need DRM, or you want that call to answer the way a real browser does,
|
|
292
|
+
provision a CDM that is already on your machine:
|
|
293
|
+
|
|
294
|
+
```sh
|
|
295
|
+
python -m apostate provision-drm --list # what was found
|
|
296
|
+
python -m apostate provision-drm # install the newest
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
That copies it into the browser's preinstalled-component directory, where it
|
|
300
|
+
registers at startup for every profile including a throwaway one, with no
|
|
301
|
+
network and without writing into the profile. It survives
|
|
302
|
+
`apostate install --force` and a Chromium upgrade.
|
|
303
|
+
|
|
304
|
+
Nothing is redistributed: the CDM travels from Google to your machine exactly as
|
|
305
|
+
it does for Chrome, and this only moves a file already there. If no CDM is
|
|
306
|
+
found, run any Chromium-based browser with a persistent profile and play a DRM
|
|
307
|
+
video once, then re-run. Measured on macOS; Linux and Windows use the same
|
|
308
|
+
command but have not been verified.
|
|
309
|
+
|
|
310
|
+
One switch matters: `--disable-component-update` gates the whole of
|
|
311
|
+
Chromium's component registration, not just downloading. With it set, **no**
|
|
312
|
+
preinstalled component registers — measured offline, zero of them — so a
|
|
313
|
+
provisioned CDM is silently inert and the browser diverges from a real Chrome
|
|
314
|
+
across every preinstalled component at once. `launch()` removes it from the
|
|
315
|
+
driver's default arguments for you. If you drive the binary yourself, do not
|
|
316
|
+
pass it — Playwright passes it by default, so use
|
|
317
|
+
`ignore_default_args=["--disable-component-update"]` (Python) or
|
|
318
|
+
`ignoreDefaultArgs: ["--disable-component-update"]` (Node). Patchright and
|
|
319
|
+
Puppeteer do not pass it.
|
|
320
|
+
|
|
321
|
+
## The browser cache
|
|
322
|
+
|
|
323
|
+
The install lives under `~/Library/Caches/apostate` on macOS,
|
|
324
|
+
`$XDG_CACHE_HOME/apostate` (or `~/.cache/apostate`) on Linux, and
|
|
325
|
+
`%LOCALAPPDATA%\apostate\cache` on Windows, keyed by Chromium version and
|
|
326
|
+
platform. `APOSTATE_CACHE_DIR` overrides it. The npm package uses the same
|
|
327
|
+
layout, so both share one install.
|
|
328
|
+
|
|
329
|
+
| Variable | Effect |
|
|
330
|
+
|---|---|
|
|
331
|
+
| `APOSTATE_CACHE_DIR` | Where the browser is installed. |
|
|
332
|
+
| `APOSTATE_BINARY` | Use this executable and skip acquisition entirely. |
|
|
333
|
+
| `APOSTATE_DOWNLOAD_BASE_URL` | Fetch archives from a mirror. The digest still comes from the package manifest. |
|
|
334
|
+
| `APOSTATE_KEEP_ARCHIVE` | Keep the verified archive after extracting, for `gh attestation verify`. |
|
|
335
|
+
|
|
336
|
+
## Integrity
|
|
337
|
+
|
|
338
|
+
The archive's SHA-256 is checked against the manifest **before** the archive is
|
|
339
|
+
opened, and a mismatch aborts without extracting anything. That manifest ships
|
|
340
|
+
inside this package rather than being fetched alongside the download, because a
|
|
341
|
+
digest served from the same place as the bytes it describes proves nothing.
|
|
342
|
+
|
|
343
|
+
Releases also carry GitHub build-provenance attestations. There is no signing
|
|
344
|
+
key to hold or rotate; verification is an optional extra step:
|
|
345
|
+
|
|
346
|
+
```sh
|
|
347
|
+
gh attestation verify apostate-152.0.7977.83-macos-arm64.zip --repo heretic-tech/apostate
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
That needs the archive, so run `python -m apostate install --keep-archive`
|
|
351
|
+
first, or download it from the release page.
|
|
352
|
+
|
|
353
|
+
## Licence
|
|
354
|
+
|
|
355
|
+
GPL-3.0-only. The browser binary is GPL-3.0-or-later.
|