mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-11-02 10:16:44 +00:00
add pgp verification support
This commit is contained in:
parent
dde2dccda4
commit
9656603930
6 changed files with 2328 additions and 0 deletions
13
build.gradle
13
build.gradle
|
@ -31,6 +31,7 @@ dependencies {
|
|||
exclude group: 'junit', module: 'junit'
|
||||
}
|
||||
implementation ('org.bouncycastle:bcprov-jdk18on:1.77')
|
||||
implementation('org.pgpainless:pgpainless-core:1.6.6')
|
||||
implementation ('de.mkammerer:argon2-jvm:2.11') {
|
||||
exclude group: 'net.java.dev.jna', module: 'jna'
|
||||
}
|
||||
|
@ -62,4 +63,16 @@ extraJavaModuleInfo {
|
|||
exports('org.json.simple.parser')
|
||||
}
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
|
226
src/main/java/com/sparrowwallet/drongo/pgp/PGPUtils.java
Normal file
226
src/main/java/com/sparrowwallet/drongo/pgp/PGPUtils.java
Normal 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());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package com.sparrowwallet.drongo.pgp;
|
||||
|
||||
public class PGPVerificationException extends Exception {
|
||||
public PGPVerificationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package com.sparrowwallet.drongo.pgp;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public record PGPVerificationResult(long keyId, String userId, Date signatureTimestamp, boolean expired) { }
|
|
@ -1,5 +1,7 @@
|
|||
open module com.sparrowwallet.drongo {
|
||||
requires org.bouncycastle.provider;
|
||||
requires org.bouncycastle.pg;
|
||||
requires org.pgpainless.core;
|
||||
requires de.mkammerer.argon2.nolibs;
|
||||
requires org.slf4j;
|
||||
requires ch.qos.logback.core;
|
||||
|
@ -11,6 +13,7 @@ open module com.sparrowwallet.drongo {
|
|||
exports com.sparrowwallet.drongo.address;
|
||||
exports com.sparrowwallet.drongo.crypto;
|
||||
exports com.sparrowwallet.drongo.wallet;
|
||||
exports com.sparrowwallet.drongo.pgp;
|
||||
exports com.sparrowwallet.drongo.policy;
|
||||
exports com.sparrowwallet.drongo.uri;
|
||||
exports com.sparrowwallet.drongo.bip47;
|
||||
|
|
2074
src/main/resources/gpg/pubkeys.asc
Normal file
2074
src/main/resources/gpg/pubkeys.asc
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue