feat: add recovery key support for E2EE agents, including configuration and documentation updates

This commit is contained in:
2026-03-05 00:56:15 +00:00
parent fc234bcb92
commit 0f900d1560
10 changed files with 284 additions and 35 deletions
+17 -2
View File
@@ -95,6 +95,7 @@ matrix:
store_path: "./agents/<agent-id>/data/crypto/" # SIEMPRE por agente, nunca compartida store_path: "./agents/<agent-id>/data/crypto/" # SIEMPRE por agente, nunca compartida
pickle_key_env: PICKLE_KEY_<ID_UPPER> # env var con clave hex pickle_key_env: PICKLE_KEY_<ID_UPPER> # env var con clave hex
trust_mode: tofu trust_mode: tofu
recovery_key_env: SSSS_RECOVERY_KEY_<ID_UPPER> # env var con base58 recovery key
``` ```
**Al crear un nuevo agente con E2EE:** **Al crear un nuevo agente con E2EE:**
@@ -102,9 +103,23 @@ matrix:
2. Añadir a `.env`: `PICKLE_KEY_<ID_UPPER>=<hex>` 2. Añadir a `.env`: `PICKLE_KEY_<ID_UPPER>=<hex>`
3. Añadir a `.env.example`: `PICKLE_KEY_<ID_UPPER>=` 3. Añadir a `.env.example`: `PICKLE_KEY_<ID_UPPER>=`
4. Usar `store_path` propio del agente (no compartir entre agentes) 4. Usar `store_path` propio del agente (no compartir entre agentes)
5. Tras arrancar, verificar cross-signing: `go run -tags goolm ./cmd/verify ...` 5. Ejecutar `cmd/verify` con `--store` y `--pickle-key` del agente:
```bash
./bin/verify --homeserver "$MATRIX_HOMESERVER" --username "<id>" \
--password "$MATRIX_PASSWORD_<AGENT>" --token "$MATRIX_TOKEN_<AGENT>" \
--store "./agents/<id>/data/crypto/" --pickle-key "$PICKLE_KEY_<AGENT>"
```
6. Guardar el recovery key en `.env` (con comillas por los espacios):
```bash
SSSS_RECOVERY_KEY_<ID_UPPER>="EsXX YYYY ZZZZ ..."
```
7. Añadir `recovery_key_env` al config.yaml:
```yaml
encryption:
recovery_key_env: SSSS_RECOVERY_KEY_<ID_UPPER>
```
Ver `docs/e2ee.md` para documentación completa de E2EE. **Sin el recovery key**, el agente arranca pero los mensajes muestran "Encrypted by a device not verified by its owner".
### 3. `agents/<agent-id>/prompts/system.md` — System prompt ### 3. `agents/<agent-id>/prompts/system.md` — System prompt
+7
View File
@@ -23,6 +23,13 @@ PICKLE_KEY_ASSISTANT_BOT=
PICKLE_KEY_ASISTENTE_2= PICKLE_KEY_ASISTENTE_2=
PICKLE_KEY_DEVOPS_BOT= PICKLE_KEY_DEVOPS_BOT=
# ── E2EE SSSS recovery keys (generados por cmd/verify) ──────
# Permite al agente importar cross-signing private keys al iniciar.
# Sin esto, los mensajes muestran "Encrypted by a device not verified by its owner".
SSSS_RECOVERY_KEY_ASSISTANT_BOT=
SSSS_RECOVERY_KEY_ASISTENTE_2=
SSSS_RECOVERY_KEY_DEVOPS_BOT=
# ── LLM providers ──────────────────────────────────────────── # ── LLM providers ────────────────────────────────────────────
OPENAI_API_KEY=sk-... OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-... # opcional, para cuando añadas el devops-bot con Claude ANTHROPIC_API_KEY=sk-ant-... # opcional, para cuando añadas el devops-bot con Claude
+2 -1
View File
@@ -116,13 +116,14 @@ matrix:
homeserver: "https://matrix-af2f3d.organic-machine.com" homeserver: "https://matrix-af2f3d.organic-machine.com"
user_id: "@asistente-2:matrix-af2f3d.organic-machine.com" user_id: "@asistente-2:matrix-af2f3d.organic-machine.com"
access_token_env: MATRIX_TOKEN_ASISTENTE2 access_token_env: MATRIX_TOKEN_ASISTENTE2
device_id: "YBFNMNMJIC" device_id: "XUGTSZJYFQ"
encryption: encryption:
enabled: true enabled: true
store_path: "./agents/asistente2/data/crypto/" store_path: "./agents/asistente2/data/crypto/"
pickle_key_env: PICKLE_KEY_ASISTENTE_2 pickle_key_env: PICKLE_KEY_ASISTENTE_2
trust_mode: tofu trust_mode: tofu
recovery_key_env: SSSS_RECOVERY_KEY_ASISTENTE_2
rooms: rooms:
listen: [] listen: []
+2 -1
View File
@@ -117,13 +117,14 @@ matrix:
homeserver: "https://matrix-af2f3d.organic-machine.com" homeserver: "https://matrix-af2f3d.organic-machine.com"
user_id: "@assistant-bot:matrix-af2f3d.organic-machine.com" user_id: "@assistant-bot:matrix-af2f3d.organic-machine.com"
access_token_env: MATRIX_TOKEN_ASSISTANT access_token_env: MATRIX_TOKEN_ASSISTANT
device_id: "ASSISTANTBOT01" device_id: "SMWMRKMHDH"
encryption: encryption:
enabled: true enabled: true
store_path: "./agents/assistant/data/crypto/" store_path: "./agents/assistant/data/crypto/"
pickle_key_env: PICKLE_KEY_ASSISTANT_BOT pickle_key_env: PICKLE_KEY_ASSISTANT_BOT
trust_mode: tofu trust_mode: tofu
recovery_key_env: SSSS_RECOVERY_KEY_ASSISTANT_BOT
rooms: rooms:
listen: [] # vacío = escucha en todos los rooms donde está invitado listen: [] # vacío = escucha en todos los rooms donde está invitado
+12
View File
@@ -56,6 +56,18 @@ func New(cfg *config.AgentConfig, rules []decision.Rule, logger *slog.Logger) (*
if err != nil { if err != nil {
return nil, fmt.Errorf("e2ee init: %w", err) return nil, fmt.Errorf("e2ee init: %w", err)
} }
// Auto-fetch cross-signing private keys from SSSS if recovery key is configured.
if envName := cfg.Matrix.Encryption.RecoveryKeyEnv; envName != "" {
if rk := os.Getenv(envName); rk != "" {
if err := matrixClient.FetchCrossSigningKeys(context.Background(), rk); err != nil {
logger.Warn("failed to fetch cross-signing keys from SSSS (non-fatal)", "err", err)
} else {
logger.Info("cross-signing private keys fetched from SSSS")
}
}
}
logger.Info("e2ee ready") logger.Info("e2ee ready")
} }
+28 -9
View File
@@ -9,6 +9,7 @@ package main
import ( import (
"context" "context"
"crypto/sha256" "crypto/sha256"
"encoding/hex"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
@@ -23,11 +24,12 @@ import (
func main() { func main() {
var ( var (
homeserver string homeserver string
username string username string
password string password string
token string token string
storePath string storePath string
pickleKeyHex string
) )
root := &cobra.Command{ root := &cobra.Command{
@@ -62,9 +64,18 @@ Requires the bot's access token and password (for UIA during key upload).`,
client.DeviceID = whoami.DeviceID client.DeviceID = whoami.DeviceID
fmt.Printf("→ Device ID: %s\n", client.DeviceID) fmt.Printf("→ Device ID: %s\n", client.DeviceID)
// Initialize crypto // Initialize crypto — use explicit pickle key if provided, else sha256(token)
sum := sha256.Sum256([]byte(token)) var pickleKey []byte
pickleKey := sum[:] if pickleKeyHex != "" {
var err error
pickleKey, err = hex.DecodeString(pickleKeyHex)
if err != nil {
return fmt.Errorf("decode pickle-key hex: %w", err)
}
} else {
sum := sha256.Sum256([]byte(token))
pickleKey = sum[:]
}
dbPath := filepath.Join(storePath, "crypto.db") dbPath := filepath.Join(storePath, "crypto.db")
if err := os.MkdirAll(filepath.Dir(dbPath), 0700); err != nil { if err := os.MkdirAll(filepath.Dir(dbPath), 0700); err != nil {
@@ -91,7 +102,7 @@ Requires the bot's access token and password (for UIA during key upload).`,
} }
fmt.Println("→ Generating and uploading cross-signing keys...") fmt.Println("→ Generating and uploading cross-signing keys...")
_, _, err = olmMachine.GenerateAndUploadCrossSigningKeysWithPassword(ctx, password, "") recoveryKey, _, err := olmMachine.GenerateAndUploadCrossSigningKeysWithPassword(ctx, password, "")
if err != nil { if err != nil {
// If keys already exist, try to just sign our device // If keys already exist, try to just sign our device
fmt.Printf(" Note: %v\n", err) fmt.Printf(" Note: %v\n", err)
@@ -101,6 +112,13 @@ Requires the bot's access token and password (for UIA during key upload).`,
fmt.Println("✓ Cross-signing keys uploaded successfully") fmt.Println("✓ Cross-signing keys uploaded successfully")
fmt.Printf("✓ Device %s is now verified by %s\n", client.DeviceID, userID) fmt.Printf("✓ Device %s is now verified by %s\n", client.DeviceID, userID)
fmt.Println()
fmt.Println("─── IMPORTANT: Save the recovery key ───")
fmt.Printf("SSSS_RECOVERY_KEY_%s=%s\n", strings.ToUpper(strings.ReplaceAll(username, "-", "_")), recoveryKey)
fmt.Println()
fmt.Println("Add this to your .env file and set recovery_key_env in the agent's config.yaml:")
fmt.Println(" encryption:")
fmt.Printf(" recovery_key_env: SSSS_RECOVERY_KEY_%s\n", strings.ToUpper(strings.ReplaceAll(username, "-", "_")))
return nil return nil
}, },
} }
@@ -110,6 +128,7 @@ Requires the bot's access token and password (for UIA during key upload).`,
root.Flags().StringVar(&password, "password", "", "Bot password (for UIA auth)") root.Flags().StringVar(&password, "password", "", "Bot password (for UIA auth)")
root.Flags().StringVar(&token, "token", "", "Bot access token") root.Flags().StringVar(&token, "token", "", "Bot access token")
root.Flags().StringVar(&storePath, "store", "./data/verify-crypto/", "Crypto store path") root.Flags().StringVar(&storePath, "store", "./data/verify-crypto/", "Crypto store path")
root.Flags().StringVar(&pickleKeyHex, "pickle-key", "", "Hex-encoded pickle key (must match agent's pickle key if sharing crypto store)")
_ = root.MarkFlagRequired("homeserver") _ = root.MarkFlagRequired("homeserver")
_ = root.MarkFlagRequired("username") _ = root.MarkFlagRequired("username")
_ = root.MarkFlagRequired("password") _ = root.MarkFlagRequired("password")
+66 -18
View File
@@ -208,28 +208,67 @@ Esto hace:
Sin este paso, los mensajes del bot mostrarán: **"Encrypted by a device not verified by its owner"**. Sin este paso, los mensajes del bot mostrarán: **"Encrypted by a device not verified by its owner"**.
```bash ```bash
go run -tags goolm ./cmd/verify \ ./bin/verify \
--homeserver "https://matrix-af2f3d.organic-machine.com" \ --homeserver "$MATRIX_HOMESERVER" \
--username "<agent-id>" \ --username "<agent-id>" \
--password "<password_del_bot>" \ --password "$MATRIX_PASSWORD_<AGENT>" \
--token "<access_token>" \ --token "$MATRIX_TOKEN_<AGENT>" \
--store "./agents/<agent-id>/data/crypto/" --store "./agents/<agent-id>/data/crypto/" \
--pickle-key "$PICKLE_KEY_<AGENT>"
``` ```
**Qué hace:** **Qué hace:**
1. Inicializa el crypto helper de mautrix 1. Inicializa el crypto helper de mautrix (usando el mismo store y pickle key que el agente)
2. Genera claves de cross-signing (master + self-signing) 2. Genera claves de cross-signing (master + self-signing + user-signing)
3. Las sube al homeserver usando UIA con la password del bot 3. Las sube al homeserver usando UIA con la password del bot
4. Firma el device del bot con la self-signing key 4. Las almacena cifradas en SSSS (Server-Side Secret Storage) en el servidor
5. Imprime un **recovery key** (base58) que permite recuperar las claves privadas
**Importante:** Si se cambia la password del bot (admin API), el token anterior se invalida. Hay que: ### 5.1 Guardar el recovery key
El comando imprime algo como:
```
─── IMPORTANT: Save the recovery key ───
SSSS_RECOVERY_KEY_MI_BOT=EsXX YYYY ZZZZ ...
```
**Añadir al `.env`** (con comillas, el recovery key tiene espacios):
```bash
SSSS_RECOVERY_KEY_MI_BOT="EsXX YYYY ZZZZ ..."
```
### 5.2 Configurar recovery_key_env en config.yaml
```yaml
encryption:
enabled: true
store_path: "./agents/<agent-id>/data/crypto/"
pickle_key_env: PICKLE_KEY_<AGENT>
trust_mode: tofu
recovery_key_env: SSSS_RECOVERY_KEY_<AGENT> # ← NUEVO
```
Esto permite que el agente recupere automáticamente las cross-signing private keys desde SSSS cada vez que arranca. Sin esto, las keys solo existen en memoria durante la sesión de verify.
**Logs esperados al arrancar con recovery key configurado:**
```
INFO cross-signing private keys fetched from SSSS
INFO e2ee ready
```
### 5.3 Si se cambia la password del bot
Cambiar la password (admin API) invalida el token anterior. Hay que:
1. Re-login para obtener nuevo token 1. Re-login para obtener nuevo token
2. Actualizar `MATRIX_TOKEN_<AGENT>` en `.env` 2. Actualizar `MATRIX_TOKEN_<AGENT>` y `MATRIX_PASSWORD_<AGENT>` en `.env`
3. Actualizar `device_id` en `config.yaml` 3. Actualizar `device_id` en `config.yaml`
4. Borrar el crypto store viejo (`agents/<id>/data/crypto/crypto.db`) 4. Borrar el crypto store viejo (`agents/<id>/data/crypto/crypto.db`)
5. Re-ejecutar `cmd/verify` 5. Re-ejecutar `cmd/verify` → obtener nuevo recovery key
6. Actualizar `SSSS_RECOVERY_KEY_<AGENT>` en `.env`
**Nota:** El pickle key (`PICKLE_KEY_<AGENT>`) NO cambia al rotar el token. Solo se regenera si se pierde. Ver `docs/e2ee.md`. **Nota:** El pickle key (`PICKLE_KEY_<AGENT>`) NO cambia al rotar el token. Solo se regenera si se pierde.
## Paso 6: Arrancar el agente ## Paso 6: Arrancar el agente
@@ -286,17 +325,24 @@ tail -f run/<agent-id>.log
# 5. Avatar y displayname # 5. Avatar y displayname
./dev-scripts/avatar.sh <id> static/<imagen>.jpg ./dev-scripts/avatar.sh <id> static/<imagen>.jpg
# 6. Verificación E2EE # 6. Generar pickle key (si no existe)
go run -tags goolm ./cmd/verify \ openssl rand -hex 32 # → guardar como PICKLE_KEY_<AGENT> en .env
# 7. Verificación E2EE + recovery key
./bin/verify \
--homeserver "$MATRIX_HOMESERVER" \ --homeserver "$MATRIX_HOMESERVER" \
--username "<id>" \ --username "<id>" \
--password "$MATRIX_PASSWORD_<AGENT>" \ --password "$MATRIX_PASSWORD_<AGENT>" \
--token "$MATRIX_TOKEN_<AGENT>" --token "$MATRIX_TOKEN_<AGENT>" \
--store "./agents/<id>/data/crypto/" \
--pickle-key "$PICKLE_KEY_<AGENT>"
# → Guardar SSSS_RECOVERY_KEY_<AGENT> en .env (con comillas)
# → Añadir recovery_key_env al config.yaml
# 7. Arrancar # 8. Arrancar
./dev-scripts/start.sh <id> ./dev-scripts/start.sh <id>
# 8. Verificar # 9. Verificar
tail -f run/<id>.log tail -f run/<id>.log
``` ```
@@ -308,7 +354,9 @@ tail -f run/<id>.log
| `M_UNKNOWN_TOKEN` | Token invalidado (password cambiada) | Re-login, actualizar `.env` | | `M_UNKNOWN_TOKEN` | Token invalidado (password cambiada) | Re-login, actualizar `.env` |
| `mismatching device ID` | Crypto store con device viejo | Borrar `agents/<id>/data/crypto/crypto.db`, actualizar `device_id` en config | | `mismatching device ID` | Crypto store con device viejo | Borrar `agents/<id>/data/crypto/crypto.db`, actualizar `device_id` en config |
| `olm account not marked as shared` | Crypto store inconsistente | Auto-recovery lo resuelve al reiniciar. Si persiste: borrar crypto.db | | `olm account not marked as shared` | Crypto store inconsistente | Auto-recovery lo resuelve al reiniciar. Si persiste: borrar crypto.db |
| `"Encrypted by device not verified"` | Falta cross-signing | Ejecutar `cmd/verify` | | `"Encrypted by device not verified"` | Falta cross-signing | Ejecutar `cmd/verify` con `--store` y `--pickle-key` del agente, guardar recovery key en `.env` |
| `cross-signing private keys not available` | Recovery key no configurada | Ejecutar `cmd/verify`, guardar recovery key, configurar `recovery_key_env` |
| `verify recovery key: invalid` | Recovery key incorrecta | Re-ejecutar `cmd/verify` para generar nueva recovery key |
| Bot no responde | Reglas no matchean | Verificar que hay regla catch-all para DMs/mentions | | Bot no responde | Reglas no matchean | Verificar que hay regla catch-all para DMs/mentions |
| `no rules registered for agent` | ID no está en `rulesRegistry` | Añadir en `cmd/launcher/main.go` | | `no rules registered for agent` | ID no está en `rulesRegistry` | Añadir en `cmd/launcher/main.go` |
| Bot muere al arrancar | Revisar logs | `tail -f run/<id>.log` | | Bot muere al arrancar | Revisar logs | `tail -f run/<id>.log` |
+5 -4
View File
@@ -169,10 +169,11 @@ type MatrixCfg struct {
} }
type EncryptionCfg struct { type EncryptionCfg struct {
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled"`
StorePath string `yaml:"store_path"` StorePath string `yaml:"store_path"`
PickleKeyEnv string `yaml:"pickle_key_env"` // env var with hex-encoded 32-byte key PickleKeyEnv string `yaml:"pickle_key_env"` // env var with hex-encoded 32-byte key
TrustMode string `yaml:"trust_mode"` // tofu | cross-signing | manual TrustMode string `yaml:"trust_mode"` // tofu | cross-signing | manual
RecoveryKeyEnv string `yaml:"recovery_key_env"` // env var with base58 SSSS recovery key for cross-signing
} }
type RoomsCfg struct { type RoomsCfg struct {
+61
View File
@@ -15,6 +15,7 @@ import (
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto" "maunium.net/go/mautrix/crypto"
"maunium.net/go/mautrix/crypto/cryptohelper" "maunium.net/go/mautrix/crypto/cryptohelper"
"maunium.net/go/mautrix/crypto/ssss"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
@@ -108,6 +109,66 @@ func (c *Client) InitCrypto(ctx context.Context, storePath, pickleKeyHex, agentI
return closer, nil return closer, nil
} }
// ssssKeyFetcher abstracts the SSSS + cross-signing key retrieval for testing.
type ssssKeyFetcher interface {
GetDefaultKeyData(ctx context.Context) (string, ssssKeyVerifier, error)
FetchCrossSigningKeysFromSSSS(ctx context.Context, key *ssss.Key) error
}
// ssssKeyVerifier abstracts the SSSS key metadata verification.
type ssssKeyVerifier interface {
VerifyRecoveryKey(keyID, recoveryKey string) (*ssss.Key, error)
}
// olmSSSSFetcher adapts *crypto.OlmMachine to the ssssKeyFetcher interface.
type olmSSSSFetcher struct {
machine *crypto.OlmMachine
}
func (o *olmSSSSFetcher) GetDefaultKeyData(ctx context.Context) (string, ssssKeyVerifier, error) {
keyID, keyData, err := o.machine.SSSS.GetDefaultKeyData(ctx)
return keyID, keyData, err
}
func (o *olmSSSSFetcher) FetchCrossSigningKeysFromSSSS(ctx context.Context, key *ssss.Key) error {
return o.machine.FetchCrossSigningKeysFromSSSS(ctx, key)
}
// FetchCrossSigningKeys retrieves cross-signing private keys from SSSS
// (server-side secret storage) using the given base58 recovery key.
// This allows the agent to sign its own device, eliminating the
// "Encrypted by a device not verified by its owner" warning.
func (c *Client) FetchCrossSigningKeys(ctx context.Context, recoveryKey string) error {
wrapper, ok := c.raw.Crypto.(*mautrixCryptoWrapper)
if !ok || wrapper == nil {
return fmt.Errorf("crypto not initialized")
}
machine := wrapper.Machine()
if machine == nil {
return fmt.Errorf("olm machine not available")
}
return fetchCrossSigningKeysCore(ctx, &olmSSSSFetcher{machine}, recoveryKey)
}
// fetchCrossSigningKeysCore contains the testable logic for SSSS key retrieval.
func fetchCrossSigningKeysCore(ctx context.Context, fetcher ssssKeyFetcher, recoveryKey string) error {
keyID, keyData, err := fetcher.GetDefaultKeyData(ctx)
if err != nil {
return fmt.Errorf("get SSSS default key: %w", err)
}
key, err := keyData.VerifyRecoveryKey(keyID, recoveryKey)
if err != nil {
return fmt.Errorf("verify recovery key: %w", err)
}
if err := fetcher.FetchCrossSigningKeysFromSSSS(ctx, key); err != nil {
return fmt.Errorf("fetch cross-signing keys from SSSS: %w", err)
}
return nil
}
// initCryptoCore contains the testable logic: pickle key resolution, store // initCryptoCore contains the testable logic: pickle key resolution, store
// creation, and auto-recovery on stale crypto.db. Returns (closer, helper, err). // creation, and auto-recovery on stale crypto.db. Returns (closer, helper, err).
func initCryptoCore(ctx context.Context, storePath, pickleKeyHex, accessToken, agentID string, initer cryptoIniter, logger *slog.Logger) (io.Closer, cryptoHelper, error) { func initCryptoCore(ctx context.Context, storePath, pickleKeyHex, accessToken, agentID string, initer cryptoIniter, logger *slog.Logger) (io.Closer, cryptoHelper, error) {
+84
View File
@@ -11,6 +11,7 @@ import (
"testing" "testing"
"maunium.net/go/mautrix/crypto" "maunium.net/go/mautrix/crypto"
"maunium.net/go/mautrix/crypto/ssss"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
) )
@@ -400,3 +401,86 @@ func TestLogCryptoDiagnosticsCore_FullHappyPath(t *testing.T) {
t.Error("expected private keys log") t.Error("expected private keys log")
} }
} }
// --- SSSS key fetcher fakes for testing fetchCrossSigningKeysCore ---
type fakeSSSSKeyVerifier struct {
key *ssss.Key
err error
}
func (f *fakeSSSSKeyVerifier) VerifyRecoveryKey(keyID, recoveryKey string) (*ssss.Key, error) {
return f.key, f.err
}
type fakeSSSSKeyFetcher struct {
keyID string
verifier ssssKeyVerifier
getErr error
fetchErr error
}
func (f *fakeSSSSKeyFetcher) GetDefaultKeyData(ctx context.Context) (string, ssssKeyVerifier, error) {
return f.keyID, f.verifier, f.getErr
}
func (f *fakeSSSSKeyFetcher) FetchCrossSigningKeysFromSSSS(ctx context.Context, key *ssss.Key) error {
return f.fetchErr
}
func TestFetchCrossSigningKeysCore_Success(t *testing.T) {
fetcher := &fakeSSSSKeyFetcher{
keyID: "key1",
verifier: &fakeSSSSKeyVerifier{key: &ssss.Key{ID: "key1"}},
}
err := fetchCrossSigningKeysCore(context.Background(), fetcher, "valid-recovery-key")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func TestFetchCrossSigningKeysCore_GetDefaultKeyFails(t *testing.T) {
fetcher := &fakeSSSSKeyFetcher{
getErr: errors.New("no default key"),
}
err := fetchCrossSigningKeysCore(context.Background(), fetcher, "any-key")
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(err.Error(), "get SSSS default key") {
t.Errorf("unexpected error: %v", err)
}
}
func TestFetchCrossSigningKeysCore_VerifyRecoveryKeyFails(t *testing.T) {
fetcher := &fakeSSSSKeyFetcher{
keyID: "key1",
verifier: &fakeSSSSKeyVerifier{err: errors.New("invalid recovery key")},
}
err := fetchCrossSigningKeysCore(context.Background(), fetcher, "bad-key")
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(err.Error(), "verify recovery key") {
t.Errorf("unexpected error: %v", err)
}
}
func TestFetchCrossSigningKeysCore_FetchFromSSSSFails(t *testing.T) {
fetcher := &fakeSSSSKeyFetcher{
keyID: "key1",
verifier: &fakeSSSSKeyVerifier{key: &ssss.Key{ID: "key1"}},
fetchErr: errors.New("decryption failed"),
}
err := fetchCrossSigningKeysCore(context.Background(), fetcher, "valid-key")
if err == nil {
t.Fatal("expected error")
}
if !strings.Contains(err.Error(), "fetch cross-signing keys from SSSS") {
t.Errorf("unexpected error: %v", err)
}
}