Skip to content

Commit 674a146

Browse files
authored
feat: implement X509Certificate class with all 25 methods/properties (#940)
1 parent c959f30 commit 674a146

20 files changed

Lines changed: 1816 additions & 439 deletions

.docs/implementation-coverage.md

Lines changed: 437 additions & 413 deletions
Large diffs are not rendered by default.

docs/content/docs/api/meta.json

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
{
2-
"title": "API Reference",
3-
"defaultOpen": true,
4-
"pages": [
5-
"index",
6-
"install",
7-
"cipher",
8-
"hash",
9-
"hmac",
10-
"random",
11-
"keys",
12-
"signing",
13-
"public-cipher",
14-
"diffie-hellman",
15-
"ecdh",
16-
"ed25519",
17-
"pbkdf2",
18-
"scrypt",
19-
"hkdf",
20-
"blake3",
21-
"subtle"
22-
]
23-
}
2+
"title": "API Reference",
3+
"defaultOpen": true,
4+
"pages": [
5+
"index",
6+
"install",
7+
"cipher",
8+
"hash",
9+
"hmac",
10+
"random",
11+
"keys",
12+
"signing",
13+
"public-cipher",
14+
"diffie-hellman",
15+
"ecdh",
16+
"ed25519",
17+
"pbkdf2",
18+
"scrypt",
19+
"hkdf",
20+
"blake3",
21+
"x509",
22+
"subtle"
23+
]
24+
}

