doipjs/src/signatures.js

150 lines
4.3 KiB
JavaScript
Raw Normal View History

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'
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
2021-04-22 07:14:21 -06:00
* @async
2023-07-13 02:40:02 -06:00
* @param {string} signature - The plaintext signature to parse
2024-01-27 09:57:46 -07:00
* @returns {Promise<Profile>}
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
// @ts-ignore
2021-11-17 07:54:48 -07:00
const issuerKeyID = sigData.signature.packets[0].issuerKeyID.toHex()
// @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 =
// @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({
// @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
}