OpenSSH CA Wire Specification
Muljax ID implements a native, zero-dependency OpenSSH certificate binary serialization and deserialization engine in apps/api/src/lib/ssh/. This document is an exhaustive engineering deep dive into the exact binary wire layout, endianness, cryptographic signing boundaries, and host verification pipelines conforming strictly to OpenSSH’s certificate specification (IETF draft-miller-ssh-cert) and RFC 4251.
1. Governing Protocol Standards
Section titled “1. Governing Protocol Standards”The OpenSSH certificate implementation adheres strictly to four foundational standards:
- RFC 4251: The Secure Shell (SSH) Protocol Architecture (§ 5: Data Type Representations). Defines the canonical network byte order, variable-length byte arrays, and string serialization formats.
- OpenSSH Certificate Specification (IETF draft-miller-ssh-cert): OpenSSH certificate format version 1 (
CERT01), covering user/host key certificates, critical options, extensions, and signature wrappers (see also OpenSSHPROTOCOL§ 1.3). - RFC 8032: Edwards-Curve Digital Signature Algorithm (Ed25519). Defines the pure Ed25519 signature algorithm and 32-byte point serialization used across all user keys and CA signatures.
- PROTOCOL.krl: OpenSSH Key Revocation List (KRL) binary format specification. Defines the wire format for KRL sections, including revoked keys, certificates, key IDs, and serial-number ranges.
2. Binary Wire Types & Endianness (RFC 4251 § 5)
Section titled “2. Binary Wire Types & Endianness (RFC 4251 § 5)”All primitive data types are serialized in network byte order (big-endian). The implementation in apps/api/src/lib/ssh/wire.ts uses contiguous Uint8Array allocations and DataView operations without any intermediate string conversions:
| Wire Type | Wire Size | Memory Representation | Implementation Detail (SSHWriter) |
|---|---|---|---|
uint32 |
4 bytes | 32-bit unsigned integer | this.view.setUint32(this.offset, value, false) |
uint64 |
8 bytes | 64-bit unsigned bigint |
this.view.setBigUint64(this.offset, value, false) |
string |
4 + n bytes |
uint32 length prefix followed by UTF-8 bytes |
Prefixed length in bytes, followed by TextEncoder.encode(str) |
bytes |
4 + n bytes |
uint32 length prefix followed by raw octets |
Prefixed length in bytes, followed by Uint8Array payload |
raw |
n bytes |
Raw unadorned octets | Direct slice copy without length prefix (used to embed pre-built buffers) |
writeUint32(value: number): this { this.ensureCapacity(4); this.view.setUint32(this.offset, value, false); // false = big-endian this.offset += 4; return this;}
writeUint64(value: bigint): this { this.ensureCapacity(8); this.view.setBigUint64(this.offset, value, false); this.offset += 8; return this;}3. Certificate Binary Wire Layout
Section titled “3. Certificate Binary Wire Layout”For an Ed25519 user certificate (ssh-ed25519-cert-v01@openssh.com), the complete binary payload consists of two distinct segments:
- Pre-Signature Body (
toSign): Fields 1 through 13 concatenated sequentially. This exact byte stream is passed to the cryptographic signing algorithm. - Signature Block (
signature): Field 14, an encapsulating byte wrapper containing the signature algorithm name and the raw 64-byte Ed25519 signature.
flowchart TD
subgraph Certificate["OpenSSH Binary Certificate"]
subgraph toSign["Pre-Signature Body (toSign) - Fields 1 through 13"]
F1["1. certType (string)"]
F2["2. nonce (bytes)"]
F3["3. rawClientPublicKey (bytes)"]
F4["4. serial (uint64)"]
F5["5. type (uint32)"]
F6["6. keyId (string)"]
F7["7. principals (bytes blob)"]
F8["8. validAfter (uint64)"]
F9["9. validBefore (uint64)"]
F10["10. criticalOptions (bytes blob)"]
F11["11. extensions (bytes blob)"]
F12["12. reserved (bytes blob)"]
F13["13. caPublicWire (bytes blob)"]
end
subgraph sigBlock["Field 14: Signature Block (bytes)"]
F14A["14a. sigAlgorithm (string)"]
F14B["14b. rawSignature (bytes)"]
end
toSign -->|Signed by CA Ed25519 Key| sigBlock
end
Complete Field-by-Field Specification
Section titled “Complete Field-by-Field Specification”| # | Field Name | Wire Type | Length (Bytes) | Exact Description & Code Representation |
|---|---|---|---|---|
| 1 | certType |
string |
4 + 35 = 39 | Constant "ssh-ed25519-cert-v01@openssh.com". Defines the certificate key type. |
| 2 | nonce |
bytes |
4 + 16 = 20 | 16 cryptographically secure random bytes generated via crypto.getRandomValues(new Uint8Array(16)). Prevents replay and duplicate signature collisions. |
| 3 | pk |
bytes |
4 + 32 = 36 | The certified client’s 32-byte raw Ed25519 public key. Extracted from the user’s registered public key. |
| 4 | serial |
uint64 |
8 | Big-endian 64-bit monotonic unsigned integer. Generated via BigInt(Date.now()) * 65536n + BigInt(randBytes[0] % 65536). |
| 5 | type |
uint32 |
4 | 1 for SSH_CERT_TYPE_USER (2 for SSH_CERT_TYPE_HOST). Platform mints User certificates. |
| 6 | keyId |
string |
4 + k |
Human-readable identity string (e.g. "alice@example.com"). Logged by sshd upon server login. |
| 7 | principals |
bytes |
4 + p |
Length-prefixed wire buffer containing a sequence of string usernames (e.g. alice, ubuntu). |
| 8 | validAfter |
uint64 |
8 | UNIX epoch start timestamp in seconds. Set to nowSeconds - 60n (60-second backdate to tolerate client clock skew). |
| 9 | validBefore |
uint64 |
8 | UNIX epoch expiration timestamp in seconds. Set to nowSeconds + BigInt(ttlSeconds). |
| 10 | criticalOptions |
bytes |
4 + c |
Length-prefixed wire buffer of name-value string pairs. Empty in standard user certificates (0x00000000). |
| 11 | extensions |
bytes |
4 + e |
Length-prefixed wire buffer of standard OpenSSH capability flags with empty value bytes. |
| 12 | reserved |
bytes |
4 | Reserved for future protocol expansion. Always serialized as 4 zero bytes (0x00000000). |
| 13 | caPublicWire |
bytes |
4 + 51 = 55 | Length-prefixed wire buffer containing the CA’s public key: string "ssh-ed25519" (15 bytes) + bytes 32-byte raw CA public key (36 bytes). |
| 14 | signature |
bytes |
4 + 83 = 87 | Length-prefixed signature envelope containing: • sigAlgorithm: string "ssh-ed25519" (15 bytes)• rawSignature: bytes 64-byte Ed25519 signature (68 bytes) |
4. Complex Field Serialization Mechanics
Section titled “4. Complex Field Serialization Mechanics”Field 7: Principals Wire Buffer (principals)
Section titled “Field 7: Principals Wire Buffer (principals)”The principals field is not a simple string; it is a nested wire buffer prefixed by an outer uint32 byte count:
const principalsWriter = new SSHWriter();for (const principal of data.principals) { principalsWriter.writeString(principal); // 4-byte len + UTF-8 bytes}writer.writeBytes(principalsWriter.toUint8Array()); // Outer 4-byte len + inner blobIf the certificate authorizes principals ["alice", "admin"]:
principalsWriterencodes:00 00 00 05+"alice"(9 bytes)00 00 00 05+"admin"(9 bytes)- Total inner bytes = 18 bytes.
- The outer certificate writer writes
00 00 00 12(18 in hex) followed by the 18 bytes.
Field 11: Extensions Wire Buffer (extensions)
Section titled “Field 11: Extensions Wire Buffer (extensions)”OpenSSH extensions are structured as name/data pairs:
name:stringidentifying the extension.data:bytescontaining extension-specific data. For standard OpenSSH permission flags, this payload is empty (auint32of 0).
Muljax ID mints certificates with the five standard interactive extensions enabled by default:
permit-X11-forwardingpermit-agent-forwardingpermit-port-forwardingpermit-ptypermit-user-rc
Each extension is written as:
extWriter.writeString(extName); // 4-byte len + name UTF-8extWriter.writeBytes(new Uint8Array(0)); // 4-byte len of 0 (0x00000000)Field 14: Signature Envelope
Section titled “Field 14: Signature Envelope”The signature is generated over the entire unadorned toSign buffer (Fields 1–13):
// 1. Sign the pre-signature payloadconst signatureBuffer = await crypto.subtle.sign( { name: "Ed25519" }, ca.privateKey, unsignedPayload);const rawSignature = new Uint8Array(signatureBuffer); // Exactly 64 bytes
// 2. Build the inner signature blockconst sigBlockWriter = new SSHWriter();sigBlockWriter.writeString("ssh-ed25519"); // 4 + 11 = 15 bytessigBlockWriter.writeBytes(rawSignature); // 4 + 64 = 68 bytesconst sigBlock = sigBlockWriter.toUint8Array(); // Exactly 83 bytes
// 3. Append to the full wire certificate with outer length prefixfullWriter.writeRaw(toSign);fullWriter.writeBytes(sigBlock); // 4 bytes len (0x00000053) + 83 bytes5. Serial Number Generation & Skew Invariants
Section titled “5. Serial Number Generation & Skew Invariants”Monotonic Microsecond Serials
Section titled “Monotonic Microsecond Serials”Serial = (Date.now() * 65536) + (randBytes[0] % 65536)const randBytes = new Uint32Array(1);crypto.getRandomValues(randBytes);serial = BigInt(Date.now()) * 65536n + BigInt(randBytes[0] % 65536);Benefits:
- Strict Monotonicity: Certificates issued at later timestamps always have strictly higher serial numbers.
- Uniqueness Under Concurrency: Two certificates issued within the exact same millisecond have a
1 / 65536collision probability, preventing sequence collisions on distributed edge workers. - Direct Revocation Indexing: The 64-bit integer is stored directly as a indexed string in the
ssh_certificates.serialtable for instant lookups.
Clock Skew Tolerance
Section titled “Clock Skew Tolerance”To prevent certificates from being rejected due to minor time differences between Cloudflare edge workers and target Linux servers, validAfter is backdated by exactly 60 seconds:
const nowSeconds = BigInt(Math.floor(Date.now() / 1000));const validAfter = nowSeconds - 60n; // 60s clock skew windowconst validBefore = nowSeconds + BigInt(ttlSeconds);6. Single-Line File Encoding
Section titled “6. Single-Line File Encoding”When exported for user workstations, the binary certificate byte stream is base64-encoded and formatted into the standard single-line string representation conforming to OpenSSH file conventions:
ssh-ed25519-cert-v01@openssh.com AAAA...[base64-wire-bytes]... alice@example.com-certImplemented in apps/api/src/lib/ssh/certificate.ts:
export function formatOpenSshCertificate( certWireBytes: Uint8Array, comment?: string,): string { const b64 = base64Encode(certWireBytes); return comment ? `${ED25519_CERT_KEY_TYPE} ${b64} ${comment}\n` : `${ED25519_CERT_KEY_TYPE} ${b64}\n`;}7. Target Host Verification Pipeline (sshd)
Section titled “7. Target Host Verification Pipeline (sshd)”When an OpenSSH client connects to a server presenting an Ed25519 user certificate:
sequenceDiagram
autonumber
actor Client as SSH Client (ssh)
participant SSHD as Server Daemon (sshd)
participant CAKeys as /etc/ssh/trusted_user_ca.pub
participant KRL as /etc/ssh/revoked_keys
Client->>SSHD: SSH_MSG_USERAUTH_REQUEST (publickey, ssh-ed25519-cert-v01@openssh.com)
SSHD->>SSHD: 1. Unpack wire fields 1 through 14
SSHD->>CAKeys: 2. Match field 13 (caPublicWire) against TrustedUserCAKeys
SSHD->>SSHD: 3. Verify Ed25519 signature (field 14b) over fields 1–13 using CA key
SSHD->>SSHD: 4. Check validAfter <= current_time <= validBefore
SSHD->>SSHD: 5. Check requested login user exists in field 7 (principals)
SSHD->>KRL: 6. Check field 4 (serial) is not present in RevokedKeys
SSHD-->>Client: SSH_MSG_USERAUTH_SUCCESS
- Unpack Wire Fields:
sshdreads fields 1–14 using its internalsshkey_from_blobparser. - CA Trust Verification:
sshdextracts field 13 (caPublicWire) and verifies that the key exists in/etc/ssh/trusted_user_ca.pub(configured viaTrustedUserCAKeys). - Cryptographic Signature Verification:
sshdcomputescrypto_sign_ed25519_verify(sig, toSign, ca_pk)over fields 1 through 13. - Temporal Bounds Check: Verifies that the host system’s current time satisfies
validAfter <= now <= validBefore. - Principal Authorization: Verifies that the username being authenticated matches at least one entry in the certificate’s
principalslist. - Revocation Check (KRL): If
RevokedKeys /etc/ssh/revoked_keysis enabled insshd_config,sshdextracts the certificate’s 64-bitserialand confirms that it does not appear in the revocation list.
8. Key Revocation List (KRL) Wire Specification (PROTOCOL.krl)
Section titled “8. Key Revocation List (KRL) Wire Specification (PROTOCOL.krl)”OpenSSH’s native binary KRL format provides maximum space efficiency when revoking certificates by serial number across host fleets. Muljax ID builds binary KRL payloads conforming directly to OpenSSH PROTOCOL.krl via apps/api/src/lib/ssh/krl.ts.
KRL File Header Layout
Section titled “KRL File Header Layout”| Field | Wire Type | Length (Bytes) | Description |
|---|---|---|---|
magic |
uint64 |
8 | Constant 0x5353484b524c0a00ULL (ASCII "SSHKRL\n\0"). |
format_version |
uint32 |
4 | Fixed integer 1. |
krl_version |
uint64 |
8 | Monotonic version counter incremented upon list update. |
generated_date |
uint64 |
8 | UNIX timestamp in seconds when the KRL was generated. |
flags |
uint64 |
8 | Reserved bit flags (0x0000000000000000). |
reserved |
string |
4 | Length-prefixed string; empty in standard KRL (0x00000000). |
comment |
string |
4 + c |
Optional UTF-8 descriptive comment. |
Certificate Revocation Section (KRL_SECTION_CERTIFICATES = 1)
Section titled “Certificate Revocation Section (KRL_SECTION_CERTIFICATES = 1)”Following the 44-byte header, revoked certificates are grouped into certificate sections:
section_type(uint8): Fixed byte0x01(KRL_SECTION_CERTIFICATES).section_data(bytes blob): Length-prefixed inner payload containing:ca_key(string): Wire-serialized CA public key (or empty string for wildcard CA matching).reserved(string): Empty wire string (0x00000000).- Certificate Subsections:
cert_section_type(uint8):0x20(KRL_SECTION_CERT_SERIAL_LIST).cert_section_data(bytes blob): Sequence of 64-bit big-endian integers (uint64 serial) for each revoked certificate.