docs/content/docs/api/x509.mdx

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
---
2+
title: X509 Certificates
3+
description: Parse, inspect, and validate X.509 certificates
4+
---
5+
6+
import { Callout } from 'fumadocs-ui/components/callout';
7+
import { TypeTable } from 'fumadocs-ui/components/type-table';
8+
9+
The `X509Certificate` class provides a complete implementation for working with X.509 certificates — the standard format used in TLS/SSL, code signing, and PKI systems. Parse certificates, extract properties, validate hostnames, and verify signatures.
10+
11+
<Callout type="info" title="Common Use Cases">
12+
**Certificate pinning** in mobile apps, **mTLS client certificate**
13+
validation, **certificate chain verification**, **hostname matching** for
14+
custom TLS implementations, and **extracting public keys** from certificates.
15+
</Callout>
16+
17+
## Table of Contents
18+
19+
- [Theory](#theory)
20+
- [Class: X509Certificate](#class-x509certificate)
21+
- [Properties](#properties)
22+
- [Methods](#methods)
23+
- [Real-World Examples](#real-world-examples)
24+
25+
## Theory
26+
27+
X.509 is the standard format for public key certificates. A certificate binds an identity (subject) to a public key, signed by a Certificate Authority (CA).
28+
29+
Key concepts:
30+
31+
1. **Subject / Issuer**: Distinguished Names identifying the certificate holder and signer.
32+
2. **Validity Period**: Time window during which the certificate is valid.
33+
3. **Subject Alternative Name (SAN)**: Additional identities (DNS names, IPs, emails) the certificate is valid for.
34+
4. **Fingerprint**: A hash of the certificate used for identification (not security).
35+
5. **CA flag**: Whether the certificate can sign other certificates.
36+
37+
---
38+
39+
## Class: X509Certificate
40+
41+
### Constructor
42+
43+
```ts
44+
import { X509Certificate } from 'react-native-quick-crypto';
45+
46+
const cert = new X509Certificate(pemString);
47+
```
48+
49+
**Parameters:**
50+
51+
<TypeTable
52+
type={{
53+
buffer: {
54+
description: 'PEM or DER encoded certificate data.',
55+
type: 'string | Buffer | TypedArray | DataView',
56+
},
57+
}}
58+
/>
59+
60+
Accepts both PEM-encoded strings (beginning with `-----BEGIN CERTIFICATE-----`) and DER-encoded binary data.
61+
62+
---
63+
64+
## Properties
65+
66+
All properties are lazily computed and cached on first access.
67+
68+
| Property | Type | Description |
69+
| :---------------------- | :---------- | :--------------------------------------------------------- |
70+
| `subject` | `string` | Distinguished name of the certificate subject |
71+
| `issuer` | `string` | Distinguished name of the issuing CA |
72+
| `subjectAltName` | `string` | Subject Alternative Name extension |
73+
| `infoAccess` | `string` | Authority Information Access extension |
74+
| `validFrom` | `string` | "Not Before" date as a string |
75+
| `validTo` | `string` | "Not After" date as a string |
76+
| `validFromDate` | `Date` | "Not Before" as a JavaScript Date object |
77+
| `validToDate` | `Date` | "Not After" as a JavaScript Date object |
78+
| `serialNumber` | `string` | Certificate serial number (uppercase hex) |
79+
| `signatureAlgorithm` | `string` | Signature algorithm name (e.g., `sha256WithRSAEncryption`) |
80+
| `signatureAlgorithmOid` | `string` | Signature algorithm OID |
81+
| `fingerprint` | `string` | SHA-1 fingerprint (colon-separated hex) |
82+
| `fingerprint256` | `string` | SHA-256 fingerprint (colon-separated hex) |
83+
| `fingerprint512` | `string` | SHA-512 fingerprint (colon-separated hex) |
84+
| `extKeyUsage` | `string[]` | Extended key usage OIDs (also available as `keyUsage`) |
85+
| `ca` | `boolean` | Whether this is a CA certificate |
86+
| `raw` | `Buffer` | Raw DER-encoded certificate bytes |
87+
| `publicKey` | `KeyObject` | The certificate's public key as a KeyObject |
88+
| `issuerCertificate` | `undefined` | Always `undefined` (no TLS context in React Native) |
89+
90+
```ts
91+
const cert = new X509Certificate(pemString);
92+
93+
console.log(cert.subject);
94+
// C=US\nST=California\nO=Example\nCN=example.com
95+
96+
console.log(cert.fingerprint256);
97+
// AB:CD:EF:12:34:...
98+
99+
console.log(cert.ca);
100+
// true
101+
102+
console.log(cert.publicKey.type);
103+
// 'public'
104+
```
105+
106+
---
107+
108+
## Methods
109+
110+
### x509.checkHost(name[, options])
111+
112+
Checks whether the certificate matches the given hostname.
113+
114+
<TypeTable
115+
type={{
116+
name: { description: 'The hostname to check.', type: 'string' },
117+
options: {
118+
description: 'Optional check configuration.',
119+
type: 'CheckOptions',
120+
},
121+
}}
122+
/>
123+
124+
**Returns:** `string | undefined` — The matched hostname, or `undefined` if no match.
125+
126+
```ts
127+
const cert = new X509Certificate(pemString);
128+
129+
cert.checkHost('example.com'); // 'example.com'
130+
cert.checkHost('wrong.com'); // undefined
131+
132+
// Disable wildcard matching
133+
cert.checkHost('sub.example.com', { wildcards: false });
134+
```
135+
136+
#### CheckOptions
137+
138+
| Option | Type | Default | Description |
139+
| :---------------------- | :--------------------------------- | :---------- | :---------------------------------- |
140+
| `subject` | `'default' \| 'always' \| 'never'` | `'default'` | When to check the subject CN |
141+
| `wildcards` | `boolean` | `true` | Allow wildcard certificate matching |
142+
| `partialWildcards` | `boolean` | `true` | Allow partial wildcard matching |
143+
| `multiLabelWildcards` | `boolean` | `false` | Allow multi-label wildcard matching |
144+
| `singleLabelSubdomains` | `boolean` | `false` | Match single-label subdomains |
145+
146+
### x509.checkEmail(email[, options])
147+
148+
Checks whether the certificate matches the given email address.
149+
150+
**Returns:** `string | undefined` — The matched email, or `undefined` if no match.
151+
152+
```ts
153+
cert.checkEmail('user@example.com'); // 'user@example.com' or undefined
154+
```
155+
156+
### x509.checkIP(ip)
157+
158+
Checks whether the certificate matches the given IP address.
159+
160+
**Returns:** `string | undefined` — The matched IP, or `undefined` if no match.
161+
162+
```ts
163+
cert.checkIP('127.0.0.1'); // '127.0.0.1'
164+
cert.checkIP('192.168.1.1'); // undefined
165+
```
166+
167+
### x509.checkIssued(otherCert)
168+
169+
Checks whether this certificate was issued by `otherCert`.
170+
171+
**Returns:** `boolean`
172+
173+
```ts
174+
// Self-signed certificate
175+
cert.checkIssued(cert); // true
176+
177+
// Chain validation
178+
rootCert.checkIssued(intermediateCert); // true or false
179+
```
180+
181+
### x509.checkPrivateKey(privateKey)
182+
183+
Checks whether the given private key matches this certificate's public key.
184+
185+
**Returns:** `boolean`
186+
187+
```ts
188+
import { createPrivateKey } from 'react-native-quick-crypto';
189+
190+
const privKey = createPrivateKey(privateKeyPem);
191+
cert.checkPrivateKey(privKey); // true
192+
```
193+
194+
### x509.verify(publicKey)
195+
196+
Verifies that the certificate was signed with the given public key.
197+
198+
**Returns:** `boolean`
199+
200+
```ts
201+
// For self-signed certificates
202+
cert.verify(cert.publicKey); // true
203+
```
204+
205+
### x509.toString()
206+
207+
Returns the PEM-encoded certificate string.
208+
209+
**Returns:** `string`
210+
211+
### x509.toJSON()
212+
213+
Returns the PEM-encoded certificate string (same as `toString()`).
214+
215+
**Returns:** `string`
216+
217+
### x509.toLegacyObject()
218+
219+
Returns a plain object with legacy certificate fields.
220+
221+
**Returns:** `object`
222+
223+
---
224+
225+
## Real-World Examples
226+
227+
### Certificate Pinning
228+
229+
```ts
230+
import { X509Certificate } from 'react-native-quick-crypto';
231+
232+
const PINNED_FINGERPRINT = 'AB:CD:EF:...';
233+
234+
function validateServerCert(pemCert: string): boolean {
235+
const cert = new X509Certificate(pemCert);
236+
237+
// Check fingerprint
238+
if (cert.fingerprint256 !== PINNED_FINGERPRINT) {
239+
return false;
240+
}
241+
242+
// Check validity
243+
const now = new Date();
244+
if (now < cert.validFromDate || now > cert.validToDate) {
245+
return false;
246+
}
247+
248+
return true;
249+
}
250+
```
251+
252+
### Hostname Verification
253+
254+
```ts
255+
import { X509Certificate } from 'react-native-quick-crypto';
256+
257+
function verifyHostname(pemCert: string, hostname: string): boolean {
258+
const cert = new X509Certificate(pemCert);
259+
return cert.checkHost(hostname) !== undefined;
260+
}
261+
```
262+
263+
### Extract Public Key from Certificate
264+
265+
```ts
266+
import { X509Certificate } from 'react-native-quick-crypto';
267+
268+
const cert = new X509Certificate(pemCert);
269+
const publicKey = cert.publicKey;
270+
271+
// Use the public key for encryption or verification
272+
console.log(publicKey.type); // 'public'
273+
console.log(publicKey.asymmetricKeyType); // 'rsa'
274+
```
275+
276+
### Validate Certificate Chain
277+
278+
```ts
279+
import { X509Certificate } from 'react-native-quick-crypto';
280+
281+
function validateChain(leafPem: string, issuerPem: string): boolean {
282+
const leaf = new X509Certificate(leafPem);
283+
const issuerCert = new X509Certificate(issuerPem);
284+
285+
// Check the leaf was issued by the issuer
286+
if (!issuerCert.checkIssued(leaf)) {
287+
return false;
288+
}
289+
290+
// Verify the leaf's signature with issuer's public key
291+
if (!leaf.verify(issuerCert.publicKey)) {
292+
return false;
293+
}
294+
295+
return true;
296+
}
297+
```

docs/data/coverage.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,36 @@ export const COVERAGE_DATA: CoverageCategory[] = [
130130
},
131131
{
132132
name: 'X509Certificate',
133-
status: 'missing',
133+
subItems: [
134+
{ name: 'new X509Certificate(buffer)', status: 'implemented' },
135+
{ name: 'ca', status: 'implemented' },
136+
{ name: 'checkEmail', status: 'implemented' },
137+
{ name: 'checkHost', status: 'implemented' },
138+
{ name: 'checkIP', status: 'implemented' },
139+
{ name: 'checkIssued', status: 'implemented' },
140+
{ name: 'checkPrivateKey', status: 'implemented' },
141+
{ name: 'fingerprint', status: 'implemented' },
142+
{ name: 'fingerprint256', status: 'implemented' },
143+
{ name: 'fingerprint512', status: 'implemented' },
144+
{ name: 'infoAccess', status: 'implemented' },
145+
{ name: 'issuer', status: 'implemented' },
146+
{ name: 'issuerCertificate', status: 'implemented' },
147+
{ name: 'extKeyUsage', status: 'implemented' },
148+
{ name: 'keyUsage', status: 'implemented' },
149+
{ name: 'signatureAlgorithm', status: 'implemented' },
150+
{ name: 'signatureAlgorithmOid', status: 'implemented' },
151+
{ name: 'publicKey', status: 'implemented' },
152+
{ name: 'raw', status: 'implemented' },
153+
{ name: 'serialNumber', status: 'implemented' },
154+
{ name: 'subject', status: 'implemented' },
155+
{ name: 'subjectAltName', status: 'implemented' },
156+
{ name: 'toJSON', status: 'implemented' },
157+
{ name: 'toLegacyObject', status: 'implemented' },
158+
{ name: 'toString', status: 'implemented' },
159+
{ name: 'validFrom', status: 'implemented' },
160+
{ name: 'validTo', status: 'implemented' },
161+
{ name: 'verify', status: 'implemented' },
162+
],
134163
},
135164
],
136165
},

example/src/hooks/useTestsList.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import '../tests/subtle/supports';
4343
import '../tests/subtle/getPublicKey';
4444
import '../tests/subtle/wrap_unwrap';
4545
import '../tests/utils/utils_tests';
46+
import '../tests/x509/x509_tests';
4647

4748
export const useTestsList = (): [
4849
TestSuites,

0 commit comments

Comments
 (0)