# internal/autocert

Automated SSL certificate management using the ACME protocol (Let's Encrypt and compatible CAs).

## Overview

### Purpose

This package provides complete SSL certificate lifecycle management:

This package is split between proxy-side certificate serving and helper-side ACME work:

- **Proxy role:** file-backed certificate cache + scheduler in the main process
- **Helper role:** spawned oneshot binary that performs obtain/renew work and exits

- ACME account registration and management
- Certificate issuance via DNS-01 challenge
- Automatic renewal scheduling (1 month before expiry)
- SNI-based certificate selection for multi-domain setups

### Primary Consumers

- `goutils/server` - TLS handshake certificate provider
- `internal/api/v1/cert/` - REST API for certificate management
- Configuration loading via `internal/config/`

### Non-goals

- HTTP-01 challenge support
- Certificate transparency log monitoring
- OCSP stapling
- Private CA support (except via custom CADirURL)

### Stability

Internal package with stable public APIs. ACME protocol compliance depends on lego library.

## Public API

### Config (`config.go`)

```go
type Config struct {
    Email       string                       // ACME account email
    Domains     []string                     // Domains to certify
    CertPath    string                       // Output cert path
    KeyPath     string                       // Output key path
    Extra       []ConfigExtra                // Additional cert configs
    ACMEKeyPath string                       // ACME account private key
    Provider    string                       // DNS provider name
    Options     map[string]strutils.Redacted // Provider options
    Resolvers   []string                     // DNS resolvers
    CADirURL    string                       // Custom ACME CA directory
    CACerts     []string                     // Custom CA certificates
    EABKid      string                       // External Account Binding Key ID
    EABHmac     string                       // External Account Binding HMAC
}

// Merge extra config with main provider
func MergeExtraConfig(mainCfg *Config, extraCfg *ConfigExtra) ConfigExtra
```

### Provider (`provider.go`)

```go
type Provider struct {
    logger       zerolog.Logger
    cfg          *Config
    user         *User
    legoCfg      *lego.Config
    client       *lego.Client
    lastFailure  time.Time
    legoCert     *certificate.Resource
    tlsCert      *tls.Certificate
    certExpiries CertExpiries
    extraProviders []*Provider
    sniMatcher   sniMatcher
}

// Create new provider (initializes extras atomically)
func NewProvider(cfg *Config, user *User, legoCfg *lego.Config) (*Provider, error)

// TLS certificate getter for SNI
func (p *Provider) GetCert(hello *tls.ClientHelloInfo) (*tls.Certificate, error)

// Certificate info for API
func (p *Provider) GetCertInfos() ([]CertInfo, error)

// Provider name ("main" or "extra[N]")
func (p *Provider) GetName() string

// Obtain certificate if not exists
func (p *Provider) ObtainCertIfNotExistsAll() error

// Force immediate renewal
func (p *Provider) ForceExpiryAll() bool

// Schedule automatic renewal
func (p *Provider) ScheduleRenewalAll(parent task.Parent)

// Print expiry dates
func (p *Provider) PrintCertExpiriesAll()
```

### User (`user.go`)

```go
type User struct {
    Email        string                    // Account email
    Registration *registration.Resource    // ACME registration
    Key          crypto.PrivateKey         // Account key
}
```

## Architecture

### Certificate Lifecycle

```mermaid
flowchart TD
    A[Start] --> B[Load Existing Cert]
    B --> C{Cert Exists?}
    C -->|Yes| D[Load Cert from Disk]
    C -->|No| E[Obtain New Cert]

    D --> F{Valid & Not Expired?}
    F -->|Yes| G[Schedule Renewal]
    F -->|No| H{Renewal Time?}
    H -->|Yes| I[Renew Certificate]
    H -->|No| G

    E --> J[Init ACME Client]
    J --> K[Register Account]
    K --> L[DNS-01 Challenge]
    L --> M[Complete Challenge]
    M --> N[Download Certificate]
    N --> O[Save to Disk]
    O --> G

    G --> P[Wait Until Renewal Time]
    P --> Q[Trigger Renewal]
    Q --> I

    I --> R[Renew via ACME]
    R --> S{Same Domains?}
    S -->|Yes| T[Bundle & Save]
    S -->|No| U[Re-obtain Certificate]
    U --> T

    T --> V[Update SNI Matcher]
    V --> G

    style E fill:#22553F,color:#fff
    style I fill:#8B8000,color:#fff
    style N fill:#22553F,color:#fff
    style U fill:#84261A,color:#fff
```

### Concurrency Model

- Certificate obtain / renew work is serialized per provider.
- Extra providers may still renew in parallel with each other.
- Each provider uses its own ACME HTTP client instance so parallel renewals do not mutate shared transport state.
- TLS certificate replacement and SNI matcher rebuilds are synchronized before new state becomes visible to handshakes.

### SNI Matching Flow

```mermaid
flowchart LR
    Client["TLS Client"] -->|ClientHello SNI| Proxy["GoDoxy Proxy"]
    Proxy -->|Certificate| Client

    subgraph "SNI Matching Process"
        direction TB
        A[Extract SNI from ClientHello] --> B{Normalize SNI}
        B --> C{Exact Match?}
        C -->|Yes| D[Return cert]
        C -->|No| E[Wildcard Suffix Tree]
        E --> F{Match Found?}
        F -->|Yes| D
        F -->|No| G[Return default cert]
    end

    style C fill:#27632A,color:#fff
    style E fill:#18597A,color:#fff
    style F fill:#836C03,color:#fff
```

### Suffix Tree Structure

```
Certificate: *.example.com, example.com, *.api.example.com

exact:
  "example.com" -> Provider_A

root:
  └── "com"
      └── "example"
          ├── "*" -> Provider_A  [wildcard at *.example.com]
          └── "api"
              └── "*" -> Provider_B  [wildcard at *.api.example.com]
```

## Configuration Surface

### Provider Types

| Type           | Description                  | Use Case                  |
| -------------- | ---------------------------- | ------------------------- |
| `local`        | No ACME, use existing cert   | Pre-existing certificates |
| `pseudo`       | Mock provider for testing    | Development               |
| ACME providers | Let's Encrypt, ZeroSSL, etc. | Production                |

### Supported DNS Providers

| Provider     | Name           | Required Options                    |
| ------------ | -------------- | ----------------------------------- |
| Cloudflare   | `cloudflare`   | `CF_API_TOKEN`                      |
| Route 53     | `route53`      | AWS credentials                     |
| DigitalOcean | `digitalocean` | `DO_API_TOKEN`                      |
| GoDaddy      | `godaddy`      | `GD_API_KEY`, `GD_API_SECRET`       |
| OVH          | `ovh`          | `OVH_ENDPOINT`, `OVH_APP_KEY`, etc. |
| CloudDNS     | `clouddns`     | GCP credentials                     |
| AzureDNS     | `azuredns`     | Azure credentials                   |
| DuckDNS      | `duckdns`      | `DUCKDNS_TOKEN`                     |

### Example Configuration

```yaml
autocert:
  provider: cloudflare
  email: admin@example.com
  domains:
    - example.com
    - "*.example.com"
  options:
    auth_token: ${CF_API_TOKEN}
  resolvers:
    - 1.1.1.1:53
```

### Extra Providers

```yaml
autocert:
  provider: cloudflare
  email: admin@example.com
  domains:
    - example.com
    - "*.example.com"
  cert_path: certs/example.com.crt
  key_path: certs/example.com.key
  options:
    auth_token: ${CF_API_TOKEN}
  extra:
    - domains:
        - api.example.com
        - "*.api.example.com"
      cert_path: certs/api.example.com.crt
      key_path: certs/api.example.com.key
```

## Dependency and Integration Map

### External Dependencies

- `github.com/go-acme/lego/v4` - ACME protocol implementation
- `github.com/rs/zerolog` - Structured logging

### Internal Dependencies

- `internal/task/task.go` - Lifetime management
- `internal/notif/` - Renewal notifications
- `internal/config/` - Configuration loading
- `internal/dnsproviders/` - DNS provider implementations

## Observability

### Logs

| Level   | When                          |
| ------- | ----------------------------- |
| `Info`  | Certificate obtained/renewed  |
| `Info`  | Registration reused           |
| `Warn`  | Renewal failure               |
| `Error` | Certificate retrieval failure |

### Notifications

- Certificate renewal success/failure
- Service startup with expiry dates

## Security Considerations

- Account private key stored at `certs/acme.key` (mode 0600)
- Certificate private keys stored at configured paths (mode 0600)
- Certificate files world-readable (mode 0644)
- ACME account email used for Let's Encrypt ToS
- EAB credentials for zero-touch enrollment

## Failure Modes and Recovery

| Failure Mode                   | Impact                     | Recovery                      |
| ------------------------------ | -------------------------- | ----------------------------- |
| DNS-01 challenge timeout       | Certificate issuance fails | Check DNS provider API        |
| Rate limiting (too many certs) | 1-hour cooldown            | Wait or use different account |
| DNS provider API error         | Renewal fails              | 1-hour cooldown, retry        |
| Certificate domains mismatch   | Must re-obtain             | Force renewal via API         |
| Account key corrupted          | Must register new account  | New key, may lose certs       |

### Failure Tracking

Last failure persisted per-certificate to prevent rate limiting:

```
File: <cert_dir>/.last_failure-<hash>
Where hash = SHA256(certPath|keyPath)[:6]
```

## Usage Examples

### Initial Setup

```go
autocertCfg := state.AutoCert
user, legoCfg, err := autocertCfg.GetLegoConfig()
if err != nil {
    return err
}

provider, err := autocert.NewProvider(autocertCfg, user, legoCfg)
if err != nil {
    return fmt.Errorf("autocert error: %w", err)
}

if err := provider.ObtainCertIfNotExistsAll(); err != nil {
    return fmt.Errorf("failed to obtain certificates: %w", err)
}

provider.ScheduleRenewalAll(state.Task())
provider.PrintCertExpiriesAll()
```

### Force Renewal via API

```go
// WebSocket endpoint: GET /api/v1/cert/renew
if provider.ForceExpiryAll() {
    // Wait for renewal to complete
    provider.WaitRenewalDone(ctx)
}
```

## Testing Notes

- `config_test.go` - Configuration validation
- `provider_test/` - Provider functionality tests
- `sni_test.go` - SNI matching tests
- `multi_cert_test.go` - Extra provider tests
- Integration tests require mock DNS provider
