netbirdio/netbird · critical

failed to verify signature of artifact keys

Error message

failed to verify signature of artifact keys

What it means

Returned by the reposign artifact-key verification path (client/internal/updater/reposign/artifact.go:185) when verifyAny fails to match the bundle signature against any of the trusted public root keys embedded in the client. This is the update supply-chain trust check: the artifact key bundle (data || little-endian timestamp) must verify against at least one root key, otherwise the artifact keys are rejected. The check fails closed by design.

Source

Thrown at client/internal/updater/reposign/artifact.go:185

	now := time.Now().UTC()
	if signature.Timestamp.After(now.Add(maxClockSkew)) {
		err := fmt.Errorf("signature timestamp is in the future: %v", signature.Timestamp)
		log.Debugf("artifact signature error: %v", err)
		return nil, err
	}
	if now.Sub(signature.Timestamp) > maxArtifactKeySignatureAge {
		err := fmt.Errorf("signature is too old: %v (created %v)", now.Sub(signature.Timestamp), signature.Timestamp)
		log.Debugf("artifact signature error: %v", err)
		return nil, err
	}

	// Reconstruct the signed message: artifact_key_data || timestamp
	msg := make([]byte, 0, len(data)+8)
	msg = append(msg, data...)
	msg = binary.LittleEndian.AppendUint64(msg, uint64(signature.Timestamp.Unix()))

	if !verifyAny(publicRootKeys, msg, signature.Signature) {
		return nil, errors.New("failed to verify signature of artifact keys")
	}

	pubKeys, err := parsePublicKeyBundle(data, tagArtifactPublic)
	if err != nil {
		log.Debugf("failed to parse public keys: %s", err)
		return nil, err
	}

	validKeys := make([]PublicKey, 0, len(pubKeys))
	for _, pubKey := range pubKeys {
		// Filter out expired keys
		if !pubKey.Metadata.ExpiresAt.IsZero() && now.After(pubKey.Metadata.ExpiresAt) {
			log.Debugf("Key %s is expired at %v (current time %v)",
				pubKey.Metadata.ID, pubKey.Metadata.ExpiresAt, now)
			continue
		}

		if revocationList != nil {

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Update the NetBird client to the latest release, which embeds the current set of trusted root keys.
  2. Re-download the update artifact from the official source and retry, to rule out corruption.
  3. If it persists on the latest client, treat it as a potential compromise: do not bypass the check, report it to the NetBird maintainers via the security policy.

Example fix

// before: catching the error and continuing with the update anyway
if _, err := reposign.VerifyArtifactKeys(data, sig); err != nil {
    log.Warnf("signature check failed, continuing: %v", err) // NEVER do this
}

// after: fail closed, abort the update
if _, err := reposign.VerifyArtifactKeys(data, sig); err != nil {
    return fmt.Errorf("refusing update, artifact key signature invalid: %w", err)
}
Defensive patterns

Strategy: try-catch

Try / catch

keys, err := reposign.VerifyArtifactKeys(data, sig, now)
if err != nil {
    // Fail closed: abort the update. Never downgrade this to a warning.
    return fmt.Errorf("update aborted, artifact keys untrusted: %w", err)
}

Prevention

When it happens

Trigger: Artifact keys were signed by a root key that postdates the root keys baked into this client build (key rotation); the downloaded keys file is corrupted or truncated so the signed bytes differ; the bundle was tampered with (MITM or compromised mirror); clock skew combined with the maxArtifactKeySignatureAge check is handled separately, so reaching this line means the signature itself is invalid.

Common situations: Running an old NetBird release after the project rotated its signing root keys; a proxied or cached download corrupting the artifact; an attacker-controlled update source. Any occurrence means the update must not proceed.

Related errors


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