Complete infrastructure platform CLI: - Container runtime (systemd-nspawn) - VoltVisor VMs (Neutron Stardust / QEMU) - Stellarium CAS (content-addressed storage) - ORAS Registry - GitOps integration - Landlock LSM security - Compose orchestration - Mesh networking Copyright (c) Armored Gates LLC. All rights reserved. Licensed under AGPSL v5.0
197 lines
6.8 KiB
Go
197 lines
6.8 KiB
Go
/*
|
|
Encrypted CDN Client — Transparent AGE encryption layer over CDN operations.
|
|
|
|
Wraps the standard CDN Client to encrypt blobs before upload and decrypt
|
|
on download. The encryption is transparent to callers — they push/pull
|
|
plaintext and the encryption happens automatically.
|
|
|
|
Architecture:
|
|
- PushBlob: plaintext → AGE encrypt → upload ciphertext
|
|
- PullBlob: download ciphertext → AGE decrypt → return plaintext
|
|
- Hash verification: hash is of PLAINTEXT (preserves CAS dedup)
|
|
- Manifests are NOT encrypted (they contain only hashes, no sensitive data)
|
|
|
|
Copyright (c) Armored Gates LLC. All rights reserved.
|
|
*/
|
|
package cdn
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/armoredgate/volt/pkg/encryption"
|
|
)
|
|
|
|
// ── Encrypted Client ─────────────────────────────────────────────────────────
|
|
|
|
// EncryptedClient wraps a CDN Client with transparent AGE encryption.
|
|
type EncryptedClient struct {
|
|
// Inner is the underlying CDN client that handles HTTP operations.
|
|
Inner *Client
|
|
|
|
// Recipients are the AGE public keys to encrypt to.
|
|
// Populated from encryption.BuildRecipients() on creation.
|
|
Recipients []string
|
|
|
|
// IdentityPath is the path to the AGE private key for decryption.
|
|
IdentityPath string
|
|
}
|
|
|
|
// NewEncryptedClient creates a CDN client with transparent encryption.
|
|
// It reads encryption keys from the standard locations.
|
|
func NewEncryptedClient() (*EncryptedClient, error) {
|
|
inner, err := NewClient()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encrypted cdn client: %w", err)
|
|
}
|
|
|
|
return NewEncryptedClientFromInner(inner)
|
|
}
|
|
|
|
// NewEncryptedClientFromInner wraps an existing CDN client with encryption.
|
|
func NewEncryptedClientFromInner(inner *Client) (*EncryptedClient, error) {
|
|
recipients, err := encryption.BuildRecipients()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encrypted cdn client: %w", err)
|
|
}
|
|
|
|
return &EncryptedClient{
|
|
Inner: inner,
|
|
Recipients: recipients,
|
|
IdentityPath: encryption.CDNIdentityPath(),
|
|
}, nil
|
|
}
|
|
|
|
// ── Encrypted Push/Pull ──────────────────────────────────────────────────────
|
|
|
|
// PushBlob encrypts plaintext data and uploads the ciphertext to the CDN.
|
|
// The hash parameter is the SHA-256 of the PLAINTEXT (for CAS addressing).
|
|
// The CDN stores the ciphertext keyed by the plaintext hash.
|
|
func (ec *EncryptedClient) PushBlob(hash string, plaintext []byte) error {
|
|
// Verify plaintext hash matches
|
|
actualHash := encSha256Hex(plaintext)
|
|
if actualHash != hash {
|
|
return fmt.Errorf("encrypted push: hash mismatch (expected %s, got %s)", hash[:12], actualHash[:12])
|
|
}
|
|
|
|
// Encrypt
|
|
ciphertext, err := encryption.Encrypt(plaintext, ec.Recipients)
|
|
if err != nil {
|
|
return fmt.Errorf("encrypted push %s: %w", hash[:12], err)
|
|
}
|
|
|
|
// Upload ciphertext — we bypass the inner client's hash check since the
|
|
// ciphertext hash won't match the plaintext hash. We use the raw HTTP upload.
|
|
return ec.pushRawBlob(hash, ciphertext)
|
|
}
|
|
|
|
// PullBlob downloads ciphertext from the CDN, decrypts it, and returns plaintext.
|
|
// The hash is verified against the decrypted plaintext.
|
|
func (ec *EncryptedClient) PullBlob(hash string) ([]byte, error) {
|
|
// Download raw (skip inner client's integrity check since it's ciphertext)
|
|
ciphertext, err := ec.pullRawBlob(hash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Decrypt
|
|
plaintext, err := encryption.Decrypt(ciphertext, ec.IdentityPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encrypted pull %s: %w", hash[:12], err)
|
|
}
|
|
|
|
// Verify plaintext integrity
|
|
actualHash := encSha256Hex(plaintext)
|
|
if actualHash != hash {
|
|
return nil, fmt.Errorf("encrypted pull %s: plaintext integrity check failed (got %s)", hash[:12], actualHash[:12])
|
|
}
|
|
|
|
return plaintext, nil
|
|
}
|
|
|
|
// BlobExists checks if a blob exists on the CDN (delegates to inner client).
|
|
func (ec *EncryptedClient) BlobExists(hash string) (bool, error) {
|
|
return ec.Inner.BlobExists(hash)
|
|
}
|
|
|
|
// PullManifest downloads a manifest (NOT encrypted — manifests contain only hashes).
|
|
func (ec *EncryptedClient) PullManifest(name string) (*Manifest, error) {
|
|
return ec.Inner.PullManifest(name)
|
|
}
|
|
|
|
// PushManifest uploads a manifest (NOT encrypted).
|
|
func (ec *EncryptedClient) PushManifest(name string, manifest *Manifest) error {
|
|
return ec.Inner.PushManifest(name, manifest)
|
|
}
|
|
|
|
// ── Raw HTTP Operations ──────────────────────────────────────────────────────
|
|
|
|
// pushRawBlob uploads raw bytes to the CDN without hash verification.
|
|
// Used for ciphertext upload where the hash is of the plaintext.
|
|
func (ec *EncryptedClient) pushRawBlob(hash string, data []byte) error {
|
|
if ec.Inner.StorageAPIKey == "" {
|
|
return fmt.Errorf("cdn push blob: StorageAPIKey not configured")
|
|
}
|
|
if ec.Inner.StorageZoneName == "" {
|
|
return fmt.Errorf("cdn push blob: StorageZoneName not configured")
|
|
}
|
|
|
|
url := fmt.Sprintf("https://%s.storage.bunnycdn.com/%s/sha256:%s",
|
|
ec.Inner.Region, ec.Inner.StorageZoneName, hash)
|
|
|
|
req, err := http.NewRequest(http.MethodPut, url, strings.NewReader(string(data)))
|
|
if err != nil {
|
|
return fmt.Errorf("cdn push blob %s: create request: %w", hash[:12], err)
|
|
}
|
|
req.Header.Set("AccessKey", ec.Inner.StorageAPIKey)
|
|
req.Header.Set("Content-Type", "application/octet-stream")
|
|
req.ContentLength = int64(len(data))
|
|
|
|
resp, err := ec.Inner.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("cdn push blob %s: %w", hash[:12], err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("cdn push blob %s: HTTP %d: %s", hash[:12], resp.StatusCode, string(body))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// pullRawBlob downloads raw bytes from the CDN without hash verification.
|
|
// Used for ciphertext download where the hash is of the plaintext.
|
|
func (ec *EncryptedClient) pullRawBlob(hash string) ([]byte, error) {
|
|
url := fmt.Sprintf("%s/sha256:%s", ec.Inner.BlobsBaseURL, hash)
|
|
|
|
resp, err := ec.Inner.HTTPClient.Get(url)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cdn pull blob %s: %w", hash[:12], err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("cdn pull blob %s: HTTP %d", hash[:12], resp.StatusCode)
|
|
}
|
|
|
|
data, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cdn pull blob %s: read body: %w", hash[:12], err)
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
func encSha256Hex(data []byte) string {
|
|
h := sha256.Sum256(data)
|
|
return hex.EncodeToString(h[:])
|
|
}
|