Skip to content

Commit 6216299

Browse files
committed
Deprecate php native openssl_x509_parse usage
1 parent 4d7c777 commit 6216299

File tree

6 files changed

+348
-114
lines changed

6 files changed

+348
-114
lines changed

CLAUDE.md

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Overview
6+
7+
This is a **FIDO2/WebAuthn framework for PHP and Symfony**, providing passwordless authentication using security keys and biometric authenticators. It's organized as a monorepo that splits into multiple packages via git-split.
8+
9+
## Development Commands
10+
11+
All commands use **Castor** (PHP task runner) defined in `castor.php`.
12+
13+
### Testing
14+
```bash
15+
# Run all tests
16+
castor phpunit
17+
18+
# Run specific test(s)
19+
castor phpunit -- --filter=TestName
20+
21+
# Run specific test file
22+
./bin/phpunit tests/library/Unit/SomeTest.php
23+
24+
# JavaScript tests (Stimulus)
25+
castor js
26+
```
27+
28+
### Code Quality
29+
```bash
30+
# Check code style (Easy Coding Standard)
31+
castor ecs
32+
33+
# Fix code style issues
34+
castor ecs_fix
35+
36+
# Check refactoring opportunities (Rector)
37+
castor rector
38+
39+
# Apply refactorings
40+
castor rector_fix
41+
42+
# Static analysis (PHPStan at max level)
43+
castor phpstan
44+
45+
# Architecture layer validation (Deptrac)
46+
castor deptrac
47+
48+
# PHP syntax check
49+
castor lint
50+
```
51+
52+
### Before Submitting PR
53+
```bash
54+
# Run all fixes and checks in sequence
55+
castor prepare_pr
56+
```
57+
58+
## Architecture
59+
60+
### Monorepo Structure
61+
62+
Three main packages in `src/`:
63+
64+
1. **`webauthn/`** (`web-auth/webauthn-lib`) - Core PHP library
65+
- Pure PHP FIDO2/WebAuthn implementation
66+
- No framework dependencies (besides Symfony components)
67+
- Handles attestation/assertion validation and cryptographic operations
68+
69+
2. **`symfony/`** (`web-auth/webauthn-symfony-bundle`) - Symfony integration
70+
- Controllers, repositories, DI configuration
71+
- Security bundle integration
72+
- Doctrine ORM credential storage
73+
74+
3. **`stimulus/`** (`web-auth/ux`) - Frontend Stimulus controllers
75+
- Browser WebAuthn API interaction
76+
77+
Packages are split to separate repos via `.gitsplit.yml`.
78+
79+
### Core Architectural Patterns
80+
81+
**Ceremony Step Pattern** - Validation is decomposed into discrete steps:
82+
- Located in `src/webauthn/src/CeremonyStep/`
83+
- Each step implements specific WebAuthn specification requirements
84+
- Examples: `CheckChallenge`, `CheckSignature`, `CheckOrigin`, `CheckCounter`
85+
- Orchestrated by `CeremonyStepManager`
86+
- Steps are registered and executed in sequence during validation ceremonies
87+
88+
**Attestation Statement Support Pattern** - Pluggable attestation format handlers:
89+
- Located in `src/webauthn/src/AttestationStatement/`
90+
- Supports: Android Key, Apple, FIDO U2F, Packed, TPM, None, Compound
91+
- Each format handler implements `AttestationStatementSupport` interface
92+
- Managed by `AttestationStatementSupportManager`
93+
94+
**Repository Pattern**:
95+
- `PublicKeyCredentialSourceRepositoryInterface` - Credential storage
96+
- `PublicKeyCredentialUserEntityRepositoryInterface` - User entity management
97+
- Doctrine implementation: `DoctrineCredentialSourceRepository`
98+
99+
**Event Dispatcher Pattern**:
100+
- PSR-14 event dispatcher for lifecycle hooks
101+
- Events in `src/webauthn/src/Event/`
102+
103+
### Key Components
104+
105+
**Core Validators** (entry points for validation logic):
106+
- `AuthenticatorAttestationResponseValidator` - Validates registration responses
107+
- `AuthenticatorAssertionResponseValidator` - Validates authentication responses
108+
109+
**Ceremony Management**:
110+
- `CeremonyStepManager` in `src/webauthn/src/CeremonyStep/CeremonyStepManager.php`
111+
- Orchestrates ceremony steps for attestation and assertion validation
112+
- Steps can be registered/customized via dependency injection
113+
114+
**Credential Management**:
115+
- `PublicKeyCredentialSource` - Represents stored credentials
116+
- `PublicKeyCredentialDescriptor` - References credentials
117+
- Counter validation to detect cloned authenticators
118+
119+
**Symfony Bundle Integration**:
120+
- Controllers in `src/symfony/src/Controller/`
121+
- DI configuration in `src/symfony/src/DependencyInjection/`
122+
- Compiler passes for service registration
123+
- Security authenticator factory integration
124+
125+
### Architectural Boundaries (Deptrac)
126+
127+
Strict layer enforcement:
128+
- **Webauthn** (core) → Vendors + MetadataService only
129+
- **SymfonyBundle** → Vendors + Webauthn + MetadataService
130+
- **UX/Stimulus** → Vendors only
131+
- **MetadataService** → Vendors only
132+
133+
Violations will fail CI checks via `castor deptrac`.
134+
135+
## Development Workflow
136+
137+
### Adding a New Ceremony Step
138+
139+
1. Create class in `src/webauthn/src/CeremonyStep/` implementing `CeremonyStep` interface
140+
2. Register in `CeremonyStepManagerFactory` or via Symfony compiler pass
141+
3. Add unit tests in `tests/library/Unit/CeremonyStep/`
142+
4. Consider both attestation and assertion ceremony contexts
143+
144+
### Adding a New Attestation Format
145+
146+
1. Create support class in `src/webauthn/src/AttestationStatement/`
147+
2. Implement `AttestationStatementSupport` interface
148+
3. Register via Symfony bundle compiler pass
149+
4. Add comprehensive tests with real attestation examples in `tests/library/Unit/AttestationStatement/`
150+
151+
### Test Structure
152+
153+
Tests are in `tests/`:
154+
- `framework/` - Framework-level integration tests
155+
- `library/` - Core library unit tests (Unit & Functional subdirs)
156+
- `symfony/` - Symfony bundle functional tests
157+
- `MDS/` - Metadata Service tests
158+
159+
## Important Context
160+
161+
### Requirements
162+
- PHP 8.2+
163+
- Extensions: ext-json, ext-openssl
164+
- Symfony 6.4, 7.0, or 8.0
165+
166+
### Code Standards
167+
- PSR-12 coding standard (enforced via ECS)
168+
- Strict types declared in all files
169+
- PHPStan at max level
170+
- Git-Flow branching strategy
171+
- Current development branch: **5.3.x**
172+
- Main branch for PRs: **5.2.x**
173+
174+
### Key Dependencies
175+
- `spomky-labs/cbor-php` - CBOR encoding/decoding
176+
- `web-auth/cose-lib` - COSE cryptography
177+
- `spomky-labs/pki-framework` - PKI operations
178+
179+
### Monorepo Development
180+
- Use `./link` script to link local packages for development
181+
- Changes in `src/*` affect corresponding split repositories
182+
- CI runs git-split automatically on push
183+
184+
### Security
185+
- Private vulnerability reporting via GitHub Security Advisories
186+
- Contact: security@spomky-labs.com
187+
- **Never** file public issues for security vulnerabilities

