sipeed/picoclaw · error

credential: keygen: ed25519 key generation failed: %w

Error message

credential: keygen: ed25519 key generation failed: %w

What it means

ed25519.GenerateKey(rand.Reader) failed during key generation. Ed25519 keygen's only failure mode is the randomness reader erroring, so this is the keygen twin of the crypto/rand failures seen in Encrypt: the OS entropy source was unavailable at the moment the private key was being produced.

Source

Thrown at pkg/credential/keygen.go:35

	home, err := os.UserHomeDir()
	if err != nil {
		return "", fmt.Errorf("credential: cannot determine home directory: %w", err)
	}
	return filepath.Join(home, ".ssh", "picoclaw_ed25519.key"), nil
}

// GenerateSSHKey generates an Ed25519 SSH key pair and writes the private key
// to path (permissions 0600) and the public key to path+".pub" (permissions 0644).
// The ~/.ssh/ directory is created with 0700 if it does not exist.
// If the files already exist they are overwritten.
func GenerateSSHKey(path string) error {
	if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
		return fmt.Errorf("credential: keygen: cannot create directory %q: %w", filepath.Dir(path), err)
	}

	pubRaw, privRaw, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		return fmt.Errorf("credential: keygen: ed25519 key generation failed: %w", err)
	}

	// Marshal private key as OpenSSH PEM.
	block, err := ssh.MarshalPrivateKey(privRaw, "")
	if err != nil {
		return fmt.Errorf("credential: keygen: marshal private key: %w", err)
	}
	privPEM := pem.EncodeToMemory(block)

	if err = os.WriteFile(path, privPEM, 0o600); err != nil {
		return fmt.Errorf("credential: keygen: write private key %q: %w", path, err)
	}

	// Marshal public key as authorized_keys line.
	sshPub, err := ssh.NewPublicKey(pubRaw)
	if err != nil {
		return fmt.Errorf("credential: keygen: marshal public key: %w", err)
	}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Verify randomness availability inside the same environment (`head -c 32 /dev/urandom` and a Go crypto/rand probe)
  2. Allow the getrandom syscall in the container's seccomp profile or update the runtime
  3. Check fd limits (`ulimit -n`) if /dev/urandom fallback is involved
  4. Retry key generation after a short delay - entropy starvation at boot resolves itself

Example fix

// before: one-shot keygen
if err := credential.GenerateSSHKey(path); err != nil {
    log.Fatal(err)
}

// after: bounded retry for transient entropy starvation
var genErr error
for i := 0; i < 3; i++ {
    if genErr = credential.GenerateSSHKey(path); genErr == nil {
        break
    }
    time.Sleep(500 * time.Millisecond)
}
if genErr != nil {
    log.Fatal(genErr)
}
Defensive patterns

Strategy: retry

Validate before calling

// Same entropy probe as for Encrypt - shared root cause.
func randOK() bool {
    b := make([]byte, 32)
    _, err := io.ReadFull(rand.Reader, b)
    return err == nil
}

Try / catch

err := credential.GenerateSSHKey(path)
for i := 0; i < 3 && err != nil && strings.Contains(err.Error(), "ed25519 key generation failed"); i++ {
    time.Sleep(500 * time.Millisecond)
    err = credential.GenerateSSHKey(path)
}

Prevention

When it happens

Trigger: GenerateSSHKey(path) when crypto/rand.Reader errors: getrandom(2) blocked by container seccomp policy, early-boot entropy not initialized, old kernel without getrandom and no readable /dev/urandom, or fd exhaustion.

Common situations: Same class as the Encrypt nonce failure: hardened containers, gVisor/old runtimes, very early boot, exotic sandboxes. Rarely seen on normal modern Linux/macOS hosts.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/a5cfba25cbf2069c. Report an issue: GitHub.