OPSV has been integrated in Keyoxide and is no longer updated https://codeberg.org/yarmo/keyoxide
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

267 lines
11 KiB

  1. <!DOCTYPE html>
  2. <html lang="en" dir="ltr">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1">
  6. <title>Open PGP Signature Verification</title>
  7. <script async defer data-domain="opsv.foss.guru" src="https://plausible.io/js/plausible.js"></script>
  8. <script src="openpgp.min.js"></script>
  9. <style media="screen">
  10. * {
  11. box-sizing: border-box;
  12. }
  13. body {
  14. background-color: #777;
  15. }
  16. .container {
  17. max-width: 640px;
  18. width: 100%;
  19. margin: 32px auto;
  20. padding: 16px 32px 32px;
  21. background-color: #fff;
  22. border-radius: 5px;
  23. border: 3px solid #333;
  24. }
  25. footer {
  26. color: #777;
  27. margin: 64px 0 0 0;
  28. }
  29. a {
  30. color: #3f9acc;
  31. }
  32. textarea {
  33. width: 100%;
  34. height: 128px;
  35. resize: vertical;
  36. }
  37. input[type="radio"] {
  38. vertical-align: sub;
  39. }
  40. input[type="submit"] {
  41. width: 100%;
  42. padding: 8px;
  43. font-size: 1.2em;
  44. text-transform: uppercase;
  45. background-color: #c3eaff;
  46. border: 1px solid #3f9acc;
  47. border-radius: 3px;
  48. border-bottom-width: 3px;
  49. cursor: pointer;
  50. }
  51. input[type="submit"]:hover {
  52. background-color: #9dd3f0;
  53. }
  54. .green {
  55. color: green;
  56. }
  57. .red {
  58. color: red;
  59. }
  60. .label {
  61. display: inline-block;
  62. margin: 0 0 8px;
  63. }
  64. </style>
  65. </head>
  66. <body>
  67. <div class="container">
  68. <h1>Open PGP Signature Verification</h1>
  69. <form id="form" method="post">
  70. <h3>Signature</h3>
  71. <textarea name="message" id="message"></textarea>
  72. <h3>Public Key (1: plaintext)</h3>
  73. <textarea name="publicKey" id="publicKey"></textarea>
  74. <h3>Public Key (2: web key directory)</h3>
  75. <input type="text" name="wkd" id="wkd" placeholder="name@domain.com">
  76. <h3>Public Key (3: HKP server)</h3>
  77. <input type="text" name="hkp_server" id="hkp_server" placeholder="https://keys.openpgp.org/">
  78. <input type="text" name="hkp_input" id="hkp_input" placeholder="Email address / key id / fingerprint">
  79. <h3>Result</h3>
  80. <p id="result">Click on the button below.</p>
  81. <p id="resultContent"></p>
  82. <input type="submit" name="submit" value="VERIFY SIGNATURE">
  83. </form>
  84. <footer>
  85. <p>
  86. <a href="/">OPSV</a> is a FOSS solution for easy PGP signature verification (<a href="https://codeberg.org/yarmo/opsv/src/branch/master/README.md">how to use</a>).
  87. <br>
  88. Made by <a href="https://yarmo.eu">Yarmo Mackenbach</a>.
  89. <br>
  90. Code hosted on <a href="https://codeberg.org/yarmo/opsv">Codeberg</a> (<a href="https://drone.private.foss.best/yarmo/opsv/">drone CI/CD</a>).
  91. <br>
  92. Uses <a href="https://github.com/openpgpjs/openpgpjs">openpgp.js</a> (version <a href="https://github.com/openpgpjs/openpgpjs/releases/tag/v4.10.4">4.10.4</a>).
  93. <br>
  94. Privacy-friendly public usage stats by <a href="https://plausible.io/opsv.foss.guru">Plausible.io</a>.
  95. </p>
  96. </footer>
  97. </div>
  98. </body>
  99. <script type="text/javascript">
  100. async function verifySignature(opts) {
  101. const elRes = document.body.querySelector("#result");
  102. const elResContent = document.body.querySelector("#resultContent");
  103. let feedback, message, verified, publicKey, fp, lookupOpts, wkd, hkp, sig, userId, keyId, sigContent;
  104. try {
  105. switch (opts.mode) {
  106. case "plaintext":
  107. publicKey = (await openpgp.key.readArmored(opts.input)).keys;
  108. break;
  109. case "wkd":
  110. wkd = new openpgp.WKD();
  111. lookupOpts = {
  112. email: opts.input
  113. };
  114. publicKey = (await wkd.lookup(lookupOpts)).keys;
  115. break;
  116. case "hkp":
  117. if (!opts.server) {opts.server = "https://keys.openpgp.org/"};
  118. hkp = new openpgp.HKP(opts.server);
  119. lookupOpts = {
  120. query: opts.input
  121. };
  122. publicKey = await hkp.lookup(lookupOpts);
  123. publicKey = (await openpgp.key.readArmored(publicKey)).keys;
  124. break;
  125. default:
  126. sig = (await openpgp.signature.readArmored(opts.message));
  127. if ('compressed' in sig.packets[0]) {
  128. sig = sig.packets[0];
  129. sigContent = (await openpgp.stream.readToEnd(await sig.packets[1].getText()));
  130. };
  131. keyId = (await sig.packets[0].issuerKeyId.toHex());
  132. userId = sig.packets[0].signersUserId;
  133. if (!keyId && !userId) {
  134. elRes.innerHTML = "The signature does not contain a valid keyId or userId.";
  135. elRes.classList.remove('green');
  136. elRes.classList.add('red');
  137. return;
  138. }
  139. if (!opts.server) {opts.server = "https://keys.openpgp.org/"};
  140. hkp = new openpgp.HKP(opts.server);
  141. lookupOpts = {
  142. query: userId ? userId : keyId
  143. };
  144. publicKey = await hkp.lookup(lookupOpts);
  145. publicKey = (await openpgp.key.readArmored(publicKey)).keys;
  146. break;
  147. }
  148. if (opts.message == null) {
  149. elRes.innerHTML = "No message was provided.";
  150. elRes.classList.remove('green');
  151. elRes.classList.add('red');
  152. return;
  153. }
  154. let readError = null;
  155. try {
  156. message = await openpgp.message.readArmored(opts.message);
  157. } catch(e) {
  158. readError = e;
  159. }
  160. try {
  161. message = await openpgp.cleartext.readArmored(opts.message);
  162. } catch(e) {
  163. readError = e;
  164. }
  165. if (message == null) {throw(readError)};
  166. fp = publicKey[0].getFingerprint();
  167. verified = await openpgp.verify({
  168. message: message,
  169. publicKeys: publicKey
  170. });
  171. } catch (e) {
  172. console.error(e);
  173. elRes.innerHTML = e;
  174. elRes.classList.remove('green');
  175. elRes.classList.add('red');
  176. return;
  177. }
  178. feedback = '';
  179. const valid = verified.signatures[0];
  180. if (sigContent) {
  181. elResContent.innerHTML = "<strong>Signature content:</strong><br><span style=\"white-space: pre-line\">"+sigContent+"</span>";
  182. }
  183. if (userId) {
  184. if (valid) {
  185. feedback += "The message was signed by the userId extracted from the signature.<br>";
  186. feedback += 'UserId: '+userId+'<br>';
  187. feedback += "Fingerprint: "+fp+"<br>";
  188. elRes.classList.remove('red');
  189. elRes.classList.add('green');
  190. } else {
  191. feedback += "The message's signature COULD NOT BE verified using the userId extracted from the signature.<br>";
  192. elRes.classList.remove('green');
  193. elRes.classList.add('red');
  194. }
  195. } else if (keyId) {
  196. if (valid) {
  197. feedback += "The message was signed by the keyId extracted from the signature.<br>";
  198. feedback += 'KeyID: '+keyId+'<br>';
  199. feedback += "Fingerprint: "+fp+"<br><br>";
  200. feedback += "!!! You should manually verify the fingerprint to confirm the signer's identity !!!";
  201. elRes.classList.remove('red');
  202. elRes.classList.add('green');
  203. } else {
  204. feedback += "The message's signature COULD NOT BE verified using the keyId extracted from the signature.<br>";
  205. elRes.classList.remove('green');
  206. elRes.classList.add('red');
  207. }
  208. } else {
  209. if (valid) {
  210. feedback += "The message was signed by the provided key ("+opts.mode+").<br>";
  211. feedback += "Fingerprint: "+fp+"<br>";
  212. elRes.classList.remove('red');
  213. elRes.classList.add('green');
  214. } else {
  215. feedback += "The message's signature COULD NOT BE verified using the provided key ("+opts.mode+").<br>";
  216. elRes.classList.remove('green');
  217. elRes.classList.add('red');
  218. }
  219. }
  220. elRes.innerHTML = feedback;
  221. };
  222. document.body.querySelector("#form").onsubmit = function (evt) {
  223. evt.preventDefault();
  224. let opts = {
  225. message: null,
  226. mode: null,
  227. input: null,
  228. server: null,
  229. };
  230. opts.message = document.body.querySelector("#message").value;
  231. if (document.body.querySelector("#publicKey").value != "") {
  232. opts.input = document.body.querySelector("#publicKey").value;
  233. opts.mode = "plaintext";
  234. } else if (document.body.querySelector("#wkd").value != "") {
  235. opts.input = document.body.querySelector("#wkd").value;
  236. opts.mode = "wkd";
  237. } else if (document.body.querySelector("#hkp_input").value != "") {
  238. opts.input = document.body.querySelector("#hkp_input").value;
  239. opts.server = document.body.querySelector("#hkp_server").value;
  240. opts.mode = "hkp";
  241. }
  242. verifySignature(opts);
  243. };
  244. </script>
  245. </html>