I have a Node project (written in TypeScript) and I want to add a function to get the subject hash of a PEM certificate and I need it to compute the hash value the same way as OpenSSL but without using OpenSSL (b/c my code needs to run on various platforms where OpenSSL may not be installed).
So suppose I have a file named my-cert.pem
. When I run openssl x509 -inform PEM -subject_hash_old -in my-cert.pem | head -1
it outputs 2889162b
I then tried the following ways of computing the hash in TypeScript to see if I can get the same value:
import crypto from 'node:crypto';
import forge from 'node-forge';
public static getSubjectHashFromPEM1(pemCertificate: string): string {
const cert = new crypto.X509Certificate(pemCertificate);
const subject = cert.subject.replaceAll('\n', ', ');
const hash = crypto.createHash('sha1').update(subject).digest('hex').slice(0, 8);
return hash;
}
public static getSubjectHashFromPEM2(pemCertificate: string): string {
const cert = forge.pki.certificateFromPem(pemCertificate);
const subject = cert.subject;
const subjectString = subject.attributes.map(attr => `${attr.shortName ?? ''}=${attr.value as string}`).join(', ');
const hash = crypto.createHash('sha1').update(subjectString).digest('hex').slice(0, 8);
return hash;
}
But both of these methods return d89c7493
which is different from 2889162b
which OpenSSL returns.
What am I doing wrong here?
I managed to get it working as below:
private static getSubjectHashOld(cert: forge.pki.Certificate): string {
// 1. Extract the subject name in ASN.1 format
const subjectAsn1 = forge.pki.distinguishedNameToAsn1(cert.subject);
// 2. Convert the subject to DER-encoded form
const derSubject = forge.asn1.toDer(subjectAsn1).getBytes();
// 3. Create an MD5 hash of the DER-encoded subject
const md = forge.md.md5.create();
md.update(derSubject, 'raw');
const md5Hash = md.digest().getBytes();
// 4. The first four bytes of the MD5 hash are the subject hash in little-endian format
const hashBuffer = forge.util.createBuffer(md5Hash.slice(0, 4), 'raw');
const hashArray = Array.from(hashBuffer.bytes()).reverse();
// 5. Convert the little-endian hash to a hexadecimal string
const subjectHash = hashArray.map(byte => ('00' + byte.charCodeAt(0).toString(16)).slice(-2)).join('');
return subjectHash;
}