tailscale/tailscale · critical

public key has incorrect length for an Ed25519 public key

Error message

public key has incorrect length for an Ed25519 public key

What it means

Thrown by parsePublicKey when a PEM block of the correct type decodes successfully but its payload is not exactly ed25519.PublicKeySize (32) bytes. distsign keys are raw Ed25519 keys wrapped in PEM, not DER/PKIX structures, so any other length means the material is not a raw Ed25519 public key.

Source

Thrown at clientupdate/distsign/distsign.go:462

	if err != nil {
		return nil, err
	}
	if len(rest) > 0 {
		return nil, errors.New("trailing PEM data")
	}
	return pub, err
}

func parsePublicKey(data []byte, typeTag string) (pub ed25519.PublicKey, rest []byte, retErr error) {
	b, rest := pem.Decode(data)
	if b == nil {
		return nil, nil, errors.New("failed to decode PEM data")
	}
	if b.Type != typeTag {
		return nil, nil, fmt.Errorf("PEM type is %q, want %q", b.Type, typeTag)
	}
	if len(b.Bytes) != ed25519.PublicKeySize {
		return nil, nil, errors.New("public key has incorrect length for an Ed25519 public key")
	}
	return ed25519.PublicKey(b.Bytes), rest, nil
}

// VerifyAny verifies whether sig is valid for msg using any of the keys.
// VerifyAny will panic if any of the keys have the wrong size for Ed25519.
func VerifyAny(keys []ed25519.PublicKey, msg, sig []byte) bool {
	for _, k := range keys {
		if ed25519consensus.Verify(k, msg, sig) {
			return true
		}
	}
	return false
}

View on GitHub (pinned to cfe32b8be6)

Solutions

  1. Regenerate the key material with distsign's own GenerateRootKey/GenerateSigningKey, which produce the exact expected PEM format
  2. If the source is openssl, extract the raw 32-byte public key (e.g. 'openssl pkey -pubin -in key.pub -outform DER | tail -c 32') and wrap it in the correct PEM block
  3. Verify the payload length is 32 bytes and the PEM Type matches the expected tag before parsing
  4. Treat a length mismatch on a fetched server bundle as possible tampering and investigate rather than silently regenerating

Example fix

# before
# key.pub is PKIX DER inside PEM -> 'public key has incorrect length for an Ed25519 public key'
$ openssl pkey -pubin -in key.pub -outform DER | tail -c 32 > raw32.pub

# after: wrap the raw 32 bytes with the expected PEM type header
$ { printf -- '-----BEGIN TAILSCALE SIGNING PUBLIC KEY-----\n'; base64 raw32.pub; printf -- '-----END TAILSCALE SIGNING PUBLIC KEY-----\n'; } > distsign.pub
Defensive patterns

Strategy: validation

Validate before calling

// Verify a PEM block is a raw Ed25519 public key before parsing.
func isRawEd25519PublicPEM(data []byte, tag string) bool {
    b, _ := pem.Decode(data)
    return b != nil && b.Type == tag && len(b.Bytes) == ed25519.PublicKeySize // 32
}

Try / catch

if _, err := distsign.ParseSigningKeyBundle(bundle); err != nil {
    if strings.Contains(err.Error(), "incorrect length for an Ed25519 public key") {
        // wrong key material (private seed, PKIX, or other algorithm) — or tampering.
        // Do NOT regenerate keys fetched from a server: investigate the source.
    }
    return err
}

Prevention

When it happens

Trigger: Embedding an Ed25519 private key or 64-byte private key blob in a PUBLIC KEY PEM block; embedding a PKIX/DER-encoded public key (typically 44 bytes) instead of the raw 32-byte key; using a key from a different algorithm (RSA, P-256) in the same PEM type tag; corrupted key bytes.

Common situations: During key rotation someone published the seed/private key PEM with the public type header; openssl-generated keys (SPKI/PKIX format) were pasted where distsign expects its custom raw format; key file truncated or mutated in transit.

Related errors


AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15). Data as JSON: /api/errors/a5c671a5bea75074. Report an issue: GitHub.