/* * Copyright (C) 2024 Thomas Wolf 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.lib; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.text.MessageFormat; import java.util.Arrays; import java.util.EnumMap; import java.util.Map; import java.util.ServiceConfigurationError; import java.util.ServiceLoader; import java.util.concurrent.ConcurrentHashMap; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevObject; import org.eclipse.jgit.revwalk.RevTag; import org.eclipse.jgit.util.RawParseUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Manages the available signers. * * @since 7.0 */ public final class SignatureVerifiers { private static final Logger LOG = LoggerFactory.getLogger(SignatureVerifiers.class); private static final byte[] PGP_PREFIX = Constants.GPG_SIGNATURE_PREFIX .getBytes(StandardCharsets.US_ASCII); private static final byte[] X509_PREFIX = Constants.CMS_SIGNATURE_PREFIX .getBytes(StandardCharsets.US_ASCII); private static final byte[] SSH_PREFIX = Constants.SSH_SIGNATURE_PREFIX .getBytes(StandardCharsets.US_ASCII); private static final Map FACTORIES = loadSignatureVerifiers(); private static final Map VERIFIERS = new ConcurrentHashMap<>(); private static Map loadSignatureVerifiers() { Map result = new EnumMap<>( GpgConfig.GpgFormat.class); try { for (SignatureVerifierFactory factory : ServiceLoader .load(SignatureVerifierFactory.class)) { GpgConfig.GpgFormat format = factory.getType(); SignatureVerifierFactory existing = result.get(format); if (existing != null) { LOG.warn("{}", //$NON-NLS-1$ MessageFormat.format( JGitText.get().signatureServiceConflict, "SignatureVerifierFactory", format, //$NON-NLS-1$ existing.getClass().getCanonicalName(), factory.getClass().getCanonicalName())); } else { result.put(format, factory); } } } catch (ServiceConfigurationError e) { LOG.error(e.getMessage(), e); } return result; } private SignatureVerifiers() { // No instantiation } /** * Retrieves a {@link Signer} that can produce signatures of the given type * {@code format}. * * @param format * {@link GpgConfig.GpgFormat} the signer must support * @return a {@link Signer}, or {@code null} if none is available */ public static SignatureVerifier get(@NonNull GpgConfig.GpgFormat format) { return VERIFIERS.computeIfAbsent(format, f -> { SignatureVerifierFactory factory = FACTORIES.get(format); if (factory == null) { return null; } return factory.create(); }); } /** * Sets a specific signature verifier to use for a specific signature type. * * @param format * signature type to set the {@code verifier} for * @param verifier * the {@link SignatureVerifier} to use for signatures of type * {@code format}; if {@code null}, a default implementation, if * available, may be used. */ public static void set(@NonNull GpgConfig.GpgFormat format, SignatureVerifier verifier) { SignatureVerifier previous; if (verifier == null) { previous = VERIFIERS.remove(format); } else { previous = VERIFIERS.put(format, verifier); } if (previous != null) { previous.clear(); } } /** * Verifies the signature on a signed commit or tag. * * @param repository * the {@link Repository} the object is from * @param config * the {@link GpgConfig} to use * @param object * to verify * @return a {@link SignatureVerifier.SignatureVerification} describing the * outcome of the verification, or {@code null} if the object does * not have a signature of a known type * @throws IOException * if an error occurs getting a public key * @throws org.eclipse.jgit.api.errors.JGitInternalException * if signature verification fails */ @Nullable public static SignatureVerifier.SignatureVerification verify( @NonNull Repository repository, @NonNull GpgConfig config, @NonNull RevObject object) 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.nextLfSkippingSplitLines(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(repository, config, 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(repository, config, data, signatureData); } return null; } /** * Verifies a given signature for some give data. * * @param repository * the {@link Repository} the object is from * @param config * the {@link GpgConfig} to use * @param data * to verify the signature of * @param signature * the given signature of the {@code data} * @return a {@link SignatureVerifier.SignatureVerification} describing the * outcome of the verification, or {@code null} if the signature * type is unknown * @throws IOException * if an error occurs getting a public key * @throws org.eclipse.jgit.api.errors.JGitInternalException * if signature verification fails */ @Nullable public static SignatureVerifier.SignatureVerification verify( @NonNull Repository repository, @NonNull GpgConfig config, byte[] data, byte[] signature) throws IOException { GpgConfig.GpgFormat format = getFormat(signature); if (format == null) { return null; } SignatureVerifier verifier = get(format); if (verifier == null) { return null; } return verifier.verify(repository, config, data, signature); } /** * Determines the type of a given signature. * * @param signature * to get the type of * @return the signature type, or {@code null} if unknown */ @Nullable public static GpgConfig.GpgFormat getFormat(byte[] signature) { if (RawParseUtils.match(signature, 0, PGP_PREFIX) > 0) { return GpgConfig.GpgFormat.OPENPGP; } if (RawParseUtils.match(signature, 0, X509_PREFIX) > 0) { return GpgConfig.GpgFormat.X509; } if (RawParseUtils.match(signature, 0, SSH_PREFIX) > 0) { return GpgConfig.GpgFormat.SSH; } return null; } }