// 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());
  }
}
