259 lines
7.0 KiB
Markdown
259 lines
7.0 KiB
Markdown
# Security
|
||||
|
|
|
|||
|
|
goget implements multiple security features to protect both the client and server during file transfers. This document covers TLS configuration, certificate handling, checksum verification, PGP support, HSTS, and SSRF protection.
|
|||
|
|
|
|||
|
|
## TLS / HTTPS
|
|||
|
|
|
|||
|
|
### Default Security
|
|||
|
|
|
|||
|
|
- **TLS 1.2 minimum** — TLS 1.0 and 1.1 are disabled
|
|||
|
|
- **Certificate verification** — Enabled by default against system CA bundle
|
|||
|
|
- **HTTP/2 support** — Automatic ALPN negotiation
|
|||
|
|
|
|||
|
|
### Certificate Pinning
|
|||
|
|
|
|||
|
|
Pin a specific certificate by its SHA-256 hash:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
goget --pinned-cert "a1b2c3d4e5f6..." --url https://example.com
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
If the server certificate's SHA-256 doesn't match, the connection is aborted.
|
|||
|
|
|
|||
|
|
### Client Certificates (mTLS)
|
|||
|
|
|
|||
|
|
Authenticate the client to the server with mutual TLS:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
goget --cert client.pem --key client.key --url https://mtls.example.com
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Custom CA Bundle
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
goget --cacert custom-ca.pem --url https://internal.example.com
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Insecure Mode
|
|||
|
|
|
|||
|
|
Disable all TLS verification (⚠ **for testing only**):
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
goget --insecure --url https://self-signed.local
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### SSLKEYLOGFILE
|
|||
|
|
|
|||
|
|
Log TLS session keys for Wireshark debugging:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
goget --ssl-key-log tls.keys --url https://example.com
|
|||
|
|
# Then: Wireshark → Preferences → TLS → (Pre-)Master-Secret log filename → tls.keys
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## HSTS (HTTP Strict Transport Security)
|
|||
|
|
|
|||
|
|
Per RFC 6797, goget maintains an HSTS cache that automatically upgrades HTTP connections to HTTPS for known hosts.
|
|||
|
|
|
|||
|
|
### How It Works
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
sequenceDiagram
|
|||
|
|
participant Client as goget
|
|||
|
|
participant Server as Server
|
|||
|
|
|
|||
|
|
Client->>Server: GET http://example.com
|
|||
|
|
Server-->>Client: 200 OK + Strict-Transport-Security: max-age=31536000
|
|||
|
|
Client->>Client: Store: example.com → 365 days
|
|||
|
|
Client->>Server: GET http://example.com/page (later)
|
|||
|
|
Client->>Client: HSTS cache hit → upgrade to HTTPS
|
|||
|
|
Client->>Server: GET https://example.com/page
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Cache Location
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
~/.config/goget/hsts
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Can be overridden with `GOGET_HSTS` environment variable or `--hsts-file`.
|
|||
|
|
|
|||
|
|
### Cache Format
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
[
|
|||
|
|
{
|
|||
|
|
"host": "example.com",
|
|||
|
|
"include_subdomains": true,
|
|||
|
|
"expires": "2027-06-01T00:00:00Z"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Expired entries are automatically pruned on load.
|
|||
|
|
|
|||
|
|
## Checksum Verification
|
|||
|
|
|
|||
|
|
Verify file integrity after download:
|
|||
|
|
|
|||
|
|
### Direct Hash
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
goget --checksum "a1b2c3d4e5f6..." --checksum-algo sha256 --url https://example.com/file.zip
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Supported algorithms:
|
|||
|
|
|
|||
|
|
| Algorithm | Flag Value | Hash Length |
|
|||
|
|
|---|---|---|
|
|||
|
|
| SHA-256 | `sha256` | 64 hex chars |
|
|||
|
|
| SHA-512 | `sha512` | 128 hex chars |
|
|||
|
|
| SHA3-256 | `sha3-256` | 64 hex chars |
|
|||
|
|
| SHA3-512 | `sha3-512` | 128 hex chars |
|
|||
|
|
| BLAKE2b | `blake2b` | 128 hex chars |
|
|||
|
|
| MD5 | `md5` | 32 hex chars |
|
|||
|
|
|
|||
|
|
### Checksum File
|
|||
|
|
|
|||
|
|
Supports standard `SHA256SUMS` format:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# SHA256SUMS file:
|
|||
|
|
# a1b2c3d4... file.zip
|
|||
|
|
# e5f6a7b8... *file.tar.gz
|
|||
|
|
|
|||
|
|
goget --checksum-file SHA256SUMS --url https://example.com/file.zip
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Both plain (`file.zip`) and binary-mode (`*file.tar.gz`) formats are supported.
|
|||
|
|
|
|||
|
|
### Error Handling
|
|||
|
|
|
|||
|
|
On mismatch:
|
|||
|
|
```
|
|||
|
|
Checksum mismatch
|
|||
|
|
Expected: a1b2c3d4...
|
|||
|
|
Actual: e5f6a7b8...
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The error is a typed `core.GogetError` with `Type: "CHECKSUM"` and `Details["expected"]` / `Details["actual"]`.
|
|||
|
|
|
|||
|
|
## PGP / GPG
|
|||
|
|
|
|||
|
|
### Signature Verification
|
|||
|
|
|
|||
|
|
Verify PGP detached signatures:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# With explicit signature file
|
|||
|
|
goget --pgp-verify --pgp-sig file.sig --pgp-key public.key --url https://example.com/file
|
|||
|
|
|
|||
|
|
# Auto-detect signature file (tries: file.asc, file.sig, file.gpg)
|
|||
|
|
goget --pgp-verify --pgp-key public.key --url https://example.com/file
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Decryption
|
|||
|
|
|
|||
|
|
Decrypt PGP-encrypted files:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# Decrypt with key and passphrase
|
|||
|
|
goget --pgp-decrypt --pgp-key private.key --pgp-passphrase "secret" --url https://example.com/file.gpg
|
|||
|
|
|
|||
|
|
# Read passphrase from file (safer)
|
|||
|
|
goget --pgp-decrypt --pgp-key private.key --pgp-passphrase-file pass.txt --url https://example.com/file.gpg
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
The decrypted file replaces the encrypted one:
|
|||
|
|
- `file.gpg` → `file`
|
|||
|
|
- `file.pgp` → `file`
|
|||
|
|
- `file.asc` → `file`
|
|||
|
|
- Other → `filename.decrypted`
|
|||
|
|
|
|||
|
|
> **Note:** PGP functionality uses `golang.org/x/crypto/openpgp`, which is frozen. A replacement will be provided in a future release.
|
|||
|
|
|
|||
|
|
## SSRF Protection
|
|||
|
|
|
|||
|
|
By default, goget blocks connections to private and internal IP ranges:
|
|||
|
|
|
|||
|
|
| Range | CIDR | Description |
|
|||
|
|
|---|---|---|
|
|||
|
|
| `10.0.0.0/8` | Private | RFC 1918 |
|
|||
|
|
| `172.16.0.0/12` | Private | RFC 1918 |
|
|||
|
|
| `127.0.0.0/8` | Loopback | Internal |
|
|||
|
|
| `169.254.0.0/16` | Link-local | Auto-configuration |
|
|||
|
|
| `::1/128` | IPv6 loopback | Internal |
|
|||
|
|
|
|||
|
|
### IPv4-Mapped IPv6 Normalization
|
|||
|
|
|
|||
|
|
DNS-controlled IPv4-mapped IPv6 addresses (e.g. `::ffff:10.0.0.1`) are now normalized to plain IPv4 before the private-IP check. This prevents an attacker from reaching internal hosts through DNS responses that embed IPv4 addresses in IPv6 form. Covered by a 17-case unit test in the transport layer.
|
|||
|
|
|
|||
|
|
Override with `--allow-private`:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
goget --allow-private --url https://internal.example.com
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## DNS Security
|
|||
|
|
|
|||
|
|
### Custom Resolvers
|
|||
|
|
|
|||
|
|
Bypass system DNS for specific queries:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
goget --dns-servers 1.1.1.1,8.8.8.8 --url https://example.com
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### IPv6-First
|
|||
|
|
|
|||
|
|
The transport dialer prefers IPv6 and only falls back to IPv4 when no IPv6 address is available. This reduces NAT-related attack surface:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# IPv6 only (abort if no IPv6)
|
|||
|
|
goget --no-ipv4 --url https://example.com
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Credential Safety
|
|||
|
|
|
|||
|
|
### Safe Error Logging
|
|||
|
|
|
|||
|
|
When a URL contains user credentials (`https://user:pass@example.com`), `core.SafeURL()` strips them from error messages and logs:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// Original: https://admin:secret@api.example.com/data
|
|||
|
|
// Logged: https://api.example.com/data
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### OAuth Error Sanitization
|
|||
|
|
|
|||
|
|
Non-2xx OAuth responses are sanitized to extract only the structured `error` / `error_description` fields (RFC 6749 §5.2). Providers that echo `client_secret` or other secrets in their error response bodies can no longer leak them into goget logs or error displays.
|
|||
|
|
|
|||
|
|
### Config Sanitization
|
|||
|
|
|
|||
|
|
`config.Save()` strips sensitive fields before writing to disk:
|
|||
|
|
- `auth.password`
|
|||
|
|
- `auth.oauth.client_secret`
|
|||
|
|
- `auth.oauth.access_token`
|
|||
|
|
- `auth.oauth.refresh_token`
|
|||
|
|
|
|||
|
|
### File Permission Hardening
|
|||
|
|
|
|||
|
|
Configuration files (`config.toml`), resume metadata sidecars (`.goget.meta`), and atomic temp files are written with mode `0600` (owner-only), so OAuth tokens, source URLs, and downloaded payloads are never world-readable on multi-user systems.
|
|||
|
|
|
|||
|
|
### Environment Variables
|
|||
|
|
|
|||
|
|
- `GOGET_CONFIG` — alternative config file path
|
|||
|
|
- `GOGET_PASSWORD` — password override (no file storage)
|
|||
|
|
- `GOGET_HSTS` — alternative HSTS cache path
|
|||
|
|
- `NO_COLOR` — disable color output
|
|||
|
|
|
|||
|
|
## Timeouts & Limits
|
|||
|
|
|
|||
|
|
| Feature | Default | Description |
|
|||
|
|
|---|---|---|
|
|||
|
|
| Connection timeout | 30s (auto) | Smart calculation based on file size |
|
|||
|
|
| Max total time | — (`--max-time`) | Abort if exceeded |
|
|||
|
|
| Max file size | — (`--max-filesize`) | Abort if server response exceeds limit |
|
|||
|
|
| Max retries | 3 | Exponential backoff with 30s cap |
|
|||
|
|
| Retry delay | 1s initial, 2× multiplier | Configurable via `--retry-delay` |
|