123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388 |
- /*
- * 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.Arrays;
- 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.annotations.Nullable;
- import org.eclipse.jgit.api.errors.JGitInternalException;
- import org.eclipse.jgit.lib.GpgConfig;
- import org.eclipse.jgit.lib.GpgSignatureVerifier;
- import org.eclipse.jgit.revwalk.RevCommit;
- import org.eclipse.jgit.revwalk.RevObject;
- import org.eclipse.jgit.revwalk.RevTag;
- import org.eclipse.jgit.util.LRUMap;
- import org.eclipse.jgit.util.RawParseUtils;
- import org.eclipse.jgit.util.StringUtils;
-
- /**
- * A {@link GpgSignatureVerifier} to verify GPG signatures using BouncyCastle.
- */
- public class BouncyCastleGpgSignatureVerifier implements GpgSignatureVerifier {
-
- 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$
- }
-
- @Override
- @Nullable
- public SignatureVerification verifySignature(@NonNull RevObject object,
- @NonNull GpgConfig config) throws IOException {
- if (object instanceof RevCommit) {
- RevCommit commit = (RevCommit) object;
- byte[] signatureData = commit.getRawGpgSignature();
- if (signatureData == null) {
- return null;
- }
- byte[] raw = commit.getRawBuffer();
- // Now remove the GPG signature
- byte[] header = { 'g', 'p', 'g', 's', 'i', 'g' };
- int start = RawParseUtils.headerStart(header, raw, 0);
- if (start < 0) {
- return null;
- }
- int end = RawParseUtils.headerEnd(raw, start);
- // start is at the beginning of the header's content
- start -= header.length + 1;
- // end is on the terminating LF; we need to skip that, too
- if (end < raw.length) {
- end++;
- }
- byte[] data = new byte[raw.length - (end - start)];
- System.arraycopy(raw, 0, data, 0, start);
- System.arraycopy(raw, end, data, start, raw.length - end);
- return verify(data, signatureData);
- } else if (object instanceof RevTag) {
- RevTag tag = (RevTag) object;
- byte[] signatureData = tag.getRawGpgSignature();
- if (signatureData == null) {
- return null;
- }
- byte[] raw = tag.getRawBuffer();
- // The signature is just tacked onto the end of the message, which
- // is last in the buffer.
- byte[] data = Arrays.copyOfRange(raw, 0,
- raw.length - signatureData.length);
- return verify(data, signatureData);
- }
- return null;
- }
-
- 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(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);
- }
-
- 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;
- }
- }
- }
|