| // Copyright (C) 2015 The Android Open Source Project | |
| // | |
| // Licensed 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 com.google.gerrit.gpg; | |
| import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString; | |
| import static com.google.gerrit.gpg.PublicKeyStore.keyToString; | |
| import static com.google.gerrit.gpg.testing.TestKeys.expiredKey; | |
| import static com.google.gerrit.gpg.testing.TestKeys.validKeyWithExpiration; | |
| import static com.google.gerrit.gpg.testing.TestKeys.validKeyWithoutExpiration; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| import static org.junit.Assert.assertEquals; | |
| import com.google.gerrit.gpg.testing.TestKey; | |
| import java.io.ByteArrayInputStream; | |
| import java.io.ByteArrayOutputStream; | |
| import java.io.InputStreamReader; | |
| import java.io.Reader; | |
| import java.text.SimpleDateFormat; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.Collections; | |
| import java.util.Date; | |
| import java.util.List; | |
| import org.bouncycastle.bcpg.ArmoredOutputStream; | |
| import org.bouncycastle.bcpg.BCPGOutputStream; | |
| 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.bc.BcPGPContentSignerBuilder; | |
| import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; | |
| import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; | |
| import org.eclipse.jgit.lib.CommitBuilder; | |
| import org.eclipse.jgit.lib.PersonIdent; | |
| import org.eclipse.jgit.lib.RefUpdate; | |
| import org.eclipse.jgit.lib.Repository; | |
| import org.eclipse.jgit.transport.PushCertificate; | |
| import org.eclipse.jgit.transport.PushCertificateIdent; | |
| import org.eclipse.jgit.transport.PushCertificateParser; | |
| import org.eclipse.jgit.transport.SignedPushConfig; | |
| import org.junit.Before; | |
| import org.junit.Test; | |
| public class PushCertificateCheckerTest { | |
| private InMemoryRepository repo; | |
| private PublicKeyStore store; | |
| private SignedPushConfig signedPushConfig; | |
| private PushCertificateChecker checker; | |
| @Before | |
| public void setUp() throws Exception { | |
| TestKey key1 = validKeyWithoutExpiration(); | |
| TestKey key3 = expiredKey(); | |
| repo = new InMemoryRepository(new DfsRepositoryDescription("repo")); | |
| store = new PublicKeyStore(repo); | |
| store.add(key1.getPublicKeyRing()); | |
| store.add(key3.getPublicKeyRing()); | |
| PersonIdent ident = new PersonIdent("A U Thor", "author@example.com"); | |
| CommitBuilder cb = new CommitBuilder(); | |
| cb.setAuthor(ident); | |
| cb.setCommitter(ident); | |
| assertEquals(RefUpdate.Result.NEW, store.save(cb)); | |
| signedPushConfig = new SignedPushConfig(); | |
| signedPushConfig.setCertNonceSeed("sekret"); | |
| signedPushConfig.setCertNonceSlopLimit(60 * 24); | |
| checker = newChecker(true); | |
| } | |
| private PushCertificateChecker newChecker(boolean checkNonce) { | |
| PublicKeyChecker keyChecker = new PublicKeyChecker().setStore(store); | |
| return new PushCertificateChecker(keyChecker) { | |
| @Override | |
| protected Repository getRepository() { | |
| return repo; | |
| } | |
| @Override | |
| protected boolean shouldClose(Repository repo) { | |
| return false; | |
| } | |
| }.setCheckNonce(checkNonce); | |
| } | |
| @Test | |
| public void validCert() throws Exception { | |
| PushCertificate cert = newSignedCert(validNonce(), validKeyWithoutExpiration()); | |
| assertNoProblems(cert); | |
| } | |
| @Test | |
| public void invalidNonce() throws Exception { | |
| PushCertificate cert = newSignedCert("invalid-nonce", validKeyWithoutExpiration()); | |
| assertProblems(cert, "Invalid nonce"); | |
| } | |
| @Test | |
| public void invalidNonceNotChecked() throws Exception { | |
| checker = newChecker(false); | |
| PushCertificate cert = newSignedCert("invalid-nonce", validKeyWithoutExpiration()); | |
| assertNoProblems(cert); | |
| } | |
| @Test | |
| public void missingKey() throws Exception { | |
| TestKey key2 = validKeyWithExpiration(); | |
| PushCertificate cert = newSignedCert(validNonce(), key2); | |
| assertProblems(cert, "No public keys found for key ID " + keyIdToString(key2.getKeyId())); | |
| } | |
| @Test | |
| public void invalidKey() throws Exception { | |
| TestKey key3 = expiredKey(); | |
| PushCertificate cert = newSignedCert(validNonce(), key3); | |
| assertProblems( | |
| cert, "Invalid public key " + keyToString(key3.getPublicKey()) + ":\n Key is expired"); | |
| } | |
| @Test | |
| public void signatureByExpiredKeyBeforeExpiration() throws Exception { | |
| TestKey key3 = expiredKey(); | |
| Date now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").parse("2005-07-10 12:00:00 -0400"); | |
| PushCertificate cert = newSignedCert(validNonce(), key3, now); | |
| assertNoProblems(cert); | |
| } | |
| private String validNonce() { | |
| return signedPushConfig | |
| .getNonceGenerator() | |
| .createNonce(repo, System.currentTimeMillis() / 1000); | |
| } | |
| private PushCertificate newSignedCert(String nonce, TestKey signingKey) throws Exception { | |
| return newSignedCert(nonce, signingKey, null); | |
| } | |
| private PushCertificate newSignedCert(String nonce, TestKey signingKey, Date now) | |
| throws Exception { | |
| PushCertificateIdent ident = | |
| new PushCertificateIdent(signingKey.getFirstUserId(), System.currentTimeMillis(), -7 * 60); | |
| String payload = | |
| "certificate version 0.1\n" | |
| + "pusher " | |
| + ident.getRaw() | |
| + "\n" | |
| + "pushee test://localhost/repo.git\n" | |
| + "nonce " | |
| + nonce | |
| + "\n" | |
| + "\n" | |
| + "0000000000000000000000000000000000000000" | |
| + " deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" | |
| + " refs/heads/master\n"; | |
| PGPSignatureGenerator gen = | |
| new PGPSignatureGenerator( | |
| new BcPGPContentSignerBuilder(signingKey.getPublicKey().getAlgorithm(), PGPUtil.SHA1)); | |
| if (now != null) { | |
| PGPSignatureSubpacketGenerator subGen = new PGPSignatureSubpacketGenerator(); | |
| subGen.setSignatureCreationTime(false, now); | |
| gen.setHashedSubpackets(subGen.generate()); | |
| } | |
| gen.init(PGPSignature.BINARY_DOCUMENT, signingKey.getPrivateKey()); | |
| gen.update(payload.getBytes(UTF_8)); | |
| PGPSignature sig = gen.generate(); | |
| ByteArrayOutputStream bout = new ByteArrayOutputStream(); | |
| try (BCPGOutputStream out = new BCPGOutputStream(new ArmoredOutputStream(bout))) { | |
| sig.encode(out); | |
| } | |
| String cert = payload + new String(bout.toByteArray(), UTF_8); | |
| Reader reader = new InputStreamReader(new ByteArrayInputStream(cert.getBytes(UTF_8)), UTF_8); | |
| PushCertificateParser parser = new PushCertificateParser(repo, signedPushConfig); | |
| return parser.parse(reader); | |
| } | |
| private void assertProblems(PushCertificate cert, String first, String... rest) throws Exception { | |
| List<String> expected = new ArrayList<>(); | |
| expected.add(first); | |
| expected.addAll(Arrays.asList(rest)); | |
| CheckResult result = checker.check(cert).getCheckResult(); | |
| assertEquals(expected, result.getProblems()); | |
| } | |
| private void assertNoProblems(PushCertificate cert) { | |
| CheckResult result = checker.check(cert).getCheckResult(); | |
| assertEquals(Collections.emptyList(), result.getProblems()); | |
| } | |
| } |