Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion src/Ceremony/RelyingParty.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ final class RelyingParty
*/
public const int MAX_CREDENTIAL_ID_LENGTH = 1023;

/**
* The {@link https://w3c.github.io/webauthn/#credential-id credential ID definition} requires
* authenticators to emit at least 16 bytes (with ≥100 bits of entropy). §7.1 does not mandate
* this check on the relying party, but a shorter ID can only come from a non-conforming or
* malicious authenticator, so it is rejected fail-closed.
*/
public const int MIN_CREDENTIAL_ID_LENGTH = 16;

private const string FMT_NONE = 'none';
private const string FMT_PACKED = 'packed';

Expand Down Expand Up @@ -163,7 +171,8 @@ private function doVerifyRegistration(
// an X.509 trust path are rejected fail-closed until the attestation layer lands.
$attestationType = $this->verifyAttestationStatement($attestationObject, $publicKey, $response->clientDataJSON);

// §7.1 step 25: credential ids are bounded at 1023 bytes.
// §7.1 step 25: credential ids are bounded at 1023 bytes. The 16-byte floor comes from the
// credential ID definition (an authenticator obligation, not a §7.1 step) — see the constant.
$credentialId = $attestedCredentialData->credentialId;

if (strlen($credentialId) > self::MAX_CREDENTIAL_ID_LENGTH) {
Expand All @@ -173,6 +182,13 @@ private function doVerifyRegistration(
);
}

if (strlen($credentialId) < self::MIN_CREDENTIAL_ID_LENGTH) {
throw new VerificationException(
VerificationException::CREDENTIAL_ID_TOO_SHORT,
'Credential ID is shorter than ' . self::MIN_CREDENTIAL_ID_LENGTH . ' bytes',
);
}

// §7.1 step 26: the credential id must not already be registered for any user.
if ($store->findCredentialByCredentialId($credentialId) !== null) {
throw new VerificationException(
Expand Down
1 change: 1 addition & 0 deletions src/Ceremony/VerificationException.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ final class VerificationException extends RuntimeException
public const string MISSING_ATTESTED_CREDENTIAL_DATA = 'missing_attested_credential_data';
public const string UNSUPPORTED_ALGORITHM = 'unsupported_algorithm';
public const string CREDENTIAL_ID_TOO_LONG = 'credential_id_too_long';
public const string CREDENTIAL_ID_TOO_SHORT = 'credential_id_too_short';
public const string CREDENTIAL_ALREADY_REGISTERED = 'credential_already_registered';
public const string CREDENTIAL_NOT_ALLOWED = 'credential_not_allowed';
public const string UNKNOWN_CREDENTIAL = 'unknown_credential';
Expand Down
8 changes: 8 additions & 0 deletions tests/Ceremony/RelyingPartyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,14 @@ public function testRegistrationRejectsOversizedCredentialId(): void
);
}

public function testRegistrationRejectsUndersizedCredentialId(): void
{
$this->assertRegistrationFails(
VerificationException::CREDENTIAL_ID_TOO_SHORT,
self::registrationCredential($this->coseEntries, credentialId: str_repeat("\x2a", 15)),
);
}

public function testRegistrationRejectsAlreadyRegisteredCredential(): void
{
$store = self::storeWith($this->registeredRecord());
Expand Down