Skip to content
Draft
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
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -695,10 +695,11 @@ There are additional notes in Best Practices / Data Handling around this.
| Name | [IANA ID](https://www.iana.org/assignments/cose/cose.xhtml#algorithms) | Supported | Notes |
| --- | --- | --- | --- |
| `ES256` | `-7` | ✅ | |
| `EdDSA` | `-8` | ❌ | |
| `ES384` | `-35` | ❌ | [^alg-needs-tests] |
| `ES512` | `-36` | ❌ | [^alg-needs-tests] |
| `RS256` | `-257` | ✅⚠️ | [^ext-vec] |
| `EdDSA` | `-8` | ✅ | Ed25519 |
| `ES384` | `-35` | ✅ | |
| `ES512` | `-36` | ✅ | |
| `Ed448` | `-53` | ✅ | |
| `RS256` | `-257` | ✅ | |

## Supported Identifiers

Expand All @@ -713,7 +714,7 @@ Due to an inability to generate responses with all formats, not all are supporte
| `android-safetynet` | ❌ | 8.5 | SafetyNet attestation has been [deprecated](https://android-developers.googleblog.com/2024/09/attestation-format-change-for-android-fido2-api.html), so this library will not be adding support. |
| `fido-u2f` | ✅ | 8.6 | YubiKeys and similar U2F stateless devices. |
| `none` | ✅ | 8.7 | Used by Apple in Safari when using Passkeys (even when direct attestation is requested) |
| `apple` | ✅⚠️ [^ext-vec], [^limited-trust-path] | 8.8 | Apple no longer appears to use this format, instead providing non-attested credentials (fmt=none). |
| `apple` | ✅⚠️ [^limited-trust-path] | 8.8 | Apple no longer appears to use this format, instead providing non-attested credentials (fmt=none). |
| `compound` | ❌ | 8.9 | This format only appears in the editor's draft of the spec and is not yet on the official registry. |

By default, the `$registration->verify()` process will reject uncertain trust paths.
Expand Down Expand Up @@ -749,6 +750,4 @@ General quickstart guide:
Intro to passkeys:
- https://developer.apple.com/videos/play/wwdc2021/10106/

[^ext-vec]: Support is based on [unofficial test vectors](https://github.com/w3c/webauthn/issues/1633).
[^limited-trust-path]: Handling of attestation trust path is limited.
[^alg-needs-tests]: This should be easy add support, but test vectors are needed
24 changes: 0 additions & 24 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -162,24 +162,6 @@ parameters:
count: 1
path: src/BinaryString.php

-
message: '#^Cannot access offset 1 on mixed\.$#'
identifier: offsetAccess.nonOffsetAccessible
count: 1
path: src/COSEKey.php

-
message: '#^Parameter \#1 \$decoded of static method Firehed\\WebAuthn\\PublicKey\\EllipticCurve\:\:fromDecodedCbor\(\) expects array\<mixed\>, mixed given\.$#'
identifier: argument.type
count: 1
path: src/COSEKey.php

-
message: '#^Parameter \#1 \$decoded of static method Firehed\\WebAuthn\\PublicKey\\RSA\:\:fromDecodedCbor\(\) expects array\<mixed\>, mixed given\.$#'
identifier: argument.type
count: 1
path: src/COSEKey.php

-
message: '#^Parameter \#1 \$value of static method Firehed\\WebAuthn\\COSE\\Algorithm\:\:from\(\) expects int\|string, mixed given\.$#'
identifier: argument.type
Expand All @@ -192,12 +174,6 @@ parameters:
count: 1
path: src/COSEKey.php

-
message: '#^Parameter \#2 \$array of function array_key_exists expects array, mixed given\.$#'
identifier: argument.type
count: 1
path: src/COSEKey.php

-
message: '#^PHPDoc tag @api has invalid value \(\(with the above caveats\)\)\: Unexpected token "the", expected ''\)'' at offset 518 on line 11$#'
identifier: phpDoc.parseError
Expand Down
13 changes: 9 additions & 4 deletions src/Attestations/Packed.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ public function verify(AuthenticatorData $data, BinaryString $clientDataHash): V

$acd = $data->getAttestedCredentialData();
$alg = COSE\Algorithm::tryFrom($this->data['alg']);
if ($alg !== $acd->coseKey->algorithm) {
throw new Exception('8.2/v3.a');
if ($alg === null) {
throw new Exception('8.2/v2: unknown algorithm');
}

if (array_key_exists('x5c', $this->data)) {
Expand All @@ -73,7 +73,7 @@ public function verify(AuthenticatorData $data, BinaryString $clientDataHash): V
$signedData->unwrap(),
$this->data['sig'],
$certPubKey,
OPENSSL_ALGO_SHA256,
$alg->getOpenSslAlgorithm(),
);

// Verify that `sig` is a valid signature over ...
Expand Down Expand Up @@ -144,13 +144,18 @@ public function verify(AuthenticatorData $data, BinaryString $clientDataHash): V
]);
} else {
// Self attestation in use
// §8.2/v4.a: alg must match credential key algorithm
if ($alg !== $acd->coseKey->algorithm) {
throw new Exception('8.2/v4.a');
}

$credentialPublicKey = $acd->coseKey->getPublicKey();

$result = openssl_verify(
$signedData->unwrap(),
$this->data['sig'],
$credentialPublicKey->getPemFormatted(),
OPENSSL_ALGO_SHA256,
$alg->getOpenSslAlgorithm(),
);

if ($result !== 1) {
Expand Down
23 changes: 20 additions & 3 deletions src/COSE/Algorithm.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,26 @@
enum Algorithm: int
{
case EcdsaSha256 = -7;
// case EcdsaSha384 = -35;
// case EcdsaSha512 = -36;
// section 8.2: EdDSA = -8;
case EcdsaSha384 = -35;
case EcdsaSha512 = -36;
case EdDSA = -8;
case Ed448 = -53;

case Rs256 = -257;

/**
* Returns the OpenSSL algorithm constant for signature verification.
*/
public function getOpenSslAlgorithm(): int
{
return match ($this) {
self::EcdsaSha256, self::Rs256 => \OPENSSL_ALGO_SHA256,
self::EcdsaSha384 => \OPENSSL_ALGO_SHA384,
self::EcdsaSha512 => \OPENSSL_ALGO_SHA512,
// EdDSA (Ed25519/Ed448) uses PureEdDSA which performs hashing
// internally. OpenSSL has no named constant for this; passing
// 0 tells openssl_verify to skip external digest computation.
self::EdDSA, self::Ed448 => 0,
};
}
}
27 changes: 25 additions & 2 deletions src/COSE/Curve.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,29 @@

case ED448 = 7; // OKP

// RFC 5480
// §2.1.1.1 OIDs for named curves
// EC curves: RFC 5480 §2.1.1.1
// EdDSA curves: RFC 8410 §3
public function getOid(): string
{
return match ($this) {
self::P256 => '1.2.840.10045.3.1.7',
self::P384 => '1.3.132.0.34',
self::P521 => '1.3.132.0.35',
self::ED25519 => '1.3.101.112',
self::ED448 => '1.3.101.113',
default => throw new UnhandledMatchError('Curve unsupported'),
};
}

/**
* Returns the coordinate size in bytes for this curve.
*/
public function getCoordinateSize(): int
{
return match ($this) {
self::P256 => 32,
self::P384 => 48,
self::P521 => 66,
default => throw new UnhandledMatchError('Curve unsupported'),
};
}
Expand All @@ -52,6 +69,8 @@
{
return match ($this) {
self::P256 => gmp_init('0xFFFFFFFF 00000001 00000000 00000000 00000000 FFFFFFFF FFFFFFFF FFFFFFFC'),
self::P384 => gmp_init('0xFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFFFF 00000000 00000000 FFFFFFFC'),

Check warning on line 72 in src/COSE/Curve.php

View workflow job for this annotation

GitHub Actions / PHPCS

Line exceeds 120 characters; contains 148 characters
self::P521 => gmp_init('0x01FF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFC'),

Check warning on line 73 in src/COSE/Curve.php

View workflow job for this annotation

GitHub Actions / PHPCS

Line exceeds 120 characters; contains 189 characters
default => throw new UnhandledMatchError('Curve unsupported'),
};
}
Expand All @@ -60,6 +79,8 @@
{
return match ($this) {
self::P256 => gmp_init('0x5AC635D8 AA3A93E7 B3EBBD55 769886BC 651D06B0 CC53B0F6 3BCE3C3E 27D2604B'),
self::P384 => gmp_init('0xB3312FA7 E23EE7E4 988E056B E3F82D19 181D9C6E FE814112 0314088F 5013875A C656398D 8A2ED19D 2A85C8ED D3EC2AEF'),

Check warning on line 82 in src/COSE/Curve.php

View workflow job for this annotation

GitHub Actions / PHPCS

Line exceeds 120 characters; contains 148 characters
self::P521 => gmp_init('0x0051 953EB961 8E1C9A1F 929A21A0 B68540EE A2DA725B 99B315F3 B8B48991 8EF109E1 56193951 EC7E937B 1652C0BD 3BB1BF07 3573DF88 3D2C34F1 EF451FD4 6B503F00'),

Check warning on line 83 in src/COSE/Curve.php

View workflow job for this annotation

GitHub Actions / PHPCS

Line exceeds 120 characters; contains 189 characters
default => throw new UnhandledMatchError('Curve unsupported'),
};
}
Expand All @@ -68,6 +89,8 @@
{
return match ($this) {
self::P256 => gmp_init('0xFFFFFFFF 00000001 00000000 00000000 00000000 FFFFFFFF FFFFFFFF FFFFFFFF'),
self::P384 => gmp_init('0xFFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFE FFFFFFFF 00000000 00000000 FFFFFFFF'),

Check warning on line 92 in src/COSE/Curve.php

View workflow job for this annotation

GitHub Actions / PHPCS

Line exceeds 120 characters; contains 148 characters
self::P521 => gmp_init('0x01FF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF'),

Check warning on line 93 in src/COSE/Curve.php

View workflow job for this annotation

GitHub Actions / PHPCS

Line exceeds 120 characters; contains 189 characters
default => throw new UnhandledMatchError('Curve unsupported'),
};
}
Expand Down
2 changes: 1 addition & 1 deletion src/COSE/KeyType.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
enum KeyType: int
{
// case Reserved = 0;
// case OctetKeyPair = 1;
case OctetKeyPair = 1;
case EllipticCurve = 2;
case Rsa = 3;
// case Symmetric = 4;
Expand Down
6 changes: 2 additions & 4 deletions src/COSEKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,21 @@
{
$decoder = new Decoder();
$decodedCbor = $decoder->decode($cbor->unwrap());
assert(is_array($decodedCbor));

// Note: these limitations may be lifted in the future
$keyType = COSE\KeyType::from($decodedCbor[self::INDEX_KEY_TYPE]);

$this->publicKey = match ($keyType) {
COSE\KeyType::EllipticCurve => PublicKey\EllipticCurve::fromDecodedCbor($decodedCbor),
COSE\KeyType::Rsa => PublicKey\RSA::fromDecodedCbor($decodedCbor),
// Other syntactially-valid key types exist, but the library
// doesn't handle them (yet?)
COSE\KeyType::OctetKeyPair => PublicKey\OctetKeyPair::fromDecodedCbor($decodedCbor),
};

assert(array_key_exists(self::INDEX_ALGORITHM, $decodedCbor));
$this->algorithm = COSE\Algorithm::from($decodedCbor[self::INDEX_ALGORITHM]);

// Future: rfc8152/13.2
// if keytype == .OctetKeyPair, set `x` and `d`
}

Check failure on line 63 in src/COSEKey.php

View workflow job for this annotation

GitHub Actions / PHPCS

Function closing brace must go on the next line following the body; found 1 blank lines before brace

/**
* FIXME: this indirection is not desirable
Expand Down
3 changes: 2 additions & 1 deletion src/GetResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ public function verify(

// 7.2.23
$credentialPublicKey = $credential->getPublicKey();
$coseKey = new COSEKey($credential->getCoseCbor());

// Spec note: the signature is over the concatenation of the authData
// and the hash of clientDataJSON. Due to the above checks (relying
Expand All @@ -184,7 +185,7 @@ public function verify(
$verificationData,
$sig,
$credentialPublicKey->getPemFormatted(),
\OPENSSL_ALGO_SHA256,
$coseKey->algorithm->getOpenSslAlgorithm(),
);
if ($result !== 1) {
$this->fail('7.2.23', 'Signature verification');
Expand Down
37 changes: 20 additions & 17 deletions src/PublicKey/EllipticCurve.php
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<?php

declare(strict_types=1);
Expand Down Expand Up @@ -43,11 +43,12 @@
private BinaryString $x,
private BinaryString $y,
) {
if ($x->getLength() !== 32) {
throw new UnexpectedValueException('X-coordinate not 32 bytes');
$coordinateSize = $curve->getCoordinateSize();
if ($x->getLength() !== $coordinateSize) {
throw new UnexpectedValueException("X-coordinate not $coordinateSize bytes");
}
if ($y->getLength() !== 32) {
throw new UnexpectedValueException('Y-coordinate not 32 bytes');
if ($y->getLength() !== $coordinateSize) {
throw new UnexpectedValueException("Y-coordinate not $coordinateSize bytes");
}
if (!$this->isOnCurve()) {
throw new VerificationError('5.8.5', 'Point not on curve');
Expand All @@ -67,22 +68,28 @@

assert(array_key_exists(COSEKey::INDEX_ALGORITHM, $decoded));
$algorithm = COSE\Algorithm::from($decoded[COSEKey::INDEX_ALGORITHM]);
// TODO: support other algorithms
if ($algorithm !== COSE\Algorithm::EcdsaSha256) {
throw new DomainException('Only ES256 is supported');
}

$curve = COSE\Curve::from($decoded[self::INDEX_CURVE]);
// WebAuthn §5.8.5 - cross-reference curve to algorithm
assert($curve === COSE\Curve::P256);
$expectedCurve = match ($algorithm) {
COSE\Algorithm::EcdsaSha256 => COSE\Curve::P256,
COSE\Algorithm::EcdsaSha384 => COSE\Curve::P384,
COSE\Algorithm::EcdsaSha512 => COSE\Curve::P521,
default => throw new DomainException('Unsupported EC algorithm: ' . $algorithm->value),
};
if ($curve !== $expectedCurve) {
throw new DomainException('Curve does not match algorithm');
}

$coordinateSize = $curve->getCoordinateSize();

if (strlen($decoded[self::INDEX_X_COORDINATE]) !== 32) {
throw new DomainException('X coordinate not 32 bytes');
if (strlen($decoded[self::INDEX_X_COORDINATE]) !== $coordinateSize) {
throw new DomainException("X coordinate not $coordinateSize bytes");
}
$x = new BinaryString($decoded[self::INDEX_X_COORDINATE]);

if (strlen($decoded[self::INDEX_Y_COORDINATE]) !== 32) {
throw new DomainException('X coordinate not 32 bytes');
if (strlen($decoded[self::INDEX_Y_COORDINATE]) !== $coordinateSize) {
throw new DomainException("Y coordinate not $coordinateSize bytes");
}
$y = new BinaryString($decoded[self::INDEX_Y_COORDINATE]);

Expand Down Expand Up @@ -117,10 +124,6 @@
// public key component
public function getPemFormatted(): string
{
if ($this->curve !== COSE\Curve::P256) {
throw new DomainException('Only P256 curves can be PEM-formatted so far');
}

$asn = new ASN\Constructed\Sequence(
new ASN\Constructed\Sequence(
new ASN\Primitive\ObjectIdentifier(self::OID),
Expand Down Expand Up @@ -154,7 +157,7 @@
// the other curves (none of which are supported yet)/
$x3 = $x ** 3; // @phpstan-ignore binaryOp.invalid (phpstan/phpstan#12123)
$ax = $a * $x; // @phpstan-ignore binaryOp.invalid
$rhs = ($x3 + $ax + $b) % $p; // @phpstan-ignore binaryOp.invalid

Check failure on line 160 in src/PublicKey/EllipticCurve.php

View workflow job for this annotation

GitHub Actions / PHPStan

Binary operation "%" between mixed and GMP results in an error.

$y2 = $y ** 2; // @phpstan-ignore binaryOp.invalid
$lhs = $y2 % $p; // @phpstan-ignore binaryOp.invalid
Expand Down
Loading
Loading