2021-01-07 08:15:50 -07:00
|
|
|
/*
|
2021-01-13 05:20:33 -07:00
|
|
|
Copyright 2021 Yarmo Mackenbach
|
2021-01-07 08:15:50 -07:00
|
|
|
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
you may not use this file except in compliance with the License.
|
|
|
|
You may obtain a copy of the License at
|
|
|
|
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
See the License for the specific language governing permissions and
|
|
|
|
limitations under the License.
|
|
|
|
*/
|
2023-07-08 00:17:13 -06:00
|
|
|
import { readCleartextMessage, verify } from 'openpgp'
|
|
|
|
import { Claim } from './claim.js'
|
2023-07-09 04:03:25 -06:00
|
|
|
import { fetchURI } from './openpgp.js'
|
2021-01-07 08:15:50 -07:00
|
|
|
|
2021-04-22 07:14:21 -06:00
|
|
|
/**
|
2021-04-22 08:00:37 -06:00
|
|
|
* @module signatures
|
2021-04-22 07:14:21 -06:00
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Extract data from a signature and fetch the associated key
|
|
|
|
* @async
|
|
|
|
* @param {string} signature - The plaintext signature to process
|
|
|
|
* @returns {Promise<object>}
|
|
|
|
*/
|
2023-07-08 00:17:13 -06:00
|
|
|
export async function process (signature) {
|
|
|
|
/** @type {import('openpgp').CleartextMessage} */
|
2021-07-09 15:44:52 -06:00
|
|
|
let sigData
|
|
|
|
const result = {
|
|
|
|
fingerprint: null,
|
|
|
|
users: [
|
|
|
|
{
|
|
|
|
userData: {},
|
|
|
|
claims: []
|
2021-04-19 03:06:29 -06:00
|
|
|
}
|
2021-07-09 15:44:52 -06:00
|
|
|
],
|
|
|
|
primaryUserIndex: null,
|
|
|
|
key: {
|
|
|
|
data: null,
|
|
|
|
fetchMethod: null,
|
|
|
|
uri: null
|
2021-01-07 08:15:50 -07:00
|
|
|
}
|
2021-07-09 15:44:52 -06:00
|
|
|
}
|
|
|
|
|
2022-03-25 16:16:46 -06:00
|
|
|
// Read the signature
|
2021-07-09 15:44:52 -06:00
|
|
|
try {
|
2023-07-08 00:17:13 -06:00
|
|
|
sigData = await readCleartextMessage({
|
2021-11-17 07:54:48 -07:00
|
|
|
cleartextMessage: signature
|
|
|
|
})
|
2022-03-25 16:16:46 -06:00
|
|
|
} catch (e) {
|
|
|
|
throw new Error(`Signature could not be read (${e.message})`)
|
2021-07-09 15:44:52 -06:00
|
|
|
}
|
2022-03-25 16:16:46 -06:00
|
|
|
|
2023-05-03 07:35:40 -06:00
|
|
|
// @ts-ignore
|
2021-11-17 07:54:48 -07:00
|
|
|
const issuerKeyID = sigData.signature.packets[0].issuerKeyID.toHex()
|
2023-05-03 07:35:40 -06:00
|
|
|
// @ts-ignore
|
2021-11-17 07:54:48 -07:00
|
|
|
const signersUserID = sigData.signature.packets[0].signersUserID
|
2021-07-09 15:44:52 -06:00
|
|
|
const preferredKeyServer =
|
2023-05-03 07:35:40 -06:00
|
|
|
// @ts-ignore
|
2021-07-09 15:44:52 -06:00
|
|
|
sigData.signature.packets[0].preferredKeyServer ||
|
|
|
|
'https://keys.openpgp.org/'
|
|
|
|
const text = sigData.getText()
|
|
|
|
const sigKeys = []
|
|
|
|
|
|
|
|
text.split('\n').forEach((line, i) => {
|
|
|
|
const match = line.match(/^([a-zA-Z0-9]*)=(.*)$/i)
|
|
|
|
if (!match) {
|
|
|
|
return
|
2021-01-07 08:15:50 -07:00
|
|
|
}
|
2021-07-09 15:44:52 -06:00
|
|
|
switch (match[1].toLowerCase()) {
|
|
|
|
case 'key':
|
|
|
|
sigKeys.push(match[2])
|
|
|
|
break
|
2021-01-07 08:17:24 -07:00
|
|
|
|
2021-07-09 15:44:52 -06:00
|
|
|
case 'proof':
|
|
|
|
result.users[0].claims.push(new Claim(match[2]))
|
|
|
|
break
|
2021-04-19 03:06:29 -06:00
|
|
|
|
2021-07-09 15:44:52 -06:00
|
|
|
default:
|
|
|
|
break
|
2021-01-07 08:15:50 -07:00
|
|
|
}
|
2021-07-09 15:44:52 -06:00
|
|
|
})
|
2021-01-07 08:15:50 -07:00
|
|
|
|
2021-07-09 15:44:52 -06:00
|
|
|
// Try overruling key
|
|
|
|
if (sigKeys.length > 0) {
|
|
|
|
try {
|
|
|
|
result.key.uri = sigKeys[0]
|
2023-07-09 04:03:25 -06:00
|
|
|
result.key.data = (await fetchURI(result.key.uri)).publicKey.key
|
2021-07-09 15:44:52 -06:00
|
|
|
result.key.fetchMethod = result.key.uri.split(':')[0]
|
|
|
|
} catch (e) {}
|
|
|
|
}
|
|
|
|
// Try WKD
|
2021-11-17 07:54:48 -07:00
|
|
|
if (!result.key.data && signersUserID) {
|
2021-07-09 15:44:52 -06:00
|
|
|
try {
|
2021-11-17 07:54:48 -07:00
|
|
|
result.key.uri = `wkd:${signersUserID}`
|
2023-07-09 04:03:25 -06:00
|
|
|
result.key.data = (await fetchURI(result.key.uri)).publicKey.key
|
2021-07-09 15:44:52 -06:00
|
|
|
result.key.fetchMethod = 'wkd'
|
|
|
|
} catch (e) {}
|
|
|
|
}
|
|
|
|
// Try HKP
|
|
|
|
if (!result.key.data) {
|
|
|
|
try {
|
|
|
|
const match = preferredKeyServer.match(/^(.*:\/\/)?([^/]*)(?:\/)?$/i)
|
2021-11-17 07:54:48 -07:00
|
|
|
result.key.uri = `hkp:${match[2]}:${issuerKeyID || signersUserID}`
|
2023-07-09 04:03:25 -06:00
|
|
|
result.key.data = (await fetchURI(result.key.uri)).publicKey.key
|
2021-07-09 15:44:52 -06:00
|
|
|
result.key.fetchMethod = 'hkp'
|
|
|
|
} catch (e) {
|
2022-03-25 16:16:46 -06:00
|
|
|
throw new Error('Public key not found')
|
2021-04-19 03:06:29 -06:00
|
|
|
}
|
2021-07-09 15:44:52 -06:00
|
|
|
}
|
2021-04-19 03:06:29 -06:00
|
|
|
|
2022-03-25 16:16:46 -06:00
|
|
|
// Verify the signature
|
2023-07-08 00:17:13 -06:00
|
|
|
const verificationResult = await verify({
|
2023-05-03 07:35:40 -06:00
|
|
|
// @ts-ignore
|
2022-03-25 16:16:46 -06:00
|
|
|
message: sigData,
|
|
|
|
verificationKeys: result.key.data
|
|
|
|
})
|
|
|
|
const { verified } = verificationResult.signatures[0]
|
|
|
|
try {
|
|
|
|
await verified
|
|
|
|
} catch (e) {
|
|
|
|
throw new Error(`Signature could not be verified (${e.message})`)
|
|
|
|
}
|
|
|
|
|
2021-07-09 15:44:52 -06:00
|
|
|
result.fingerprint = result.key.data.keyPacket.getFingerprint()
|
2021-04-19 03:06:29 -06:00
|
|
|
|
2021-07-09 15:44:52 -06:00
|
|
|
result.users[0].claims.forEach((claim) => {
|
|
|
|
claim.fingerprint = result.fingerprint
|
2021-01-07 08:15:50 -07:00
|
|
|
})
|
2021-07-09 15:44:52 -06:00
|
|
|
|
|
|
|
const primaryUserData = await result.key.data.getPrimaryUser()
|
|
|
|
let userData
|
|
|
|
|
2021-11-17 07:54:48 -07:00
|
|
|
if (signersUserID) {
|
2023-05-03 07:31:13 -06:00
|
|
|
result.key.data.users.forEach((/** @type {{ userID: { email: string; }; }} */ user) => {
|
2021-11-17 07:54:48 -07:00
|
|
|
if (user.userID.email === signersUserID) {
|
2021-07-09 15:44:52 -06:00
|
|
|
userData = user
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if (!userData) {
|
|
|
|
userData = primaryUserData.user
|
|
|
|
}
|
|
|
|
|
|
|
|
result.users[0].userData = {
|
2022-03-25 15:20:08 -06:00
|
|
|
id: userData.userID ? userData.userID.userID : null,
|
2021-11-17 07:54:48 -07:00
|
|
|
name: userData.userID ? userData.userID.name : null,
|
|
|
|
email: userData.userID ? userData.userID.email : null,
|
|
|
|
comment: userData.userID ? userData.userID.comment : null,
|
2022-03-25 15:20:08 -06:00
|
|
|
isPrimary: primaryUserData.user.userID.userID === userData.userID.userID
|
2021-07-09 15:44:52 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
result.primaryUserIndex = result.users[0].userData.isPrimary ? 0 : null
|
|
|
|
|
|
|
|
return result
|
2021-01-07 08:15:50 -07:00
|
|
|
}
|