Conversation with Gemini. This is fairly urgent, as CodeQL is starting to quibble about security of downloading software provided by backend APIs.
You said
How can I extract the code signing certificate from a dynamic library, and validate it, in a typescript environment
Gemini said
Extracting and validating a code-signing certificate in a TypeScript (Node.js) environment depends heavily on the operating system and the library format (PE/DLL for Windows or Mach-O/dylib for macOS).
Since TypeScript is just a typed layer over JavaScript, you will primarily use Node.js modules or child processes to handle the binary parsing.
Windows uses the Authenticode standard. The signature is typically stored in the "Attribute Certificate Table" of the Portable Executable (PE) file.
Option A: Pure Node.js (Recommended for Portability)
The library authenticode-js allows you to parse the signature without external dependencies.
TypeScript
import { createAuthenticodeHandler } from 'authenticode';
// 1. Load the DLL
const handler = createAuthenticodeHandler("path/to/library.dll");
if (handler) {
// 2. Extract information
console.log("Signer Common Name:", handler.signingAttribs.subject);
console.log("File Hash (Signed):", handler.fileHashSigned);
console.log("File Hash (Actual):", handler.fileHashActual);
// 3. Validation check
const isValid = handler.fileHashSigned === handler.fileHashActual;
console.log(`Signature is ${isValid ? 'Valid' : 'Invalid'}`);
}
Option B: Native Shell (Recommended for Production Trust)
For production-grade validation, it is safer to shell out to Windows' native Get-AuthenticodeSignature via PowerShell, as it handles the OS-level trust store (root CA validation).
TypeScript
import { execSync } from 'child_process';
function validateWindowsDll(path: string) {
const cmd = powershell -Command "(Get-AuthenticodeSignature '${path}').Status";
const status = execSync(cmd).toString().trim();
// Status can be 'Valid', 'HashMismatch', 'NotSigned', etc.
return status === 'Valid';
}
On macOS, signatures are stored in a dedicated load command called LC_CODE_SIGNATURE.
Using codesign via Child Process
There are few pure-JS libraries for Mach-O signature parsing. The industry standard is using the native codesign utility.
TypeScript
import { execSync } from 'child_process';
interface CertInfo {
authority?: string[];
teamIdentifier?: string;
}
function getMacSignature(path: string): CertInfo {
try {
// -dvvv provides verbose details about the signature
const output = execSync(codesign -dvvv "${path}" 2>&1).toString();
const info: CertInfo = {
authority: output.match(/Authority=(.*)/g)?.map(s => s.replace('Authority=', '')),
teamIdentifier: output.match(/TeamIdentifier=(.*)/)?.[1]
};
return info;
} catch (e) {
throw new Error("File is not signed or path is invalid");
}
}
// Validation
const isValid = execSync(codesign -v "${path}") === null; // Returns 0/null if valid
If you need a unified TypeScript API, you should implement a wrapper that detects the platform:
Feature Windows Strategy macOS Strategy
Tool Get-AuthenticodeSignature (PS) codesign
Cert Format X.509 (Authenticode) CMS (Code Signing)
Validation Checks CRL/OCSP via Windows CryptoAPI Checks Apple Trust Store / Gatekeeper
Summary Table: Libraries & Tools
Name Platform Type Best For
authenticode-js Windows NPM Package Reading cert info without native tools.
authenticode-sign Windows NPM Package Programmatic signing/modifying.
child_process Both Built-in Real-world validation (trusting the OS).
A Note on Validation
Simply extracting the certificate string is usually insufficient for security. To truly validate the library, you must:
Check Integrity: Ensure the file hash matches the signed hash.
Check Trust: Verify that the certificate chains up to a trusted Root CA (Microsoft or Apple).
Check Revocation: Ensure the certificate hasn't been revoked (CRL/OCSP).
Using the native codesign -v (macOS) and Get-AuthenticodeSignature (Windows) is the only way to reliably perform all three steps in a single call.
You said
What about for Linux ELF dynamic libs?
Gemini said
Unlike Windows and macOS, Linux does not have a single, mandatory OS-wide standard for signing user-space dynamic libraries (.so files). However, there are two common ways this is handled: IMA (Integrity Measurement Architecture) and custom ELF sections.
Because there isn't one "native" command that works everywhere, you usually have to parse the ELF structure or use specific security tools.
Many developers use tools like signelf to embed a signature into a dedicated section of the ELF file (usually named .sig or .signature).
How to extract and validate:
Since this isn't a built-in Node.js feature, you'll need to use objcopy (part of binutils) to extract the signature and then use the crypto module to validate it.
Step 1: Extract the signature section
TypeScript
import { execSync } from 'child_process';
import * as fs from 'fs';
function extractElfSignature(libPath: string, outputPath: string) {
// Extracts the raw binary data from the .sig section
execSync(objcopy --dump-section .sig=${outputPath} ${libPath});
}
Step 2: Validate using Node.js crypto
Once you have the raw signature and the original library (minus the signature section), you can verify it against a public key.
TypeScript
import * as crypto from 'crypto';
function verifyElf(libPath: string, signaturePath: string, publicKeyPath: string) {
const publicKey = fs.readFileSync(publicKeyPath);
const signature = fs.readFileSync(signaturePath);
// Crucial: You must hash the same data that was signed
// (usually the entire file minus the .sig section)
const fileBuffer = fs.readFileSync(libPath);
const verifier = crypto.createVerify('sha256');
verifier.update(fileBuffer);
const isValid = verifier.verify(publicKey, signature);
return isValid;
}
If the Linux system uses IMA, signatures are stored in the file's Extended Attributes (xattr) rather than inside the file itself. This is common in hardened environments (like automotive or high-security servers).
Extraction via getfattr
You can use the getfattr command to pull the signature from the security.ima attribute.
TypeScript
import { execSync } from 'child_process';
function getImaSignature(path: string) {
try {
// -n security.ima gets the hex-encoded signature
const output = execSync(getfattr -n security.ima --absolute-names -e hex "${path}").toString();
const hexMatch = output.match(/security.ima=0x(.*)/);
return hexMatch ? hexMatch[1] : null;
} catch (e) {
return null; // Attribute likely doesn't exist
}
}
Some distributions use the digsig toolset. If your library was signed using OpenSSL into a CMS/PKCS#7 format, you can validate it directly with the OpenSSL CLI.
TypeScript
function validateWithOpenSSL(libPath: string, sigPath: string, certPath: string) {
try {
// Validates a detached PKCS7 signature
execSync(openssl cms -verify -in ${sigPath} -inform DER -content ${libPath} -CAfile ${certPath} -binary);
return true;
} catch (e) {
return false;
}
}
Summary Comparison for Linux
Method Storage Location Common Tools Use Case
ELF Section Inside .so file objcopy, signelf Application-level security, custom verification.
IMA xattr Filesystem metadata evmctl, getfattr Kernel-level enforcement, Secure Boot.
Detached Sig External .sig file openssl, gpg Package managers, manual distributions.
Practical Recommendation
If you are building a TypeScript app that needs to verify its own plugins/libraries on Linux:
Standardize on a section name (like .signature).
Use objcopy to extract it.
Use Node’s crypto.createVerify to check it against your bundled public root certificate. This avoids needing complex OS-level configurations like IMA.
You said
Yes - I have control over both ends. It probably isn't necessary to embed the signature within the dynamic library, particularly as we supporting windows, mac and linux. Perhaps a separate signature file that signs the files signature, like ascii armor wil work.
Gemini said
Moving to a detached signature strategy is an excellent architectural choice for cross-platform support. It avoids the headache of parsing complex binary formats (PE, Mach-O, and ELF) and allows you to use a unified validation logic across all three operating systems.
By signing the file's hash (the "fingerprint") rather than embedding data into the binary, you treat the dynamic library as a "blob," making your TypeScript implementation much cleaner.
Instead of modifying the .dll, .dylib, or .so files, you generate a companion .sig file for each library.
The Signing Phase (Build Time)
Generate a SHA-256 hash of the library.
Sign that hash using your Private Key.
Save the signature as a binary file or Base64/ASCII-armored string.
The Validation Phase (Runtime in TS)
Load the library file and the signature file.
Verify the signature against the library's content using your Public Key.
You can use the built-in crypto module. This code is platform-agnostic and will work identically on Windows, macOS, and Linux.
TypeScript
import * as crypto from 'crypto';
import * as fs from 'fs';
/**
@param publicKeyPem The Public Key in PEM format
*/
function verifyLibrary(libPath: string, sigPath: string, publicKeyPem: string): boolean {
// 1. Load the public key
const publicKey = crypto.createPublicKey(publicKeyPem);
// 2. Read the library and the signature
const libraryBuffer = fs.readFileSync(libPath);
const signatureBuffer = fs.readFileSync(sigPath); // Assumes binary. Use 'base64' if ASCII armored.
// 3. Create the Verify object
// Note: Ensure the algorithm (sha256) matches your signing process
const verify = crypto.createVerify('SHA256');
verify.update(libraryBuffer);
verify.end();
// 4. Perform the verification
return verify.verify(publicKey, signatureBuffer);
}
Comparison of Formats
Since you mentioned "ASCII armor," you have two main choices for the signature format:
Format Pros Cons
Raw Binary Smallest file size; no overhead. Harder to debug; can't be easily read in a text editor.
Base64 / Armor Readable; can be stored in JSON or manifest files. ~33% larger than binary (negligible for small signatures).
Example: Using a Manifest File
If you have multiple libraries, it is often easier to use a single manifest.json rather than dozens of .sig files:
JSON
{
"libraries": {
"engine.dll": "MEUCIQDe...signature_base64...",
"libhelper.so": "MEQCIAj...signature_base64..."
}
}
Key Security Considerations
Public Key Management: Do not bundle the public key as a plain file that can be easily swapped. Ideally, hardcode the public key string into your TypeScript source or use a Secure Enclave/KMS if the environment supports it.
Streaming Large Files: If your dynamic libraries are very large (hundreds of MBs), don't use fs.readFileSync. Instead, use a ReadStream to pipe the data into the verifier to keep memory usage low:
TypeScript
const stream = fs.createReadStream(libPath);
stream.pipe(verify);
stream.on('end', () => {
const isValid = verify.verify(publicKey, signature);
});
Avoid "Time-of-Check to Time-of-Use" (TOCTOU): Ensure the library isn't swapped out by another process after you've verified it but before you load it. On Linux/macOS, you can use dlopen immediately after verification.
This "sidecar" approach is much more robust for a TypeScript environment because it shifts the complexity from binary parsing to standard cryptography, which Node.js handles natively and efficiently.
You said
Re public key management, you mean ensure the public key is in the typescript source code, not a configuration file. But it shouldn't matter that the source code is open source.
Re TOCTOU, I think just ensuring that the dynamic library is the one produced by us, will prevent a dns spoof attack that swaps out the backend API service for an attacker's one. Once installed, I would trust the dynamic libs - if users swap out them out for their own ones, that is on them.
Thirdly, to generate the .sig files compatible with the typescript crypto library, is there an existing command line tool I can use, or would it need to be a typescript program.
Gemini said
Correct—your logic regarding Open Source and the "Security Boundary" is spot on. If your code is open source, the public key is public knowledge anyway. Its presence in the source code isn't to keep the key a secret (since it's a public key), but to ensure its integrity. If it were in a config file, a local attacker could swap both the library and the public key, tricking your app into validating a malicious binary.
You do not need a custom TypeScript program to generate the signatures. You can use OpenSSL, which is the industry standard and perfectly compatible with Node.js crypto.
Step 1: Generate a Private Key (One-time)
Bash
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:4096
Step 2: Extract the Public Key (To put in your TS source)
Bash
openssl rsa -pubout -in private_key.pem -out public_key.pem
Step 3: Sign the Library (The .sig file)
This creates a binary signature of the dynamic library using SHA-256.
Bash
openssl dgst -sha256 -sign private_key.pem -out library.dll.sig library.dll
Note on Compatibility: Node's verify.verify() defaults to expecting a binary signature. If you want a Base64 string for easier distribution, you can pipe the output:
openssl dgst -sha256 -sign private_key.pem library.dll | openssl base64 -A > library.dll.sig
Since you are using OpenSSL for the .sig files, here is the corresponding TypeScript implementation using the Public Key string directly.
TypeScript
import * as crypto from 'crypto';
import * as fs from 'fs';
// Your public key (from public_key.pem) hardcoded as a constant
const PUBLIC_KEY = -----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA7...
-----END PUBLIC KEY-----;
function verifyLibrary(libPath: string, sigPath: string): boolean {
try {
const libData = fs.readFileSync(libPath);
const signature = fs.readFileSync(sigPath); // Binary format
const verify = crypto.createVerify('sha256');
verify.update(libData);
return verify.verify(PUBLIC_KEY, signature);
} catch (err) {
console.error("Verification failed:", err);
return false;
}
}
In asymmetric cryptography, the Public Key is effectively an identity. By hardcoding it, you are saying: "I only trust files signed by the person who holds the private key associated with this specific identity." An attacker can see the key, but they cannot forge a signature for it.
On TOCTOU & Trust
Your stance on "once installed, it's the user's responsibility" is the standard Owner Trust model used by most software.
The Attack you are preventing: A "Man-in-the-Middle" (MITM) or DNS spoofing attack during the download/update phase where a malicious .dll is served instead of yours.
The Result: Even if the attacker successfully hijacks the network and delivers a fake library, they cannot produce a valid .sig file because they don't have your private key. The TypeScript app will reject the library before loading it.
If you find managing .sig files for every library tedious, and your primary goal is just preventing network-level tampering, you could also use a Signed Manifest:
Calculate SHA-256 hashes for all libs.
Put them in a manifest.json.
Sign the manifest.json once.
TS validates the manifest signature, then checks if the local lib matches the hash in the manifest.
This is often easier to maintain as the number of dynamic libraries grows.
You're welcome! That approach strikes a great balance between robust security and cross-platform simplicity. Since you have control over the build pipeline, the detached signature method effectively treats your .dll, .dylib, and .so files as immutable data blobs, which is exactly what you want for stable verification.
To wrap things up, here is a quick "cheat sheet" for your implementation:
Use OpenSSL to generate the signature during your CI/CD process:
Bash
Generate the signature for the library
openssl dgst -sha256 -sign private_key.pem -out library.sig library.so
In your TypeScript environment, the verification remains consistent:
If you decide to sign very large libraries in the future, remember to use streams in Node.js instead of fs.readFileSync. This prevents your application from spiking in memory usage, as it will process the library in small chunks:
TypeScript
import * as crypto from 'crypto';
import * as fs from 'fs';
import { pipeline } from 'stream/promises';
async function verifyLargeLibrary(libPath: string, sigPath: string) {
const verify = crypto.createVerify('sha256');
const signature = fs.readFileSync(sigPath);
}
This setup is clean, professional, and avoids the "black magic" of trying to parse OS-specific binary headers. Good luck with the implementation!