tailscale/tailscale · error

no signing keys found in the bundle

Error message

no signing keys found in the bundle

What it means

Thrown by parsePublicKeyBundle when the input bundle contains zero PEM blocks of the expected type. The distsign package (used by tailscale update to verify downloadable binaries) expects a concatenation of PEM-encoded Ed25519 public keys, e.g. the distsign.pub bundle fetched from the download server or validated by RootKey.SignSigningKeys. An empty or whitespace-only input parses to zero keys and is rejected because there would be nothing to trust.

Source

Thrown at clientupdate/distsign/distsign.go:437

}

// ParseRootKeyBundle parses the bundle of PEM-encoded public root keys.
func ParseRootKeyBundle(bundle []byte) ([]ed25519.PublicKey, error) {
	return parsePublicKeyBundle(bundle, pemTypeRootPublic)
}

func parsePublicKeyBundle(bundle []byte, typeTag string) ([]ed25519.PublicKey, error) {
	var keys []ed25519.PublicKey
	for len(bundle) > 0 {
		pub, rest, err := parsePublicKey(bundle, typeTag)
		if err != nil {
			return nil, err
		}
		keys = append(keys, pub)
		bundle = rest
	}
	if len(keys) == 0 {
		return nil, errors.New("no signing keys found in the bundle")
	}
	return keys, nil
}

func parseSinglePublicKey(data []byte, typeTag string) (ed25519.PublicKey, error) {
	pub, rest, err := parsePublicKey(data, typeTag)
	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 {

View on GitHub (pinned to cfe32b8be6)

Solutions

  1. Print/inspect the bytes passed to the parser (len and first lines) to confirm the bundle is empty
  2. Fix the source: point the client at the correct distsign.pub URL or regenerate the bundle by concatenating the PEM outputs of GenerateSigningKey for each signing key
  3. If publishing manually, verify the file on the server is non-empty and starts with '-----BEGIN' before shipping
  4. Re-run the download/verification once the server serves a valid bundle

Example fix

// before
bundle, err := os.ReadFile("distsign.pub")
if err != nil { return err }
keys, err := distsign.ParseSigningKeyBundle(bundle) // errors: no signing keys found

// after
bundle, err := os.ReadFile("distsign.pub")
if err != nil { return err }
if len(bytes.TrimSpace(bundle)) == 0 {
    return fmt.Errorf("distsign.pub is empty; rebuild/republish the key bundle")
}
keys, err := distsign.ParseSigningKeyBundle(bundle)
Defensive patterns

Strategy: validation

Validate before calling

// Pre-check a key bundle before handing it to distsign.
func validBundle(bundle []byte, tag string) bool {
    rest := bundle
    n := 0
    for {
        var b *pem.Block
        b, rest = pem.Decode(rest)
        if b == nil {
            return n > 0 && len(rest) == 0
        }
        if b.Type != tag {
            return false
        }
        n++
    }
}

Try / catch

keys, err := distsign.ParseSigningKeyBundle(data)
if err != nil {
    if strings.Contains(err.Error(), "no signing keys found") {
        // empty/blank bundle: fix the source file or URL, do not retry blindly
    }
    return fmt.Errorf("parsing signing key bundle: %w", err)
}

Prevention

When it happens

Trigger: Calling distsign.ParseSigningKeyBundle / ParseRootKeyBundle (or RootKey.SignSigningKeys, which internally validates the bundle) with an empty []byte or data containing no PEM blocks; serving an empty distsign.pub file from the download server that the client fetches before verifying a download.

Common situations: Download server returns HTTP 200 with an empty body for the key-bundle URL; the bundle file was truncated or created empty during a release-pipeline failure; a wrong URL/path serves blank content; CI passes an unset environment variable holding the bundle.

Related errors


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