diff --git a/.docs/implementation-coverage.md b/.docs/implementation-coverage.md index 8ec4adfc..1ebf0532 100644 --- a/.docs/implementation-coverage.md +++ b/.docs/implementation-coverage.md @@ -1,13 +1,14 @@ # Implementation Coverage - NodeJS + This document attempts to describe the implementation status of Crypto APIs/Interfaces from Node.js in the `react-native-quick-crypto` library. > Note: This is the status for version 1.x and higher. For version `0.x` see [this document](https://github.com/margelo/react-native-quick-crypto/blob/0.x/docs/implementation-coverage.md) and the [0.x branch](https://github.com/margelo/react-native-quick-crypto/tree/0.x). -* ` ` - not implemented in Node -* ❌ - implemented in Node, not RNQC -* ✅ - implemented in Node and RNQC -* 🚧 - work in progress -* `-` - not applicable to React Native +- ` ` - not implemented in Node +- ❌ - implemented in Node, not RNQC +- ✅ - implemented in Node and RNQC +- 🚧 - work in progress +- `-` - not applicable to React Native ## Post-Quantum Cryptography (PQC) @@ -16,498 +17,521 @@ This document attempts to describe the implementation status of Crypto APIs/Inte These algorithms provide quantum-resistant cryptography. - # `Crypto` -* ✅ Class: `Certificate` - * ✅ Static method: `Certificate.exportChallenge(spkac[, encoding])` - * ✅ Static method: `Certificate.exportPublicKey(spkac[, encoding])` - * ✅ Static method: `Certificate.verifySpkac(spkac[, encoding])` -* ✅ Class: `Cipheriv` - * ✅ `cipher.final([outputEncoding])` - * ✅ `cipher.getAuthTag()` - * ✅ `cipher.setAAD(buffer[, options])` - * ✅ `cipher.setAutoPadding([autoPadding])` - * ✅ `cipher.update(data[, inputEncoding][, outputEncoding])` -* ✅ Class: `Decipheriv` - * ✅ `decipher.final([outputEncoding])` - * ✅ `decipher.setAAD(buffer[, options])` - * ✅ `decipher.setAuthTag(buffer[, encoding])` - * ✅ `decipher.setAutoPadding([autoPadding])` - * ✅ `decipher.update(data[, inputEncoding][, outputEncoding])` -* ✅ Class: `DiffieHellman` - * ✅ `diffieHellman.computeSecret(otherPublicKey[, inputEncoding][, outputEncoding])` - * ✅ `diffieHellman.generateKeys([encoding])` - * ✅ `diffieHellman.getGenerator([encoding])` - * ✅ `diffieHellman.getPrime([encoding])` - * ✅ `diffieHellman.getPrivateKey([encoding])` - * ✅ `diffieHellman.getPublicKey([encoding])` - * ✅ `diffieHellman.setPrivateKey(privateKey[, encoding])` - * ✅ `diffieHellman.setPublicKey(publicKey[, encoding])` - * ✅ `diffieHellman.verifyError` -* ✅ Class: `DiffieHellmanGroup` -* ✅ Class: `ECDH` - * ✅ static `ECDH.convertKey(key, curve[, inputEncoding[, outputEncoding[, format]]])` - * ✅ `ecdh.computeSecret(otherPublicKey[, inputEncoding][, outputEncoding])` - * ✅ `ecdh.generateKeys([encoding[, format]])` - * ✅ `ecdh.getPrivateKey([encoding])` - * ✅ `ecdh.getPublicKey([encoding][, format])` - * ✅ `ecdh.setPrivateKey(privateKey[, encoding])` - * ✅ `ecdh.setPublicKey(publicKey[, encoding])` -* ✅ Class: `Hash` - * ✅ `hash.copy([options])` - * ✅ `hash.digest([encoding])` - * ✅ `hash.update(data[, inputEncoding])` -* ✅ Class: `Hmac` - * ✅ `hmac.digest([encoding])` - * ✅ `hmac.update(data[, inputEncoding])` -* ✅ Class: `KeyObject` - * ✅ static `KeyObject.from(key)` - * ✅ `keyObject.asymmetricKeyDetails` - * ✅ `keyObject.asymmetricKeyType` - * ✅ `keyObject.export([options])` - * ✅ `keyObject.equals(otherKeyObject)` - * ✅ `keyObject.symmetricKeySize` - * ✅ `keyObject.toCryptoKey(algorithm, extractable, keyUsages)` - * ✅ `keyObject.type` -* ✅ Class: `Sign` - * ✅ `sign.sign(privateKey[, outputEncoding])` - * ✅ `sign.update(data[, inputEncoding])` -* ✅ Class: `Verify` - * ✅ `verify.update(data[, inputEncoding])` - * ✅ `verify.verify(object, signature[, signatureEncoding])` -* ❌ Class: `X509Certificate` - * ❌ `new X509Certificate(buffer)` - * ❌ `x509.ca` - * ❌ `x509.checkEmail(email[, options])` - * ❌ `x509.checkHost(name[, options])` - * ❌ `x509.checkIP(ip)` - * ❌ `x509.checkIssued(otherCert)` - * ❌ `x509.checkPrivateKey(privateKey)` - * ❌ `x509.fingerprint` - * ❌ `x509.fingerprint256` - * ❌ `x509.fingerprint512` - * ❌ `x509.infoAccess` - * ❌ `x509.issuer` - * ❌ `x509.issuerCertificate` - * ❌ `x509.extKeyUsage` - * ❌ `x509.publicKey` - * ❌ `x509.raw` - * ❌ `x509.serialNumber` - * ❌ `x509.subject` - * ❌ `x509.subjectAltName` - * ❌ `x509.toJSON()` - * ❌ `x509.toLegacyObject()` - * ❌ `x509.toString()` - * ❌ `x509.validFrom` - * ❌ `x509.validTo` - * ❌ `x509.verify(publicKey)` -* 🚧 node:crypto module methods and properties - * ✅ `crypto.argon2(algorithm, parameters, callback)` - * ✅ `crypto.argon2Sync(algorithm, parameters)` - * ✅ `crypto.checkPrime(candidate[, options], callback)` - * ✅ `crypto.checkPrimeSync(candidate[, options])` - * ✅ `crypto.constants` - * ✅ `crypto.createCipheriv(algorithm, key, iv[, options])` - * ✅ `crypto.createDecipheriv(algorithm, key, iv[, options])` - * ✅ `crypto.createDiffieHellman(prime[, primeEncoding][, generator][, generatorEncoding])` - * ✅ `crypto.createDiffieHellman(primeLength[, generator])` - * ✅ `crypto.createDiffieHellmanGroup(groupName)` - * ✅ `crypto.getDiffieHellman(groupName)` - * ✅ `crypto.createECDH(curveName)` - * ✅ `crypto.createHash(algorithm[, options])` - * ✅ `crypto.createHmac(algorithm, key[, options])` - * ✅ `crypto.createPrivateKey(key)` - * ✅ `crypto.createPublicKey(key)` - * ✅ `crypto.createSecretKey(key[, encoding])` - * ✅ `crypto.createSign(algorithm[, options])` - * ✅ `crypto.createVerify(algorithm[, options])` - * ❌ `crypto.decapsulate(key, ciphertext[, callback])` - * ✅ `crypto.diffieHellman(options[, callback])` - * ❌ `crypto.encapsulate(key[, callback])` - * `-` `crypto.fips` deprecated, not applicable to RN - * ✅ `crypto.generateKey(type, options, callback)` - * ✅ `crypto.generateKeyPair(type, options, callback)` - * ✅ `crypto.generateKeyPairSync(type, options)` - * ✅ `crypto.generateKeySync(type, options)` - * ✅ `crypto.generatePrime(size[, options[, callback]])` - * ✅ `crypto.generatePrimeSync(size[, options])` - * ✅ `crypto.getCipherInfo(nameOrNid[, options])` - * ✅ `crypto.getCiphers()` - * ✅ `crypto.getCurves()` - * `-` `crypto.getFips()` not applicable to RN - * ✅ `crypto.getHashes()` - * ✅ `crypto.getRandomValues(typedArray)` - * ✅ `crypto.hash(algorithm, data[, outputEncoding])` - * ✅ `crypto.hkdf(digest, ikm, salt, info, keylen, callback)` - * ✅ `crypto.hkdfSync(digest, ikm, salt, info, keylen)` - * ✅ `crypto.pbkdf2(password, salt, iterations, keylen, digest, callback)` - * ✅ `crypto.pbkdf2Sync(password, salt, iterations, keylen, digest)` - * ✅ `crypto.privateDecrypt(privateKey, buffer)` - * ✅ `crypto.privateEncrypt(privateKey, buffer)` - * ✅ `crypto.publicDecrypt(key, buffer)` - * ✅ `crypto.publicEncrypt(key, buffer)` - * ✅ `crypto.randomBytes(size[, callback])` - * ✅ `crypto.randomFill(buffer[, offset][, size], callback)` - * ✅ `crypto.randomFillSync(buffer[, offset][, size])` - * ✅ `crypto.randomInt([min, ]max[, callback])` - * ✅ `crypto.randomUUID([options])` - * ✅ `crypto.scrypt(password, salt, keylen[, options], callback)` - * ✅ `crypto.scryptSync(password, salt, keylen[, options])` - * `-` `crypto.secureHeapUsed()` not applicable to RN - * `-` `crypto.setEngine(engine[, flags])` not applicable to RN - * `-` `crypto.setFips(bool)` not applicable to RN - * ✅ `crypto.sign(algorithm, data, key[, callback])` - * ✅ `crypto.subtle` (see below) - * ✅ `crypto.timingSafeEqual(a, b)` - * ✅ `crypto.verify(algorithm, data, key, signature[, callback])` - * ✅ `crypto.webcrypto` (see below) +- ✅ Class: `Certificate` + - ✅ Static method: `Certificate.exportChallenge(spkac[, encoding])` + - ✅ Static method: `Certificate.exportPublicKey(spkac[, encoding])` + - ✅ Static method: `Certificate.verifySpkac(spkac[, encoding])` +- ✅ Class: `Cipheriv` + - ✅ `cipher.final([outputEncoding])` + - ✅ `cipher.getAuthTag()` + - ✅ `cipher.setAAD(buffer[, options])` + - ✅ `cipher.setAutoPadding([autoPadding])` + - ✅ `cipher.update(data[, inputEncoding][, outputEncoding])` +- ✅ Class: `Decipheriv` + - ✅ `decipher.final([outputEncoding])` + - ✅ `decipher.setAAD(buffer[, options])` + - ✅ `decipher.setAuthTag(buffer[, encoding])` + - ✅ `decipher.setAutoPadding([autoPadding])` + - ✅ `decipher.update(data[, inputEncoding][, outputEncoding])` +- ✅ Class: `DiffieHellman` + - ✅ `diffieHellman.computeSecret(otherPublicKey[, inputEncoding][, outputEncoding])` + - ✅ `diffieHellman.generateKeys([encoding])` + - ✅ `diffieHellman.getGenerator([encoding])` + - ✅ `diffieHellman.getPrime([encoding])` + - ✅ `diffieHellman.getPrivateKey([encoding])` + - ✅ `diffieHellman.getPublicKey([encoding])` + - ✅ `diffieHellman.setPrivateKey(privateKey[, encoding])` + - ✅ `diffieHellman.setPublicKey(publicKey[, encoding])` + - ✅ `diffieHellman.verifyError` +- ✅ Class: `DiffieHellmanGroup` +- ✅ Class: `ECDH` + - ✅ static `ECDH.convertKey(key, curve[, inputEncoding[, outputEncoding[, format]]])` + - ✅ `ecdh.computeSecret(otherPublicKey[, inputEncoding][, outputEncoding])` + - ✅ `ecdh.generateKeys([encoding[, format]])` + - ✅ `ecdh.getPrivateKey([encoding])` + - ✅ `ecdh.getPublicKey([encoding][, format])` + - ✅ `ecdh.setPrivateKey(privateKey[, encoding])` + - ✅ `ecdh.setPublicKey(publicKey[, encoding])` +- ✅ Class: `Hash` + - ✅ `hash.copy([options])` + - ✅ `hash.digest([encoding])` + - ✅ `hash.update(data[, inputEncoding])` +- ✅ Class: `Hmac` + - ✅ `hmac.digest([encoding])` + - ✅ `hmac.update(data[, inputEncoding])` +- ✅ Class: `KeyObject` + - ✅ static `KeyObject.from(key)` + - ✅ `keyObject.asymmetricKeyDetails` + - ✅ `keyObject.asymmetricKeyType` + - ✅ `keyObject.export([options])` + - ✅ `keyObject.equals(otherKeyObject)` + - ✅ `keyObject.symmetricKeySize` + - ✅ `keyObject.toCryptoKey(algorithm, extractable, keyUsages)` + - ✅ `keyObject.type` +- ✅ Class: `Sign` + - ✅ `sign.sign(privateKey[, outputEncoding])` + - ✅ `sign.update(data[, inputEncoding])` +- ✅ Class: `Verify` + - ✅ `verify.update(data[, inputEncoding])` + - ✅ `verify.verify(object, signature[, signatureEncoding])` +- ✅ Class: `X509Certificate` + - ✅ `new X509Certificate(buffer)` + - ✅ `x509.ca` + - ✅ `x509.checkEmail(email[, options])` + - ✅ `x509.checkHost(name[, options])` + - ✅ `x509.checkIP(ip)` + - ✅ `x509.checkIssued(otherCert)` + - ✅ `x509.checkPrivateKey(privateKey)` + - ✅ `x509.fingerprint` + - ✅ `x509.fingerprint256` + - ✅ `x509.fingerprint512` + - ✅ `x509.infoAccess` + - ✅ `x509.issuer` + - ✅ `x509.issuerCertificate` + - ✅ `x509.extKeyUsage` + - ✅ `x509.keyUsage` + - ✅ `x509.signatureAlgorithm` + - ✅ `x509.signatureAlgorithmOid` + - ✅ `x509.publicKey` + - ✅ `x509.raw` + - ✅ `x509.serialNumber` + - ✅ `x509.subject` + - ✅ `x509.subjectAltName` + - ✅ `x509.toJSON()` + - ✅ `x509.toLegacyObject()` + - ✅ `x509.toString()` + - ✅ `x509.validFrom` + - ✅ `x509.validTo` + - ✅ `x509.verify(publicKey)` +- 🚧 node:crypto module methods and properties + - ✅ `crypto.argon2(algorithm, parameters, callback)` + - ✅ `crypto.argon2Sync(algorithm, parameters)` + - ✅ `crypto.checkPrime(candidate[, options], callback)` + - ✅ `crypto.checkPrimeSync(candidate[, options])` + - ✅ `crypto.constants` + - ✅ `crypto.createCipheriv(algorithm, key, iv[, options])` + - ✅ `crypto.createDecipheriv(algorithm, key, iv[, options])` + - ✅ `crypto.createDiffieHellman(prime[, primeEncoding][, generator][, generatorEncoding])` + - ✅ `crypto.createDiffieHellman(primeLength[, generator])` + - ✅ `crypto.createDiffieHellmanGroup(groupName)` + - ✅ `crypto.getDiffieHellman(groupName)` + - ✅ `crypto.createECDH(curveName)` + - ✅ `crypto.createHash(algorithm[, options])` + - ✅ `crypto.createHmac(algorithm, key[, options])` + - ✅ `crypto.createPrivateKey(key)` + - ✅ `crypto.createPublicKey(key)` + - ✅ `crypto.createSecretKey(key[, encoding])` + - ✅ `crypto.createSign(algorithm[, options])` + - ✅ `crypto.createVerify(algorithm[, options])` + - ❌ `crypto.decapsulate(key, ciphertext[, callback])` + - ✅ `crypto.diffieHellman(options[, callback])` + - ❌ `crypto.encapsulate(key[, callback])` + - `-` `crypto.fips` deprecated, not applicable to RN + - ✅ `crypto.generateKey(type, options, callback)` + - ✅ `crypto.generateKeyPair(type, options, callback)` + - ✅ `crypto.generateKeyPairSync(type, options)` + - ✅ `crypto.generateKeySync(type, options)` + - ✅ `crypto.generatePrime(size[, options[, callback]])` + - ✅ `crypto.generatePrimeSync(size[, options])` + - ✅ `crypto.getCipherInfo(nameOrNid[, options])` + - ✅ `crypto.getCiphers()` + - ✅ `crypto.getCurves()` + - `-` `crypto.getFips()` not applicable to RN + - ✅ `crypto.getHashes()` + - ✅ `crypto.getRandomValues(typedArray)` + - ✅ `crypto.hash(algorithm, data[, outputEncoding])` + - ✅ `crypto.hkdf(digest, ikm, salt, info, keylen, callback)` + - ✅ `crypto.hkdfSync(digest, ikm, salt, info, keylen)` + - ✅ `crypto.pbkdf2(password, salt, iterations, keylen, digest, callback)` + - ✅ `crypto.pbkdf2Sync(password, salt, iterations, keylen, digest)` + - ✅ `crypto.privateDecrypt(privateKey, buffer)` + - ✅ `crypto.privateEncrypt(privateKey, buffer)` + - ✅ `crypto.publicDecrypt(key, buffer)` + - ✅ `crypto.publicEncrypt(key, buffer)` + - ✅ `crypto.randomBytes(size[, callback])` + - ✅ `crypto.randomFill(buffer[, offset][, size], callback)` + - ✅ `crypto.randomFillSync(buffer[, offset][, size])` + - ✅ `crypto.randomInt([min, ]max[, callback])` + - ✅ `crypto.randomUUID([options])` + - ✅ `crypto.scrypt(password, salt, keylen[, options], callback)` + - ✅ `crypto.scryptSync(password, salt, keylen[, options])` + - `-` `crypto.secureHeapUsed()` not applicable to RN + - `-` `crypto.setEngine(engine[, flags])` not applicable to RN + - `-` `crypto.setFips(bool)` not applicable to RN + - ✅ `crypto.sign(algorithm, data, key[, callback])` + - ✅ `crypto.subtle` (see below) + - ✅ `crypto.timingSafeEqual(a, b)` + - ✅ `crypto.verify(algorithm, data, key, signature[, callback])` + - ✅ `crypto.webcrypto` (see below) ## `crypto.diffieHellman` -| type | Status | -| --------- | :----: | -| `dh` | ✅ | -| `ec` | ✅ | -| `x448` | ✅ | -| `x25519` | ✅ | + +| type | Status | +| -------- | :----: | +| `dh` | ✅ | +| `ec` | ✅ | +| `x448` | ✅ | +| `x25519` | ✅ | ## `crypto.generateKey` -| type | Status | -| --------- | :----: | -| `aes` | ✅ | -| `hmac` | ✅ | + +| type | Status | +| ------ | :----: | +| `aes` | ✅ | +| `hmac` | ✅ | ## `crypto.generateKeyPair` + | type | Status | | --------- | :----: | -| `rsa` | ✅ | -| `rsa-pss` | ✅ | -| `dsa` | ✅ | -| `ec` | ✅ | -| `ed25519` | ✅ | -| `ed448` | ✅ | -| `x25519` | ✅ | -| `x448` | ✅ | -| `dh` | ✅ | +| `rsa` | ✅ | +| `rsa-pss` | ✅ | +| `dsa` | ✅ | +| `ec` | ✅ | +| `ed25519` | ✅ | +| `ed448` | ✅ | +| `x25519` | ✅ | +| `x448` | ✅ | +| `dh` | ✅ | ## `crypto.generateKeyPairSync` + | type | Status | | --------- | :----: | -| `rsa` | ✅ | -| `rsa-pss` | ✅ | -| `dsa` | ✅ | -| `ec` | ✅ | -| `ed25519` | ✅ | -| `ed448` | ✅ | -| `x25519` | ✅ | -| `x448` | ✅ | -| `dh` | ✅ | +| `rsa` | ✅ | +| `rsa-pss` | ✅ | +| `dsa` | ✅ | +| `ec` | ✅ | +| `ed25519` | ✅ | +| `ed448` | ✅ | +| `x25519` | ✅ | +| `x448` | ✅ | +| `dh` | ✅ | ## `crypto.generateKeySync` -| type | Status | -| --------- | :----: | -| `aes` | ✅ | -| `hmac` | ✅ | + +| type | Status | +| ------ | :----: | +| `aes` | ✅ | +| `hmac` | ✅ | ## `crypto.sign` + | Algorithm | Status | -| --------- | :----: | -| `RSASSA-PKCS1-v1_5` | ✅ | -| `RSA-PSS` | ✅ | -| `ECDSA` | ✅ | -| `Ed25519` | ✅ | -| `Ed448` | ✅ | -| `HMAC` | ✅ | +| ------------------- | :----: | +| `RSASSA-PKCS1-v1_5` | ✅ | +| `RSA-PSS` | ✅ | +| `ECDSA` | ✅ | +| `Ed25519` | ✅ | +| `Ed448` | ✅ | +| `HMAC` | ✅ | ## `crypto.verify` + | Algorithm | Status | -| --------- | :----: | -| `RSASSA-PKCS1-v1_5` | ✅ | -| `RSA-PSS` | ✅ | -| `ECDSA` | ✅ | -| `Ed25519` | ✅ | -| `Ed448` | ✅ | -| `HMAC` | ✅ | +| ------------------- | :----: | +| `RSASSA-PKCS1-v1_5` | ✅ | +| `RSA-PSS` | ✅ | +| `ECDSA` | ✅ | +| `Ed25519` | ✅ | +| `Ed448` | ✅ | +| `HMAC` | ✅ | ## Extended Ciphers (Beyond Node.js API) These ciphers are **not available in Node.js** but are provided by RNQC via libsodium for mobile use cases requiring extended nonces. -| Cipher | Key | Nonce | Tag | AAD | Notes | -| ------ | :-: | :---: | :-: | :-: | ----- | -| `xchacha20-poly1305` | 32B | 24B | 16B | ✅ | AEAD with extended nonce | -| `xsalsa20-poly1305` | 32B | 24B | 16B | ❌ | Authenticated encryption (secretbox) | -| `xsalsa20` | 32B | 24B | - | - | Stream cipher (no authentication) | +| Cipher | Key | Nonce | Tag | AAD | Notes | +| -------------------- | :-: | :---: | :-: | :-: | ------------------------------------ | +| `xchacha20-poly1305` | 32B | 24B | 16B | ✅ | AEAD with extended nonce | +| `xsalsa20-poly1305` | 32B | 24B | 16B | ❌ | Authenticated encryption (secretbox) | +| `xsalsa20` | 32B | 24B | - | - | Stream cipher (no authentication) | > **Note:** These ciphers require `SODIUM_ENABLED=1` on both iOS and Android. # `WebCrypto` -* ✅ Class: `Crypto` - * ✅ `crypto.subtle` - * ✅ `crypto.getRandomValues(typedArray)` - * ✅ `crypto.randomUUID()` -* ✅ Class: `CryptoKey` - * ✅ `cryptoKey.algorithm` - * ✅ `cryptoKey.extractable` - * ✅ `cryptoKey.type` - * ✅ `cryptoKey.usages` -* ✅ Class: `CryptoKeyPair` - * ✅ `cryptoKeyPair.privateKey` - * ✅ `cryptoKeyPair.publicKey` -* 🚧 Class: `CryptoSubtle` - * (see below) +- ✅ Class: `Crypto` + - ✅ `crypto.subtle` + - ✅ `crypto.getRandomValues(typedArray)` + - ✅ `crypto.randomUUID()` +- ✅ Class: `CryptoKey` + - ✅ `cryptoKey.algorithm` + - ✅ `cryptoKey.extractable` + - ✅ `cryptoKey.type` + - ✅ `cryptoKey.usages` +- ✅ Class: `CryptoKeyPair` + - ✅ `cryptoKeyPair.privateKey` + - ✅ `cryptoKeyPair.publicKey` +- 🚧 Class: `CryptoSubtle` + - (see below) # `SubtleCrypto` -* 🚧 Class: `SubtleCrypto` - * ✅ static `supports(operation, algorithm[, lengthOrAdditionalAlgorithm])` - * ❌ `subtle.decapsulateBits(decapsulationAlgorithm, decapsulationKey, ciphertext)` - * ❌ `subtle.decapsulateKey(decapsulationAlgorithm, decapsulationKey, ciphertext, sharedKeyAlgorithm, extractable, usages)` - * ✅ `subtle.decrypt(algorithm, key, data)` - * ✅ `subtle.deriveBits(algorithm, baseKey, length)` - * ✅ `subtle.deriveKey(algorithm, baseKey, derivedKeyAlgorithm, extractable, keyUsages)` - * 🚧 `subtle.digest(algorithm, data)` - * ❌ `subtle.encapsulateBits(encapsulationAlgorithm, encapsulationKey)` - * ❌ `subtle.encapsulateKey(encapsulationAlgorithm, encapsulationKey, sharedKeyAlgorithm, extractable, usages)` - * 🚧 `subtle.encrypt(algorithm, key, data)` - * 🚧 `subtle.exportKey(format, key)` - * 🚧 `subtle.generateKey(algorithm, extractable, keyUsages)` - * ✅ `subtle.getPublicKey(key, keyUsages)` - * 🚧 `subtle.importKey(format, keyData, algorithm, extractable, keyUsages)` - * ✅ `subtle.sign(algorithm, key, data)` - * ✅ `subtle.unwrapKey(format, wrappedKey, unwrappingKey, unwrapAlgo, unwrappedKeyAlgo, extractable, keyUsages)` - * ✅ `subtle.verify(algorithm, key, signature, data)` - * ✅ `subtle.wrapKey(format, key, wrappingKey, wrapAlgo)` +- 🚧 Class: `SubtleCrypto` + - ✅ static `supports(operation, algorithm[, lengthOrAdditionalAlgorithm])` + - ❌ `subtle.decapsulateBits(decapsulationAlgorithm, decapsulationKey, ciphertext)` + - ❌ `subtle.decapsulateKey(decapsulationAlgorithm, decapsulationKey, ciphertext, sharedKeyAlgorithm, extractable, usages)` + - ✅ `subtle.decrypt(algorithm, key, data)` + - ✅ `subtle.deriveBits(algorithm, baseKey, length)` + - ✅ `subtle.deriveKey(algorithm, baseKey, derivedKeyAlgorithm, extractable, keyUsages)` + - 🚧 `subtle.digest(algorithm, data)` + - ❌ `subtle.encapsulateBits(encapsulationAlgorithm, encapsulationKey)` + - ❌ `subtle.encapsulateKey(encapsulationAlgorithm, encapsulationKey, sharedKeyAlgorithm, extractable, usages)` + - 🚧 `subtle.encrypt(algorithm, key, data)` + - 🚧 `subtle.exportKey(format, key)` + - 🚧 `subtle.generateKey(algorithm, extractable, keyUsages)` + - ✅ `subtle.getPublicKey(key, keyUsages)` + - 🚧 `subtle.importKey(format, keyData, algorithm, extractable, keyUsages)` + - ✅ `subtle.sign(algorithm, key, data)` + - ✅ `subtle.unwrapKey(format, wrappedKey, unwrappingKey, unwrapAlgo, unwrappedKeyAlgo, extractable, keyUsages)` + - ✅ `subtle.verify(algorithm, key, signature, data)` + - ✅ `subtle.wrapKey(format, key, wrappingKey, wrapAlgo)` ## `subtle.decrypt` -| Algorithm | Status | -| --------- | :----: | -| `RSA-OAEP` | ✅ | -| `AES-CTR` | ✅ | -| `AES-CBC` | ✅ | -| `AES-GCM` | ✅ | -| `AES-OCB` | ✅ | -| `ChaCha20-Poly1305` | ✅ | + +| Algorithm | Status | +| ------------------- | :----: | +| `RSA-OAEP` | ✅ | +| `AES-CTR` | ✅ | +| `AES-CBC` | ✅ | +| `AES-GCM` | ✅ | +| `AES-OCB` | ✅ | +| `ChaCha20-Poly1305` | ✅ | ## `subtle.deriveBits` + | Algorithm | Status | -| --------- | :----: | -| `Argon2d` | ✅ | -| `Argon2i` | ✅ | -| `Argon2id` | ✅ | -| `ECDH` | ✅ | -| `X25519` | ✅ | -| `X448` | ✅ | -| `HKDF` | ✅ | -| `PBKDF2` | ✅ | +| ---------- | :----: | +| `Argon2d` | ✅ | +| `Argon2i` | ✅ | +| `Argon2id` | ✅ | +| `ECDH` | ✅ | +| `X25519` | ✅ | +| `X448` | ✅ | +| `HKDF` | ✅ | +| `PBKDF2` | ✅ | ## `subtle.deriveKey` + | Algorithm | Status | -| --------- | :----: | -| `Argon2d` | ✅ | -| `Argon2i` | ✅ | -| `Argon2id` | ✅ | -| `ECDH` | ✅ | -| `HKDF` | ✅ | -| `PBKDF2` | ✅ | -| `X25519` | ✅ | -| `X448` | ✅ | +| ---------- | :----: | +| `Argon2d` | ✅ | +| `Argon2i` | ✅ | +| `Argon2id` | ✅ | +| `ECDH` | ✅ | +| `HKDF` | ✅ | +| `PBKDF2` | ✅ | +| `X25519` | ✅ | +| `X448` | ✅ | ## `subtle.digest` + | Algorithm | Status | -| --------- | :----: | -| `cSHAKE128` | ❌ | -| `cSHAKE256` | ❌ | -| `SHA-1` | ✅ | -| `SHA-256` | ✅ | -| `SHA-384` | ✅ | -| `SHA-512` | ✅ | -| `SHA3-256` | ❌ | -| `SHA3-384` | ❌ | -| `SHA3-512` | ❌ | +| ----------- | :----: | +| `cSHAKE128` | ❌ | +| `cSHAKE256` | ❌ | +| `SHA-1` | ✅ | +| `SHA-256` | ✅ | +| `SHA-384` | ✅ | +| `SHA-512` | ✅ | +| `SHA3-256` | ❌ | +| `SHA3-384` | ❌ | +| `SHA3-512` | ❌ | ## `subtle.encrypt` + | Algorithm | Status | | ------------------- | :----: | -| `AES-CTR` | ✅ | -| `AES-CBC` | ✅ | -| `AES-GCM` | ✅ | -| `AES-OCB` | ✅ | -| `ChaCha20-Poly1305` | ✅ | -| `RSA-OAEP` | ✅ | +| `AES-CTR` | ✅ | +| `AES-CBC` | ✅ | +| `AES-GCM` | ✅ | +| `AES-OCB` | ✅ | +| `ChaCha20-Poly1305` | ✅ | +| `RSA-OAEP` | ✅ | ## `subtle.exportKey` + | Key Type | `spki` | `pkcs8` | `jwk` | `raw` | `raw-secret` | `raw-public` | `raw-seed` | | ------------------- | :----: | :-----: | :---: | :---: | :----------: | :----------: | :--------: | -| `AES-CBC` | | | ✅ | ✅ | ✅ | | | -| `AES-CTR` | | | ✅ | ✅ | ✅ | | | -| `AES-GCM` | | | ✅ | ✅ | ✅ | | | -| `AES-KW` | | | ✅ | ✅ | ✅ | | | -| `AES-OCB` | | | ✅ | ✅ | ✅ | | | -| `ChaCha20-Poly1305` | | | ✅ | | ✅ | | | -| `ECDH` | ✅ | ✅ | ✅ | ✅ | | ✅ | | -| `ECDSA` | ✅ | ✅ | ✅ | ✅ | | ✅ | | -| `Ed25519` | ✅ | ✅ | ✅ | ✅ | | ✅ | | -| `Ed448` | ✅ | ✅ | ✅ | ✅ | | ✅ | | -| `HMAC` | | | ✅ | ✅ | ✅ | | | -| `ML-DSA-44` | ✅ | ✅ | ✅ | | | ✅ | ✅ | -| `ML-DSA-65` | ✅ | ✅ | ✅ | | | ✅ | ✅ | -| `ML-DSA-87` | ✅ | ✅ | ✅ | | | ✅ | ✅ | -| `ML-KEM-512` | ❌ | ❌ | | | | ❌ | ❌ | -| `ML-KEM-768` | ❌ | ❌ | | | | ❌ | ❌ | -| `ML-KEM-1024` | ❌ | ❌ | | | | ❌ | ❌ | -| `RSA-OAEP` | ✅ | ✅ | ✅ | | | | | -| `RSA-PSS` | ✅ | ✅ | ✅ | | | | | -| `RSASSA-PKCS1-v1_5` | ✅ | ✅ | ✅ | | | | | - -* ` ` - not implemented in Node -* ❌ - implemented in Node, not RNQC -* ✅ - implemented in Node and RNQC +| `AES-CBC` | | | ✅ | ✅ | ✅ | | | +| `AES-CTR` | | | ✅ | ✅ | ✅ | | | +| `AES-GCM` | | | ✅ | ✅ | ✅ | | | +| `AES-KW` | | | ✅ | ✅ | ✅ | | | +| `AES-OCB` | | | ✅ | ✅ | ✅ | | | +| `ChaCha20-Poly1305` | | | ✅ | | ✅ | | | +| `ECDH` | ✅ | ✅ | ✅ | ✅ | | ✅ | | +| `ECDSA` | ✅ | ✅ | ✅ | ✅ | | ✅ | | +| `Ed25519` | ✅ | ✅ | ✅ | ✅ | | ✅ | | +| `Ed448` | ✅ | ✅ | ✅ | ✅ | | ✅ | | +| `HMAC` | | | ✅ | ✅ | ✅ | | | +| `ML-DSA-44` | ✅ | ✅ | ✅ | | | ✅ | ✅ | +| `ML-DSA-65` | ✅ | ✅ | ✅ | | | ✅ | ✅ | +| `ML-DSA-87` | ✅ | ✅ | ✅ | | | ✅ | ✅ | +| `ML-KEM-512` | ❌ | ❌ | | | | ❌ | ❌ | +| `ML-KEM-768` | ❌ | ❌ | | | | ❌ | ❌ | +| `ML-KEM-1024` | ❌ | ❌ | | | | ❌ | ❌ | +| `RSA-OAEP` | ✅ | ✅ | ✅ | | | | | +| `RSA-PSS` | ✅ | ✅ | ✅ | | | | | +| `RSASSA-PKCS1-v1_5` | ✅ | ✅ | ✅ | | | | | + +- ` ` - not implemented in Node +- ❌ - implemented in Node, not RNQC +- ✅ - implemented in Node and RNQC ## `subtle.generateKey` ### `CryptoKeyPair` algorithms + | Algorithm | Status | -| --------- | :----: | -| `ECDH` | ✅ | -| `ECDSA` | ✅ | -| `Ed25519` | ✅ | -| `Ed448` | ✅ | -| `ML-DSA-44` | ✅ | -| `ML-DSA-65` | ✅ | -| `ML-DSA-87` | ✅ | -| `ML-KEM-512` | ❌ | -| `ML-KEM-768` | ❌ | -| `ML-KEM-1024` | ❌ | -| `RSA-OAEP` | ✅ | -| `RSA-PSS` | ✅ | -| `RSASSA-PKCS1-v1_5` | ✅ | -| `X25519` | ✅ | -| `X448` | ✅ | +| ------------------- | :----: | +| `ECDH` | ✅ | +| `ECDSA` | ✅ | +| `Ed25519` | ✅ | +| `Ed448` | ✅ | +| `ML-DSA-44` | ✅ | +| `ML-DSA-65` | ✅ | +| `ML-DSA-87` | ✅ | +| `ML-KEM-512` | ❌ | +| `ML-KEM-768` | ❌ | +| `ML-KEM-1024` | ❌ | +| `RSA-OAEP` | ✅ | +| `RSA-PSS` | ✅ | +| `RSASSA-PKCS1-v1_5` | ✅ | +| `X25519` | ✅ | +| `X448` | ✅ | ### `CryptoKey` algorithms + | Algorithm | Status | -| --------- | :----: | -| `AES-CTR` | ✅ | -| `AES-CBC` | ✅ | -| `AES-GCM` | ✅ | -| `AES-KW` | ✅ | -| `AES-OCB` | ✅ | -| `ChaCha20-Poly1305` | ✅ | -| `HMAC` | ✅ | -| `KMAC128` | ❌ | -| `KMAC256` | ❌ | +| ------------------- | :----: | +| `AES-CTR` | ✅ | +| `AES-CBC` | ✅ | +| `AES-GCM` | ✅ | +| `AES-KW` | ✅ | +| `AES-OCB` | ✅ | +| `ChaCha20-Poly1305` | ✅ | +| `HMAC` | ✅ | +| `KMAC128` | ❌ | +| `KMAC256` | ❌ | ## `subtle.importKey` + | Key Type | `spki` | `pkcs8` | `jwk` | `raw` | `raw-secret` | `raw-public` | `raw-seed` | | ------------------- | :----: | :-----: | :---: | :---: | :----------: | :----------: | :--------: | -| `Argon2d` | | | | | ✅ | | | -| `Argon2i` | | | | | ✅ | | | -| `Argon2id` | | | | | ✅ | | | -| `AES-CBC` | | | ✅ | ✅ | ✅ | | | -| `AES-CTR` | | | ✅ | ✅ | ✅ | | | -| `AES-GCM` | | | ✅ | ✅ | ✅ | | | -| `AES-KW` | | | ✅ | ✅ | ✅ | | | -| `AES-OCB` | | | ✅ | ✅ | ✅ | | | -| `ChaCha20-Poly1305` | | | ✅ | | ✅ | | | -| `ECDH` | ✅ | ✅ | ✅ | ✅ | | ✅ | | -| `ECDSA` | ✅ | ✅ | ✅ | ✅ | | ✅ | | -| `Ed25519` | ✅ | ✅ | ✅ | ✅ | | ✅ | | -| `Ed448` | ✅ | ✅ | ✅ | ✅ | | ✅ | | -| `HKDF` | | | | ✅ | ✅ | | | -| `HMAC` | | | ✅ | ✅ | ✅ | | | -| `ML-DSA-44` | ✅ | ✅ | ✅ | | | ✅ | ✅ | -| `ML-DSA-65` | ✅ | ✅ | ✅ | | | ✅ | ✅ | -| `ML-DSA-87` | ✅ | ✅ | ✅ | | | ✅ | ✅ | -| `ML-KEM-512` | ❌ | ❌ | | | | ❌ | ❌ | -| `ML-KEM-768` | ❌ | ❌ | | | | ❌ | ❌ | -| `ML-KEM-1024` | ❌ | ❌ | | | | ❌ | ❌ | -| `PBKDF2` | | | | ✅ | ✅ | | | -| `RSA-OAEP` | ✅ | ✅ | ✅ | | | | | -| `RSA-PSS` | ✅ | ✅ | ✅ | | | | | -| `RSASSA-PKCS1-v1_5` | ✅ | ✅ | ✅ | | | | | -| `X25519` | ✅ | ✅ | ✅ | ✅ | | ✅ | | -| `X448` | ✅ | ✅ | ✅ | ✅ | | ✅ | | +| `Argon2d` | | | | | ✅ | | | +| `Argon2i` | | | | | ✅ | | | +| `Argon2id` | | | | | ✅ | | | +| `AES-CBC` | | | ✅ | ✅ | ✅ | | | +| `AES-CTR` | | | ✅ | ✅ | ✅ | | | +| `AES-GCM` | | | ✅ | ✅ | ✅ | | | +| `AES-KW` | | | ✅ | ✅ | ✅ | | | +| `AES-OCB` | | | ✅ | ✅ | ✅ | | | +| `ChaCha20-Poly1305` | | | ✅ | | ✅ | | | +| `ECDH` | ✅ | ✅ | ✅ | ✅ | | ✅ | | +| `ECDSA` | ✅ | ✅ | ✅ | ✅ | | ✅ | | +| `Ed25519` | ✅ | ✅ | ✅ | ✅ | | ✅ | | +| `Ed448` | ✅ | ✅ | ✅ | ✅ | | ✅ | | +| `HKDF` | | | | ✅ | ✅ | | | +| `HMAC` | | | ✅ | ✅ | ✅ | | | +| `ML-DSA-44` | ✅ | ✅ | ✅ | | | ✅ | ✅ | +| `ML-DSA-65` | ✅ | ✅ | ✅ | | | ✅ | ✅ | +| `ML-DSA-87` | ✅ | ✅ | ✅ | | | ✅ | ✅ | +| `ML-KEM-512` | ❌ | ❌ | | | | ❌ | ❌ | +| `ML-KEM-768` | ❌ | ❌ | | | | ❌ | ❌ | +| `ML-KEM-1024` | ❌ | ❌ | | | | ❌ | ❌ | +| `PBKDF2` | | | | ✅ | ✅ | | | +| `RSA-OAEP` | ✅ | ✅ | ✅ | | | | | +| `RSA-PSS` | ✅ | ✅ | ✅ | | | | | +| `RSASSA-PKCS1-v1_5` | ✅ | ✅ | ✅ | | | | | +| `X25519` | ✅ | ✅ | ✅ | ✅ | | ✅ | | +| `X448` | ✅ | ✅ | ✅ | ✅ | | ✅ | | ## `subtle.sign` + | Algorithm | Status | -| --------- | :----: | -| `ECDSA` | ✅ | -| `Ed25519` | ✅ | -| `Ed448` | ✅ | -| `HMAC` | ✅ | -| `KMAC128` | ❌ | -| `KMAC256` | ❌ | -| `ML-DSA-44` | ✅ | -| `ML-DSA-65` | ✅ | -| `ML-DSA-87` | ✅ | -| `RSA-PSS` | ✅ | -| `RSASSA-PKCS1-v1_5` | ✅ | +| ------------------- | :----: | +| `ECDSA` | ✅ | +| `Ed25519` | ✅ | +| `Ed448` | ✅ | +| `HMAC` | ✅ | +| `KMAC128` | ❌ | +| `KMAC256` | ❌ | +| `ML-DSA-44` | ✅ | +| `ML-DSA-65` | ✅ | +| `ML-DSA-87` | ✅ | +| `RSA-PSS` | ✅ | +| `RSASSA-PKCS1-v1_5` | ✅ | ## `subtle.unwrapKey` ### wrapping algorithms + | Algorithm | Status | | ------------------- | :----: | -| `AES-CBC` | ✅ | -| `AES-CTR` | ✅ | -| `AES-GCM` | ✅ | -| `AES-KW` | ✅ | -| `AES-OCB` | ✅ | -| `ChaCha20-Poly1305` | ✅ | -| `RSA-OAEP` | ✅ | +| `AES-CBC` | ✅ | +| `AES-CTR` | ✅ | +| `AES-GCM` | ✅ | +| `AES-KW` | ✅ | +| `AES-OCB` | ✅ | +| `ChaCha20-Poly1305` | ✅ | +| `RSA-OAEP` | ✅ | ### unwrapped key algorithms + | Algorithm | Status | -| --------- | :----: | -| `AES-CBC` | ✅ | -| `AES-CTR` | ✅ | -| `AES-GCM` | ✅ | -| `AES-KW` | ✅ | -| `AES-OCB` | ✅ | -| `ChaCha20-Poly1305` | ✅ | -| `ECDH` | ✅ | -| `ECDSA` | ✅ | -| `Ed25519` | ✅ | -| `Ed448` | ✅ | -| `HMAC` | ✅ | -| `ML-DSA-44` | ✅ | -| `ML-DSA-65` | ✅ | -| `ML-DSA-87` | ✅ | -| `ML-KEM-512` | ❌ | -| `ML-KEM-768` | ❌ | -| `ML-KEM-1024` | ❌ | -| `RSA-OAEP` | ✅ | -| `RSA-PSS` | ✅ | -| `RSASSA-PKCS1-v1_5` | ✅ | -| `X25519` | ✅ | -| `X448` | ✅ | +| ------------------- | :----: | +| `AES-CBC` | ✅ | +| `AES-CTR` | ✅ | +| `AES-GCM` | ✅ | +| `AES-KW` | ✅ | +| `AES-OCB` | ✅ | +| `ChaCha20-Poly1305` | ✅ | +| `ECDH` | ✅ | +| `ECDSA` | ✅ | +| `Ed25519` | ✅ | +| `Ed448` | ✅ | +| `HMAC` | ✅ | +| `ML-DSA-44` | ✅ | +| `ML-DSA-65` | ✅ | +| `ML-DSA-87` | ✅ | +| `ML-KEM-512` | ❌ | +| `ML-KEM-768` | ❌ | +| `ML-KEM-1024` | ❌ | +| `RSA-OAEP` | ✅ | +| `RSA-PSS` | ✅ | +| `RSASSA-PKCS1-v1_5` | ✅ | +| `X25519` | ✅ | +| `X448` | ✅ | ## `subtle.verify` + | Algorithm | Status | -| --------- | :----: | -| `ECDSA` | ✅ | -| `Ed25519` | ✅ | -| `Ed448` | ✅ | -| `HMAC` | ✅ | -| `KMAC128` | ❌ | -| `KMAC256` | ❌ | -| `ML-DSA-44` | ✅ | -| `ML-DSA-65` | ✅ | -| `ML-DSA-87` | ✅ | -| `RSA-PSS` | ✅ | -| `RSASSA-PKCS1-v1_5` | ✅ | +| ------------------- | :----: | +| `ECDSA` | ✅ | +| `Ed25519` | ✅ | +| `Ed448` | ✅ | +| `HMAC` | ✅ | +| `KMAC128` | ❌ | +| `KMAC256` | ❌ | +| `ML-DSA-44` | ✅ | +| `ML-DSA-65` | ✅ | +| `ML-DSA-87` | ✅ | +| `RSA-PSS` | ✅ | +| `RSASSA-PKCS1-v1_5` | ✅ | ## `subtle.wrapKey` ### wrapping algorithms + | Algorithm | Status | | ------------------- | :----: | -| `AES-CBC` | ✅ | -| `AES-CTR` | ✅ | -| `AES-GCM` | ✅ | -| `AES-KW` | ✅ | -| `AES-OCB` | ✅ | -| `ChaCha20-Poly1305` | ✅ | -| `RSA-OAEP` | ✅ | +| `AES-CBC` | ✅ | +| `AES-CTR` | ✅ | +| `AES-GCM` | ✅ | +| `AES-KW` | ✅ | +| `AES-OCB` | ✅ | +| `ChaCha20-Poly1305` | ✅ | +| `RSA-OAEP` | ✅ | diff --git a/docs/content/docs/api/meta.json b/docs/content/docs/api/meta.json index 970d0711..a2e4696c 100644 --- a/docs/content/docs/api/meta.json +++ b/docs/content/docs/api/meta.json @@ -1,23 +1,24 @@ { - "title": "API Reference", - "defaultOpen": true, - "pages": [ - "index", - "install", - "cipher", - "hash", - "hmac", - "random", - "keys", - "signing", - "public-cipher", - "diffie-hellman", - "ecdh", - "ed25519", - "pbkdf2", - "scrypt", - "hkdf", - "blake3", - "subtle" - ] -} \ No newline at end of file + "title": "API Reference", + "defaultOpen": true, + "pages": [ + "index", + "install", + "cipher", + "hash", + "hmac", + "random", + "keys", + "signing", + "public-cipher", + "diffie-hellman", + "ecdh", + "ed25519", + "pbkdf2", + "scrypt", + "hkdf", + "blake3", + "x509", + "subtle" + ] +} diff --git a/docs/content/docs/api/x509.mdx b/docs/content/docs/api/x509.mdx new file mode 100644 index 00000000..584157a0 --- /dev/null +++ b/docs/content/docs/api/x509.mdx @@ -0,0 +1,297 @@ +--- +title: X509 Certificates +description: Parse, inspect, and validate X.509 certificates +--- + +import { Callout } from 'fumadocs-ui/components/callout'; +import { TypeTable } from 'fumadocs-ui/components/type-table'; + +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. + + + **Certificate pinning** in mobile apps, **mTLS client certificate** + validation, **certificate chain verification**, **hostname matching** for + custom TLS implementations, and **extracting public keys** from certificates. + + +## Table of Contents + +- [Theory](#theory) +- [Class: X509Certificate](#class-x509certificate) +- [Properties](#properties) +- [Methods](#methods) +- [Real-World Examples](#real-world-examples) + +## Theory + +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). + +Key concepts: + +1. **Subject / Issuer**: Distinguished Names identifying the certificate holder and signer. +2. **Validity Period**: Time window during which the certificate is valid. +3. **Subject Alternative Name (SAN)**: Additional identities (DNS names, IPs, emails) the certificate is valid for. +4. **Fingerprint**: A hash of the certificate used for identification (not security). +5. **CA flag**: Whether the certificate can sign other certificates. + +--- + +## Class: X509Certificate + +### Constructor + +```ts +import { X509Certificate } from 'react-native-quick-crypto'; + +const cert = new X509Certificate(pemString); +``` + +**Parameters:** + + + +Accepts both PEM-encoded strings (beginning with `-----BEGIN CERTIFICATE-----`) and DER-encoded binary data. + +--- + +## Properties + +All properties are lazily computed and cached on first access. + +| Property | Type | Description | +| :---------------------- | :---------- | :--------------------------------------------------------- | +| `subject` | `string` | Distinguished name of the certificate subject | +| `issuer` | `string` | Distinguished name of the issuing CA | +| `subjectAltName` | `string` | Subject Alternative Name extension | +| `infoAccess` | `string` | Authority Information Access extension | +| `validFrom` | `string` | "Not Before" date as a string | +| `validTo` | `string` | "Not After" date as a string | +| `validFromDate` | `Date` | "Not Before" as a JavaScript Date object | +| `validToDate` | `Date` | "Not After" as a JavaScript Date object | +| `serialNumber` | `string` | Certificate serial number (uppercase hex) | +| `signatureAlgorithm` | `string` | Signature algorithm name (e.g., `sha256WithRSAEncryption`) | +| `signatureAlgorithmOid` | `string` | Signature algorithm OID | +| `fingerprint` | `string` | SHA-1 fingerprint (colon-separated hex) | +| `fingerprint256` | `string` | SHA-256 fingerprint (colon-separated hex) | +| `fingerprint512` | `string` | SHA-512 fingerprint (colon-separated hex) | +| `extKeyUsage` | `string[]` | Extended key usage OIDs (also available as `keyUsage`) | +| `ca` | `boolean` | Whether this is a CA certificate | +| `raw` | `Buffer` | Raw DER-encoded certificate bytes | +| `publicKey` | `KeyObject` | The certificate's public key as a KeyObject | +| `issuerCertificate` | `undefined` | Always `undefined` (no TLS context in React Native) | + +```ts +const cert = new X509Certificate(pemString); + +console.log(cert.subject); +// C=US\nST=California\nO=Example\nCN=example.com + +console.log(cert.fingerprint256); +// AB:CD:EF:12:34:... + +console.log(cert.ca); +// true + +console.log(cert.publicKey.type); +// 'public' +``` + +--- + +## Methods + +### x509.checkHost(name[, options]) + +Checks whether the certificate matches the given hostname. + + + +**Returns:** `string | undefined` — The matched hostname, or `undefined` if no match. + +```ts +const cert = new X509Certificate(pemString); + +cert.checkHost('example.com'); // 'example.com' +cert.checkHost('wrong.com'); // undefined + +// Disable wildcard matching +cert.checkHost('sub.example.com', { wildcards: false }); +``` + +#### CheckOptions + +| Option | Type | Default | Description | +| :---------------------- | :--------------------------------- | :---------- | :---------------------------------- | +| `subject` | `'default' \| 'always' \| 'never'` | `'default'` | When to check the subject CN | +| `wildcards` | `boolean` | `true` | Allow wildcard certificate matching | +| `partialWildcards` | `boolean` | `true` | Allow partial wildcard matching | +| `multiLabelWildcards` | `boolean` | `false` | Allow multi-label wildcard matching | +| `singleLabelSubdomains` | `boolean` | `false` | Match single-label subdomains | + +### x509.checkEmail(email[, options]) + +Checks whether the certificate matches the given email address. + +**Returns:** `string | undefined` — The matched email, or `undefined` if no match. + +```ts +cert.checkEmail('user@example.com'); // 'user@example.com' or undefined +``` + +### x509.checkIP(ip) + +Checks whether the certificate matches the given IP address. + +**Returns:** `string | undefined` — The matched IP, or `undefined` if no match. + +```ts +cert.checkIP('127.0.0.1'); // '127.0.0.1' +cert.checkIP('192.168.1.1'); // undefined +``` + +### x509.checkIssued(otherCert) + +Checks whether this certificate was issued by `otherCert`. + +**Returns:** `boolean` + +```ts +// Self-signed certificate +cert.checkIssued(cert); // true + +// Chain validation +rootCert.checkIssued(intermediateCert); // true or false +``` + +### x509.checkPrivateKey(privateKey) + +Checks whether the given private key matches this certificate's public key. + +**Returns:** `boolean` + +```ts +import { createPrivateKey } from 'react-native-quick-crypto'; + +const privKey = createPrivateKey(privateKeyPem); +cert.checkPrivateKey(privKey); // true +``` + +### x509.verify(publicKey) + +Verifies that the certificate was signed with the given public key. + +**Returns:** `boolean` + +```ts +// For self-signed certificates +cert.verify(cert.publicKey); // true +``` + +### x509.toString() + +Returns the PEM-encoded certificate string. + +**Returns:** `string` + +### x509.toJSON() + +Returns the PEM-encoded certificate string (same as `toString()`). + +**Returns:** `string` + +### x509.toLegacyObject() + +Returns a plain object with legacy certificate fields. + +**Returns:** `object` + +--- + +## Real-World Examples + +### Certificate Pinning + +```ts +import { X509Certificate } from 'react-native-quick-crypto'; + +const PINNED_FINGERPRINT = 'AB:CD:EF:...'; + +function validateServerCert(pemCert: string): boolean { + const cert = new X509Certificate(pemCert); + + // Check fingerprint + if (cert.fingerprint256 !== PINNED_FINGERPRINT) { + return false; + } + + // Check validity + const now = new Date(); + if (now < cert.validFromDate || now > cert.validToDate) { + return false; + } + + return true; +} +``` + +### Hostname Verification + +```ts +import { X509Certificate } from 'react-native-quick-crypto'; + +function verifyHostname(pemCert: string, hostname: string): boolean { + const cert = new X509Certificate(pemCert); + return cert.checkHost(hostname) !== undefined; +} +``` + +### Extract Public Key from Certificate + +```ts +import { X509Certificate } from 'react-native-quick-crypto'; + +const cert = new X509Certificate(pemCert); +const publicKey = cert.publicKey; + +// Use the public key for encryption or verification +console.log(publicKey.type); // 'public' +console.log(publicKey.asymmetricKeyType); // 'rsa' +``` + +### Validate Certificate Chain + +```ts +import { X509Certificate } from 'react-native-quick-crypto'; + +function validateChain(leafPem: string, issuerPem: string): boolean { + const leaf = new X509Certificate(leafPem); + const issuerCert = new X509Certificate(issuerPem); + + // Check the leaf was issued by the issuer + if (!issuerCert.checkIssued(leaf)) { + return false; + } + + // Verify the leaf's signature with issuer's public key + if (!leaf.verify(issuerCert.publicKey)) { + return false; + } + + return true; +} +``` diff --git a/docs/data/coverage.ts b/docs/data/coverage.ts index ebd7a85a..33f8110c 100644 --- a/docs/data/coverage.ts +++ b/docs/data/coverage.ts @@ -130,7 +130,36 @@ export const COVERAGE_DATA: CoverageCategory[] = [ }, { name: 'X509Certificate', - status: 'missing', + subItems: [ + { name: 'new X509Certificate(buffer)', status: 'implemented' }, + { name: 'ca', status: 'implemented' }, + { name: 'checkEmail', status: 'implemented' }, + { name: 'checkHost', status: 'implemented' }, + { name: 'checkIP', status: 'implemented' }, + { name: 'checkIssued', status: 'implemented' }, + { name: 'checkPrivateKey', status: 'implemented' }, + { name: 'fingerprint', status: 'implemented' }, + { name: 'fingerprint256', status: 'implemented' }, + { name: 'fingerprint512', status: 'implemented' }, + { name: 'infoAccess', status: 'implemented' }, + { name: 'issuer', status: 'implemented' }, + { name: 'issuerCertificate', status: 'implemented' }, + { name: 'extKeyUsage', status: 'implemented' }, + { name: 'keyUsage', status: 'implemented' }, + { name: 'signatureAlgorithm', status: 'implemented' }, + { name: 'signatureAlgorithmOid', status: 'implemented' }, + { name: 'publicKey', status: 'implemented' }, + { name: 'raw', status: 'implemented' }, + { name: 'serialNumber', status: 'implemented' }, + { name: 'subject', status: 'implemented' }, + { name: 'subjectAltName', status: 'implemented' }, + { name: 'toJSON', status: 'implemented' }, + { name: 'toLegacyObject', status: 'implemented' }, + { name: 'toString', status: 'implemented' }, + { name: 'validFrom', status: 'implemented' }, + { name: 'validTo', status: 'implemented' }, + { name: 'verify', status: 'implemented' }, + ], }, ], }, diff --git a/example/src/hooks/useTestsList.ts b/example/src/hooks/useTestsList.ts index 100c12a5..88b3cdcc 100644 --- a/example/src/hooks/useTestsList.ts +++ b/example/src/hooks/useTestsList.ts @@ -43,6 +43,7 @@ import '../tests/subtle/supports'; import '../tests/subtle/getPublicKey'; import '../tests/subtle/wrap_unwrap'; import '../tests/utils/utils_tests'; +import '../tests/x509/x509_tests'; export const useTestsList = (): [ TestSuites, diff --git a/example/src/tests/x509/x509_tests.ts b/example/src/tests/x509/x509_tests.ts new file mode 100644 index 00000000..6cb33520 --- /dev/null +++ b/example/src/tests/x509/x509_tests.ts @@ -0,0 +1,310 @@ +import { test } from '../util'; +import { assert } from 'chai'; +import { + X509Certificate, + createPrivateKey, + generateKeyPairSync, + Buffer, +} from 'react-native-quick-crypto'; + +const SUITE = 'x509'; + +const certPem = `-----BEGIN CERTIFICATE----- +MIIEgDCCA2igAwIBAgIUYX7QpAhywlWSvMIGfIhcXyF1S6kwDQYJKoZIhvcNAQEL +BQAwezELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM +DVNhbiBGcmFuY2lzY28xEjAQBgNVBAoMCVJOUUMgVGVzdDEQMA4GA1UECwwHVGVz +dGluZzEZMBcGA1UEAwwQdGVzdC5leGFtcGxlLmNvbTAgFw0yNjAyMTYyMjM5MTRa +GA8yMTI2MDEyMzIyMzkxNFowezELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlm +b3JuaWExFjAUBgNVBAcMDVNhbiBGcmFuY2lzY28xEjAQBgNVBAoMCVJOUUMgVGVz +dDEQMA4GA1UECwwHVGVzdGluZzEZMBcGA1UEAwwQdGVzdC5leGFtcGxlLmNvbTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMNdAMBDbRU6Sowe7xs+N2Lr +lrLXYjMjOxIm3ycfuQCK4EpmaJ+WLNctTF8DP7bfo9U0ItJawAbFdMVLWKOSzHmb +ZpCpB9qUSycBOdKgcepgHm9seoF8IdQWSXF5MNx73e6KOITPNfQ1XAQ/bcNMQ52Z +rDQBj/Usu4+VOKiL+9sjFoP8z2MLhHKrVcmuJFLmZek84wWT5zkbaBSRC4ZP6xTk +wITP5OGGmpTliZ1ZfvZ1bce+H0pPiDDJB1P1sOFhUW+f9eABUQnNUB95XnqY78Sd +zhwvgYLsBZIMFCu8tLv6TT/kp2eqIPnr7KVSI6PqVA2KeYaIzAtcJCrfyj59/0EC +AwEAAaOB+TCB9jAdBgNVHQ4EFgQUyvtMod1JR/MyOywSthOoSzudfckwHwYDVR0j +BBgwFoAUyvtMod1JR/MyOywSthOoSzudfckwQgYDVR0RBDswOYIQdGVzdC5leGFt +cGxlLmNvbYINKi5leGFtcGxlLmNvbYcEfwAAAYEQdGVzdEBleGFtcGxlLmNvbTAP +BgNVHRMBAf8EBTADAQH/MAsGA1UdDwQEAwICpDAdBgNVHSUEFjAUBggrBgEFBQcD +AQYIKwYBBQUHAwIwMwYIKwYBBQUHAQEEJzAlMCMGCCsGAQUFBzABhhdodHRwOi8v +b2NzcC5leGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEARipMQsNnagHHLQxz +zSbiKKB6Qrxt0k4IwEIyIKb4daZaXw9viMkS9ULm0uHmO7HcOr6wUYdmv+swFsC +yu5E8ZgFqZHJGw62Yi6fhNSloaLNN9rYnOZfUj0aWnN0OA8vClfNom/vYTe4kENU +VTDP1dkPbo12jWJ4bOhchW28GSjU7heosi8tNsFr5H7cdAwXKnOmU0MqeJ+dHCda +1MiZWlDTeV2q8HRIKPuH5xmwgVZO3U7C85NekB7tZIvf5fArvKPRQ0/mzcvk+F6A +/tQwqNjZv+XgUNnZJkUkYAQ5nJg50Osf1oxR182oAjR2yqXL3qBUfg563wgleVFY +KxJhZsg== +-----END CERTIFICATE-----`; + +const privKeyPem = `-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDDXQDAQ20VOkqM +Hu8bPjdi65ay12IzIzsSJt8nH7kAiuBKZmiflizXLUxfAz+236PVNCLSWsAGxXTF +S1ijksx5m2aQqQfalEsnATnSoHHqYB5vbHqBfCHUFklxeTDce93uijiEzzX0NVwE +P23DTEOdmaw0AY/1LLuPlTioi/vbIxaD/M9jC4Ryq1XJriRS5mXpPOMFk+c5G2gU +kQuGT+sU5MCEz+ThhpqU5YmdWX72dW3Hvh9KT4gwyQdT9bDhYVFvn/XgAVEJzVAf +eV56mO/Enc4cL4GC7AWSDBQrvLS7+k0/5KdnqiD56+ylUiOj6lQNinmGiMwLXCQq +38o+ff9BAgMBAAECggEAG8LeBfQu4+WAzWY3QmRLbjudvQkyk6NjKVervegEm96g +M60CG1dgRNgo3OwzGXNbLm32WspO35zJ1KAPLE4CehoKmkONcafsAVLBGudLeMDd +jPuEcbJS2PatILpWJqaqvqhr5/d3/8gLV4aEkaGHOYsqTMjst4F6n6iBQMuE57qA ++ubXy8nQD7ufSKPIxdD4jHg226m1FjiKnDArD4H1iIHC1xdt7E5FG5KRzwPFXU4B +kwSqh9GFFS7JdSoqwa+8MVZP2IHGrwpIkUnVEedgkA+pbVXFVfKvfOUUQ01t14Tc +OjT819vIj2av+yDfW5q0fUWdOQrs7ZLsVBLrazJnzwKBgQDiYCdeZCFh2T8GdYd8 +ZDnpLaMrFSJAYiRXbNo1aKhLGE5gee9t0dLw5wM0ARBlXb/kCFnK5HmPi9CxukuQ +EbCqIQ1+NsQNGS307QXSoQjT38uwrlp9tROb8RdaTE9xBc9gJVEcgmPDnrY0zesE +9DrUfMR7auqyYzhWSwzuPdkvdwKBgQDc7eaVoDGf0cLfQ0ezOj5NLuLve+lza0Tf +YlC2csTpzfn0GPGZdj1MOvUFYIk/r1mPDWT0vJbtlpn73lVGypKgDEAWUYvVLaKW +escMQN9Xmf1a+0Cxi+rIFUhjkLdSAdyzGawl+7rXmDXNyFx5DWAUhQTfrF2gLbZ3 +Cpgf9zqlBwKBgHsVDrK6vI/IIAVyB51xnS8UOkBleD8LXXkPXUFmywIxkAPSqITM +beW/pTU0UubaZ0gj5jZzrUiIG4tWoFkP1T9bQ0vZmRUKGLuv19ei6PrSFpzU36yz +tJq4JhtZnGP2Zb9/6q8Wkgm9lJH3WA5UgFwiDm6QPlWJrwr0OW6bwCeXAoGBAMBH +VR34M/hSiXXiim6UTFDEc8HWaFGJlIGOgYyoynRqThaB9xOG8sZ7sXAimpEQvbNh +BvJxiDHzlsS8th9MgtxEjSpfgoHgm9a3uLETbM5DOVuLvLxJd+b3ju8IrmPzNu+x +ckAEnJKy6HDW5pR8bZiuRJWe4EVeQ6XLVKbNdv7VAoGAPjEGKlGP+AVbrMQIQfEL +8AiNUKgQsaxAAQfwY3VC0kO9KoLDda/Gcq1CL3stZQplQMl0fKcNilXvHxqR+9UF +gtrj/IA4TEfNQrONszLvU5zJl4ENNLsZcEcUDVOXA+3WaNn2UZhJy8+Te8pXLhjI +FRFj+ZJzGB1ap637vnnRI+U= +-----END PRIVATE KEY-----`; + +const expectedSha1 = + 'EE:E2:BF:1F:B5:0C:AF:E4:AC:27:B7:88:2F:62:55:0D:A3:46:6F:94'; +const expectedSha256 = + '0A:71:7E:E8:7B:1C:C3:A7:2D:93:E5:13:DA:B9:69:99:D3:06:56:C8:66:62:EB:A3:F6:BF:87:75:64:2F:C3:11'; +const expectedSha512 = + 'BE:57:03:CD:09:14:7A:56:CB:CF:BF:57:FF:68:3A:EE:93:10:B3:04:39:C3:A9:99:1F:1B:4C:89:A2:1E:76:3B:96:49:79:6A:62:F1:F2:04:A9:6E:26:8A:0A:A4:4C:DF:C1:E9:39:25:8F:C1:D6:80:9A:20:B6:E7:2C:DC:6D:42'; + +// --- Construction --- + +test(SUITE, 'constructs from PEM string', () => { + const x509 = new X509Certificate(certPem); + assert.isOk(x509); +}); + +test(SUITE, 'constructs from Buffer', () => { + const x509 = new X509Certificate(Buffer.from(certPem)); + assert.isOk(x509); +}); + +test(SUITE, 'throws on invalid input', () => { + assert.throws(() => { + new X509Certificate('invalid'); + }); +}); + +// --- String properties --- + +test(SUITE, 'subject contains CN', () => { + const x509 = new X509Certificate(certPem); + assert.include(x509.subject, 'test.example.com'); +}); + +test(SUITE, 'issuer matches subject (self-signed)', () => { + const x509 = new X509Certificate(certPem); + assert.strictEqual(x509.subject, x509.issuer); +}); + +test(SUITE, 'subjectAltName contains DNS entries', () => { + const x509 = new X509Certificate(certPem); + assert.include(x509.subjectAltName, 'test.example.com'); +}); + +test(SUITE, 'subjectAltName contains IP', () => { + const x509 = new X509Certificate(certPem); + assert.include(x509.subjectAltName, '127.0.0.1'); +}); + +test(SUITE, 'subjectAltName contains email', () => { + const x509 = new X509Certificate(certPem); + assert.include(x509.subjectAltName, 'test@example.com'); +}); + +test(SUITE, 'infoAccess contains OCSP URI', () => { + const x509 = new X509Certificate(certPem); + assert.include(x509.infoAccess, 'ocsp.example.com'); +}); + +test(SUITE, 'validFrom is a date string', () => { + const x509 = new X509Certificate(certPem); + assert.include(x509.validFrom, 'Feb 16'); + assert.include(x509.validFrom, '2026'); + assert.include(x509.validFrom, 'GMT'); +}); + +test(SUITE, 'validTo is a date string', () => { + const x509 = new X509Certificate(certPem); + assert.include(x509.validTo, 'Jan 23'); + assert.include(x509.validTo, '2126'); + assert.include(x509.validTo, 'GMT'); +}); + +test(SUITE, 'serialNumber is hex string', () => { + const x509 = new X509Certificate(certPem); + assert.strictEqual( + x509.serialNumber, + '617ED0A40872C25592BCC2067C885C5F21754BA9', + ); +}); + +test(SUITE, 'signatureAlgorithm returns algorithm name', () => { + const x509 = new X509Certificate(certPem); + assert.include(x509.signatureAlgorithm.toLowerCase(), 'sha256'); +}); + +// --- Fingerprints --- + +test(SUITE, 'fingerprint returns SHA-1 colon hex', () => { + const x509 = new X509Certificate(certPem); + assert.strictEqual(x509.fingerprint, expectedSha1); +}); + +test(SUITE, 'fingerprint256 returns SHA-256 colon hex', () => { + const x509 = new X509Certificate(certPem); + assert.strictEqual(x509.fingerprint256, expectedSha256); +}); + +test(SUITE, 'fingerprint512 returns SHA-512 colon hex', () => { + const x509 = new X509Certificate(certPem); + assert.strictEqual(x509.fingerprint512, expectedSha512); +}); + +// --- Date properties --- + +test(SUITE, 'validFromDate returns Date object', () => { + const x509 = new X509Certificate(certPem); + assert.instanceOf(x509.validFromDate, Date); + assert.strictEqual(x509.validFromDate.getUTCFullYear(), 2026); +}); + +test(SUITE, 'validToDate returns Date object', () => { + const x509 = new X509Certificate(certPem); + assert.instanceOf(x509.validToDate, Date); + assert.strictEqual(x509.validToDate.getUTCFullYear(), 2126); +}); + +// --- Key & CA --- + +test(SUITE, 'ca returns true for CA certificate', () => { + const x509 = new X509Certificate(certPem); + assert.isTrue(x509.ca); +}); + +test(SUITE, 'publicKey returns a key object', () => { + const x509 = new X509Certificate(certPem); + const pk = x509.publicKey; + assert.strictEqual(pk.type, 'public'); +}); + +test(SUITE, 'keyUsage returns array of strings', () => { + const x509 = new X509Certificate(certPem); + assert.isArray(x509.keyUsage); + assert.isAbove(x509.keyUsage.length, 0); +}); + +// --- Raw/PEM --- + +test(SUITE, 'raw returns DER Buffer', () => { + const x509 = new X509Certificate(certPem); + const raw = x509.raw; + assert.isTrue(Buffer.isBuffer(raw)); + assert.isAbove(raw.length, 0); +}); + +test(SUITE, 'toString returns PEM string', () => { + const x509 = new X509Certificate(certPem); + assert.include(x509.toString(), '-----BEGIN CERTIFICATE-----'); +}); + +test(SUITE, 'toJSON returns same as toString', () => { + const x509 = new X509Certificate(certPem); + assert.strictEqual(x509.toJSON(), x509.toString()); +}); + +// --- Name checks --- + +test(SUITE, 'checkHost matches exact hostname', () => { + const x509 = new X509Certificate(certPem); + assert.strictEqual(x509.checkHost('test.example.com'), 'test.example.com'); +}); + +test(SUITE, 'checkHost returns undefined for non-matching', () => { + const x509 = new X509Certificate(certPem); + assert.isUndefined(x509.checkHost('other.domain.com')); +}); + +test(SUITE, 'checkHost matches wildcard', () => { + const x509 = new X509Certificate(certPem); + assert.ok(x509.checkHost('sub.example.com')); +}); + +test(SUITE, 'checkEmail matches', () => { + const x509 = new X509Certificate(certPem); + assert.strictEqual(x509.checkEmail('test@example.com'), 'test@example.com'); +}); + +test(SUITE, 'checkEmail returns undefined for non-matching', () => { + const x509 = new X509Certificate(certPem); + assert.isUndefined(x509.checkEmail('wrong@example.com')); +}); + +test(SUITE, 'checkIP matches 127.0.0.1', () => { + const x509 = new X509Certificate(certPem); + assert.strictEqual(x509.checkIP('127.0.0.1'), '127.0.0.1'); +}); + +test(SUITE, 'checkIP returns undefined for non-matching', () => { + const x509 = new X509Certificate(certPem); + assert.isUndefined(x509.checkIP('192.168.1.1')); +}); + +// --- Verification --- + +test(SUITE, 'verify with matching public key returns true', () => { + const x509 = new X509Certificate(certPem); + assert.isTrue(x509.verify(x509.publicKey)); +}); + +test(SUITE, 'checkPrivateKey with matching key returns true', () => { + const x509 = new X509Certificate(certPem); + const privKey = createPrivateKey(privKeyPem); + assert.isTrue(x509.checkPrivateKey(privKey)); +}); + +test(SUITE, 'checkPrivateKey with non-matching key returns false', () => { + const x509 = new X509Certificate(certPem); + const { privateKey } = generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }); + const otherKey = createPrivateKey(privateKey as string); + assert.isFalse(x509.checkPrivateKey(otherKey)); +}); + +// --- Cross-cert --- + +test(SUITE, 'checkIssued returns true for self-signed', () => { + const x509 = new X509Certificate(certPem); + assert.isTrue(x509.checkIssued(x509)); +}); + +test(SUITE, 'issuerCertificate returns undefined', () => { + const x509 = new X509Certificate(certPem); + assert.isUndefined(x509.issuerCertificate); +}); + +// --- Serialization --- + +test(SUITE, 'toLegacyObject returns object with expected fields', () => { + const x509 = new X509Certificate(certPem); + const obj = x509.toLegacyObject(); + assert.isObject(obj); + assert.property(obj, 'subject'); + assert.property(obj, 'issuer'); + assert.property(obj, 'serialNumber'); + assert.property(obj, 'fingerprint'); + assert.property(obj, 'fingerprint256'); + assert.property(obj, 'fingerprint512'); + assert.property(obj, 'valid_from'); + assert.property(obj, 'valid_to'); + assert.property(obj, 'raw'); +}); diff --git a/packages/react-native-quick-crypto/android/CMakeLists.txt b/packages/react-native-quick-crypto/android/CMakeLists.txt index 823aaeee..d947a058 100644 --- a/packages/react-native-quick-crypto/android/CMakeLists.txt +++ b/packages/react-native-quick-crypto/android/CMakeLists.txt @@ -57,6 +57,7 @@ add_library( ../cpp/scrypt/HybridScrypt.cpp ../cpp/sign/HybridSignHandle.cpp ../cpp/sign/HybridVerifyHandle.cpp + ../cpp/x509/HybridX509Certificate.cpp ../cpp/utils/HybridUtils.cpp ../cpp/utils/QuickCryptoUtils.cpp ${BLAKE3_SOURCES} @@ -93,6 +94,7 @@ include_directories( "../cpp/sign" "../cpp/scrypt" "../cpp/utils" + "../cpp/x509" "../deps/blake3/c" "../deps/fastpbkdf2" "../deps/ncrypto/include" diff --git a/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.hpp b/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.hpp index 21d64696..e92dfd75 100644 --- a/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.hpp +++ b/packages/react-native-quick-crypto/cpp/keys/HybridKeyObjectHandle.hpp @@ -39,13 +39,14 @@ class HybridKeyObjectHandle : public HybridKeyObjectHandleSpec { double getSymmetricKeySize() override; - KeyObjectData& getKeyObjectData() { - return data_; - } const KeyObjectData& getKeyObjectData() const { return data_; } + void setKeyObjectData(KeyObjectData data) { + data_ = std::move(data); + } + private: KeyObjectData data_; diff --git a/packages/react-native-quick-crypto/cpp/keys/KeyObjectData.hpp b/packages/react-native-quick-crypto/cpp/keys/KeyObjectData.hpp index 282b59db..f597ec4f 100644 --- a/packages/react-native-quick-crypto/cpp/keys/KeyObjectData.hpp +++ b/packages/react-native-quick-crypto/cpp/keys/KeyObjectData.hpp @@ -1,3 +1,5 @@ +#pragma once + #include #include diff --git a/packages/react-native-quick-crypto/cpp/x509/HybridX509Certificate.cpp b/packages/react-native-quick-crypto/cpp/x509/HybridX509Certificate.cpp new file mode 100644 index 00000000..0503cf3d --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/x509/HybridX509Certificate.cpp @@ -0,0 +1,174 @@ +#include "HybridX509Certificate.hpp" +#include "../keys/HybridKeyObjectHandle.hpp" +#include "../keys/KeyObjectData.hpp" +#include "QuickCryptoUtils.hpp" +#include + +namespace margelo::nitro::crypto { + +std::string HybridX509Certificate::bioToString(ncrypto::BIOPointer bio) const { + if (!bio) + return ""; + BUF_MEM* mem = bio; + if (!mem || mem->length == 0) + return ""; + return std::string(mem->data, mem->length); +} + +void HybridX509Certificate::init(const std::shared_ptr& buffer) { + ncrypto::Buffer buf{.data = reinterpret_cast(buffer->data()), .len = buffer->size()}; + auto result = ncrypto::X509Pointer::Parse(buf); + if (!result) { + throw std::runtime_error("Failed to parse X509 certificate"); + } + cert_ = std::move(result.value); +} + +std::string HybridX509Certificate::subject() { + return bioToString(cert_.view().getSubject()); +} + +std::string HybridX509Certificate::subjectAltName() { + return bioToString(cert_.view().getSubjectAltName()); +} + +std::string HybridX509Certificate::issuer() { + return bioToString(cert_.view().getIssuer()); +} + +std::string HybridX509Certificate::infoAccess() { + return bioToString(cert_.view().getInfoAccess()); +} + +std::string HybridX509Certificate::validFrom() { + return bioToString(cert_.view().getValidFrom()); +} + +std::string HybridX509Certificate::validTo() { + return bioToString(cert_.view().getValidTo()); +} + +double HybridX509Certificate::validFromDate() { + return static_cast(cert_.view().getValidFromTime()) * 1000.0; +} + +double HybridX509Certificate::validToDate() { + return static_cast(cert_.view().getValidToTime()) * 1000.0; +} + +std::string HybridX509Certificate::signatureAlgorithm() { + auto algo = cert_.view().getSignatureAlgorithm(); + if (!algo.has_value()) + return ""; + return std::string(algo.value()); +} + +std::string HybridX509Certificate::signatureAlgorithmOid() { + return cert_.view().getSignatureAlgorithmOID().value_or(""); +} + +std::string HybridX509Certificate::serialNumber() { + auto serial = cert_.view().getSerialNumber(); + if (!serial) + return ""; + return std::string(static_cast(serial.get()), serial.size()); +} + +std::string HybridX509Certificate::fingerprint() { + return cert_.view().getFingerprint(ncrypto::Digest::SHA1).value_or(""); +} + +std::string HybridX509Certificate::fingerprint256() { + return cert_.view().getFingerprint(ncrypto::Digest::SHA256).value_or(""); +} + +std::string HybridX509Certificate::fingerprint512() { + return cert_.view().getFingerprint(ncrypto::Digest::SHA512).value_or(""); +} + +std::shared_ptr HybridX509Certificate::raw() { + auto bio = cert_.view().toDER(); + if (!bio) { + throw std::runtime_error("Failed to export certificate as DER"); + } + BUF_MEM* mem = bio; + return ToNativeArrayBuffer(reinterpret_cast(mem->data), mem->length); +} + +std::string HybridX509Certificate::pem() { + return bioToString(cert_.view().toPEM()); +} + +std::shared_ptr HybridX509Certificate::publicKey() { + auto result = cert_.view().getPublicKey(); + if (!result) { + throw std::runtime_error("Failed to extract public key from certificate"); + } + auto handle = std::make_shared(); + handle->setKeyObjectData(KeyObjectData::CreateAsymmetric(KeyType::PUBLIC, std::move(result.value))); + return handle; +} + +std::vector HybridX509Certificate::keyUsage() { + std::vector usages; + cert_.view().enumUsages([&](const char* usage) { usages.emplace_back(usage); }); + return usages; +} + +bool HybridX509Certificate::ca() { + return cert_.view().isCA(); +} + +bool HybridX509Certificate::checkIssued(const std::shared_ptr& other) { + auto otherCert = std::dynamic_pointer_cast(other); + if (!otherCert) { + throw std::runtime_error("Invalid X509Certificate"); + } + return cert_.view().isIssuedBy(otherCert->cert_.view()); +} + +bool HybridX509Certificate::checkPrivateKey(const std::shared_ptr& key) { + auto handle = std::dynamic_pointer_cast(key); + if (!handle) { + throw std::runtime_error("Invalid key object"); + } + return cert_.view().checkPrivateKey(handle->getKeyObjectData().GetAsymmetricKey()); +} + +bool HybridX509Certificate::verify(const std::shared_ptr& key) { + auto handle = std::dynamic_pointer_cast(key); + if (!handle) { + throw std::runtime_error("Invalid key object"); + } + return cert_.view().checkPublicKey(handle->getKeyObjectData().GetAsymmetricKey()); +} + +std::optional HybridX509Certificate::checkHost(const std::string& name, double flags) { + ncrypto::DataPointer peername; + auto match = cert_.view().checkHost(name, static_cast(flags), &peername); + if (match == ncrypto::X509View::CheckMatch::MATCH) { + if (peername) { + return std::string(static_cast(peername.get()), peername.size()); + } + return name; + } + return std::nullopt; +} + +std::optional HybridX509Certificate::checkEmail(const std::string& email, double flags) { + auto match = cert_.view().checkEmail(email, static_cast(flags)); + if (match == ncrypto::X509View::CheckMatch::MATCH) { + return email; + } + return std::nullopt; +} + +std::optional HybridX509Certificate::checkIP(const std::string& ip) { + auto match = cert_.view().checkIp(ip, 0); + if (match == ncrypto::X509View::CheckMatch::MATCH) { + return ip; + } + return std::nullopt; +} + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/cpp/x509/HybridX509Certificate.hpp b/packages/react-native-quick-crypto/cpp/x509/HybridX509Certificate.hpp new file mode 100644 index 00000000..705feb3e --- /dev/null +++ b/packages/react-native-quick-crypto/cpp/x509/HybridX509Certificate.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include "HybridX509CertificateHandleSpec.hpp" +#include +#include + +namespace margelo::nitro::crypto { + +class HybridX509Certificate : public HybridX509CertificateHandleSpec { + public: + HybridX509Certificate() : HybridObject(TAG) {} + + void init(const std::shared_ptr& buffer) override; + + std::string subject() override; + std::string subjectAltName() override; + std::string issuer() override; + std::string infoAccess() override; + std::string validFrom() override; + std::string validTo() override; + double validFromDate() override; + double validToDate() override; + std::string signatureAlgorithm() override; + std::string signatureAlgorithmOid() override; + std::string serialNumber() override; + + std::string fingerprint() override; + std::string fingerprint256() override; + std::string fingerprint512() override; + + std::shared_ptr raw() override; + std::string pem() override; + + std::shared_ptr publicKey() override; + std::vector keyUsage() override; + + bool ca() override; + bool checkIssued(const std::shared_ptr& other) override; + bool checkPrivateKey(const std::shared_ptr& key) override; + bool verify(const std::shared_ptr& key) override; + + std::optional checkHost(const std::string& name, double flags) override; + std::optional checkEmail(const std::string& email, double flags) override; + std::optional checkIP(const std::string& ip) override; + + private: + ncrypto::X509Pointer cert_; + std::string bioToString(ncrypto::BIOPointer bio) const; +}; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitro.json b/packages/react-native-quick-crypto/nitro.json index 76e26fb8..dab16f3c 100644 --- a/packages/react-native-quick-crypto/nitro.json +++ b/packages/react-native-quick-crypto/nitro.json @@ -82,6 +82,9 @@ }, "VerifyHandle": { "cpp": "HybridVerifyHandle" + }, + "X509CertificateHandle": { + "cpp": "HybridX509Certificate" } }, "ignorePaths": ["node_modules", "lib"] diff --git a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCrypto+autolinking.cmake b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCrypto+autolinking.cmake index d39000d6..4b1b0e8a 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCrypto+autolinking.cmake +++ b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCrypto+autolinking.cmake @@ -58,6 +58,7 @@ target_sources( ../nitrogen/generated/shared/c++/HybridSignHandleSpec.cpp ../nitrogen/generated/shared/c++/HybridVerifyHandleSpec.cpp ../nitrogen/generated/shared/c++/HybridUtilsSpec.cpp + ../nitrogen/generated/shared/c++/HybridX509CertificateHandleSpec.cpp # Android-specific Nitrogen C++ sources ) diff --git a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp index 89711208..295474cb 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp +++ b/packages/react-native-quick-crypto/nitrogen/generated/android/QuickCryptoOnLoad.cpp @@ -40,6 +40,7 @@ #include "HybridSignHandle.hpp" #include "HybridUtils.hpp" #include "HybridVerifyHandle.hpp" +#include "HybridX509Certificate.hpp" namespace margelo::nitro::crypto { @@ -278,6 +279,15 @@ int initialize(JavaVM* vm) { return std::make_shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "X509CertificateHandle", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridX509Certificate\" is not default-constructible! " + "Create a public constructor that takes zero arguments to be able to autolink this HybridObject."); + return std::make_shared(); + } + ); }); } diff --git a/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm b/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm index d942efe4..1699b8de 100644 --- a/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm +++ b/packages/react-native-quick-crypto/nitrogen/generated/ios/QuickCryptoAutolinking.mm @@ -35,6 +35,7 @@ #include "HybridSignHandle.hpp" #include "HybridUtils.hpp" #include "HybridVerifyHandle.hpp" +#include "HybridX509Certificate.hpp" @interface QuickCryptoAutolinking : NSObject @end @@ -270,6 +271,15 @@ + (void) load { return std::make_shared(); } ); + HybridObjectRegistry::registerHybridObjectConstructor( + "X509CertificateHandle", + []() -> std::shared_ptr { + static_assert(std::is_default_constructible_v, + "The HybridObject \"HybridX509Certificate\" is not default-constructible! " + "Create a public constructor that takes zero arguments to be able to autolink this HybridObject."); + return std::make_shared(); + } + ); } @end diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridX509CertificateHandleSpec.cpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridX509CertificateHandleSpec.cpp new file mode 100644 index 00000000..853e5d6e --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridX509CertificateHandleSpec.cpp @@ -0,0 +1,46 @@ +/// +/// HybridX509CertificateHandleSpec.cpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#include "HybridX509CertificateHandleSpec.hpp" + +namespace margelo::nitro::crypto { + + void HybridX509CertificateHandleSpec::loadHybridMethods() { + // load base methods/properties + HybridObject::loadHybridMethods(); + // load custom methods/properties + registerHybrids(this, [](Prototype& prototype) { + prototype.registerHybridMethod("init", &HybridX509CertificateHandleSpec::init); + prototype.registerHybridMethod("subject", &HybridX509CertificateHandleSpec::subject); + prototype.registerHybridMethod("subjectAltName", &HybridX509CertificateHandleSpec::subjectAltName); + prototype.registerHybridMethod("issuer", &HybridX509CertificateHandleSpec::issuer); + prototype.registerHybridMethod("infoAccess", &HybridX509CertificateHandleSpec::infoAccess); + prototype.registerHybridMethod("validFrom", &HybridX509CertificateHandleSpec::validFrom); + prototype.registerHybridMethod("validTo", &HybridX509CertificateHandleSpec::validTo); + prototype.registerHybridMethod("validFromDate", &HybridX509CertificateHandleSpec::validFromDate); + prototype.registerHybridMethod("validToDate", &HybridX509CertificateHandleSpec::validToDate); + prototype.registerHybridMethod("signatureAlgorithm", &HybridX509CertificateHandleSpec::signatureAlgorithm); + prototype.registerHybridMethod("signatureAlgorithmOid", &HybridX509CertificateHandleSpec::signatureAlgorithmOid); + prototype.registerHybridMethod("serialNumber", &HybridX509CertificateHandleSpec::serialNumber); + prototype.registerHybridMethod("fingerprint", &HybridX509CertificateHandleSpec::fingerprint); + prototype.registerHybridMethod("fingerprint256", &HybridX509CertificateHandleSpec::fingerprint256); + prototype.registerHybridMethod("fingerprint512", &HybridX509CertificateHandleSpec::fingerprint512); + prototype.registerHybridMethod("raw", &HybridX509CertificateHandleSpec::raw); + prototype.registerHybridMethod("pem", &HybridX509CertificateHandleSpec::pem); + prototype.registerHybridMethod("publicKey", &HybridX509CertificateHandleSpec::publicKey); + prototype.registerHybridMethod("keyUsage", &HybridX509CertificateHandleSpec::keyUsage); + prototype.registerHybridMethod("ca", &HybridX509CertificateHandleSpec::ca); + prototype.registerHybridMethod("checkIssued", &HybridX509CertificateHandleSpec::checkIssued); + prototype.registerHybridMethod("checkPrivateKey", &HybridX509CertificateHandleSpec::checkPrivateKey); + prototype.registerHybridMethod("verify", &HybridX509CertificateHandleSpec::verify); + prototype.registerHybridMethod("checkHost", &HybridX509CertificateHandleSpec::checkHost); + prototype.registerHybridMethod("checkEmail", &HybridX509CertificateHandleSpec::checkEmail); + prototype.registerHybridMethod("checkIP", &HybridX509CertificateHandleSpec::checkIP); + }); + } + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridX509CertificateHandleSpec.hpp b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridX509CertificateHandleSpec.hpp new file mode 100644 index 00000000..73544ab4 --- /dev/null +++ b/packages/react-native-quick-crypto/nitrogen/generated/shared/c++/HybridX509CertificateHandleSpec.hpp @@ -0,0 +1,96 @@ +/// +/// HybridX509CertificateHandleSpec.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + +// Forward declaration of `HybridKeyObjectHandleSpec` to properly resolve imports. +namespace margelo::nitro::crypto { class HybridKeyObjectHandleSpec; } +// Forward declaration of `HybridX509CertificateHandleSpec` to properly resolve imports. +namespace margelo::nitro::crypto { class HybridX509CertificateHandleSpec; } + +#include +#include +#include +#include "HybridKeyObjectHandleSpec.hpp" +#include +#include "HybridX509CertificateHandleSpec.hpp" +#include + +namespace margelo::nitro::crypto { + + using namespace margelo::nitro; + + /** + * An abstract base class for `X509CertificateHandle` + * Inherit this class to create instances of `HybridX509CertificateHandleSpec` in C++. + * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. + * @example + * ```cpp + * class HybridX509CertificateHandle: public HybridX509CertificateHandleSpec { + * public: + * HybridX509CertificateHandle(...): HybridObject(TAG) { ... } + * // ... + * }; + * ``` + */ + class HybridX509CertificateHandleSpec: public virtual HybridObject { + public: + // Constructor + explicit HybridX509CertificateHandleSpec(): HybridObject(TAG) { } + + // Destructor + ~HybridX509CertificateHandleSpec() override = default; + + public: + // Properties + + + public: + // Methods + virtual void init(const std::shared_ptr& buffer) = 0; + virtual std::string subject() = 0; + virtual std::string subjectAltName() = 0; + virtual std::string issuer() = 0; + virtual std::string infoAccess() = 0; + virtual std::string validFrom() = 0; + virtual std::string validTo() = 0; + virtual double validFromDate() = 0; + virtual double validToDate() = 0; + virtual std::string signatureAlgorithm() = 0; + virtual std::string signatureAlgorithmOid() = 0; + virtual std::string serialNumber() = 0; + virtual std::string fingerprint() = 0; + virtual std::string fingerprint256() = 0; + virtual std::string fingerprint512() = 0; + virtual std::shared_ptr raw() = 0; + virtual std::string pem() = 0; + virtual std::shared_ptr publicKey() = 0; + virtual std::vector keyUsage() = 0; + virtual bool ca() = 0; + virtual bool checkIssued(const std::shared_ptr& other) = 0; + virtual bool checkPrivateKey(const std::shared_ptr& key) = 0; + virtual bool verify(const std::shared_ptr& key) = 0; + virtual std::optional checkHost(const std::string& name, double flags) = 0; + virtual std::optional checkEmail(const std::string& email, double flags) = 0; + virtual std::optional checkIP(const std::string& ip) = 0; + + protected: + // Hybrid Setup + void loadHybridMethods() override; + + protected: + // Tag for logging + static constexpr auto TAG = "X509CertificateHandle"; + }; + +} // namespace margelo::nitro::crypto diff --git a/packages/react-native-quick-crypto/src/index.ts b/packages/react-native-quick-crypto/src/index.ts index 264f68d9..5c69b937 100644 --- a/packages/react-native-quick-crypto/src/index.ts +++ b/packages/react-native-quick-crypto/src/index.ts @@ -17,6 +17,7 @@ import * as random from './random'; import * as ecdh from './ecdh'; import * as dh from './diffie-hellman'; import { Certificate } from './certificate'; +import { X509Certificate } from './x509certificate'; import { getCurves } from './ec'; import { constants } from './constants'; @@ -46,6 +47,7 @@ const QuickCrypto = { ...utils, ...subtle, Certificate, + X509Certificate, getCurves, constants, Buffer, @@ -81,6 +83,8 @@ export default QuickCrypto; export * from './argon2'; export * from './blake3'; export { Certificate } from './certificate'; +export { X509Certificate } from './x509certificate'; +export type { CheckOptions, X509LegacyObject } from './x509certificate'; export * from './cipher'; export * from './ed'; export * from './keys'; diff --git a/packages/react-native-quick-crypto/src/specs/x509certificate.nitro.ts b/packages/react-native-quick-crypto/src/specs/x509certificate.nitro.ts new file mode 100644 index 00000000..9a549894 --- /dev/null +++ b/packages/react-native-quick-crypto/src/specs/x509certificate.nitro.ts @@ -0,0 +1,38 @@ +import type { HybridObject } from 'react-native-nitro-modules'; +import type { KeyObjectHandle } from './keyObjectHandle.nitro'; + +export interface X509CertificateHandle + extends HybridObject<{ ios: 'c++'; android: 'c++' }> { + init(buffer: ArrayBuffer): void; + + subject(): string; + subjectAltName(): string; + issuer(): string; + infoAccess(): string; + validFrom(): string; + validTo(): string; + validFromDate(): number; + validToDate(): number; + signatureAlgorithm(): string; + signatureAlgorithmOid(): string; + serialNumber(): string; + + fingerprint(): string; + fingerprint256(): string; + fingerprint512(): string; + + raw(): ArrayBuffer; + pem(): string; + + publicKey(): KeyObjectHandle; + keyUsage(): string[]; + + ca(): boolean; + checkIssued(other: X509CertificateHandle): boolean; + checkPrivateKey(key: KeyObjectHandle): boolean; + verify(key: KeyObjectHandle): boolean; + + checkHost(name: string, flags: number): string | undefined; + checkEmail(email: string, flags: number): string | undefined; + checkIP(ip: string): string | undefined; +} diff --git a/packages/react-native-quick-crypto/src/x509certificate.ts b/packages/react-native-quick-crypto/src/x509certificate.ts new file mode 100644 index 00000000..318b05e7 --- /dev/null +++ b/packages/react-native-quick-crypto/src/x509certificate.ts @@ -0,0 +1,277 @@ +import { NitroModules } from 'react-native-nitro-modules'; +import { Buffer } from '@craftzdog/react-native-buffer'; +import type { X509CertificateHandle } from './specs/x509certificate.nitro'; +import { PublicKeyObject, KeyObject } from './keys'; +import type { BinaryLike } from './utils'; +import { binaryLikeToArrayBuffer } from './utils'; + +const X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT = 0x1; +const X509_CHECK_FLAG_NO_WILDCARDS = 0x2; +const X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS = 0x4; +const X509_CHECK_FLAG_MULTI_LABEL_WILDCARDS = 0x8; +const X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS = 0x10; +const X509_CHECK_FLAG_NEVER_CHECK_SUBJECT = 0x20; + +export interface X509LegacyObject { + subject: string; + issuer: string; + subjectaltname: string; + infoAccess: string; + ca: boolean; + modulus: undefined; + bits: undefined; + exponent: undefined; + valid_from: string; + valid_to: string; + fingerprint: string; + fingerprint256: string; + fingerprint512: string; + ext_key_usage: string[]; + serialNumber: string; + raw: Buffer; +} + +export interface CheckOptions { + subject?: 'default' | 'always' | 'never'; + wildcards?: boolean; + partialWildcards?: boolean; + multiLabelWildcards?: boolean; + singleLabelSubdomains?: boolean; +} + +function getFlags(options?: CheckOptions): number { + if (!options) return 0; + + let flags = 0; + + if (options.subject === 'always') { + flags |= X509_CHECK_FLAG_ALWAYS_CHECK_SUBJECT; + } else if (options.subject === 'never') { + flags |= X509_CHECK_FLAG_NEVER_CHECK_SUBJECT; + } + + if (options.wildcards === false) { + flags |= X509_CHECK_FLAG_NO_WILDCARDS; + } + + if (options.partialWildcards === false) { + flags |= X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS; + } + + if (options.multiLabelWildcards === true) { + flags |= X509_CHECK_FLAG_MULTI_LABEL_WILDCARDS; + } + + if (options.singleLabelSubdomains === true) { + flags |= X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS; + } + + return flags; +} + +export class X509Certificate { + private readonly handle: X509CertificateHandle; + private readonly cache = new Map(); + + constructor(buffer: BinaryLike) { + this.handle = NitroModules.createHybridObject( + 'X509CertificateHandle', + ); + + let ab: ArrayBuffer; + if (typeof buffer === 'string') { + ab = Buffer.from(buffer).buffer as ArrayBuffer; + } else { + ab = binaryLikeToArrayBuffer(buffer); + } + + this.handle.init(ab); + } + + private cached(key: string, compute: () => T): T { + if (this.cache.has(key)) { + return this.cache.get(key) as T; + } + const value = compute(); + this.cache.set(key, value); + return value; + } + + get subject(): string { + return this.cached('subject', () => this.handle.subject()); + } + + get subjectAltName(): string { + return this.cached('subjectAltName', () => this.handle.subjectAltName()); + } + + get issuer(): string { + return this.cached('issuer', () => this.handle.issuer()); + } + + get infoAccess(): string { + return this.cached('infoAccess', () => this.handle.infoAccess()); + } + + get validFrom(): string { + return this.cached('validFrom', () => this.handle.validFrom()); + } + + get validTo(): string { + return this.cached('validTo', () => this.handle.validTo()); + } + + get validFromDate(): Date { + return this.cached( + 'validFromDate', + () => new Date(this.handle.validFromDate()), + ); + } + + get validToDate(): Date { + return this.cached( + 'validToDate', + () => new Date(this.handle.validToDate()), + ); + } + + get fingerprint(): string { + return this.cached('fingerprint', () => this.handle.fingerprint()); + } + + get fingerprint256(): string { + return this.cached('fingerprint256', () => this.handle.fingerprint256()); + } + + get fingerprint512(): string { + return this.cached('fingerprint512', () => this.handle.fingerprint512()); + } + + get extKeyUsage(): string[] { + return this.cached('extKeyUsage', () => this.handle.keyUsage()); + } + + get keyUsage(): string[] { + return this.extKeyUsage; + } + + get serialNumber(): string { + return this.cached('serialNumber', () => this.handle.serialNumber()); + } + + get signatureAlgorithm(): string { + return this.cached('signatureAlgorithm', () => + this.handle.signatureAlgorithm(), + ); + } + + get signatureAlgorithmOid(): string { + return this.cached('signatureAlgorithmOid', () => + this.handle.signatureAlgorithmOid(), + ); + } + + get ca(): boolean { + return this.cached('ca', () => this.handle.ca()); + } + + get raw(): Buffer { + return this.cached('raw', () => Buffer.from(this.handle.raw())); + } + + get publicKey(): PublicKeyObject { + return this.cached( + 'publicKey', + () => new PublicKeyObject(this.handle.publicKey()), + ); + } + + get issuerCertificate(): undefined { + return undefined; + } + + checkHost(name: string, options?: CheckOptions): string | undefined { + if (typeof name !== 'string') { + throw new TypeError('The "name" argument must be a string'); + } + return this.handle.checkHost(name, getFlags(options)); + } + + checkEmail(email: string, options?: CheckOptions): string | undefined { + if (typeof email !== 'string') { + throw new TypeError('The "email" argument must be a string'); + } + return this.handle.checkEmail(email, getFlags(options)); + } + + checkIP(ip: string): string | undefined { + if (typeof ip !== 'string') { + throw new TypeError('The "ip" argument must be a string'); + } + return this.handle.checkIP(ip); + } + + checkIssued(otherCert: X509Certificate): boolean { + if (!(otherCert instanceof X509Certificate)) { + throw new TypeError( + 'The "otherCert" argument must be an instance of X509Certificate', + ); + } + return this.handle.checkIssued(otherCert.handle); + } + + checkPrivateKey(pkey: KeyObject): boolean { + if (!(pkey instanceof KeyObject)) { + throw new TypeError( + 'The "pkey" argument must be an instance of KeyObject', + ); + } + if (pkey.type !== 'private') { + throw new TypeError('The "pkey" argument must be a private key'); + } + return this.handle.checkPrivateKey(pkey.handle); + } + + verify(pkey: KeyObject): boolean { + if (!(pkey instanceof KeyObject)) { + throw new TypeError( + 'The "pkey" argument must be an instance of KeyObject', + ); + } + if (pkey.type !== 'public') { + throw new TypeError( + `The "pkey" argument must be a public key, got '${pkey.type}'`, + ); + } + return this.handle.verify(pkey.handle); + } + + toString(): string { + return this.cached('pem', () => this.handle.pem()); + } + + toJSON(): string { + return this.toString(); + } + + toLegacyObject(): X509LegacyObject { + return { + subject: this.subject, + issuer: this.issuer, + subjectaltname: this.subjectAltName, + infoAccess: this.infoAccess, + ca: this.ca, + modulus: undefined, + bits: undefined, + exponent: undefined, + valid_from: this.validFrom, + valid_to: this.validTo, + fingerprint: this.fingerprint, + fingerprint256: this.fingerprint256, + fingerprint512: this.fingerprint512, + ext_key_usage: this.keyUsage, + serialNumber: this.serialNumber, + raw: this.raw, + }; + } +}