feat: refactor keys to openpgp, use Profile class

This commit is contained in:
Yarmo Mackenbach 2023-07-09 12:01:09 +02:00
parent a30339272a
commit 149ac6f71e
No known key found for this signature in database
GPG key ID: 3C57D093219103A3
3 changed files with 146 additions and 120 deletions

View file

@ -18,7 +18,7 @@ export { Persona } from './persona.js'
export { Claim } from './claim.js' export { Claim } from './claim.js'
export { ServiceProvider } from './serviceProvider.js' export { ServiceProvider } from './serviceProvider.js'
export * as proofs from './proofs.js' export * as proofs from './proofs.js'
export * as keys from './keys.js' export * as openpgp from './openpgp.js'
export * as asp from './asp.js' export * as asp from './asp.js'
export * as signatures from './signatures.js' export * as signatures from './signatures.js'
export * as enums from './enums.js' export * as enums from './enums.js'

View file

@ -19,10 +19,13 @@ import { readKey, PublicKey } from 'openpgp'
import HKP from '@openpgp/hkp-client' import HKP from '@openpgp/hkp-client'
import WKD from '@openpgp/wkd-client' import WKD from '@openpgp/wkd-client'
import { Claim } from './claim.js' import { Claim } from './claim.js'
import { ProfileType, PublicKeyEncoding, PublicKeyFetchMethod, PublicKeyType } from './enums.js'
import { Profile } from './profile.js'
import { Persona } from './persona.js'
/** /**
* Functions related to the fetching and handling of keys * Functions related to OpenPGP Profiles
* @module keys * @module openpgp
*/ */
/** /**
@ -30,7 +33,7 @@ import { Claim } from './claim.js'
* @function * @function
* @param {string} identifier - Fingerprint or email address * @param {string} identifier - Fingerprint or email address
* @param {string} [keyserverDomain=keys.openpgp.org] - Domain of the keyserver * @param {string} [keyserverDomain=keys.openpgp.org] - Domain of the keyserver
* @returns {Promise<PublicKey>} * @returns {Promise<Profile>}
* @example * @example
* const key1 = doip.keys.fetchHKP('alice@domain.tld'); * const key1 = doip.keys.fetchHKP('alice@domain.tld');
* const key2 = doip.keys.fetchHKP('123abc123abc'); * const key2 = doip.keys.fetchHKP('123abc123abc');
@ -40,61 +43,79 @@ export async function fetchHKP (identifier, keyserverDomain) {
? `https://${keyserverDomain}` ? `https://${keyserverDomain}`
: 'https://keys.openpgp.org' : 'https://keys.openpgp.org'
// @ts-ignore
const hkp = new HKP(keyserverBaseUrl) const hkp = new HKP(keyserverBaseUrl)
const lookupOpts = { const lookupOpts = {
query: identifier query: identifier
} }
const publicKey = await hkp const publicKeyArmored = await hkp
.lookup(lookupOpts) .lookup(lookupOpts)
.catch((error) => { .catch((error) => {
throw new Error(`Key does not exist or could not be fetched (${error})`) throw new Error(`Key does not exist or could not be fetched (${error})`)
}) })
if (!publicKey) { if (!publicKeyArmored) {
throw new Error('Key does not exist or could not be fetched') throw new Error('Key does not exist or could not be fetched')
} }
return await readKey({ const publicKey = await readKey({
armoredKey: publicKey armoredKey: publicKeyArmored
}) })
.catch((error) => { .catch((error) => {
throw new Error(`Key could not be read (${error})`) throw new Error(`Key could not be read (${error})`)
}) })
const profile = await parsePublicKey(publicKey)
profile.publicKey.keyType = PublicKeyType.OPENPGP
profile.publicKey.encoding = PublicKeyEncoding.ARMORED_PGP
profile.publicKey.encodedKey = publicKey.armor()
profile.publicKey.key = publicKey
profile.publicKey.fetch.method = PublicKeyFetchMethod.HKP
profile.publicKey.fetch.query = identifier
return profile
} }
/** /**
* Fetch a public key using Web Key Directory * Fetch a public key using Web Key Directory
* @function * @function
* @param {string} identifier - Identifier of format 'username@domain.tld` * @param {string} identifier - Identifier of format 'username@domain.tld`
* @returns {Promise<PublicKey>} * @returns {Promise<Profile>}
* @example * @example
* const key = doip.keys.fetchWKD('alice@domain.tld'); * const key = doip.keys.fetchWKD('alice@domain.tld');
*/ */
export async function fetchWKD (identifier) { export async function fetchWKD (identifier) {
// @ts-ignore
const wkd = new WKD() const wkd = new WKD()
const lookupOpts = { const lookupOpts = {
email: identifier email: identifier
} }
const publicKey = await wkd const publicKeyBinary = await wkd
.lookup(lookupOpts) .lookup(lookupOpts)
.catch((/** @type {Error} */ error) => { .catch((/** @type {Error} */ error) => {
throw new Error(`Key does not exist or could not be fetched (${error})`) throw new Error(`Key does not exist or could not be fetched (${error})`)
}) })
if (!publicKey) { if (!publicKeyBinary) {
throw new Error('Key does not exist or could not be fetched') throw new Error('Key does not exist or could not be fetched')
} }
return await readKey({ const publicKey = await readKey({
binaryKey: publicKey binaryKey: publicKeyBinary
}) })
.catch((error) => { .catch((error) => {
throw new Error(`Key could not be read (${error})`) throw new Error(`Key could not be read (${error})`)
}) })
const profile = await parsePublicKey(publicKey)
profile.publicKey.keyType = PublicKeyType.OPENPGP
profile.publicKey.encoding = PublicKeyEncoding.ARMORED_PGP
profile.publicKey.encodedKey = publicKey.armor()
profile.publicKey.key = publicKey
profile.publicKey.fetch.method = PublicKeyFetchMethod.WKD
profile.publicKey.fetch.query = identifier
return profile
} }
/** /**
@ -102,7 +123,7 @@ export async function fetchWKD (identifier) {
* @function * @function
* @param {string} username - Keybase username * @param {string} username - Keybase username
* @param {string} fingerprint - Fingerprint of key * @param {string} fingerprint - Fingerprint of key
* @returns {Promise<PublicKey>} * @returns {Promise<Profile>}
* @example * @example
* const key = doip.keys.fetchKeybase('alice', '123abc123abc'); * const key = doip.keys.fetchKeybase('alice', '123abc123abc');
*/ */
@ -126,19 +147,30 @@ export async function fetchKeybase (username, fingerprint) {
throw new Error(`Error fetching Keybase key: ${e.message}`) throw new Error(`Error fetching Keybase key: ${e.message}`)
} }
return await readKey({ const publicKey = await readKey({
armoredKey: rawKeyContent armoredKey: rawKeyContent
}) })
.catch((error) => { .catch((error) => {
throw new Error(`Key does not exist or could not be fetched (${error})`) throw new Error(`Key does not exist or could not be fetched (${error})`)
}) })
const profile = await parsePublicKey(publicKey)
profile.publicKey.keyType = PublicKeyType.OPENPGP
profile.publicKey.encoding = PublicKeyEncoding.ARMORED_PGP
profile.publicKey.encodedKey = publicKey.armor()
profile.publicKey.key = publicKey
profile.publicKey.fetch.method = PublicKeyFetchMethod.HTTP
profile.publicKey.fetch.query = null
profile.publicKey.fetch.resolvedUrl = keyLink
return profile
} }
/** /**
* Get a public key from plaintext data * Get a public key from plaintext data
* @function * @function
* @param {string} rawKeyContent - Plaintext ASCII-formatted public key data * @param {string} rawKeyContent - Plaintext ASCII-formatted public key data
* @returns {Promise<PublicKey>} * @returns {Promise<Profile>}
* @example * @example
* const plainkey = `-----BEGIN PGP PUBLIC KEY BLOCK----- * const plainkey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
* *
@ -156,14 +188,20 @@ export async function fetchPlaintext (rawKeyContent) {
throw new Error(`Key could not be read (${error})`) throw new Error(`Key could not be read (${error})`)
}) })
return publicKey const profile = await parsePublicKey(publicKey)
profile.publicKey.keyType = PublicKeyType.OPENPGP
profile.publicKey.encoding = PublicKeyEncoding.ARMORED_PGP
profile.publicKey.encodedKey = publicKey.armor()
profile.publicKey.key = publicKey
return profile
} }
/** /**
* Fetch a public key using an URI * Fetch a public key using an URI
* @function * @function
* @param {string} uri - URI that defines the location of the key * @param {string} uri - URI that defines the location of the key
* @returns {Promise<PublicKey>} * @returns {Promise<Profile>}
* @example * @example
* const key1 = doip.keys.fetchURI('hkp:alice@domain.tld'); * const key1 = doip.keys.fetchURI('hkp:alice@domain.tld');
* const key2 = doip.keys.fetchURI('hkp:123abc123abc'); * const key2 = doip.keys.fetchURI('hkp:123abc123abc');
@ -209,7 +247,7 @@ export async function fetchURI (uri) {
* This function will also try and parse the input as a plaintext key * This function will also try and parse the input as a plaintext key
* @function * @function
* @param {string} identifier - URI that defines the location of the key * @param {string} identifier - URI that defines the location of the key
* @returns {Promise<PublicKey>} * @returns {Promise<Profile>}
* @example * @example
* const key1 = doip.keys.fetch('alice@domain.tld'); * const key1 = doip.keys.fetch('alice@domain.tld');
* const key2 = doip.keys.fetch('123abc123abc'); * const key2 = doip.keys.fetch('123abc123abc');
@ -218,42 +256,40 @@ export async function fetch (identifier) {
const re = /([a-zA-Z0-9@._=+-]*)(?::([a-zA-Z0-9@._=+-]*))?/ const re = /([a-zA-Z0-9@._=+-]*)(?::([a-zA-Z0-9@._=+-]*))?/
const match = identifier.match(re) const match = identifier.match(re)
let pubKey = null let profile = null
// Attempt plaintext // Attempt plaintext
if (!pubKey) { try {
try { profile = await fetchPlaintext(identifier)
pubKey = await fetchPlaintext(identifier) } catch (e) {}
} catch (e) {}
}
// Attempt WKD // Attempt WKD
if (!pubKey && identifier.includes('@')) { if (!profile && identifier.includes('@')) {
try { try {
pubKey = await fetchWKD(match[1]) profile = await fetchWKD(match[1])
} catch (e) {} } catch (e) {}
} }
// Attempt HKP // Attempt HKP
if (!pubKey) { if (!profile) {
pubKey = await fetchHKP( profile = await fetchHKP(
match[2] ? match[2] : match[1], match[2] ? match[2] : match[1],
match[2] ? match[1] : null match[2] ? match[1] : null
) )
} }
if (!pubKey) { if (!profile) {
throw new Error('Key does not exist or could not be fetched') throw new Error('Key does not exist or could not be fetched')
} }
return pubKey return profile
} }
/** /**
* Process a public key to get user data and claims * Process a public key to get user data and claims
* @function * @function
* @param {PublicKey} publicKey - The public key to process * @param {PublicKey} publicKey - The public key to process
* @returns {Promise<object>} * @returns {Promise<Profile>}
* @example * @example
* const key = doip.keys.fetchURI('hkp:alice@domain.tld'); * const key = doip.keys.fetchURI('hkp:alice@domain.tld');
* const data = doip.keys.process(key); * const data = doip.keys.process(key);
@ -261,7 +297,7 @@ export async function fetch (identifier) {
* console.log(claim.uri); * console.log(claim.uri);
* }); * });
*/ */
export async function process (publicKey) { async function parsePublicKey (publicKey) {
if (!(publicKey && (publicKey instanceof PublicKey))) { if (!(publicKey && (publicKey instanceof PublicKey))) {
throw new Error('Invalid public key') throw new Error('Invalid public key')
} }
@ -269,47 +305,37 @@ export async function process (publicKey) {
const fingerprint = publicKey.getFingerprint() const fingerprint = publicKey.getFingerprint()
const primaryUser = await publicKey.getPrimaryUser() const primaryUser = await publicKey.getPrimaryUser()
const users = publicKey.users const users = publicKey.users
const usersOutput = [] const personas = []
users.forEach((user, i) => { users.forEach((user, i) => {
usersOutput[i] = { const pe = new Persona(user.userID.name, [])
userData: { pe.setIdentifier(user.userID.userID)
id: user.userID ? user.userID.userID : null, pe.setDescription(user.userID.comment)
name: user.userID ? user.userID.name : null, pe.setEmailAddress(user.userID.email)
email: user.userID ? user.userID.email : null,
comment: user.userID ? user.userID.comment : null,
isPrimary: primaryUser.index === i,
isRevoked: false
},
claims: []
}
if ('selfCertifications' in user && user.selfCertifications.length > 0) { if ('selfCertifications' in user && user.selfCertifications.length > 0) {
const selfCertification = user.selfCertifications.sort((e1, e2) => e2.created.getTime() - e1.created.getTime())[0] const selfCertification = user.selfCertifications.sort((e1, e2) => e2.created.getTime() - e1.created.getTime())[0]
if (selfCertification.revoked) {
pe.revoke()
}
const notations = selfCertification.rawNotations const notations = selfCertification.rawNotations
usersOutput[i].claims = notations pe.claims = notations
.filter( .filter(
({ name, humanReadable }) => ({ name, humanReadable }) =>
humanReadable && (name === 'proof@ariadne.id' || name === 'proof@metacode.biz') humanReadable && (name === 'proof@ariadne.id' || name === 'proof@metacode.biz')
) )
.map( .map(
({ value }) => ({ value }) =>
new Claim(new TextDecoder().decode(value), fingerprint) new Claim(new TextDecoder().decode(value), `openpgp4fpr:${fingerprint}`)
) )
usersOutput[i].userData.isRevoked = selfCertification.revoked
} }
personas.push(pe)
}) })
return { const pr = new Profile(ProfileType.OPENPGP, `openpgp4fpr:${fingerprint}`, personas)
fingerprint, pr.primaryPersonaIndex = primaryUser.index
users: usersOutput,
primaryUserIndex: primaryUser.index, return pr
key: {
data: publicKey,
fetchMethod: null,
uri: null
}
}
} }

View file

@ -18,7 +18,7 @@ import chaiAsPromised from 'chai-as-promised'
use(chaiAsPromised) use(chaiAsPromised)
import { PublicKey } from 'openpgp' import { PublicKey } from 'openpgp'
import { keys } from '../src/index.js' import { openpgp, Profile } from '../src/index.js'
const pubKeyFingerprint = "3637202523e7c1309ab79e99ef2dc5827b445f4b" const pubKeyFingerprint = "3637202523e7c1309ab79e99ef2dc5827b445f4b"
const pubKeyEmail = "test@doip.rocks" const pubKeyEmail = "test@doip.rocks"
@ -90,115 +90,115 @@ Q+AZdYCbM0hdBjP4xdKZcpqak8ksb+aQFXjGacDL/XN4VrP+tBGxkqIqreoDcgIb
=tVW7 =tVW7
-----END PGP PUBLIC KEY BLOCK-----` -----END PGP PUBLIC KEY BLOCK-----`
describe('keys.fetch', () => { describe('openpgp.fetch', () => {
it('should be a function (1 argument)', () => { it('should be a function (1 argument)', () => {
expect(keys.fetch).to.be.a('function') expect(openpgp.fetch).to.be.a('function')
expect(keys.fetch).to.have.length(1) expect(openpgp.fetch).to.have.length(1)
}) })
it('should return a Key object when provided a valid fingerprint', async () => { it('should return a Key object when provided a valid fingerprint', async () => {
expect( expect(
await keys.fetch(pubKeyFingerprint) await openpgp.fetch(pubKeyFingerprint)
).to.be.instanceOf(PublicKey) ).to.be.instanceOf(Profile)
}).timeout('12s') }).timeout('12s')
it('should return a Key object when provided a valid email address', async () => { it('should return a Key object when provided a valid email address', async () => {
expect( expect(
await keys.fetch(pubKeyEmail) await openpgp.fetch(pubKeyEmail)
).to.be.instanceOf(PublicKey) ).to.be.instanceOf(Profile)
}).timeout('12s') }).timeout('12s')
it('should reject when provided an invalid email address', () => { it('should reject when provided an invalid email address', () => {
return expect( return expect(
keys.fetch('invalid@doip.rocks') openpgp.fetch('invalid@doip.rocks')
).to.eventually.be.rejectedWith('Key does not exist or could not be fetched') ).to.eventually.be.rejectedWith('Key does not exist or could not be fetched')
}).timeout('12s') }).timeout('12s')
}) })
describe('keys.fetchURI', () => { describe('openpgp.fetchURI', () => {
it('should be a function (1 argument)', () => { it('should be a function (1 argument)', () => {
expect(keys.fetchURI).to.be.a('function') expect(openpgp.fetchURI).to.be.a('function')
expect(keys.fetchURI).to.have.length(1) expect(openpgp.fetchURI).to.have.length(1)
}) })
it('should return a Key object when provided a hkp: uri', async () => { it('should return a Key object when provided a hkp: uri', async () => {
expect( expect(
await keys.fetchURI(`hkp:${pubKeyFingerprint}`) await openpgp.fetchURI(`hkp:${pubKeyFingerprint}`)
).to.be.instanceOf(PublicKey) ).to.be.instanceOf(Profile)
}).timeout('12s') }).timeout('12s')
it('should reject when provided an invalid uri', () => { it('should reject when provided an invalid uri', () => {
return expect( return expect(
keys.fetchURI(`inv:${pubKeyFingerprint}`) openpgp.fetchURI(`inv:${pubKeyFingerprint}`)
).to.eventually.be.rejectedWith('Invalid URI protocol') ).to.eventually.be.rejectedWith('Invalid URI protocol')
}).timeout('12s') }).timeout('12s')
}) })
describe('keys.fetchHKP', () => { describe('openpgp.fetchHKP', () => {
it('should be a function (2 arguments)', () => { it('should be a function (2 arguments)', () => {
expect(keys.fetchHKP).to.be.a('function') expect(openpgp.fetchHKP).to.be.a('function')
expect(keys.fetchHKP).to.have.length(2) expect(openpgp.fetchHKP).to.have.length(2)
}) })
it('should return a Key object when provided a valid fingerprint', async () => { it('should return a Key object when provided a valid fingerprint', async () => {
expect(await keys.fetchHKP(pubKeyFingerprint)).to.be.instanceOf( expect(await openpgp.fetchHKP(pubKeyFingerprint)).to.be.instanceOf(
PublicKey Profile
) )
}).timeout('12s') }).timeout('12s')
it('should return a Key object when provided a valid email address', async () => { it('should return a Key object when provided a valid email address', async () => {
expect(await keys.fetchHKP(pubKeyEmail)).to.be.instanceOf( expect(await openpgp.fetchHKP(pubKeyEmail)).to.be.instanceOf(
PublicKey Profile
) )
}).timeout('12s') }).timeout('12s')
it('should reject when provided an invalid fingerprint', async () => { it('should reject when provided an invalid fingerprint', async () => {
return expect( return expect(
keys.fetchHKP('4637202523e7c1309ab79e99ef2dc5827b445f4b') openpgp.fetchHKP('4637202523e7c1309ab79e99ef2dc5827b445f4b')
).to.eventually.be.rejectedWith( ).to.eventually.be.rejectedWith(
'Key does not exist or could not be fetched' 'Key does not exist or could not be fetched'
) )
}).timeout('12s') }).timeout('12s')
it('should reject when provided an invalid email address', async () => { it('should reject when provided an invalid email address', async () => {
return expect( return expect(
keys.fetchHKP('invalid@doip.rocks') openpgp.fetchHKP('invalid@doip.rocks')
).to.eventually.be.rejectedWith( ).to.eventually.be.rejectedWith(
'Key does not exist or could not be fetched' 'Key does not exist or could not be fetched'
) )
}).timeout('12s') }).timeout('12s')
}) })
describe('keys.fetchPlaintext', () => { describe('openpgp.fetchPlaintext', () => {
it('should be a function (1 argument)', () => { it('should be a function (1 argument)', () => {
expect(keys.fetchPlaintext).to.be.a('function') expect(openpgp.fetchPlaintext).to.be.a('function')
expect(keys.fetchPlaintext).to.have.length(1) expect(openpgp.fetchPlaintext).to.have.length(1)
}) })
it('should return a Key object', async () => { it('should return a Key object', async () => {
expect(await keys.fetchPlaintext(pubKeyPlaintext)).to.be.instanceOf( expect(await openpgp.fetchPlaintext(pubKeyPlaintext)).to.be.instanceOf(
PublicKey Profile
) )
}).timeout('12s') }).timeout('12s')
}) })
describe('keys.process', () => { // describe('openpgp.process', () => {
it('should be a function (1 argument)', () => { // it('should be a function (1 argument)', () => {
expect(keys.process).to.be.a('function') // expect(openpgp.process).to.be.a('function')
expect(keys.process).to.have.length(1) // expect(openpgp.process).to.have.length(1)
}) // })
it('should return an object with specific keys', async () => { // it('should return an object with specific openpgp', async () => {
const pubKey = await keys.fetchPlaintext(pubKeyPlaintext) // const pubKey = await openpgp.fetchPlaintext(pubKeyPlaintext)
const obj = await keys.process(pubKey) // const obj = await openpgp.process(pubKey)
expect(obj).to.have.keys([ // expect(obj).to.have.openpgp([
'users', // 'users',
'fingerprint', // 'fingerprint',
'primaryUserIndex', // 'primaryUserIndex',
'key', // 'key',
]) // ])
}) // })
it('should ignore non-proof notations', async () => { // it('should ignore non-proof notations', async () => {
const pubKey = await keys.fetchPlaintext(pubKeyWithOtherNotations) // const pubKey = await openpgp.fetchPlaintext(pubKeyWithOtherNotations)
const obj = await keys.process(pubKey) // const obj = await openpgp.process(pubKey)
expect(obj.users).to.be.lengthOf(1) // expect(obj.users).to.be.lengthOf(1)
expect(obj.users[0].claims).to.be.lengthOf(1) // expect(obj.users[0].claims).to.be.lengthOf(1)
expect(obj.users[0].claims[0].uri).to.be.equal('dns:yarmo.eu?type=TXT') // expect(obj.users[0].claims[0].uri).to.be.equal('dns:yarmo.eu?type=TXT')
}) // })
it('should properly handle revoked UIDs', async () => { // it('should properly handle revoked UIDs', async () => {
const pubKey = await keys.fetchPlaintext(pubKeyWithRevokedUID) // const pubKey = await openpgp.fetchPlaintext(pubKeyWithRevokedUID)
const obj = await keys.process(pubKey) // const obj = await openpgp.process(pubKey)
expect(obj.users).to.be.lengthOf(2) // expect(obj.users).to.be.lengthOf(2)
expect(obj.users[0].userData.isRevoked).to.be.true // expect(obj.users[0].userData.isRevoked).to.be.true
expect(obj.users[1].userData.isRevoked).to.be.false // expect(obj.users[1].userData.isRevoked).to.be.false
}) // })
}) // })