FiloSottile/age · error

mismatched private and public SSH key

Error message

mismatched private and public SSH key

What it means

This error is returned by age's SSH identity Unwrap when decrypting an age-encrypted file with an SSH private key. After successfully decrypting the file key, the library compares the public key derived from the provided private key against the public key recorded in the age header stanza. A mismatch means the private key supplied does not correspond to the SSH public key the file was encrypted to, so the decrypted key cannot be trusted and is discarded.

Source

Thrown at agessh/encrypted_keys.go:132

	case *ed25519.PrivateKey:
		decrypted, err = NewEd25519Identity(*k)
		pubKey = k.Public().(ed25519.PublicKey)
	// ParseRawPrivateKey returns inconsistent types. See Issue 429.
	case ed25519.PrivateKey:
		decrypted, err = NewEd25519Identity(k)
		pubKey = k.Public().(ed25519.PublicKey)
	case *rsa.PrivateKey:
		decrypted, err = NewRSAIdentity(k)
		pubKey = &k.PublicKey
	default:
		return nil, fmt.Errorf("unexpected SSH key type: %T", k)
	}
	if err != nil {
		return nil, fmt.Errorf("invalid SSH key: %v", err)
	}

	if exp := i.pubKey.(ssh.CryptoPublicKey).CryptoPublicKey(); !pubKey.Equal(exp) {
		return nil, fmt.Errorf("mismatched private and public SSH key")
	}

	i.decrypted = decrypted
	return i.decrypted.Unwrap(stanzas)
}

View on GitHub (pinned to b74dce4cdb)

Solutions

  1. Locate the SSH public key recorded in the age file header stanza and supply the matching private key file to age.Decrypt
  2. Verify the pairing locally: run ssh-keygen -y -f <private-key-file> and compare output against the public key used for encryption
  3. List all candidate private keys (ssh.ParseRawPrivateKey on each) and pass them all in the []age.Identity slice so the matching one is found
  4. If the key was rotated, restore the original private key from backup or have the sender re-encrypt the file to the current public key

Example fix

// before
identity, _ := ssh.ParseRawPrivateKey(pemBytes)
recipient, _ := agessh.NewRSARecipient(identity.(*rsa.PublicKey))
// decrypting a file encrypted to a different key fails:
// "mismatched private and public SSH key"

// after
// pass the identity matching the public key in the file header
identity, err := ssh.ParseRawPrivateKey(correctPemBytes)
if err != nil { log.Fatal(err) }
i, _ := agessh.NewRSAIdentity(identity.(*rsa.PrivateKey))
out, err := age.Decrypt(ciphertext, []age.Identity{i})
Defensive patterns

Strategy: validation

Validate before calling

func validateSSHKeyPair(priv interface{ Public() crypto.PublicKey }) error {
	pub, ok := priv.(ssh.CryptoPublicKey)
	if !ok {
		return errors.New("key has no public part")
	}
	if pub.CryptoPublicKey() == nil {
		return errors.New("empty public key")
	}
	return nil
}

Type guard

func isSSHPrivateKey(k interface{ Public() crypto.PublicKey }) bool {
	_, ok := k.(ssh.CryptoPublicKey)
	return ok
}

Prevention

When it happens

Trigger: Calling age.Decrypt with an ssh.ParseRawPrivateKey-parsed private key whose derived public key (via CryptoPublicKey) does not equal the pubKey stored in the encrypted-SSH stanza; typically the wrong identity file was passed to the identity list.

Common situations: A user has multiple SSH keys and passes the wrong one (e.g. id_ed25519 instead of the id_rsa that was used to encrypt); key files were rotated or regenerated on the same filename; a deploy pipeline points at a default ~/.ssh/id_* key that differs from the intended one; the age file was encrypted to a teammate's public key.

Related errors


AI-assisted analysis of FiloSottile/age@b74dce4cdb (2026-08-31). Data as JSON: /api/errors/8eb30c39754ce7d1. Report an issue: GitHub.