bitwarden/server · error · BadRequestException

WebAuthn prf keys must have public-key during rotation.

Error message

WebAuthn prf keys must have public-key during rotation.

What it means

Thrown by WebAuthnLoginKeyRotationValidator when a rotation entry matched a PRF-enabled credential (matched by Id) and has a non-null EncryptedUserKey, but its EncryptedPublicKey is null. The credential needs both the re-encrypted user key and public key to remain valid, so a null public key is rejected.

Source

Thrown at src/Api/KeyManagement/Validators/WebAuthnLoginKeyRotationValidator.cs:51

            return result;
        }

        foreach (var webAuthnCredential in validCredentials)
        {
            var keyToRotate = keysToRotate.FirstOrDefault(c => c.Id == webAuthnCredential.Id);
            if (keyToRotate == null)
            {
                throw new BadRequestException("All existing webauthn prf keys must be included in the rotation.");
            }

            if (keyToRotate.EncryptedUserKey == null)
            {
                throw new BadRequestException("WebAuthn prf keys must have user-key during rotation.");
            }

            if (keyToRotate.EncryptedPublicKey == null)
            {
                throw new BadRequestException("WebAuthn prf keys must have public-key during rotation.");
            }

            result.Add(keyToRotate.ToWebAuthnRotateKeyData());
        }

        return result;
    }
}

View on GitHub (pinned to e93b962371)

Solutions

  1. Re-encrypt and populate both EncryptedUserKey and EncryptedPublicKey for every PRF-enabled credential.
  2. Add a pre-send assertion that every key entry has non-null EncryptedUserKey and EncryptedPublicKey.
  3. If a credential cannot be fully re-encrypted, disable its PRF or remove the passkey before rotating.

Example fix

// before
{ id: c.id, encryptedUserKey: reencrypt(c.userKey) }

// after
{ id: c.id, encryptedUserKey: reencrypt(c.userKey), encryptedPublicKey: reencrypt(c.publicKey) }
Defensive patterns

Strategy: validation

Validate before calling

const missingPubKey = payload.keys.filter(k => k.encryptedPublicKey == null);
if (missingPubKey.length) {
  throw new Error(`WebAuthn keys missing public key: ${missingPubKey.map(k => k.id).join(', ')}`);
}

Type guard

function hasWebAuthnPublicKey(k: { encryptedPublicKey?: string | null }): boolean {
  return k.encryptedPublicKey != null && k.encryptedPublicKey.length > 0;
}

Try / catch

try {
  await api.rotateKey(payload);
} catch (e) {
  if (e.status === 400 && /must have public-key/i.test(e.message)) {
    payload.keys = payload.keys.map(k => ({ ...k, encryptedPublicKey: k.encryptedPublicKey ?? reencrypt(publicKeyFor(k.id)) }));
    return api.rotateKey(payload);
  }
  throw e;
}

Prevention

When it happens

Trigger: The keys array includes the credential Id and EncryptedUserKey but EncryptedPublicKey is null or was omitted during request construction.

Common situations: Client populated the user key but forgot the public key field; a copy/paste or template only carried EncryptedUserKey; serialization dropped the public-key field.

Related errors


AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13). Data as JSON: /api/errors/0b96364642f7c59d. Report an issue: GitHub.