diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java index 88ed6ecd..10970fc2 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/generation/KeyRingBuilder.java @@ -143,9 +143,6 @@ public class KeyRingBuilder implements KeyRingBuilderInterface { @Override public PGPSecretKeyRing build() throws NoSuchAlgorithmException, PGPException, InvalidAlgorithmParameterException { - if (userIds.isEmpty()) { - throw new IllegalStateException("At least one user-id is required."); - } PGPDigestCalculator keyFingerprintCalculator = ImplementationFactory.getInstance().getV4FingerprintCalculator(); PBESecretKeyEncryptor secretKeyEncryptor = buildSecretKeyEncryptor(keyFingerprintCalculator); PBESecretKeyDecryptor secretKeyDecryptor = buildSecretKeyDecryptor(); @@ -157,19 +154,35 @@ public class KeyRingBuilder implements KeyRingBuilderInterface { PGPContentSignerBuilder signer = buildContentSigner(certKey); PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(signer); - // Prepare primary user-id sig SignatureSubpackets hashedSubPacketGenerator = primaryKeySpec.getSubpacketGenerator(); hashedSubPacketGenerator.setIssuerFingerprintAndKeyId(certKey.getPublicKey()); - hashedSubPacketGenerator.setPrimaryUserId(); if (expirationDate != null) { hashedSubPacketGenerator.setKeyExpirationTime(certKey.getPublicKey(), expirationDate); } + if (!userIds.isEmpty()) { + hashedSubPacketGenerator.setPrimaryUserId(); + } + PGPSignatureSubpacketGenerator generator = new PGPSignatureSubpacketGenerator(); SignatureSubpacketsHelper.applyTo(hashedSubPacketGenerator, generator); PGPSignatureSubpacketVector hashedSubPackets = generator.generate(); + PGPKeyRingGenerator ringGenerator; + if (userIds.isEmpty()) { + ringGenerator = new PGPKeyRingGenerator( + certKey, + keyFingerprintCalculator, + hashedSubPackets, + null, + signer, + secretKeyEncryptor); + } else { + String primaryUserId = userIds.entrySet().iterator().next().getKey(); + ringGenerator = new PGPKeyRingGenerator( + SignatureType.POSITIVE_CERTIFICATION.getCode(), certKey, + primaryUserId, keyFingerprintCalculator, + hashedSubPackets, null, signer, secretKeyEncryptor); + } - PGPKeyRingGenerator ringGenerator = buildRingGenerator( - certKey, signer, keyFingerprintCalculator, hashedSubPackets, secretKeyEncryptor); addSubKeys(certKey, ringGenerator); // Generate secret key ring with only primary user id @@ -182,7 +195,9 @@ public class KeyRingBuilder implements KeyRingBuilderInterface { PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKeyRing.getSecretKey(), secretKeyDecryptor); Iterator> userIdIterator = this.userIds.entrySet().iterator(); - userIdIterator.next(); // Skip primary user id + if (userIdIterator.hasNext()) { + userIdIterator.next(); // Skip primary user id + } while (userIdIterator.hasNext()) { Map.Entry additionalUserId = userIdIterator.next(); String userIdString = additionalUserId.getKey(); @@ -217,19 +232,6 @@ public class KeyRingBuilder implements KeyRingBuilderInterface { return secretKeyRing; } - private PGPKeyRingGenerator buildRingGenerator(PGPKeyPair certKey, - PGPContentSignerBuilder signer, - PGPDigestCalculator keyFingerprintCalculator, - PGPSignatureSubpacketVector hashedSubPackets, - PBESecretKeyEncryptor secretKeyEncryptor) - throws PGPException { - String primaryUserId = userIds.entrySet().iterator().next().getKey(); - return new PGPKeyRingGenerator( - SignatureType.POSITIVE_CERTIFICATION.getCode(), certKey, - primaryUserId, keyFingerprintCalculator, - hashedSubPackets, null, signer, secretKeyEncryptor); - } - private void addSubKeys(PGPKeyPair primaryKey, PGPKeyRingGenerator ringGenerator) throws NoSuchAlgorithmException, PGPException, InvalidAlgorithmParameterException { for (KeySpec subKeySpec : subkeySpecs) { diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java index 809ea003..4c4c6689 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/consumer/CertificateValidator.java @@ -138,6 +138,7 @@ public final class CertificateValidator { } boolean anyUserIdValid = false; + boolean hasAnyUserIds = !userIdSignatures.keySet().isEmpty(); for (String userId : userIdSignatures.keySet()) { if (!userIdSignatures.get(userId).isEmpty()) { PGPSignature current = userIdSignatures.get(userId).get(0); @@ -149,7 +150,7 @@ public final class CertificateValidator { } } - if (!anyUserIdValid) { + if (hasAnyUserIds && !anyUserIdValid) { throw new SignatureValidationException("No valid user-id found.", rejections); } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java new file mode 100644 index 00000000..8b022a21 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/key/generation/GenerateKeyWithoutUserIdTest.java @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.generation; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.util.io.Streams; +import org.junit.JUtils; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; +import org.pgpainless.decryption_verification.OpenPgpMetadata; +import org.pgpainless.encryption_signing.EncryptionOptions; +import org.pgpainless.encryption_signing.EncryptionResult; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.eddsa.EdDSACurve; +import org.pgpainless.key.generation.type.xdh.XDHSpec; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.util.DateUtil; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.Date; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GenerateKeyWithoutUserIdTest { + + @Test + public void generateKeyWithoutUserId() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + Date expirationDate = DateUtil.toSecondsPrecision(new Date(DateUtil.now().getTime() + 1000 * 6000)); + PGPSecretKeyRing secretKey = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.CERTIFY_OTHER)) + .addSubkey(KeySpec.getBuilder(KeyType.EDDSA(EdDSACurve._Ed25519), KeyFlag.SIGN_DATA)) + .addSubkey(KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .setExpirationDate(expirationDate) + .build(); + + KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); + assertNull(info.getPrimaryUserId()); + assertTrue(info.getUserIds().isEmpty()); + JUtils.assertDateEquals(expirationDate, info.getPrimaryKeyExpirationDate()); + + InputStream plaintextIn = new ByteArrayInputStream("Hello, World!\n".getBytes()); + ByteArrayOutputStream ciphertextOut = new ByteArrayOutputStream(); + SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys(); + PGPPublicKeyRing certificate = PGPainless.extractCertificate(secretKey); + + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertextOut) + .withOptions(ProducerOptions.signAndEncrypt( + EncryptionOptions.get() + .addRecipient(certificate), + SigningOptions.get() + .addSignature(protector, secretKey) + )); + Streams.pipeAll(plaintextIn, encryptionStream); + encryptionStream.close(); + + EncryptionResult result = encryptionStream.getResult(); + assertTrue(result.isEncryptedFor(certificate)); + + ByteArrayInputStream ciphertextIn = new ByteArrayInputStream(ciphertextOut.toByteArray()); + ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); + DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextIn) + .withOptions(ConsumerOptions.get() + .addDecryptionKey(secretKey) + .addVerificationCert(certificate)); + + Streams.pipeAll(decryptionStream, plaintextOut); + decryptionStream.close(); + + OpenPgpMetadata metadata = decryptionStream.getResult(); + + assertTrue(metadata.containsVerifiedSignatureFrom(certificate)); + assertTrue(metadata.isEncrypted()); + } +}