// Copyright (C) 2026 Dcntrlized // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published // by the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . // Package authmgmt is Maestro's client for the shared auth service's // management endpoints (security theme M2 §3). Maestro is only the CA and the // UI here: it lists pending first-contact requests, forwards the owner's // approve/revoke decisions to the auth service, and reads its key // attestation. Enforcement always stays inside the auth service (D5). package authmgmt import ( "bytes" "context" "encoding/json" "fmt" "net/http" "os" "path/filepath" "time" "git.dcntrlized.org/Dcntrlized/Maestro/lib/configsign" "git.dcntrlized.org/Dcntrlized/Maestro/lib/env" ) // AuthServiceURL is the default DNS name of the shared auth service on the // dcntrlized network. After rev 9 the fleet-wide internal transport is // plaintext HTTP on the single :8080 listener; there is no mTLS anywhere. const AuthServiceURL = "http://dcntrlized-auth:8080" // Client talks to the auth service over plaintext HTTP. It no longer presents // any client certificate: the peer gate was removed fleet-wide in rev 9. type Client struct { baseURL string http *http.Client } // Attestation is the auth service's public signing key statement (D3): the key // a peer should verify access tokens with, plus the fingerprint Maestro pins // as an app-level check (M2 §5). Rev 9 removed the peer trust store, the // pending/approved authorization flow and every management call that used to // live in this package. type Attestation struct { PublicKey string `json:"public_key"` Fingerprint string `json:"fingerprint"` Kid string `json:"kid"` IssuedAt string `json:"issued_at"` } // New builds the plaintext management client. There is no identity to // provision and no transport to configure after the PKI removal. func New() (*Client, error) { baseURL := "" if env.IsDev() { baseURL = os.Getenv("DCNTRLIZED_AUTH_URL") } if baseURL == "" { baseURL = os.Getenv("AUTH_SERVICE_URL") } if baseURL == "" { baseURL = AuthServiceURL } return &Client{ baseURL: baseURL, http: &http.Client{ Timeout: 10 * time.Second, }, }, nil } // Scheme returns the scheme every satellite peer edge uses after rev 9: // always "http" (plaintext on the single :8080 listener). Kept as a function // so the call sites (manifest fetch, action routing) do not hard-code it. func Scheme() string { return "http" } // ConfigToken mints a fresh, single-use ES256 config token for one service // (contract rev 9 §17.3.6). It loads (or creates) Maestro's DEDICATED // config-signing keypair from the data root and returns the compact JWS. The // service holds only the public half (DCNTRLIZED_CONFIG_PUBKEY) and verifies; // Maestro only ever mints. Wire format is frozen in §17.3.3. func ConfigToken(serviceName string) (string, error) { root, err := dataRoot() if err != nil { return "", err } signer, err := configsign.LoadOrCreate(root) if err != nil { return "", err } return signer.Mint(serviceName, time.Now()) } // dataRoot resolves the directory holding Maestro's private key material // (DCNTRLIZED_INSTALL_DIR → XDG → HOME), mirroring the containers package's // resolution. Kept local to avoid an import cycle: containers imports authmgmt. func dataRoot() (string, error) { if dir := os.Getenv("DCNTRLIZED_INSTALL_DIR"); dir != "" { return dir, nil } if xdg := os.Getenv("XDG_DATA_HOME"); xdg != "" { return filepath.Join(xdg, ".dcntrlized"), nil } if xdgHome := os.Getenv("XDG_HOME_DIR"); xdgHome != "" { return filepath.Join(xdgHome, ".dcntrlized"), nil } if home := os.Getenv("HOME"); home != "" { return filepath.Join(home, ".dcntrlized"), nil } return filepath.Join("/data", ".dcntrlized"), nil } // Attestation returns the auth service's public key statement. func (c *Client) Attestation(ctx context.Context) (*Attestation, error) { var att Attestation if err := c.do(ctx, http.MethodGet, "/internal/attestation", nil, &att); err != nil { return nil, err } return &att, nil } func (c *Client) do(ctx context.Context, method, path string, body, out any) error { var payload []byte if body != nil { payload, _ = json.Marshal(body) } req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bytes.NewReader(payload)) if err != nil { return err } if body != nil { req.Header.Set("Content-Type", "application/json") } resp, err := c.http.Do(req) if err != nil { return fmt.Errorf("auth service %s %s: %w", method, path, err) } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("auth service %s %s: status %d", method, path, resp.StatusCode) } if out != nil { if err := json.NewDecoder(resp.Body).Decode(out); err != nil { return fmt.Errorf("decode auth service response: %w", err) } } return nil }