blob: f4fed409733c5b093c42ecbbdfe8d8c11b527b05 [file] [log] [blame]
/*
* Copyright (C) 2021, Thomas Wolf <thomas.wolf@paranor.ch> and others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.gpg.bc.internal;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.Security;
import java.text.MessageFormat;
import java.time.Instant;
import java.util.Date;
import java.util.Iterator;
import java.util.Locale;
import org.bouncycastle.bcpg.sig.IssuerFingerprint;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.openpgp.PGPSignatureSubpacketVector;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
import org.bouncycastle.util.encoders.Hex;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.lib.AbstractGpgSignatureVerifier;
import org.eclipse.jgit.lib.GpgConfig;
import org.eclipse.jgit.lib.GpgSignatureVerifier;
import org.eclipse.jgit.util.LRUMap;
import org.eclipse.jgit.util.StringUtils;
/**
* A {@link GpgSignatureVerifier} to verify GPG signatures using BouncyCastle.
*/
public class BouncyCastleGpgSignatureVerifier
extends AbstractGpgSignatureVerifier {
private static void registerBouncyCastleProviderIfNecessary() {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
/**
* Creates a new instance and registers the BouncyCastle security provider
* if needed.
*/
public BouncyCastleGpgSignatureVerifier() {
registerBouncyCastleProviderIfNecessary();
}
// To support more efficient signature verification of multiple objects we
// cache public keys once found in a LRU cache.
private static final Object NO_KEY = new Object();
private LRUMap<String, Object> byFingerprint = new LRUMap<>(16, 200);
private LRUMap<String, Object> bySigner = new LRUMap<>(16, 200);
@Override
public String getName() {
return "bc"; //$NON-NLS-1$
}
static PGPSignature parseSignature(InputStream in)
throws IOException, PGPException {
try (InputStream sigIn = PGPUtil.getDecoderStream(in)) {
JcaPGPObjectFactory pgpFactory = new JcaPGPObjectFactory(sigIn);
Object obj = pgpFactory.nextObject();
if (obj instanceof PGPCompressedData) {
obj = new JcaPGPObjectFactory(
((PGPCompressedData) obj).getDataStream()).nextObject();
}
if (obj instanceof PGPSignatureList) {
return ((PGPSignatureList) obj).get(0);
}
return null;
}
}
@Override
public SignatureVerification verify(@NonNull GpgConfig config, byte[] data,
byte[] signatureData)
throws IOException {
PGPSignature signature = null;
String fingerprint = null;
String signer = null;
String keyId = null;
try (InputStream sigIn = new ByteArrayInputStream(signatureData)) {
signature = parseSignature(sigIn);
if (signature != null) {
// Try to figure out something to find the public key with.
if (signature.hasSubpackets()) {
PGPSignatureSubpacketVector packets = signature
.getHashedSubPackets();
IssuerFingerprint fingerprintPacket = packets
.getIssuerFingerprint();
if (fingerprintPacket != null) {
fingerprint = Hex
.toHexString(fingerprintPacket.getFingerprint())
.toLowerCase(Locale.ROOT);
}
signer = packets.getSignerUserID();
if (signer != null) {
signer = BouncyCastleGpgSigner.extractSignerId(signer);
}
}
keyId = Long.toUnsignedString(signature.getKeyID(), 16)
.toLowerCase(Locale.ROOT);
} else {
throw new JGitInternalException(BCText.get().nonSignatureError);
}
} catch (PGPException e) {
throw new JGitInternalException(BCText.get().signatureParseError,
e);
}
Date signatureCreatedAt = signature.getCreationTime();
if (fingerprint == null && signer == null && keyId == null) {
return new VerificationResult(signatureCreatedAt, null, null, null,
false, false, TrustLevel.UNKNOWN,
BCText.get().signatureNoKeyInfo);
}
if (fingerprint != null && keyId != null
&& !fingerprint.endsWith(keyId)) {
return new VerificationResult(signatureCreatedAt, signer, fingerprint,
null, false, false, TrustLevel.UNKNOWN,
MessageFormat.format(BCText.get().signatureInconsistent,
keyId, fingerprint));
}
if (fingerprint == null && keyId != null) {
fingerprint = keyId;
}
// Try to find the public key
String keySpec = '<' + signer + '>';
Object cached = null;
PGPPublicKey publicKey = null;
try {
cached = byFingerprint.get(fingerprint);
if (cached != null) {
if (cached instanceof PGPPublicKey) {
publicKey = (PGPPublicKey) cached;
}
} else if (!StringUtils.isEmptyOrNull(signer)) {
cached = bySigner.get(signer);
if (cached != null) {
if (cached instanceof PGPPublicKey) {
publicKey = (PGPPublicKey) cached;
}
}
}
if (cached == null) {
publicKey = BouncyCastleGpgKeyLocator.findPublicKey(fingerprint,
keySpec);
}
} catch (IOException | PGPException e) {
throw new JGitInternalException(
BCText.get().signatureKeyLookupError, e);
}
if (publicKey == null) {
if (cached == null) {
byFingerprint.put(fingerprint, NO_KEY);
byFingerprint.put(keyId, NO_KEY);
if (signer != null) {
bySigner.put(signer, NO_KEY);
}
}
return new VerificationResult(signatureCreatedAt, signer,
fingerprint, null, false, false, TrustLevel.UNKNOWN,
BCText.get().signatureNoPublicKey);
}
if (cached == null) {
byFingerprint.put(fingerprint, publicKey);
byFingerprint.put(keyId, publicKey);
if (signer != null) {
bySigner.put(signer, publicKey);
}
}
String user = null;
Iterator<String> userIds = publicKey.getUserIDs();
if (!StringUtils.isEmptyOrNull(signer)) {
while (userIds.hasNext()) {
String userId = userIds.next();
if (BouncyCastleGpgKeyLocator.containsSigningKey(userId,
keySpec)) {
user = userId;
break;
}
}
}
if (user == null) {
userIds = publicKey.getUserIDs();
if (userIds.hasNext()) {
user = userIds.next();
}
}
boolean expired = false;
long validFor = publicKey.getValidSeconds();
if (validFor > 0 && signatureCreatedAt != null) {
Instant expiredAt = publicKey.getCreationTime().toInstant()
.plusSeconds(validFor);
expired = expiredAt.isBefore(signatureCreatedAt.toInstant());
}
// Trust data is not defined in OpenPGP; the format is implementation
// specific. We don't use the GPG trustdb but simply the trust packet
// on the public key, if present. Even if present, it may or may not
// be set.
byte[] trustData = publicKey.getTrustData();
TrustLevel trust = parseGpgTrustPacket(trustData);
boolean verified = false;
try {
signature.init(
new JcaPGPContentVerifierBuilderProvider()
.setProvider(BouncyCastleProvider.PROVIDER_NAME),
publicKey);
signature.update(data);
verified = signature.verify();
} catch (PGPException e) {
throw new JGitInternalException(
BCText.get().signatureVerificationError, e);
}
return new VerificationResult(signatureCreatedAt, signer, fingerprint, user,
verified, expired, trust, null);
}
@Override
public SignatureVerification verify(byte[] data, byte[] signatureData)
throws IOException {
throw new UnsupportedOperationException(
"Call verify(GpgConfig, byte[], byte[]) instead."); //$NON-NLS-1$
}
private TrustLevel parseGpgTrustPacket(byte[] packet) {
if (packet == null || packet.length < 6) {
// A GPG trust packet has at least 6 bytes.
return TrustLevel.UNKNOWN;
}
if (packet[2] != 'g' || packet[3] != 'p' || packet[4] != 'g') {
// Not a GPG trust packet
return TrustLevel.UNKNOWN;
}
int trust = packet[0] & 0x0F;
switch (trust) {
case 0: // No determined/set
case 1: // Trust expired; i.e., calculation outdated or key expired
case 2: // Undefined: not enough information to set
return TrustLevel.UNKNOWN;
case 3:
return TrustLevel.NEVER;
case 4:
return TrustLevel.MARGINAL;
case 5:
return TrustLevel.FULL;
case 6:
return TrustLevel.ULTIMATE;
default:
return TrustLevel.UNKNOWN;
}
}
@Override
public void clear() {
byFingerprint.clear();
bySigner.clear();
}
private static class VerificationResult implements SignatureVerification {
private final Date creationDate;
private final String signer;
private final String keyUser;
private final String fingerprint;
private final boolean verified;
private final boolean expired;
private final @NonNull TrustLevel trustLevel;
private final String message;
public VerificationResult(Date creationDate, String signer,
String fingerprint, String user, boolean verified,
boolean expired, @NonNull TrustLevel trust, String message) {
this.creationDate = creationDate;
this.signer = signer;
this.fingerprint = fingerprint;
this.keyUser = user;
this.verified = verified;
this.expired = expired;
this.trustLevel = trust;
this.message = message;
}
@Override
public Date getCreationDate() {
return creationDate;
}
@Override
public String getSigner() {
return signer;
}
@Override
public String getKeyUser() {
return keyUser;
}
@Override
public String getKeyFingerprint() {
return fingerprint;
}
@Override
public boolean isExpired() {
return expired;
}
@Override
public TrustLevel getTrustLevel() {
return trustLevel;
}
@Override
public String getMessage() {
return message;
}
@Override
public boolean getVerified() {
return verified;
}
}
}