11"use client" ;
22
33import { useState , useEffect } from "react" ;
4- import * as forge from "node-forge " ;
4+ import * as x509 from "@peculiar/x509 " ;
55import { useForm } from "react-hook-form" ;
66import { zodResolver } from "@hookform/resolvers/zod" ;
77import * as z from "zod" ;
@@ -53,16 +53,17 @@ const downloadFile = (content: string, filename: string) => {
5353} ;
5454
5555// Calculate certificate fingerprint (SHA-256)
56- const getCertificateFingerprint = ( cert : forge . pki . Certificate ) : string => {
57- const der = forge . asn1 . toDer ( forge . pki . certificateToAsn1 ( cert ) ) . getBytes ( ) ;
58- const md = forge . md . sha256 . create ( ) ;
59- md . update ( der ) ;
60- const digest = md . digest ( ) . toHex ( ) ;
61- return digest . toUpperCase ( ) . match ( / .{ 2 } / g) ?. join ( ':' ) || '' ;
56+ const getCertificateFingerprint = async ( cert : x509 . X509Certificate ) : Promise < string > => {
57+ const thumbprint = await cert . getThumbprint ( crypto ) ;
58+ const hex = Array . from ( new Uint8Array ( thumbprint ) )
59+ . map ( b => b . toString ( 16 ) . padStart ( 2 , '0' ) )
60+ . join ( '' )
61+ . toUpperCase ( ) ;
62+ return hex . match ( / .{ 2 } / g) ?. join ( ':' ) || '' ;
6263} ;
6364
6465// Get certificate serial number
65- const getCertificateSerialNumber = ( cert : forge . pki . Certificate ) : string => {
66+ const getCertificateSerialNumber = ( cert : x509 . X509Certificate ) : string => {
6667 return cert . serialNumber ;
6768} ;
6869
@@ -77,47 +78,81 @@ const parseCertFile = async (file: File): Promise<{ serialNumber: string, finger
7778 // Extract only the FIRST certificate from the chain
7879 const certMatch = text . match ( / - - - - - B E G I N C E R T I F I C A T E - - - - - [ \s \S ] * ?- - - - - E N D C E R T I F I C A T E - - - - - / ) ;
7980 if ( ! certMatch ) {
80- throw new Error ( "No valid certificate found" ) ;
81+ console . error ( "No certificate block found in file" ) ;
82+ toast . error ( "No certificate found in file" ) ;
83+ return null ;
8184 }
8285
8386 const firstCertPem = certMatch [ 0 ] ;
84- const cert = forge . pki . certificateFromPem ( firstCertPem ) ;
8587
86- // Validate this is a developer certificate (not a CA certificate)
87- const basicConstraintsExt = cert . getExtension ( 'basicConstraints' ) ;
88- if ( basicConstraintsExt && ( basicConstraintsExt as any ) . cA === true ) {
89- throw new Error ( "This is a CA certificate, not a developer certificate. Please upload your developer certificate." ) ;
88+ let cert : x509 . X509Certificate ;
89+ try {
90+ cert = new x509 . X509Certificate ( firstCertPem ) ;
91+ } catch ( parseError ) {
92+ console . error ( "Failed to parse certificate:" , parseError ) ;
93+ toast . error ( "Invalid certificate format" ) ;
94+ return null ;
95+ }
96+
97+ // Check if this is a CA certificate (warning only, not blocking)
98+ try {
99+ const basicConstraintsExt = cert . getExtension ( x509 . id_ce_basicConstraints ) ;
100+ if ( basicConstraintsExt ) {
101+ const bc = new x509 . BasicConstraintsExtension ( basicConstraintsExt . value ) ;
102+ if ( bc . ca === true ) {
103+ console . warn ( "Warning: This appears to be a CA certificate" ) ;
104+ toast . error ( "This is a CA certificate. Please upload your developer certificate (the first one in the chain)." ) ;
105+ return null ;
106+ }
107+ }
108+ } catch ( extError ) {
109+ // Extension check failed, continue anyway
110+ console . warn ( "Could not check basicConstraints extension:" , extError ) ;
90111 }
91112
92113 // Extract fields
93- const cn = cert . subject . getField ( 'CN' ) ?. value as string ;
94- const org = cert . subject . getField ( 'O' ) ?. value as string ;
114+ const cn = cert . subject . split ( ',' ) . find ( s => s . trim ( ) . startsWith ( 'CN=' ) ) ?. split ( '=' ) [ 1 ] ?. trim ( ) ;
115+ const org = cert . subject . split ( ',' ) . find ( s => s . trim ( ) . startsWith ( 'O=' ) ) ?. split ( '=' ) [ 1 ] ?. trim ( ) ;
95116
96- // Validate organization (optional check, can be removed if too strict )
117+ // Log organization info (warning only )
97118 if ( org && org !== 'KernelSU Module Developers' ) {
98- console . warn ( `Warning: Certificate organization is "${ org } ", expected "KernelSU Module Developers"` ) ;
119+ console . warn ( `Certificate organization: "${ org } " ( expected "KernelSU Module Developers") ` ) ;
99120 }
100121
122+ // Get serial number and fingerprint
123+ const serialNumber = getCertificateSerialNumber ( cert ) ;
124+ const fingerprint = await getCertificateFingerprint ( cert ) ;
125+
126+ console . log ( "Certificate parsed successfully:" ) ;
127+ console . log ( "- Serial Number:" , serialNumber ) ;
128+ console . log ( "- CN:" , cn ) ;
129+ console . log ( "- Organization:" , org ) ;
130+
101131 return {
102- serialNumber : getCertificateSerialNumber ( cert ) ,
103- fingerprint : getCertificateFingerprint ( cert ) ,
132+ serialNumber,
133+ fingerprint,
104134 text : firstCertPem ,
105135 cn
106136 } ;
107137 }
108138
109139 // Try to parse as CSR
110140 if ( text . includes ( 'BEGIN CERTIFICATE REQUEST' ) ) {
141+ console . warn ( "File contains CSR, not certificate" ) ;
142+ toast . error ( "This is a certificate request (CSR), not a certificate. Please upload your issued certificate." ) ;
111143 return {
112144 serialNumber : '' ,
113145 fingerprint : '' ,
114146 text : text
115147 } ;
116148 }
117149
118- throw new Error ( "Invalid file format" ) ;
150+ console . error ( "File does not contain certificate or CSR" ) ;
151+ toast . error ( "Invalid file: No certificate found. Please upload a .pem or .crt file containing a certificate." ) ;
152+ return null ;
119153 } catch ( e ) {
120- console . error ( e ) ;
154+ console . error ( "Error parsing certificate file:" , e ) ;
155+ toast . error ( `Error reading file: ${ ( e as Error ) . message } ` ) ;
121156 return null ;
122157 }
123158} ;
@@ -491,20 +526,20 @@ function QueryForm({ t }: { t: typeof locales.en }) {
491526
492527 try {
493528 const text = await file . text ( ) ;
494- const cert = forge . pki . certificateFromPem ( text ) ;
529+ const cert = new x509 . X509Certificate ( text ) ;
495530
496- const cn = cert . subject . getField ( 'CN' ) ?. value as string || 'Unknown' ;
497- const issuerCN = cert . issuer . getField ( 'CN' ) ?. value as string || 'Unknown' ;
531+ const cn = cert . subject . split ( ',' ) . find ( s => s . trim ( ) . startsWith ( 'CN=' ) ) ?. split ( '=' ) [ 1 ] ?. trim ( ) || 'Unknown' ;
532+ const issuerCN = cert . issuer . split ( ',' ) . find ( s => s . trim ( ) . startsWith ( 'CN=' ) ) ?. split ( '=' ) [ 1 ] ?. trim ( ) || 'Unknown' ;
498533 const now = new Date ( ) ;
499534
500535 setResult ( {
501536 cn,
502- fingerprint : getCertificateFingerprint ( cert ) ,
537+ fingerprint : await getCertificateFingerprint ( cert ) ,
503538 serialNumber : cert . serialNumber ,
504539 issuer : issuerCN ,
505- validFrom : cert . validity . notBefore . toLocaleDateString ( ) ,
506- validTo : cert . validity . notAfter . toLocaleDateString ( ) ,
507- isValid : now >= cert . validity . notBefore && now <= cert . validity . notAfter
540+ validFrom : cert . notBefore . toLocaleDateString ( ) ,
541+ validTo : cert . notAfter . toLocaleDateString ( ) ,
542+ isValid : now >= cert . notBefore && now <= cert . notAfter
508543 } ) ;
509544 toast . success ( t . query . found ) ;
510545 } catch ( e ) {
@@ -588,10 +623,10 @@ function RevokeForm({ t }: { t: typeof locales.en }) {
588623 const result = await parseCertFile ( file ) ;
589624 if ( result && result . serialNumber ) {
590625 form . setValue ( "serial_number" , result . serialNumber ) ;
591- toast . success ( t . common . import_success ) ;
592- } else {
593- toast . error ( t . common . import_error ) ;
626+ toast . success ( `✅ ${ t . common . import_success } \nSerial: ${ result . serialNumber . substring ( 0 , 16 ) } ...` ) ;
627+ console . log ( "Certificate imported:" , result . cn || "Unknown CN" ) ;
594628 }
629+ // Error messages are already shown by parseCertFile
595630 e . target . value = "" ;
596631 } ;
597632
0 commit comments