mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-12-26 01:56: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'
|
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')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
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 {
|
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;
|
||||||
|
|
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