109 lines
3.7 KiB
JavaScript
109 lines
3.7 KiB
JavaScript
const sodium = require('sodium-native');
|
|
|
|
// Parse a minisign key represented as a base64 string.
|
|
// Throws exceptions on invalid keys.
|
|
function parseKey(key_str) {
|
|
const key_info = Buffer.from(key_str, 'base64');
|
|
|
|
const id = key_info.subarray(2, 10);
|
|
const key = key_info.subarray(10);
|
|
|
|
if (key.byteLength !== sodium.crypto_sign_PUBLICKEYBYTES) {
|
|
throw new Error('invalid public key given');
|
|
}
|
|
|
|
return {
|
|
id: id,
|
|
key: key
|
|
};
|
|
}
|
|
|
|
// Parse a buffer containing the contents of a minisign signature file.
|
|
// Throws exceptions on invalid signature files.
|
|
function parseSignature(sig_buf) {
|
|
const untrusted_header = Buffer.from('untrusted comment: ');
|
|
const trusted_header = Buffer.from('trusted comment: ');
|
|
|
|
// Validate untrusted comment header, and skip
|
|
if (!sig_buf.subarray(0, untrusted_header.byteLength).equals(untrusted_header)) {
|
|
throw new Error('invalid minisign signature: bad untrusted comment header');
|
|
}
|
|
sig_buf = sig_buf.subarray(untrusted_header.byteLength);
|
|
|
|
// Skip untrusted comment
|
|
sig_buf = sig_buf.subarray(sig_buf.indexOf('\n') + 1);
|
|
|
|
// Read and skip signature info
|
|
const sig_info_end = sig_buf.indexOf('\n');
|
|
const sig_info = Buffer.from(sig_buf.subarray(0, sig_info_end).toString(), 'base64');
|
|
sig_buf = sig_buf.subarray(sig_info_end + 1);
|
|
|
|
// Extract components of signature info
|
|
const algorithm = sig_info.subarray(0, 2);
|
|
const key_id = sig_info.subarray(2, 10);
|
|
const signature = sig_info.subarray(10);
|
|
|
|
// Validate trusted comment header, and skip
|
|
if (!sig_buf.subarray(0, trusted_header.byteLength).equals(trusted_header)) {
|
|
throw new Error('invalid minisign signature: bad trusted comment header');
|
|
}
|
|
sig_buf = sig_buf.subarray(trusted_header.byteLength);
|
|
|
|
// Read and skip trusted comment
|
|
const trusted_comment_end = sig_buf.indexOf('\n');
|
|
const trusted_comment = sig_buf.subarray(0, trusted_comment_end);
|
|
sig_buf = sig_buf.subarray(trusted_comment_end + 1);
|
|
|
|
// Read and skip global signature; handle missing trailing newline, just in case
|
|
let global_sig_end = sig_buf.indexOf('\n');
|
|
if (global_sig_end == -1) global_sig_end = sig_buf.length;
|
|
const global_sig = Buffer.from(sig_buf.subarray(0, global_sig_end).toString(), 'base64');
|
|
sig_buf = sig_buf.subarray(sig_info_end + 1); // this might be length+1, but that's allowed
|
|
|
|
// Validate that all data has been consumed
|
|
if (sig_buf.length !== 0) {
|
|
throw new Error('invalid minisign signature: trailing bytes');
|
|
}
|
|
|
|
return {
|
|
algorithm: algorithm,
|
|
key_id: key_id,
|
|
signature: signature,
|
|
trusted_comment: trusted_comment,
|
|
global_signature: global_sig,
|
|
};
|
|
}
|
|
|
|
// Given a parsed key, parsed signature file, and raw file content, verifies the signature,
|
|
// including the global signature (hence validating the trusted comment). Does not throw.
|
|
// Returns 'true' if the signature is valid for this file, 'false' otherwise.
|
|
function verifySignature(pubkey, signature, file_content) {
|
|
if (!signature.key_id.equals(pubkey.id)) {
|
|
return false;
|
|
}
|
|
|
|
let signed_content;
|
|
if (signature.algorithm.equals(Buffer.from('ED'))) {
|
|
signed_content = Buffer.alloc(sodium.crypto_generichash_BYTES_MAX);
|
|
sodium.crypto_generichash(signed_content, file_content);
|
|
} else {
|
|
signed_content = file_content;
|
|
}
|
|
if (!sodium.crypto_sign_verify_detached(signature.signature, signed_content, pubkey.key)) {
|
|
return false;
|
|
}
|
|
|
|
const global_signed_content = Buffer.concat([signature.signature, signature.trusted_comment]);
|
|
if (!sodium.crypto_sign_verify_detached(signature.global_signature, global_signed_content, pubkey.key)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
module.exports = {
|
|
parseKey,
|
|
parseSignature,
|
|
verifySignature,
|
|
};
|