diff --git a/.gitignore b/.gitignore index c2debe9..f2b7f4e 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,4 @@ test/version_tmp tmp # Skip our compiled jruby-pgp.jar -lib/pgp/jruby-pgp.jar +# lib/pgp/jruby-pgp.jar diff --git a/.rbenv-version b/.rbenv-version index 467ee9a..e9bb632 100644 --- a/.rbenv-version +++ b/.rbenv-version @@ -1 +1 @@ -jruby-1.7.2 +jruby-1.7.10 diff --git a/.ruby-gemset b/.ruby-gemset new file mode 100644 index 0000000..e97c8b2 --- /dev/null +++ b/.ruby-gemset @@ -0,0 +1 @@ +jruby-pgp diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..e9bb632 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +jruby-1.7.10 diff --git a/ext/org/sgonyea/pgp/Signer.java b/ext/org/sgonyea/pgp/Signer.java index 0177a85..de227cb 100644 --- a/ext/org/sgonyea/pgp/Signer.java +++ b/ext/org/sgonyea/pgp/Signer.java @@ -10,124 +10,156 @@ package org.sgonyea.pgp; -import java.io.ByteArrayInputStream; +import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.NoSuchProviderException; -import java.security.SecureRandom; -import java.security.Security; -import java.util.Date; import java.util.Iterator; -import org.bouncycastle.bcpg.BCPGOutputStream; import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.openpgp.*; +import org.bouncycastle.bcpg.BCPGOutputStream; +import org.bouncycastle.openpgp.PGPCompressedData; +import org.bouncycastle.openpgp.PGPCompressedDataGenerator; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPLiteralData; +import org.bouncycastle.openpgp.PGPLiteralDataGenerator; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; +import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; public class Signer { + private String passphrase; + private PGPSecretKeyRingCollection _privateKeys; + + public Signer() { + } + + public Signer(PGPSecretKeyRingCollection privateKeys) { + setPrivateKeys(privateKeys); + } + + /** + * Accessor and Attribute Helper Methods + **/ + public PGPSecretKeyRingCollection getPrivateKeys() { + return _privateKeys; + } + + public void setPrivateKeys(PGPSecretKeyRingCollection privateKeys) { + _privateKeys = privateKeys; + } + + public void setPassphrase(String passphrase) { + this.passphrase = passphrase; + } + + private PGPSecretKey findSecretKey() throws PGPException, NoSuchProviderException { + @SuppressWarnings("rawtypes") + Iterator keyRingIter = _privateKeys.getKeyRings(); + while (keyRingIter.hasNext()) { + PGPSecretKeyRing keyRing = (PGPSecretKeyRing) keyRingIter.next(); + + @SuppressWarnings("rawtypes") + Iterator keyIter = keyRing.getSecretKeys(); + while (keyIter.hasNext()) { + PGPSecretKey key = (PGPSecretKey) keyIter.next(); - private PGPSecretKeyRingCollection _privateKeys; + if (key.isSigningKey()) { + return key; + } + } + } - private String passphrase; + throw new IllegalArgumentException("Can't find signing key in key ring."); + } - public Signer() { } - public Signer(PGPSecretKeyRingCollection privateKeys) { - setPrivateKeys(privateKeys); - } + public byte[] signData(byte[] inputData) throws Exception { + ByteArrayOutputStream signatureByteArrayOutputStream = new ByteArrayOutputStream(); + ArmoredOutputStream armoredSignatureOutputStream = new ArmoredOutputStream(signatureByteArrayOutputStream); - /** - * Accessor and Attribute Helper Methods - **/ - public PGPSecretKeyRingCollection getPrivateKeys() { - return _privateKeys; - } + PGPSecretKey pgpSigningKey = findSecretKey(); + PGPPrivateKey pgpPrivateKey = pgpSigningKey.extractPrivateKey(new JcePBESecretKeyDecryptorBuilder() + .setProvider("BC").build(passphrase.toCharArray())); + PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(new JcaPGPContentSignerBuilder( + pgpSigningKey.getPublicKey().getAlgorithm(), PGPUtil.SHA1).setProvider("BC")); - public void setPrivateKeys(PGPSecretKeyRingCollection privateKeys) { - _privateKeys = privateKeys; - } + signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, pgpPrivateKey); - public void setPassphrase(String passphrase) { - this.passphrase = passphrase; - } + @SuppressWarnings("rawtypes") + Iterator iter = pgpSigningKey.getPublicKey().getUserIDs(); + if (iter.hasNext()) { + PGPSignatureSubpacketGenerator subpacketGenerator = new PGPSignatureSubpacketGenerator(); - private PGPSecretKey findSecretKey() - throws PGPException, NoSuchProviderException { - Iterator keyRingIter = _privateKeys.getKeyRings(); - while (keyRingIter.hasNext()) - { - PGPSecretKeyRing keyRing = (PGPSecretKeyRing)keyRingIter.next(); + subpacketGenerator.setSignerUserID(false, (String) iter.next()); + signatureGenerator.setHashedSubpackets(subpacketGenerator.generate()); + } - Iterator keyIter = keyRing.getSecretKeys(); - while (keyIter.hasNext()) - { - PGPSecretKey key = (PGPSecretKey)keyIter.next(); + PGPCompressedDataGenerator compressor = new PGPCompressedDataGenerator(PGPCompressedData.ZLIB); - if (key.isSigningKey()) - { - return key; - } - } - } + BCPGOutputStream pgpOutputStream = new BCPGOutputStream(compressor.open(armoredSignatureOutputStream)); - throw new IllegalArgumentException("Can't find signing key in key ring."); - } + signatureGenerator.generateOnePassVersion(false).encode(pgpOutputStream); - public byte[] signData(byte[] clearData) - throws Exception { - String fileName = "something.txt"; - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - ArmoredOutputStream out = new ArmoredOutputStream(bos); + PGPLiteralDataGenerator literalDataGenerator = new PGPLiteralDataGenerator(); + OutputStream literalDataGeneratorOutputStream = literalDataGenerator.open(pgpOutputStream, + PGPLiteralData.BINARY, PGPLiteralDataGenerator.CONSOLE, inputData.length, PGPLiteralDataGenerator.NOW); - PGPSecretKey pgpSec = findSecretKey(); - PGPPrivateKey pgpPrivKey = pgpSec.extractPrivateKey(new JcePBESecretKeyDecryptorBuilder().setProvider("BC").build(passphrase.toCharArray())); - PGPSignatureGenerator sGen = new PGPSignatureGenerator(new JcaPGPContentSignerBuilder(pgpSec.getPublicKey().getAlgorithm(), PGPUtil.SHA1).setProvider("BC")); + literalDataGeneratorOutputStream.write(inputData); + signatureGenerator.update(inputData); - sGen.init(PGPSignature.BINARY_DOCUMENT, pgpPrivKey); + literalDataGenerator.close(); - Iterator it = pgpSec.getPublicKey().getUserIDs(); - if (it.hasNext()) - { - PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator(); + signatureGenerator.generate().encode(pgpOutputStream); - spGen.setSignerUserID(false, (String)it.next()); - sGen.setHashedSubpackets(spGen.generate()); - } + compressor.close(); - PGPCompressedDataGenerator cGen = new PGPCompressedDataGenerator( - PGPCompressedData.ZLIB); + armoredSignatureOutputStream.close(); - BCPGOutputStream bOut = new BCPGOutputStream(cGen.open(out)); + return signatureByteArrayOutputStream.toByteArray(); + } - sGen.generateOnePassVersion(false).encode(bOut); + public String signDataDetached(String inputFileName) throws Exception { + ByteArrayOutputStream signatureByteArrayOutputStream = new ByteArrayOutputStream(); + ArmoredOutputStream armoredSignatureOutputStream = new ArmoredOutputStream(signatureByteArrayOutputStream); - File file = new File(fileName); - PGPLiteralDataGenerator lGen = new PGPLiteralDataGenerator(); - OutputStream lOut = lGen.open(bOut, - PGPLiteralData.BINARY, PGPLiteralDataGenerator.CONSOLE, - clearData.length, PGPLiteralDataGenerator.NOW); - int ch; + PGPSecretKey pgpSigningKey = findSecretKey(); + PGPPrivateKey pgpPrivateKey = pgpSigningKey.extractPrivateKey(new JcePBESecretKeyDecryptorBuilder() + .setProvider("BC").build(passphrase.toCharArray())); + PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(new JcaPGPContentSignerBuilder( + pgpSigningKey.getPublicKey().getAlgorithm(), PGPUtil.SHA1).setProvider("BC")); - lOut.write(clearData); - sGen.update(clearData); + signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, pgpPrivateKey); - lGen.close(); + BCPGOutputStream pgpOutputStream = new BCPGOutputStream(armoredSignatureOutputStream); - sGen.generate().encode(bOut); + InputStream inputFileInputStream = new BufferedInputStream(new FileInputStream(inputFileName)); - cGen.close(); + int ch; + while ((ch = inputFileInputStream.read()) >= 0) { + signatureGenerator.update((byte) ch); + } - out.close(); + signatureGenerator.generate().encode(pgpOutputStream); - return bos.toByteArray(); - } + inputFileInputStream.close(); + armoredSignatureOutputStream.close(); + String signatureString = new String(signatureByteArrayOutputStream.toByteArray()); + + signatureByteArrayOutputStream.close(); + + pgpOutputStream.close(); + + return signatureString; + } } diff --git a/ext/org/sgonyea/pgp/Verifier.java b/ext/org/sgonyea/pgp/Verifier.java index 3c12801..3825bd2 100644 --- a/ext/org/sgonyea/pgp/Verifier.java +++ b/ext/org/sgonyea/pgp/Verifier.java @@ -10,95 +10,136 @@ package org.sgonyea.pgp; -import org.sgonyea.pgp.VerificationFailedException; - +import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; -import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; -import java.security.NoSuchProviderException; -import java.security.SecureRandom; -import java.security.Security; -import java.util.Date; -import java.util.Iterator; - -import org.bouncycastle.bcpg.ArmoredOutputStream; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.openpgp.*; - -import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder; +import java.security.GeneralSecurityException; + +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.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureList; +import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentVerifierBuilderProvider; -import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; public class Verifier { + private PGPPublicKeyRingCollection _publicKeys; + + public Verifier() { + } + + /** + * Accessor and Attribute Helper Methods + **/ + public PGPPublicKeyRingCollection getPublicKeys() { + return _publicKeys; + } + + public void setPublicKeys(PGPPublicKeyRingCollection keys) { + _publicKeys = keys; + } + + public byte[] verifyStream(InputStream inputStream) throws Exception, VerificationFailedException { + InputStream pgpInputStream = PGPUtil.getDecoderStream(inputStream); + + PGPObjectFactory pgpFactory = new PGPObjectFactory(pgpInputStream); + PGPCompressedData pgpCompressedData = (PGPCompressedData) pgpFactory.nextObject(); + pgpFactory = new PGPObjectFactory(pgpCompressedData.getDataStream()); + PGPOnePassSignatureList pgpSignatureList = (PGPOnePassSignatureList) pgpFactory.nextObject(); + PGPOnePassSignature pgpOnePassSignature = pgpSignatureList.get(0); + + PGPLiteralData pgpLiteralData = (PGPLiteralData) pgpFactory.nextObject(); + + PGPPublicKey signingKey = _publicKeys.getPublicKey(pgpOnePassSignature.getKeyID()); + + if (signingKey == null) { + throw new VerificationFailedException("Error: Public key with signature's ID could not be found."); + } - private PGPPublicKeyRingCollection _publicKeys; + pgpOnePassSignature.init(new JcaPGPContentVerifierBuilderProvider().setProvider("BC"), signingKey); - public Verifier() { } - /** - * Accessor and Attribute Helper Methods - **/ - public PGPPublicKeyRingCollection getPublicKeys() { - return _publicKeys; - } + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - public void setPublicKeys(PGPPublicKeyRingCollection keys) { - _publicKeys = keys; - } + InputStream inputFileInputStream = pgpLiteralData.getInputStream(); + int ch; + while ((ch = inputFileInputStream.read()) >= 0) { + pgpOnePassSignature.update((byte) ch); + outputStream.write(ch); + } + outputStream.close(); - public byte[] verifyStream(InputStream inStream) - throws Exception, VerificationFailedException - { - InputStream in = PGPUtil.getDecoderStream(inStream); + PGPSignatureList pgpSignature = (PGPSignatureList) pgpFactory.nextObject(); - PGPObjectFactory pgpFact = new PGPObjectFactory(in); + if (!pgpOnePassSignature.verify(pgpSignature.get(0))) { + throw new VerificationFailedException("Error: Signature could not be verified."); + } - PGPCompressedData c1 = (PGPCompressedData)pgpFact.nextObject(); + byte[] returnBytes = outputStream.toByteArray(); + outputStream.close(); - pgpFact = new PGPObjectFactory(c1.getDataStream()); + return returnBytes; + } - PGPOnePassSignatureList p1 = (PGPOnePassSignatureList)pgpFact.nextObject(); + public boolean verifyDetachedSignature(String fileName, String signature) throws GeneralSecurityException, + IOException, PGPException, VerificationFailedException { + InputStream signatureInputStream = new BufferedInputStream(new ByteArrayInputStream(signature.getBytes())); - PGPOnePassSignature ops = p1.get(0); + boolean isVerified = verifyDetachedSignature(fileName, signatureInputStream); - PGPLiteralData p2 = (PGPLiteralData)pgpFact.nextObject(); + signatureInputStream.close(); - InputStream dIn = p2.getInputStream(); - int ch; + return isVerified; + } - PGPPublicKey key = _publicKeys.getPublicKey(ops.getKeyID()); - ByteArrayOutputStream out = new ByteArrayOutputStream(); + private boolean verifyDetachedSignature(String fileName, InputStream signature) throws GeneralSecurityException, + IOException, PGPException, VerificationFailedException { + signature = PGPUtil.getDecoderStream(signature); - if(key == null) { - throw new VerificationFailedException("Error: Signature could not be verified."); - } + PGPObjectFactory pgpFactory = new PGPObjectFactory(signature); + PGPSignatureList pgpSignatureList; - ops.init(new JcaPGPContentVerifierBuilderProvider().setProvider("BC"), key); + Object pgpObject = pgpFactory.nextObject(); + if (pgpObject instanceof PGPCompressedData) { + PGPCompressedData compressedData = (PGPCompressedData) pgpObject; - while ((ch = dIn.read()) >= 0) - { - ops.update((byte)ch); - out.write(ch); - } + pgpFactory = new PGPObjectFactory(compressedData.getDataStream()); - out.close(); + pgpSignatureList = (PGPSignatureList) pgpFactory.nextObject(); + } else { + pgpSignatureList = (PGPSignatureList) pgpObject; + } - PGPSignatureList p3 = (PGPSignatureList)pgpFact.nextObject(); + PGPSignature pgpSignature = pgpSignatureList.get(0); + PGPPublicKey pgpPublicKey = _publicKeys.getPublicKey(pgpSignature.getKeyID()); - if (!ops.verify(p3.get(0))) { - throw new VerificationFailedException("Error: Signature could not be verified."); - } + InputStream inputFileInputStream = new BufferedInputStream(new FileInputStream(fileName)); + if (pgpPublicKey == null) { + inputFileInputStream.close(); + throw new VerificationFailedException("Error: Public key with signature's ID could not be found."); + } - byte[] returnBytes = out.toByteArray(); - out.close(); + pgpSignature.init(new JcaPGPContentVerifierBuilderProvider().setProvider("BC"), pgpPublicKey); - return returnBytes; + int ch; + while ((ch = inputFileInputStream.read()) >= 0) { + pgpSignature.update((byte) ch); + } + inputFileInputStream.close(); - } + if (!pgpSignature.verify()) { + throw new VerificationFailedException("Error: Signature could not be verified."); + } + return true; + } } diff --git a/lib/pgp/jruby-pgp.jar b/lib/pgp/jruby-pgp.jar new file mode 100644 index 0000000..1de1426 Binary files /dev/null and b/lib/pgp/jruby-pgp.jar differ diff --git a/lib/pgp/signer.rb b/lib/pgp/signer.rb index 5574b29..5036a46 100644 --- a/lib/pgp/signer.rb +++ b/lib/pgp/signer.rb @@ -15,6 +15,11 @@ def sign(data) String.from_java_bytes(signed_data) end + def sign_detached(filename) + # Ruby bug JRUBY-6389 + sign_data_detached(filename.to_s) + end + def sign_file(file_path) sign File.read(file_path) end @@ -36,4 +41,4 @@ def keyring_from_stream(stream) end end -end \ No newline at end of file +end diff --git a/lib/pgp/verifier.rb b/lib/pgp/verifier.rb index e856ffb..5f7d20c 100644 --- a/lib/pgp/verifier.rb +++ b/lib/pgp/verifier.rb @@ -16,6 +16,11 @@ def verify(signed_data) String.from_java_bytes(verified_data) end + def verify_detached(signature, filename) + # Ruby bug JRUBY-6389 + verify_detached_signature(signature.to_s, filename.to_s) + end + def decrypt_file(file_path) decrypt File.read(file_path) end @@ -35,4 +40,4 @@ def keyring_from_stream(stream) end end -end \ No newline at end of file +end diff --git a/lib/pgp/version.rb b/lib/pgp/version.rb index f90a8f7..5780d8c 100644 --- a/lib/pgp/version.rb +++ b/lib/pgp/version.rb @@ -1,3 +1,3 @@ module PGP - VERSION = '0.3.0' + VERSION = '0.3.1' end diff --git a/spec/lib/pgp/signer_spec.rb b/spec/lib/pgp/signer_spec.rb index 0176aeb..a851987 100644 --- a/spec/lib/pgp/signer_spec.rb +++ b/spec/lib/pgp/signer_spec.rb @@ -12,6 +12,7 @@ end let(:unsigned_file) { Fixtures_Path.join('signed_file.txt') } + let(:unencrypted_file) { Fixtures_Path.join('unencrypted_file.txt') } let(:unsigned_data) { File.read(unsigned_file)} let(:signed_file) { Fixtures_Path.join('signed_file.txt.asc') } let(:verifier) do @@ -21,11 +22,16 @@ end describe '#sign' do - it "signs" do verifier.verify(signer.sign(unsigned_data)).should == unsigned_data end + end + describe '#sign_detached' do + it "signs" do + signature = signer.sign_detached(unencrypted_file) + verifier.verify_detached(signature, unencrypted_file).should == true + end end describe "encrypting and signing" do @@ -40,5 +46,4 @@ verifier.verify(decryptor.decrypt(encryptor.encrypt(signer.sign("something fabulous")))).should == "something fabulous" end end - end diff --git a/spec/lib/pgp/verifier_spec.rb b/spec/lib/pgp/verifier_spec.rb index fe6fbf1..be445c9 100644 --- a/spec/lib/pgp/verifier_spec.rb +++ b/spec/lib/pgp/verifier_spec.rb @@ -6,28 +6,49 @@ let(:verifier) { PGP::Verifier.new } let(:unsigned_file) { Fixtures_Path.join('signed_file.txt') } let(:signed_file) { Fixtures_Path.join('signed_file.txt.asc') } + let(:file_signature) { Fixtures_Path.join('signed_file_signature.asc') } describe '#verify' do before do verifier.add_keys_from_file(public_key_path) end - context 'When the Public Key is from a file' do + context 'When the public key is from a file' do it "verifies" do verifier.verify(File.read(signed_file)).should == File.read(unsigned_file) end end - context 'When the public key cannot verify a signature' do let(:public_key_path) { Fixtures_Path.join('wrong_public_key_for_signature.asc').to_s } it "should raise an exception" do expect { verifier.verify(File.read(signed_file)) - }.to raise_exception(org.sgonyea.pgp.VerificationFailedException, /Signature could not be verified/) + }.to raise_exception(org.sgonyea.pgp.VerificationFailedException, /key.*could not be found/) end end end + describe '#verify_detached' do + before do + verifier.add_keys_from_file(public_key_path) + end + + context 'When the public key is from a file' do + it "verifies" do + verifier.verify_detached(File.read(file_signature), unsigned_file).should == true + end + end + + context 'When the public key cannot verify a signature' do + let(:public_key_path) { Fixtures_Path.join('wrong_public_key_for_signature.asc').to_s } + + it "should raise an exception" do + expect { + verifier.verify_detached(File.read(file_signature), unsigned_file) + }.to raise_exception(org.sgonyea.pgp.VerificationFailedException, /ID.*could not be found/) + end + end + end end diff --git a/spec/support/fixtures/signed_file_signature.asc b/spec/support/fixtures/signed_file_signature.asc new file mode 100644 index 0000000..c673288 --- /dev/null +++ b/spec/support/fixtures/signed_file_signature.asc @@ -0,0 +1,11 @@ +-----BEGIN PGP SIGNATURE----- +Version: BCPG v1.47 + +iQEcBAABAgAGBQJTVqdcAAoJEJU54ioziO4kvQkIAJTP7BqbPxPnvXl8W8FBPx3Z +aTFS6edoL6XJpFbDj0FmUIn2fplE478/4NcSVXwZsvfM9dzwKglhxIGrJCFoh4by +Bb89T00hFnU7HyqSZ5mLaKCjwRW5C0xGQhP2eG4PfdHIR0hGFwiyop1K7fyJYQK/ +6UU0qCnbqu6FfqTwJS4TcbwDOfWayp5IYQky0zxqRc03E7I3jkEo5omJrAqpd9kW +YOfa3JkjaAkKtDhP5p/Nr0qUIzzm8Zgp4vQqcTBMiZOUPQNKDgnrOS4dpKSAyaV0 +R4hMDhiGLsKdzoMuPmGn53TFxIBlGwef+ih/Aoiu52D9Q1MLK5NIBLxo1swWz4I= +=W4tR +-----END PGP SIGNATURE-----