netbirdio/netbird · error

failed to parse revocation list: %w

Error message

failed to parse revocation list: %w

What it means

reposign.ParseRevocationList failed on the optional revocation file. Beyond JSON unmarshalling, it enforces that last_updated and expires_at are present and non-zero, and that every key in the revoked map is a valid 16-hex-char KeyID (reposign/revocation.go:39-87). A missing field, a hand-edited key ID, or passing the .sig file fails.

Source

Thrown at client/cmd/signer/artifactsign.go:243

		return fmt.Errorf("read signature file: %w", err)
	}

	signature, err := reposign.ParseSignature(sigBytes)
	if err != nil {
		return fmt.Errorf("failed to parse signature: %w", err)
	}

	// Read optional revocation list
	var revocationList *reposign.RevocationList
	if revocationFile != "" {
		revData, err := os.ReadFile(revocationFile)
		if err != nil {
			return fmt.Errorf("read revocation file: %w", err)
		}

		revocationList, err = reposign.ParseRevocationList(revData)
		if err != nil {
			return fmt.Errorf("failed to parse revocation list: %w", err)
		}
	}

	// Validate artifact key(s)
	validKeys, err := reposign.ValidateArtifactKeys(rootPublicKeys, artifactKeyData, *signature, revocationList)
	if err != nil {
		return fmt.Errorf("artifact key verification failed: %w", err)
	}

	cmd.Println("✅ Artifact key(s) verified successfully")
	cmd.Printf("Signed by root key: %s\n", signature.KeyID)
	cmd.Printf("Signature timestamp: %s\n", signature.Timestamp.Format("2006-01-02 15:04:05 MST"))
	cmd.Printf("\nValid artifact keys (%d):\n", len(validKeys))
	for i, key := range validKeys {
		cmd.Printf("  [%d] Key ID: %s\n", i+1, key.Metadata.ID)
		cmd.Printf("      Created: %s\n", key.Metadata.CreatedAt.Format("2006-01-02 15:04:05 MST"))
		if !key.Metadata.ExpiresAt.IsZero() {
			cmd.Printf("      Expires: %s\n", key.Metadata.ExpiresAt.Format("2006-01-02 15:04:05 MST"))

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Validate shape: jq '.revoked, .last_updated, .expires_at' <file> must all be present, and revoked keys must be 16-char hex strings
  2. Only produce lists via `signer create-revocation-list` / `signer extend-revocation-list`
  3. Confirm you passed the list JSON, not its .sig
  4. Re-download the file if the download may be truncated

Example fix

// before (hand-edited list missing expires_at)
{"revoked":{"deadbeefdeadbeef":"2026-01-01T00:00:00Z"}}

// after (created by the signer, all fields present)
./signer create-revocation-list --revocation-list-file revocation-list.json --private-root-key root-key.pem
Defensive patterns

Strategy: validation

Validate before calling

var probe struct {
    Revoked     map[string]time.Time `json:"revoked"`
    LastUpdated time.Time            `json:"last_updated"`
    ExpiresAt   time.Time            `json:"expires_at"`
}
if err := json.Unmarshal(revData, &probe); err != nil || probe.LastUpdated.IsZero() || probe.ExpiresAt.IsZero() {
    log.Fatalf("revocation list malformed: %v", err)
}

Prevention

When it happens

Trigger: Hand-written or trimmed revocation JSON missing expires_at or last_updated; a revoked map key that is not 16 hex chars; passing revocation-list.json.sig to --revocation-file; malformed JSON from a partial download.

Common situations: Ops edits the revocation list directly instead of using create/extend-revocation-list; the .sig and the list get swapped in scripts.

Understand the failure class

Related errors


AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16). Data as JSON: /api/errors/eabb98f35257bdb8. Report an issue: GitHub.