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.
|
|
|
|
*/
|
2024-01-27 09:57:46 -07:00
|
|
|
import { CleartextMessage, PublicKey, readCleartextMessage, verify } from 'openpgp'
|
2023-07-08 00:17:13 -06:00
|
|
|
import { Claim } from './claim.js'
|
2023-07-09 04:03:25 -06:00
|
|
|
import { fetchURI } from './openpgp.js'
|
2023-07-13 02:40:02 -06:00
|
|
|
import { Profile } from './profile.js'
|
|
|
|
import { ProfileType, PublicKeyEncoding, PublicKeyType } from './enums.js'
|
|
|
|
import { Persona } from './persona.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
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
2023-07-13 02:40:02 -06:00
|
|
|
* Extract the profile from a signature and fetch the associated key
|
|
|
|
* @param {string} signature - The plaintext signature to parse
|
2024-01-27 10:00:55 -07:00
|
|
|
* @returns {Promise<Profile>} The profile obtained from the signature
|
2021-04-22 07:14:21 -06:00
|
|
|
*/
|
2023-07-13 02:40:02 -06:00
|
|
|
export async function parse (signature) {
|
2024-01-27 09:57:46 -07:00
|
|
|
/** @type {CleartextMessage} */
|
2021-07-09 15:44:52 -06:00
|
|
|
let sigData
|
|
|
|
|
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 = []
|
2023-07-13 02:40:02 -06:00
|
|
|
const claims = []
|
2021-07-09 15:44:52 -06:00
|
|
|
|
|
|
|
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':
|
2023-07-13 02:40:02 -06:00
|
|
|
claims.push(new Claim(match[2]))
|
2021-07-09 15:44:52 -06:00
|
|
|
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
|
|
|
|
2023-07-13 02:40:02 -06:00
|
|
|
const obtainedKey = {
|
|
|
|
query: null,
|
|
|
|
data: null,
|
|
|
|
method: null
|
|
|
|
}
|
|
|
|
|
|
|
|
// Try key identifier found in the signature
|
2021-07-09 15:44:52 -06:00
|
|
|
if (sigKeys.length > 0) {
|
|
|
|
try {
|
2023-07-13 02:40:02 -06:00
|
|
|
obtainedKey.query = sigKeys[0]
|
2024-01-27 09:57:46 -07:00
|
|
|
/** @type {PublicKey} */
|
2023-07-13 02:40:02 -06:00
|
|
|
obtainedKey.data = (await fetchURI(obtainedKey.query)).publicKey.key
|
|
|
|
obtainedKey.method = obtainedKey.query.split(':')[0]
|
2021-07-09 15:44:52 -06:00
|
|
|
} catch (e) {}
|
|
|
|
}
|
|
|
|
// Try WKD
|
2023-07-13 02:40:02 -06:00
|
|
|
if (!obtainedKey.data && signersUserID) {
|
2021-07-09 15:44:52 -06:00
|
|
|
try {
|
2023-07-13 02:40:02 -06:00
|
|
|
obtainedKey.query = signersUserID
|
|
|
|
obtainedKey.data = (await fetchURI(`wkd:${signersUserID}`)).publicKey.key
|
|
|
|
obtainedKey.method = 'wkd'
|
2021-07-09 15:44:52 -06:00
|
|
|
} catch (e) {}
|
|
|
|
}
|
|
|
|
// Try HKP
|
2023-07-13 02:40:02 -06:00
|
|
|
if (!obtainedKey.data) {
|
2021-07-09 15:44:52 -06:00
|
|
|
try {
|
|
|
|
const match = preferredKeyServer.match(/^(.*:\/\/)?([^/]*)(?:\/)?$/i)
|
2023-07-13 02:40:02 -06:00
|
|
|
obtainedKey.query = issuerKeyID || signersUserID
|
|
|
|
obtainedKey.data = (await fetchURI(`hkp:${match[2]}:${obtainedKey.query}`)).publicKey.key
|
|
|
|
obtainedKey.method = 'hkp'
|
2021-07-09 15:44:52 -06:00
|
|
|
} 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
|
|
|
|
2023-07-13 02:40:02 -06:00
|
|
|
const primaryUserData = await obtainedKey.data.getPrimaryUser()
|
|
|
|
const fingerprint = obtainedKey.data.getFingerprint()
|
|
|
|
|
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,
|
2023-07-13 02:40:02 -06:00
|
|
|
verificationKeys: obtainedKey.data
|
2022-03-25 16:16:46 -06:00
|
|
|
})
|
|
|
|
const { verified } = verificationResult.signatures[0]
|
|
|
|
try {
|
|
|
|
await verified
|
|
|
|
} catch (e) {
|
|
|
|
throw new Error(`Signature could not be verified (${e.message})`)
|
|
|
|
}
|
|
|
|
|
2023-07-13 02:40:02 -06:00
|
|
|
// Build the persona
|
|
|
|
const persona = new Persona(primaryUserData.user.userID.name, [])
|
|
|
|
persona.setIdentifier(primaryUserData.user.userID.userID)
|
|
|
|
persona.setDescription(primaryUserData.user.userID.comment || null)
|
|
|
|
persona.setEmailAddress(primaryUserData.user.userID.email || null)
|
|
|
|
persona.claims = claims
|
|
|
|
.map(
|
|
|
|
({ value }) =>
|
|
|
|
new Claim(new TextDecoder().decode(value), `openpgp4fpr:${fingerprint}`)
|
|
|
|
)
|
|
|
|
|
|
|
|
const profile = new Profile(ProfileType.OPENPGP, `openpgp4fpr:${fingerprint}`, [persona])
|
|
|
|
|
|
|
|
profile.publicKey.keyType = PublicKeyType.OPENPGP
|
|
|
|
profile.publicKey.encoding = PublicKeyEncoding.ARMORED_PGP
|
|
|
|
profile.publicKey.encodedKey = obtainedKey.data.armor()
|
|
|
|
profile.publicKey.key = obtainedKey.data
|
|
|
|
profile.publicKey.fetch.method = obtainedKey.method
|
|
|
|
profile.publicKey.fetch.query = obtainedKey.query
|
|
|
|
|
|
|
|
return profile
|
2021-01-07 08:15:50 -07:00
|
|
|
}
|