Search code examples
opensslssl-certificatex509certificate

How to get subject hash of a PEM certificate


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?


Solution

  • 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;
    }