src/webauthn/src/AttestationStatement/AndroidKeyAttestationStatementSupport.php

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
use SpomkyLabs\Pki\ASN1\Type\Constructed\Sequence;
1515
use SpomkyLabs\Pki\ASN1\Type\Primitive\OctetString;
1616
use SpomkyLabs\Pki\ASN1\Type\Tagged\ExplicitTagging;
17+
use SpomkyLabs\Pki\CryptoEncoding\PEM;
18+
use SpomkyLabs\Pki\X509\Certificate\Certificate;
1719
use Webauthn\AuthenticatorData;
1820
use Webauthn\Event\AttestationStatementLoaded;
1921
use Webauthn\Event\CanDispatchEvents;
@@ -117,6 +119,15 @@ public function isValid(
117119
) === 1;
118120
}
119121

122+
/**
123+
* @param string $certificate
124+
* @param string $clientDataHash
125+
* @param AuthenticatorData $authenticatorData
126+
* @return void
127+
* @throws AttestationStatementVerificationException
128+
*
129+
* @see https://www.w3.org/TR/webauthn-3/#sctn-android-key-attestation
130+
*/
120131
private function checkCertificate(
121132
string $certificate,
122133
string $clientDataHash,
@@ -157,27 +168,27 @@ private function checkCertificate(
157168
);
158169

159170
/*---------------------------*/
160-
$certDetails = openssl_x509_parse($certificate);
171+
/**
172+
* Check Android Key Attestation Statement Certificate requirements
173+
*
174+
* @see https://w3c.github.io/webauthn/#sctn-key-attstn-cert-requirements
175+
*/
176+
$cert = Certificate::fromPEM(PEM::fromString($certificate));
177+
$extensions = $cert->tbsCertificate()->extensions();
161178

162179
//Find Android KeyStore Extension with OID "1.3.6.1.4.1.11129.2.1.17" in certificate extensions
163-
is_array(
164-
$certDetails
165-
) || throw AttestationStatementVerificationException::create('The certificate is not valid');
166-
array_key_exists('extensions', $certDetails) || throw AttestationStatementVerificationException::create(
167-
'The certificate has no extension'
168-
);
169-
is_array($certDetails['extensions']) || throw AttestationStatementVerificationException::create(
170-
'The certificate has no extension'
171-
);
172-
array_key_exists(
173-
'1.3.6.1.4.1.11129.2.1.17',
174-
$certDetails['extensions']
175-
) || throw AttestationStatementVerificationException::create(
180+
$extensions->has('1.3.6.1.4.1.11129.2.1.17') || throw AttestationStatementVerificationException::create(
176181
'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is missing'
177182
);
178-
$extension = $certDetails['extensions']['1.3.6.1.4.1.11129.2.1.17'];
179-
$extensionAsAsn1 = Sequence::fromDER($extension);
180-
$extensionAsAsn1->has(4);
183+
$androidExtension = $extensions->get('1.3.6.1.4.1.11129.2.1.17');
184+
// Get the extension ASN.1 structure: SEQUENCE { OID, [BOOLEAN], OCTET STRING }
185+
$extensionAsn1 = $androidExtension->toASN1();
186+
// The last element is the extnValue OCTET STRING containing the actual extension data
187+
$extnValueOctetString = $extensionAsn1->at($extensionAsn1->count() - 1)->asOctetString();
188+
$extensionData = $extnValueOctetString->string();
189+
190+
// Parse the Android extension value structure
191+
$extensionAsAsn1 = Sequence::fromDER($extensionData);
181192

182193
//Check that attestationChallenge is set to the clientDataHash.
183194
$extensionAsAsn1->has(4) || throw AttestationStatementVerificationException::create(

src/webauthn/src/AttestationStatement/AppleAttestationStatementSupport.php

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
use Cose\Key\Key;
1111
use Cose\Key\RsaKey;
1212
use Psr\EventDispatcher\EventDispatcherInterface;
13+
use SpomkyLabs\Pki\ASN1\Type\UnspecifiedType;
14+
use SpomkyLabs\Pki\CryptoEncoding\PEM;
15+
use SpomkyLabs\Pki\X509\Certificate\Certificate;
1316
use Webauthn\AuthenticatorData;
1417
use Webauthn\Event\AttestationStatementLoaded;
1518
use Webauthn\Event\CanDispatchEvents;
@@ -105,6 +108,15 @@ public function isValid(
105108
return true;
106109
}
107110

111+
/**
112+
* @param string $certificate
113+
* @param string $clientDataHash
114+
* @param AuthenticatorData $authenticatorData
115+
* @return void
116+
* @throws AttestationStatementVerificationException
117+
*
118+
* @see https://www.w3.org/TR/webauthn-3/#sctn-apple-anonymous-attestation
119+
*/
108120
private function checkCertificateAndGetPublicKey(
109121
string $certificate,
110122
string $clientDataHash,
@@ -146,32 +158,28 @@ private function checkCertificateAndGetPublicKey(
146158
);
147159

148160
/*---------------------------*/
149-
$certDetails = openssl_x509_parse($certificate);
161+
$cert = Certificate::fromPEM(PEM::fromString($certificate));
162+
$extensions = $cert->tbsCertificate()->extensions();
150163

151164
//Find Apple Extension with OID "1.2.840.113635.100.8.2" in certificate extensions
152-
is_array(
153-
$certDetails
154-
) || throw AttestationStatementVerificationException::create('The certificate is not valid');
155-
array_key_exists('extensions', $certDetails) || throw AttestationStatementVerificationException::create(
156-
'The certificate has no extension'
157-
);
158-
is_array($certDetails['extensions']) || throw AttestationStatementVerificationException::create(
159-
'The certificate has no extension'
160-
);
161-
array_key_exists(
162-
'1.2.840.113635.100.8.2',
163-
$certDetails['extensions']
164-
) || throw AttestationStatementVerificationException::create(
165+
$extensions->has('1.2.840.113635.100.8.2') || throw AttestationStatementVerificationException::create(
165166
'The certificate extension "1.2.840.113635.100.8.2" is missing'
166167
);
167-
$extension = $certDetails['extensions']['1.2.840.113635.100.8.2'];
168+
$appleExtension = $extensions->get('1.2.840.113635.100.8.2');
169+
// Get the extension ASN.1 structure: SEQUENCE { OID, [BOOLEAN], OCTET STRING }
170+
$extensionAsn1 = $appleExtension->toASN1();
171+
// The last element is the extnValue OCTET STRING containing the actual extension data
172+
$extnValueOctetString = $extensionAsn1->at($extensionAsn1->count() - 1)->asOctetString();
173+
$extensionData = $extnValueOctetString->string();
168174

169175
$nonceToHash = $authenticatorData->authData . $clientDataHash;
170-
$nonce = hash('sha256', $nonceToHash);
176+
$nonce = hash('sha256', $nonceToHash, true);
177+
178+
// Parse the Apple extension value structure: SEQUENCE { [1] OCTET STRING(nonce) }
179+
$extensionSeq = UnspecifiedType::fromDER($extensionData)->asSequence();
180+
$taggedElement = $extensionSeq->at(0)->asTagged();
181+
$nonceFromCert = $taggedElement->asExplicit()->asOctetString()->string();
171182

172-
//'3024a1220420' corresponds to the Sequence+Explicitly Tagged Object + Octet Object
173-
'3024a1220420' . $nonce === bin2hex(
174-
(string) $extension
175-
) || throw AttestationStatementVerificationException::create('The client data hash is not valid');
183+
strcmp($nonce, $nonceFromCert) === 0 || throw AttestationStatementVerificationException::create('The client data hash is not valid');
176184
}
177185
}

0 commit comments

Comments
 (0)