blob: 0b92b3d878e83570a02cd2a87948f17631fba1fd [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.processors.pgp;
import org.apache.nifi.pgp.service.api.PGPPrivateKeyService;
import org.apache.nifi.pgp.util.PGPSecretKeyGenerator;
import org.apache.nifi.processors.pgp.attributes.CompressionAlgorithm;
import org.apache.nifi.processors.pgp.attributes.FileEncoding;
import org.apache.nifi.processors.pgp.attributes.HashAlgorithm;
import org.apache.nifi.processors.pgp.attributes.SigningStrategy;
import org.apache.nifi.processors.pgp.io.KeyIdentifierConverter;
import org.apache.nifi.reporting.InitializationException;
import org.apache.nifi.util.LogMessage;
import org.apache.nifi.util.MockFlowFile;
import org.apache.nifi.util.TestRunner;
import org.apache.nifi.util.TestRunners;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPOnePassSignature;
import org.bouncycastle.openpgp.PGPOnePassSignatureList;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider;
import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class SignContentPGPTest {
private static final String PASSPHRASE = UUID.randomUUID().toString();
private static final String DATA = String.class.getName();
private static final byte[] DATA_BINARY = DATA.getBytes(StandardCharsets.UTF_8);
private static final String SERVICE_ID = PGPPrivateKeyService.class.getName();
private static final int SIGNATURE_VERSION = 4;
private static PGPPrivateKey rsaPrivateKey;
private static PGPPublicKey rsaPublicKey;
@Mock
private PGPPrivateKeyService privateKeyService;
private TestRunner runner;
@BeforeAll
public static void setKeys() throws Exception {
final PGPSecretKey rsaSecretKey = PGPSecretKeyGenerator.generateRsaSecretKey(PASSPHRASE.toCharArray());
rsaPublicKey = rsaSecretKey.getPublicKey();
final PBESecretKeyDecryptor decryptor = new JcePBESecretKeyDecryptorBuilder().build(PASSPHRASE.toCharArray());
rsaPrivateKey = rsaSecretKey.extractPrivateKey(decryptor);
}
@BeforeEach
public void setRunner() throws InitializationException {
runner = TestRunners.newTestRunner(new SignContentPGP());
when(privateKeyService.getIdentifier()).thenReturn(SERVICE_ID);
runner.addControllerService(SERVICE_ID, privateKeyService);
runner.enableControllerService(privateKeyService);
runner.setProperty(SignContentPGP.PRIVATE_KEY_SERVICE, SERVICE_ID);
}
@Test
public void testFailurePrivateKeyIdParsingException() {
runner.setProperty(SignContentPGP.PRIVATE_KEY_ID, String.class.getSimpleName());
runner.enqueue(DATA);
runner.run();
assertFailureErrorLogged();
}
@Test
public void testFailureServiceException() {
final String privateKeyId = KeyIdentifierConverter.format((rsaPrivateKey.getKeyID()));
runner.setProperty(SignContentPGP.PRIVATE_KEY_ID, privateKeyId);
when(privateKeyService.findPrivateKey(eq(rsaPrivateKey.getKeyID()))).thenThrow(new RuntimeException());
runner.enqueue(DATA);
runner.run();
assertFailureErrorLogged();
}
@Test
public void testFailurePrivateKeyIdNotFound() {
final String privateKeyId = KeyIdentifierConverter.format((rsaPrivateKey.getKeyID()));
runner.setProperty(SignContentPGP.PRIVATE_KEY_ID, privateKeyId);
when(privateKeyService.findPrivateKey(eq(rsaPrivateKey.getKeyID()))).thenReturn(Optional.empty());
runner.enqueue(DATA);
runner.run();
assertFailureErrorLogged();
}
@Test
public void testSuccessFileEncodingAsciiHashAlgorithmSha512() throws PGPException, IOException {
assertSuccess(FileEncoding.ASCII, HashAlgorithm.SHA512, SigningStrategy.SIGNED);
}
@Test
public void testSuccessFileEncodingBinaryHashAlgorithmSha512() throws PGPException, IOException {
assertSuccess(FileEncoding.BINARY, HashAlgorithm.SHA512, SigningStrategy.SIGNED);
}
@Test
public void testSuccessFileEncodingBinaryUncompressedHashAlgorithmSha256() throws PGPException, IOException {
runner.setProperty(SignContentPGP.COMPRESSION_ALGORITHM, CompressionAlgorithm.UNCOMPRESSED.toString());
assertSuccess(FileEncoding.BINARY, HashAlgorithm.SHA256, SigningStrategy.SIGNED);
}
@Test
public void testSuccessFileEncodingBinaryHashAlgorithmSha256() throws PGPException, IOException {
assertSuccess(FileEncoding.BINARY, HashAlgorithm.SHA256, SigningStrategy.SIGNED);
}
@Test
public void testSuccessDetachedFileEncodingBinaryHashAlgorithmSha256() throws PGPException, IOException {
assertSuccess(FileEncoding.BINARY, HashAlgorithm.SHA256, SigningStrategy.DETACHED);
}
private void setPrivateKey() {
final String privateKeyId = KeyIdentifierConverter.format((rsaPrivateKey.getKeyID()));
runner.setProperty(SignContentPGP.PRIVATE_KEY_ID, privateKeyId);
when(privateKeyService.findPrivateKey(eq(rsaPrivateKey.getKeyID()))).thenReturn(Optional.of(rsaPrivateKey));
}
private void assertSuccess(final FileEncoding fileEncoding, final HashAlgorithm hashAlgorithm, final SigningStrategy signingStrategy) throws PGPException, IOException {
setPrivateKey();
runner.setProperty(SignContentPGP.FILE_ENCODING, fileEncoding.toString());
runner.setProperty(SignContentPGP.HASH_ALGORITHM, hashAlgorithm.toString());
runner.setProperty(SignContentPGP.SIGNING_STRATEGY, signingStrategy.toString());
runner.enqueue(DATA);
runner.run();
runner.assertTransferCount(SignContentPGP.SUCCESS, 1);
final MockFlowFile flowFile = runner.getFlowFilesForRelationship(SignContentPGP.SUCCESS).iterator().next();
assertFlowFileAttributesFound(flowFile, fileEncoding, hashAlgorithm);
if (SigningStrategy.DETACHED == signingStrategy) {
assertDetachedSignatureVerified(flowFile);
} else {
assertSignatureVerified(flowFile);
}
}
private void assertFailureErrorLogged() {
runner.assertAllFlowFilesTransferred(SignContentPGP.FAILURE);
final Optional<LogMessage> optionalLogMessage = runner.getLogger().getErrorMessages().stream().findFirst();
assertTrue(optionalLogMessage.isPresent());
}
private void assertFlowFileAttributesFound(final MockFlowFile flowFile, final FileEncoding fileEncoding, final HashAlgorithm hashAlgorithm) throws PGPException {
flowFile.assertAttributeEquals(PGPAttributeKey.FILE_ENCODING, fileEncoding.toString());
flowFile.assertAttributeExists(PGPAttributeKey.COMPRESS_ALGORITHM);
flowFile.assertAttributeExists(PGPAttributeKey.COMPRESS_ALGORITHM_ID);
final String signatureAlgorithm = PGPUtil.getSignatureName(rsaPrivateKey.getPublicKeyPacket().getAlgorithm(), hashAlgorithm.getId());
final String keyId = KeyIdentifierConverter.format((rsaPrivateKey.getKeyID()));
flowFile.assertAttributeExists(PGPAttributeKey.SIGNATURE_CREATED);
flowFile.assertAttributeEquals(PGPAttributeKey.SIGNATURE_ALGORITHM, signatureAlgorithm);
flowFile.assertAttributeEquals(PGPAttributeKey.SIGNATURE_HASH_ALGORITHM_ID, Integer.toString(hashAlgorithm.getId()));
flowFile.assertAttributeEquals(PGPAttributeKey.SIGNATURE_KEY_ALGORITHM_ID, Integer.toString(rsaPrivateKey.getPublicKeyPacket().getAlgorithm()));
flowFile.assertAttributeEquals(PGPAttributeKey.SIGNATURE_KEY_ID, keyId);
flowFile.assertAttributeEquals(PGPAttributeKey.SIGNATURE_TYPE_ID, Integer.toString(PGPSignature.BINARY_DOCUMENT));
flowFile.assertAttributeEquals(PGPAttributeKey.SIGNATURE_VERSION, Integer.toString(SIGNATURE_VERSION));
}
private void assertDetachedSignatureVerified(final MockFlowFile signatureFlowFile) throws IOException, PGPException {
final InputStream signatureContentStream = PGPUtil.getDecoderStream(signatureFlowFile.getContentStream());
final PGPObjectFactory objectFactory = new JcaPGPObjectFactory(signatureContentStream);
final PGPSignatureList signatureList = (PGPSignatureList) objectFactory.nextObject();
final PGPSignature signature = signatureList.iterator().next();
signature.init(new JcaPGPContentVerifierBuilderProvider(), rsaPublicKey);
signature.update(DATA_BINARY);
final boolean verified = signature.verify();
assertTrue(verified);
}
private void assertSignatureVerified(final MockFlowFile flowFile) throws IOException, PGPException {
final InputStream flowFileContentStream = PGPUtil.getDecoderStream(flowFile.getContentStream());
final PGPObjectFactory objectFactory = new JcaPGPObjectFactory(flowFileContentStream);
final PGPCompressedData compressedData = (PGPCompressedData) objectFactory.nextObject();
final InputStream dataInputStream = compressedData.getDataStream();
final PGPObjectFactory dataObjectFactory = new JcaPGPObjectFactory(dataInputStream);
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
PGPOnePassSignature onePassSignature = null;
boolean verified = false;
for (final Object object : dataObjectFactory) {
if (object instanceof PGPOnePassSignatureList) {
final PGPOnePassSignatureList onePassSignatureList = (PGPOnePassSignatureList) object;
onePassSignature = onePassSignatureList.iterator().next();
onePassSignature.init(new JcaPGPContentVerifierBuilderProvider(), rsaPublicKey);
} else if (object instanceof PGPLiteralData) {
if (onePassSignature == null) {
throw new IllegalStateException("One-Pass Signature not found before Literal Data");
}
final PGPLiteralData literalData = (PGPLiteralData) object;
final InputStream literalInputStream = literalData.getDataStream();
int read;
while ((read = literalInputStream.read()) >= 0) {
onePassSignature.update((byte) read);
outputStream.write(read);
}
} else if (object instanceof PGPSignatureList) {
if (onePassSignature == null) {
throw new IllegalStateException("One-Pass Signature not found before Signature");
}
final PGPSignatureList signatureList = (PGPSignatureList) object;
final PGPSignature signature = signatureList.iterator().next();
verified = onePassSignature.verify(signature);
} else {
throw new IllegalStateException(String.format("Unexpected PGP Object Found [%s]", object.getClass()));
}
}
assertTrue(verified);
final String literal = new String(outputStream.toByteArray(), StandardCharsets.UTF_8);
assertEquals(DATA, literal);
}
}