add pgp verification support

This commit is contained in:
Craig Raw 2024-02-22 13:33:26 +02:00
parent dde2dccda4
commit 9656603930
6 changed files with 2328 additions and 0 deletions

View file

@ -31,6 +31,7 @@ dependencies {
exclude group: 'junit', module: 'junit' exclude group: 'junit', module: 'junit'
} }
implementation ('org.bouncycastle:bcprov-jdk18on:1.77') implementation ('org.bouncycastle:bcprov-jdk18on:1.77')
implementation('org.pgpainless:pgpainless-core:1.6.6')
implementation ('de.mkammerer:argon2-jvm:2.11') { implementation ('de.mkammerer:argon2-jvm:2.11') {
exclude group: 'net.java.dev.jna', module: 'jna' exclude group: 'net.java.dev.jna', module: 'jna'
} }
@ -62,4 +63,16 @@ extraJavaModuleInfo {
exports('org.json.simple.parser') exports('org.json.simple.parser')
} }
module('jnacl-1.0.0.jar', 'eu.neilalexander.jnacl', '1.0.0') module('jnacl-1.0.0.jar', 'eu.neilalexander.jnacl', '1.0.0')
module('pgpainless-core-1.6.6.jar', 'org.pgpainless.core', '1.6.6') {
exports('org.pgpainless')
exports('org.pgpainless.key')
exports('org.pgpainless.key.parsing')
exports('org.pgpainless.decryption_verification')
exports('org.pgpainless.exception')
exports('org.pgpainless.signature')
exports('org.pgpainless.util')
requires('org.bouncycastle.provider')
requires('org.bouncycastle.pg')
requires('org.slf4j')
}
} }

View file

