openpgpjs/src/key/user.js
larabr d3e75de23d openpgp.encrypt: use encryptionKeys to determine preferred hash algo when signing
In `openpgp.sign`, the signing key preferences are considered instead,
since no "recipient keys" are available.

The hash algo selection logic has been reworked as follows:
if `config.preferredHashAlgo` appears in the prefs of all recipients, we pick it;
otherwise, we use the strongest supported algo (note: SHA256 is always implicitly supported by all keys),
as long as it is compatible with the signing key (e.g. ECC keys require minimum digest sizes).

Previously, only the preferences of the signing key were used to determine the hash algo to use,
but this is in contrast to the RFC: https://www.rfc-editor.org/rfc/rfc9580.html#section-5.2.3.16-2 .
Also, an algo stronger than `config.preferredHashAlgo` would be used, if the signing key
declared it as first preference.

With this change, `config.preferredHashAlgo` is picked even if it's weaker than the
preferences of the recipient keys.
2024-10-30 19:06:44 +01:00

274 lines
11 KiB
JavaScript

/**
* @module key/User
*/
import enums from '../enums';
import util from '../util';
import { PacketList } from '../packet';
import { mergeSignatures, isDataRevoked, createSignaturePacket } from './helper';
import defaultConfig from '../config';
/**
* Class that represents an user ID or attribute packet and the relevant signatures.
* @param {UserIDPacket|UserAttributePacket} userPacket - packet containing the user info
* @param {Key} mainKey - reference to main Key object containing the primary key and subkeys that the user is associated with
*/
class User {
constructor(userPacket, mainKey) {
this.userID = userPacket.constructor.tag === enums.packet.userID ? userPacket : null;
this.userAttribute = userPacket.constructor.tag === enums.packet.userAttribute ? userPacket : null;
this.selfCertifications = [];
this.otherCertifications = [];
this.revocationSignatures = [];
this.mainKey = mainKey;
}
/**
* Transforms structured user data to packetlist
* @returns {PacketList}
*/
toPacketList() {
const packetlist = new PacketList();
packetlist.push(this.userID || this.userAttribute);
packetlist.push(...this.revocationSignatures);
packetlist.push(...this.selfCertifications);
packetlist.push(...this.otherCertifications);
return packetlist;
}
/**
* Shallow clone
* @returns {User}
*/
clone() {
const user = new User(this.userID || this.userAttribute, this.mainKey);
user.selfCertifications = [...this.selfCertifications];
user.otherCertifications = [...this.otherCertifications];
user.revocationSignatures = [...this.revocationSignatures];
return user;
}
/**
* Generate third-party certifications over this user and its primary key
* @param {Array<PrivateKey>} signingKeys - Decrypted private keys for signing
* @param {Date} [date] - Date to use as creation date of the certificate, instead of the current time
* @param {Object} config - Full configuration
* @returns {Promise<User>} New user with new certifications.
* @async
*/
async certify(signingKeys, date, config) {
const primaryKey = this.mainKey.keyPacket;
const dataToSign = {
userID: this.userID,
userAttribute: this.userAttribute,
key: primaryKey
};
const user = new User(dataToSign.userID || dataToSign.userAttribute, this.mainKey);
user.otherCertifications = await Promise.all(signingKeys.map(async function(privateKey) {
if (!privateKey.isPrivate()) {
throw new Error('Need private key for signing');
}
if (privateKey.hasSameFingerprintAs(primaryKey)) {
throw new Error("The user's own key can only be used for self-certifications");
}
const signingKey = await privateKey.getSigningKey(undefined, date, undefined, config);
return createSignaturePacket(dataToSign, [privateKey], signingKey.keyPacket, {
// Most OpenPGP implementations use generic certification (0x10)
signatureType: enums.signature.certGeneric,
keyFlags: [enums.keyFlags.certifyKeys | enums.keyFlags.signData]
}, date, undefined, undefined, undefined, config);
}));
await user.update(this, date, config);
return user;
}
/**
* Checks if a given certificate of the user is revoked
* @param {SignaturePacket} certificate - The certificate to verify
* @param {PublicSubkeyPacket|
* SecretSubkeyPacket|
* PublicKeyPacket|
* SecretKeyPacket} [keyPacket] The key packet to verify the signature, instead of the primary key
* @param {Date} [date] - Use the given date for verification instead of the current time
* @param {Object} config - Full configuration
* @returns {Promise<Boolean>} True if the certificate is revoked.
* @async
*/
async isRevoked(certificate, keyPacket, date = new Date(), config = defaultConfig) {
const primaryKey = this.mainKey.keyPacket;
return isDataRevoked(primaryKey, enums.signature.certRevocation, {
key: primaryKey,
userID: this.userID,
userAttribute: this.userAttribute
}, this.revocationSignatures, certificate, keyPacket, date, config);
}
/**
* Verifies the user certificate.
* @param {SignaturePacket} certificate - A certificate of this user
* @param {Array<PublicKey>} verificationKeys - Array of keys to verify certificate signatures
* @param {Date} [date] - Use the given date instead of the current time
* @param {Object} config - Full configuration
* @returns {Promise<true|null>} true if the certificate could be verified, or null if the verification keys do not correspond to the certificate
* @throws if the user certificate is invalid.
* @async
*/
async verifyCertificate(certificate, verificationKeys, date = new Date(), config) {
const that = this;
const primaryKey = this.mainKey.keyPacket;
const dataToVerify = {
userID: this.userID,
userAttribute: this.userAttribute,
key: primaryKey
};
const { issuerKeyID } = certificate;
const issuerKeys = verificationKeys.filter(key => key.getKeys(issuerKeyID).length > 0);
if (issuerKeys.length === 0) {
return null;
}
await Promise.all(issuerKeys.map(async key => {
const signingKey = await key.getSigningKey(issuerKeyID, certificate.created, undefined, config);
if (certificate.revoked || await that.isRevoked(certificate, signingKey.keyPacket, date, config)) {
throw new Error('User certificate is revoked');
}
try {
await certificate.verify(signingKey.keyPacket, enums.signature.certGeneric, dataToVerify, date, undefined, config);
} catch (e) {
throw util.wrapError('User certificate is invalid', e);
}
}));
return true;
}
/**
* Verifies all user certificates
* @param {Array<PublicKey>} verificationKeys - Array of keys to verify certificate signatures
* @param {Date} [date] - Use the given date instead of the current time
* @param {Object} config - Full configuration
* @returns {Promise<Array<{
* keyID: module:type/keyid~KeyID,
* valid: Boolean | null
* }>>} List of signer's keyID and validity of signature.
* Signature validity is null if the verification keys do not correspond to the certificate.
* @async
*/
async verifyAllCertifications(verificationKeys, date = new Date(), config) {
const that = this;
const certifications = this.selfCertifications.concat(this.otherCertifications);
return Promise.all(certifications.map(async certification => ({
keyID: certification.issuerKeyID,
valid: await that.verifyCertificate(certification, verificationKeys, date, config).catch(() => false)
})));
}
/**
* Verify User. Checks for existence of self signatures, revocation signatures
* and validity of self signature.
* @param {Date} date - Use the given date instead of the current time
* @param {Object} config - Full configuration
* @returns {Promise<true>} Status of user.
* @throws {Error} if there are no valid self signatures.
* @async
*/
async verify(date = new Date(), config) {
if (!this.selfCertifications.length) {
throw new Error('No self-certifications found');
}
const that = this;
const primaryKey = this.mainKey.keyPacket;
const dataToVerify = {
userID: this.userID,
userAttribute: this.userAttribute,
key: primaryKey
};
// TODO replace when Promise.some or Promise.any are implemented
let exception;
for (let i = this.selfCertifications.length - 1; i >= 0; i--) {
try {
const selfCertification = this.selfCertifications[i];
if (selfCertification.revoked || await that.isRevoked(selfCertification, undefined, date, config)) {
throw new Error('Self-certification is revoked');
}
try {
await selfCertification.verify(primaryKey, enums.signature.certGeneric, dataToVerify, date, undefined, config);
} catch (e) {
throw util.wrapError('Self-certification is invalid', e);
}
return true;
} catch (e) {
exception = e;
}
}
throw exception;
}
/**
* Update user with new components from specified user
* @param {User} sourceUser - Source user to merge
* @param {Date} date - Date to verify the validity of signatures
* @param {Object} config - Full configuration
* @returns {Promise<undefined>}
* @async
*/
async update(sourceUser, date, config) {
const primaryKey = this.mainKey.keyPacket;
const dataToVerify = {
userID: this.userID,
userAttribute: this.userAttribute,
key: primaryKey
};
// self signatures
await mergeSignatures(sourceUser, this, 'selfCertifications', date, async function(srcSelfSig) {
try {
await srcSelfSig.verify(primaryKey, enums.signature.certGeneric, dataToVerify, date, false, config);
return true;
} catch (e) {
return false;
}
});
// other signatures
await mergeSignatures(sourceUser, this, 'otherCertifications', date);
// revocation signatures
await mergeSignatures(sourceUser, this, 'revocationSignatures', date, function(srcRevSig) {
return isDataRevoked(primaryKey, enums.signature.certRevocation, dataToVerify, [srcRevSig], undefined, undefined, date, config);
});
}
/**
* Revokes the user
* @param {SecretKeyPacket} primaryKey - decrypted private primary key for revocation
* @param {Object} reasonForRevocation - optional, object indicating the reason for revocation
* @param {module:enums.reasonForRevocation} reasonForRevocation.flag optional, flag indicating the reason for revocation
* @param {String} reasonForRevocation.string optional, string explaining the reason for revocation
* @param {Date} date - optional, override the creationtime of the revocation signature
* @param {Object} [config] - Full configuration, defaults to openpgp.config
* @returns {Promise<User>} New user with revocation signature.
* @async
*/
async revoke(
primaryKey,
{
flag: reasonForRevocationFlag = enums.reasonForRevocation.noReason,
string: reasonForRevocationString = ''
} = {},
date = new Date(),
config = defaultConfig
) {
const dataToSign = {
userID: this.userID,
userAttribute: this.userAttribute,
key: primaryKey
};
const user = new User(dataToSign.userID || dataToSign.userAttribute, this.mainKey);
user.revocationSignatures.push(await createSignaturePacket(dataToSign, [], primaryKey, {
signatureType: enums.signature.certRevocation,
reasonForRevocationFlag: enums.write(enums.reasonForRevocation, reasonForRevocationFlag),
reasonForRevocationString
}, date, undefined, undefined, false, config));
await user.update(this);
return user;
}
}
export default User;