Understanding SSH Public Key Formats: ssh-keygen .pub vs PEM File Conversion and Fingerprint Generation


2 views

The ssh-keygen tool generates public keys in OpenSSH's proprietary format, while PEM (Privacy Enhanced Mail) format follows X.509 standards. These formats serve different purposes in the SSH ecosystem:

# OpenSSH format (generated by ssh-keygen)
ssh-rsa AAAAB3NzaC1yc2EAAA... user@host

# PEM format (common in web servers)
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----

The ssh-keygen -l command expects the OpenSSH format because it needs to process the key data in a specific way to generate the fingerprint. When you feed it a PEM file, it fails because:

  • PEM files contain additional headers/footers
  • The base64 encoding structure differs
  • PEM may include certificate chains while OpenSSH expects raw public keys

To work with both formats interchangeably, you'll need conversion tools. Here are practical methods:

# Convert OpenSSH public key to PEM format
ssh-keygen -f id_rsa.pub -e -m PEM > id_rsa.pem

# Convert PEM to OpenSSH format
ssh-keygen -f id_rsa.pem -i -m PKCS8 > id_rsa_openssh.pub

# Extract fingerprint from PEM file (alternative method)
openssl rsa -in private_key.pem -pubout -outform der | openssl md5 -c

For different key formats, use these commands:

# For OpenSSH .pub files
ssh-keygen -lf id_rsa.pub

# For PEM certificates
openssl x509 -in certificate.pem -noout -fingerprint -sha256

# For raw RSA public keys
openssl rsa -in key.pem -pubin -outform der | openssl sha256 -binary | openssl base64

Here's how to handle conversions programmatically using Python's cryptography library:

from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend

# Load PEM file
with open("key.pem", "rb") as key_file:
    pem_key = serialization.load_pem_public_key(
        key_file.read(),
        backend=default_backend()
    )

# Convert to OpenSSH format
openssh_key = pem_key.public_bytes(
    encoding=serialization.Encoding.OpenSSH,
    format=serialization.PublicFormat.OpenSSH
)

print(openssh_key.decode('utf-8'))

When converting between formats:

  • Always verify the fingerprint matches after conversion
  • Never expose private keys during conversion processes
  • Use SHA-256 instead of MD5 for fingerprint generation when possible
  • Be aware that some older systems might still require MD5 fingerprints

When working with SSH keys, you'll encounter two primary formats for public keys:

# ssh-keygen format (RFC 4716)
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ... user@host

# PEM format (RFC 7468)
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArZN/ME3LC...
-----END PUBLIC KEY-----

The ssh-keygen generated .pub files use the RFC 4716 format, while PEM files follow RFC 7468. The main differences:

  • Structure: The .pub format includes the key type, base64-encoded key, and optional comment
  • Usage: SSH primarily uses the .pub format for authorized_keys
  • Compatibility: Most SSH tools expect the RFC 4716 format

To get the fingerprint of a PEM-encoded public key:

# Convert PEM to SSH public key format
openssl rsa -in id_rsa -pubout -outform PEM | ssh-keygen -f /dev/stdin -i -m PKCS8

# Get fingerprint directly from PEM
openssl pkey -in public.pem -pubin -outform der | openssl md5 -c

Example conversion workflow:

# Generate a new key pair
openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -pubout -out public.pem

# Convert PEM to SSH format
ssh-keygen -i -m PKCS8 -f public.pem > public_ssh.pub

# Verify fingerprints match
openssl pkey -in public.pem -pubin -outform der | openssl md5 -c
ssh-keygen -l -f public_ssh.pub

If you encounter "not a public key file" errors:

  • Ensure your PEM file contains the public key (not private)
  • Verify the PEM headers are correct (BEGIN/END PUBLIC KEY)
  • Try explicitly specifying the input format: ssh-keygen -i -m PEM -f key.pem

Python example to extract fingerprints from both formats:

import hashlib
import base64
import re

def ssh_pubkey_fingerprint(pubkey):
    # For standard ssh pubkey format
    key_type, b64_data, _ = pubkey.split()
    key_bytes = base64.b64decode(b64_data)
    return hashlib.md5(key_bytes).hexdigest()

def pem_fingerprint(pem_data):
    # For PEM format
    b64_data = "".join(pem_data.splitlines()[1:-1])
    key_bytes = base64.b64decode(b64_data)
    return hashlib.md5(key_bytes).hexdigest()