@ -0,0 +1,226 @@
package com.sparrowwallet.drongo.pgp;
import org.bouncycastle.bcpg.ArmoredInputStream;
import org.bouncycastle.gpg.keybox.KeyBlob;
import org.bouncycastle.gpg.keybox.PublicKeyRingBlob;
import org.bouncycastle.gpg.keybox.bc.BcKeyBox;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.util.io.Streams;
import org.pgpainless.PGPainless;
import org.pgpainless.decryption_verification.*;
import org.pgpainless.key.SubkeyIdentifier;
import org.pgpainless.util.ArmoredInputStreamFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
public class PGPUtils {
private static final Logger log = LoggerFactory.getLogger(PGPUtils.class);
public static final String APPLICATION_KEYRING = "/gpg/pubkeys.asc";
public static final String PUBRING_GPG = "pubring.gpg";
public static final String PUBRING_KBX = "pubring.kbx";
public static PGPVerificationResult verify(InputStream publicKeyStream, InputStream contentStream, InputStream detachedSignatureStream) throws IOException, PGPVerificationException {
PGPPublicKeyRing publicKeyRing = null;
if(publicKeyStream != null) {
publicKeyRing = PGPainless.readKeyRing().publicKeyRing(publicKeyStream);
if(publicKeyRing == null) {
throw new PGPVerificationException("Invalid public key provided");
}
}
PGPPublicKeyRingCollection userPgpPublicKeyRingCollection = getUserKeyRingCollection();
PGPPublicKeyRingCollection appPgpPublicKeyRingCollection = getApplicationKeyRingCollection();
try {
ConsumerOptions options = ConsumerOptions.get();
if(publicKeyRing != null) {
options.addVerificationCert(publicKeyRing);
}
if(userPgpPublicKeyRingCollection != null) {
options.addVerificationCerts(userPgpPublicKeyRingCollection);
}
if(appPgpPublicKeyRingCollection != null) {
options.addVerificationCerts(appPgpPublicKeyRingCollection);
}
if(detachedSignatureStream != null) {
options.addVerificationOfDetachedSignatures(detachedSignatureStream);
}
DecryptionStream verificationStream = PGPainless.decryptAndOrVerify()
.onInputStream(contentStream)
.withOptions(options);
Streams.drain(verificationStream);
verificationStream.close();
MessageMetadata result = verificationStream.getMetadata();
if(result.isVerifiedSigned()) {
for(SignatureVerification signatureVerification : result.getVerifiedSignatures()) {
SubkeyIdentifier subkeyIdentifier = signatureVerification.getSigningKey();
if(subkeyIdentifier != null) {
PGPPublicKey signedByKey = null;
long primaryKeyId = subkeyIdentifier.getPrimaryKeyId();
if(publicKeyRing != null && publicKeyRing.getPublicKey(primaryKeyId) != null) {
signedByKey = publicKeyRing.getPublicKey(primaryKeyId);
log.debug("Signed using provided public key");
} else if(appPgpPublicKeyRingCollection != null && appPgpPublicKeyRingCollection.getPublicKey(primaryKeyId) != null
&& !isExpired(appPgpPublicKeyRingCollection.getPublicKey(primaryKeyId))) {
signedByKey = appPgpPublicKeyRingCollection.getPublicKey(primaryKeyId);
log.debug("Signed using application public key");
} else if(userPgpPublicKeyRingCollection != null) {
signedByKey = userPgpPublicKeyRingCollection.getPublicKey(primaryKeyId);
log.debug("Signed using user public key");
} else if(appPgpPublicKeyRingCollection != null && appPgpPublicKeyRingCollection.getPublicKey(primaryKeyId) != null) {
signedByKey = appPgpPublicKeyRingCollection.getPublicKey(primaryKeyId);
log.debug("Signed using expired application public key");
} else {
log.debug("Could not find public key for primary key id " + primaryKeyId);
}
String userId = subkeyIdentifier.getPrimaryKeyFingerprint().prettyPrint();
boolean expired = false;
if(signedByKey != null) {
Iterator<String> userIds = signedByKey.getUserIDs();
if(userIds.hasNext()) {
userId = userIds.next();
}
expired = isExpired(signedByKey);
}
return new PGPVerificationResult(primaryKeyId, userId, signatureVerification.getSignature().getCreationTime(), expired);
}
}
}
if(!result.getRejectedDetachedSignatures().isEmpty()) {
throw new PGPVerificationException(result.getRejectedDetachedSignatures().get(0).getValidationException().getMessage());
} else if(!result.getRejectedInlineSignatures().isEmpty()) {
throw new PGPVerificationException(result.getRejectedInlineSignatures().get(0).getValidationException().getMessage());
}
throw new PGPVerificationException("No signatures found");
} catch(Exception e) {
log.warn("Failed to verify signature", e);
throw new PGPVerificationException(e.getMessage());
}
}
private static PGPPublicKeyRingCollection getApplicationKeyRingCollection() throws IOException {
try(InputStream pubKeyStream = PGPUtils.class.getResourceAsStream(APPLICATION_KEYRING)) {
if(pubKeyStream != null) {
return PGPainless.readKeyRing().publicKeyRingCollection(pubKeyStream);
}
} catch(Exception e) {
log.warn("Error loading application key rings", e);
}
return null;
}
private static PGPPublicKeyRingCollection getUserKeyRingCollection() throws IOException {
try {
File gnupgPubRing = getGnuPGPubRing();
if(gnupgPubRing != null) {
if(gnupgPubRing.getName().equals(PUBRING_GPG)) {
return PGPainless.readKeyRing().publicKeyRingCollection(new FileInputStream(gnupgPubRing));
} else if(gnupgPubRing.getName().equals(PUBRING_KBX)) {
BcKeyBox bcKeyBox = new BcKeyBox(new FileInputStream(gnupgPubRing));
List<PGPPublicKeyRing> rings = new ArrayList<>();
for(KeyBlob keyBlob : bcKeyBox.getKeyBlobs()) {
if(keyBlob instanceof PublicKeyRingBlob publicKeyRingBlob) {
rings.add(publicKeyRingBlob.getPGPPublicKeyRing());
}
}
return new PGPPublicKeyRingCollection(rings);
}
}
} catch(Exception e) {
log.warn("Error loading user key rings: " + e.getMessage());
}
return null;
}
private static File getGnuPGPubRing() {
File gnupgHome = getGnuPGHome();
if(gnupgHome.exists()) {
File gpgPubRing = new File(gnupgHome, PUBRING_GPG);
if(gpgPubRing.exists()) {
return gpgPubRing;
}
File kbxPubRing = new File(gnupgHome, PUBRING_KBX);
if(kbxPubRing.exists()) {
return kbxPubRing;
}
}
return null;
}
private static File getGnuPGHome() {
String gnupgHome = System.getenv("GNUPGHOME");
if(gnupgHome != null && !gnupgHome.isEmpty()) {
File envHome = new File(gnupgHome);
if(envHome.exists()) {
return envHome;
}
}
if(isWindows()) {
return new File(System.getenv("APPDATA"), "gnupg");
}
return new File(System.getProperty("user.home"), ".gnupg");
}
private static boolean isWindows() {
String osName = System.getProperty("os.name");
return (osName != null && osName.toLowerCase(Locale.ROOT).startsWith("windows"));
}
public static boolean signatureContainsManifest(File signatureFile) {
try(OpenPgpInputStream openPgpInputStream = new OpenPgpInputStream(new FileInputStream(signatureFile))) {
openPgpInputStream.reset();
if(openPgpInputStream.isAsciiArmored()) {
ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpInputStream);
if(armorIn.isClearText()) {
return true;
}
}
return openPgpInputStream.isLikelyOpenPgpMessage();
} catch(IOException e) {
log.debug("Error opening signature file", e);
return false;
}
}
public static boolean isExpired(PGPPublicKey publicKey) {
long validSeconds = publicKey.getValidSeconds();
if(validSeconds == 0) {
return false;
}
Instant instant = publicKey.getCreationTime().toInstant();
LocalDateTime creationDateTime = instant.atZone(ZoneId.systemDefault()).toLocalDateTime();
LocalDateTime expiryDateTime = creationDateTime.plusSeconds(validSeconds);
return expiryDateTime.isBefore(LocalDateTime.now());
}
}

