dsig
Version:
Digital Signature with OpenPGP
186 lines (158 loc) • 7.46 kB
JavaScript
/*
** DSIG -- Digital Signature with OpenPGP
** Copyright (c) 2015-2022 Dr. Ralf S. Engelschall <rse@engelschall.com>
** Licensed under LGPL 3.0 <https://spdx.org/licenses/LGPL-3.0-only>
*/
/* external requirements */
const openpgp = require("openpgp")
const sha512 = require("hash.js/lib/hash/sha/512")
/* the API class */
module.exports = class DSIG {
/* generate a private/public key pair */
static async keygen (userName, userEmail, passPhrase) {
/* generate key pair */
const keypair = await openpgp.generateKey({
userIDs: [ { name: userName, email: userEmail } ],
passphrase: passPhrase,
curve: openpgp.enums.curve.curve25519,
config: {
showVersion: true,
versionString: "DSIG-1.0 <placeholder1>",
showComment: true,
commentString: "<placeholder2>"
}
})
/* calculate fingerprint */
let fp = (await openpgp.readKey({ armoredKey: keypair.publicKey })).getFingerprint()
fp = fp.toUpperCase().replace(/([0-9A-F]{4})(?=.)/g, "$1-")
/* post-adjust private/public keys */
const privateKey = keypair.privateKey
.replace(/<placeholder1>/, "OpenPGP Private Key")
.replace(/<placeholder2>/, `${userName} <${userEmail}> [${fp}]`)
const publicKey = keypair.publicKey
.replace(/<placeholder1>/, "OpenPGP Public Key")
.replace(/<placeholder2>/, `${userName} <${userEmail}> [${fp}]`)
/* return key pair */
return { privateKey, publicKey }
}
/* calculate fingerprint of public key */
static async fingerprint (publicKey) {
/* read public key */
const key = await openpgp.readKey({ armoredKey: publicKey })
/* verify integrity of public key */
const result = await key.verifyPrimaryKey().then(() => "").catch((err) => err)
if (result !== "")
throw new Error(`invalid public key (integrity check failed: ${result}`)
/* extract fingerprint */
let fp = key.getFingerprint()
fp = fp.toUpperCase().replace(/([0-9A-F]{4})(?=.)/g, "$1-")
/* return fingerprint */
return fp
}
/* sign payload with private key */
static async sign (payload, privateKey, passPhrase, metaInfo = null) {
/* read comment from private key */
const [ , user ] = privateKey.match(/\r?\nComment: *([^\r\n]+)/)
if (user === undefined)
throw new Error("invalid private key (comment line not found)")
/* read private key */
let key = await openpgp.readKey({ armoredKey: privateKey })
if (key.isPrivate())
key = await openpgp.decryptKey({ privateKey: key, passphrase: passPhrase })
/* verify integrity of private key */
const result = await key.verifyPrimaryKey().then(() => "").catch((err) => err)
if (result !== "")
throw new Error(`invalid private key (integrity check failed: ${result}`)
/* determine creation time and size */
const issued = (new Date()).toISOString()
/* calculate message digest on payload ourself */
let payloadDigest
let payloadLength
if (payload !== null) {
payloadDigest = sha512().update(payload).digest("hex")
payloadDigest = payloadDigest.toUpperCase()
payloadLength = payload.length
}
/* define message */
let msg = `DSIG-Issued: ${issued}\r\n`
if (payload !== null) {
msg += `DSIG-Payload-Length: ${payloadLength}\r\n`
const value = payloadDigest
.replace(/([0-9A-F-]{4})(?=.)/g, "$1-")
.replace(/([0-9A-F-]{80})(?=.)/g, "$1\r\n ")
.replace(/-$/mg, "")
msg += `DSIG-Payload-Digest:\r\n ${value}\r\n`
}
if (metaInfo !== null) {
metaInfo = metaInfo.replace(/\r?\n/g, "\r\n")
msg += `\r\n${metaInfo}`
}
/* verify clear-signed signature with public key */
msg = await openpgp.createCleartextMessage({ text: msg })
let sig = await openpgp.sign({
message: msg,
signingKeys: [ key ],
config: {
showVersion: true,
versionString: "DSIG-1.0 OpenPGP Digital Signature",
showComment: true,
commentString: user
}
})
sig = sig.replace(/^\r?\n/, "")
.replace(/(\r?\n)\r?\n$/, "$1")
return sig
}
/* verify payload with public key and fingerprint */
static async verify (payload, signature, publicKey, fingerPrint = null) {
/* read public key */
const key = await openpgp.readKey({ armoredKey: publicKey })
/* verify integrity of public key */
let result = await key.verifyPrimaryKey().then(() => "").catch((err) => err)
if (result !== "")
throw new Error(`invalid public key (integrity check failed: ${result}`)
if (fingerPrint !== null) {
const fingerprint = fingerPrint.toLowerCase().replace(/[^a-fA-F0-9]/g, "")
if (key.getFingerprint() !== fingerprint)
throw new Error("invalid public key (fingerprint does not match)")
}
/* read clear-signed signature */
const sig = await openpgp.readCleartextMessage({ cleartextMessage: signature })
/* verify clear-signed signature with public key */
result = await openpgp.verify({ message: sig, verificationKeys: [ key ] })
/* ensure that the signature validated successfully */
if (!( typeof result === "object"
&& typeof result.signatures === "object"
&& result.signatures instanceof Array
&& result.signatures.length === 1
&& (await result.signatures[0].verified) === true))
throw new Error("invalid digital signature")
/* parse embedded key/value information */
let m = result.data.match(/^((?:DSIG-[a-zA-Z0-9-]+: *[^\r\n]*(?:\r?\n +[^\r\n]+)*\r?\n)+)(?:\r?\n((?:.|\r?\n)*))?$/)
if (m === null)
throw new Error("invalid signature message")
let [ , headers, metaInfo ] = m
const header = {}
const re = /(DSIG-[a-zA-Z0-9-]+): *([^\r\n]*(?:\r?\n +[^\r\n]+)*)\r?\n/g
while ((m = re.exec(headers)) !== null) {
let [ , key, value ] = m
value = value.replace(/^ +/, "").replace(/\r?\n +/g, "").replace(/ +$/, "")
header[key] = value
}
if (metaInfo === undefined)
metaInfo = null
/* verify payload integrity */
if (payload !== null) {
/* sanity check for existing message digest */
if (header["DSIG-Payload-Digest"] === undefined)
throw new Error("DSIG-Payload-Digest header missing")
/* calculate message digest on payload ourself */
const sha = sha512().update(payload).digest("hex").toUpperCase()
/* compare message digests */
if (sha !== header["DSIG-Payload-Digest"].replace(/-/g, ""))
throw new Error("DSIG-Payload-Digest does not match")
}
/* provide embedded key/value information */
return metaInfo
}
}