View file

@ -0,0 +1,7 @@
package com.sparrowwallet.drongo.pgp;
public class PGPVerificationException extends Exception {
public PGPVerificationException(String message) {
super(message);
}
}

View file

@ -0,0 +1,5 @@
package com.sparrowwallet.drongo.pgp;
import java.util.Date;
public record PGPVerificationResult(long keyId, String userId, Date signatureTimestamp, boolean expired) { }

View file

@ -1,5 +1,7 @@
open module com.sparrowwallet.drongo { open module com.sparrowwallet.drongo {
requires org.bouncycastle.provider; requires org.bouncycastle.provider;
requires org.bouncycastle.pg;
requires org.pgpainless.core;
requires de.mkammerer.argon2.nolibs; requires de.mkammerer.argon2.nolibs;
requires org.slf4j; requires org.slf4j;
requires ch.qos.logback.core; requires ch.qos.logback.core;
@ -11,6 +13,7 @@ open module com.sparrowwallet.drongo {
exports com.sparrowwallet.drongo.address; exports com.sparrowwallet.drongo.address;
exports com.sparrowwallet.drongo.crypto; exports com.sparrowwallet.drongo.crypto;
exports com.sparrowwallet.drongo.wallet; exports com.sparrowwallet.drongo.wallet;
exports com.sparrowwallet.drongo.pgp;
exports com.sparrowwallet.drongo.policy; exports com.sparrowwallet.drongo.policy;
exports com.sparrowwallet.drongo.uri; exports com.sparrowwallet.drongo.uri;
exports com.sparrowwallet.drongo.bip47; exports com.sparrowwallet.drongo.bip47;

File diff suppressed because it is too large Load diff