//
// Copyright 2022 The Sigstore Authors.
//
// 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 gpg

import (
	"crypto"
	"crypto/x509"
	"fmt"
	"io"
	"os"
	"time"

	// TODO: this package is unmaintained except for security fixes.
	// New applications should consider a more focused, modern alternative to OpenPGP for their specific task.
	// If you are required to interoperate with OpenPGP systems and need a maintained package, consider a community fork.
	// See https://golang.org/issue/44226
	"golang.org/x/crypto/openpgp/packet" //nolint: staticcheck
	"golang.org/x/crypto/openpgp/s2k"    //nolint: staticcheck

	"github.com/sigstore/gitsign/internal"
)

// This file implements gnupg's "status protocol". When the --status-fd argument
// is passed, gpg will output machine-readable status updates to that fd.
// Details on the "protocol" can be found at https://git.io/vFFKC

type Status string

const (
	// BEGIN_SIGNING
	//   Mark the start of the actual signing process. This may be used as an
	//   indication that all requested secret keys are ready for use.
	StatusBeginSigning Status = "BEGIN_SIGNING"

	// SIG_CREATED <type> <pk_algo> <hash_algo> <class> <timestamp> <keyfpr>
	//   A signature has been created using these parameters.
	//   Values for type <type> are:
	//     - D :: detached
	//     - C :: cleartext
	//     - S :: standard
	//   (only the first character should be checked)
	//
	//   <class> are 2 hex digits with the OpenPGP signature class.
	//
	//   Note, that TIMESTAMP may either be a number of seconds since Epoch
	//   or an ISO 8601 string which can be detected by the presence of the
	//   letter 'T'.
	StatusSigCreated Status = "SIG_CREATED"

	// NEWSIG [<signers_uid>]
	//   Is issued right before a signature verification starts.  This is
	//   useful to define a context for parsing ERROR status messages.
	//   arguments are currently defined.  If SIGNERS_UID is given and is
	//   not "-" this is the percent escape value of the OpenPGP Signer's
	//   User ID signature sub-packet.
	StatusNewSig Status = "NEWSIG"

	// GOODSIG  <long_keyid_or_fpr>  <username>
	//   The signature with the keyid is good.  For each signature only one
	//   of the codes GOODSIG, BADSIG, EXPSIG, EXPKEYSIG, REVKEYSIG or
	//   ERRSIG will be emitted.  In the past they were used as a marker
	//   for a new signature; new code should use the NEWSIG status
	//   instead.  The username is the primary one encoded in UTF-8 and %XX
	//   escaped. The fingerprint may be used instead of the long keyid if
	//   it is available.  This is the case with CMS and might eventually
	//   also be available for OpenPGP.
	StatusGoodSig Status = "GOODSIG"

	// BADSIG <long_keyid_or_fpr> <username>
	//   The signature with the keyid has not been verified okay. The username is
	//   the primary one encoded in UTF-8 and %XX escaped. The fingerprint may be
	//   used instead of the long keyid if it is available. This is the case with
	//   CMS and might eventually also be available for OpenPGP.
	StatusBadSig Status = "BADSIG"

	// ERRSIG <keyid> <pkalgo> <hashalgo> <sig_class> <time> <rc>
	//
	//   It was not possible to check the signature. This may be caused by a
	//   missing public key or an unsupported algorithm. A RC of 4 indicates
	//   unknown algorithm, a 9 indicates a missing public key. The other fields
	//   give more information about this signature. sig_class is a 2 byte hex-
	//   value. The fingerprint may be used instead of the keyid if it is
	//   available. This is the case with gpgsm and might eventually also be
	//  available for OpenPGP.
	//
	//   Note, that TIME may either be the number of seconds since Epoch or an ISO
	//   8601 string. The latter can be detected by the presence of the letter
	//   ‘T’.
	StatusErrSig Status = "ERRSIG"

	// TRUST_
	//   These are several similar status codes:
	//
	//   - TRUST_UNDEFINED <error_token>
	//   - TRUST_NEVER     <error_token>
	//   - TRUST_MARGINAL  [0  [<validation_model>]]
	//   - TRUST_FULLY     [0  [<validation_model>]]
	//   - TRUST_ULTIMATE  [0  [<validation_model>]]
	//
	//   For good signatures one of these status lines are emitted to
	//   indicate the validity of the key used to create the signature.
	//   The error token values are currently only emitted by gpgsm.
	//
	//   VALIDATION_MODEL describes the algorithm used to check the
	//   validity of the key.  The defaults are the standard Web of Trust
	//   model for gpg and the standard X.509 model for gpgsm.  The
	//   defined values are
	//
	//      - pgp   :: The standard PGP WoT.
	//      - shell :: The standard X.509 model.
	//      - chain :: The chain model.
	//      - steed :: The STEED model.
	//      - tofu  :: The TOFU model
	//
	//   Note that the term =TRUST_= in the status names is used for
	//   historic reasons; we now speak of validity.
	StatusTrustFully Status = "TRUST_FULLY"

	prefix = "[GNUPG:] "
)

type StatusWriter struct {
	w io.Writer
}

func NewStatusWriter(w io.Writer) *StatusWriter {
	return &StatusWriter{
		w: w,
	}
}

func NewStatusWriterFromFD(fd uintptr) *StatusWriter {
	const (
		unixStdout = 1
		unixStderr = 2
	)

	var statusFile io.Writer
	// Even though Windows uses different numbers, we always equate 1/2 with
	// stdout/stderr because Git always passes `--status-fd=1`.
	switch fd {
	case unixStdout:
		statusFile = os.Stdout
	case unixStderr:
		statusFile = os.Stderr
	default:
		// TODO: debugging output if this fails
		statusFile = os.NewFile(fd, "status")
	}

	return NewStatusWriter(statusFile)
}

func (w *StatusWriter) Emit(s Status) {
	fmt.Fprintln(w.w, prefix+string(s))
}

func (w *StatusWriter) Emitf(s Status, format string, args ...interface{}) {
	fmt.Fprint(w.w, prefix)
	fmt.Fprint(w.w, string(s))
	fmt.Fprintf(w.w, " "+format+"\n", args...)
}

func (w *StatusWriter) EmitSigCreated(cert *x509.Certificate, isDetached bool) {
	// SIG_CREATED arguments
	var (
		sigType                    string
		pkAlgo, hashAlgo, sigClass byte
		now                        int64
		fpr                        string
	)

	if isDetached {
		sigType = "D"
	} else {
		sigType = "S"
	}

	switch cert.SignatureAlgorithm {
	case x509.SHA1WithRSA, x509.SHA256WithRSA, x509.SHA384WithRSA, x509.SHA512WithRSA:
		pkAlgo = byte(packet.PubKeyAlgoRSA)
	case x509.ECDSAWithSHA1, x509.ECDSAWithSHA256, x509.ECDSAWithSHA384, x509.ECDSAWithSHA512:
		pkAlgo = byte(packet.PubKeyAlgoECDSA)
	}

	switch cert.SignatureAlgorithm {
	case x509.SHA1WithRSA, x509.ECDSAWithSHA1:
		hashAlgo, _ = s2k.HashToHashId(crypto.SHA1)
	case x509.SHA256WithRSA, x509.ECDSAWithSHA256:
		hashAlgo, _ = s2k.HashToHashId(crypto.SHA256)
	case x509.SHA384WithRSA, x509.ECDSAWithSHA384:
		hashAlgo, _ = s2k.HashToHashId(crypto.SHA384)
	case x509.SHA512WithRSA, x509.ECDSAWithSHA512:
		hashAlgo, _ = s2k.HashToHashId(crypto.SHA512)
	}

	// gpgsm seems to always use 0x00
	sigClass = 0
	now = time.Now().Unix()
	fpr = internal.CertHexFingerprint(cert)

	w.Emitf(StatusSigCreated, "%s %d %d %02x %d %s", sigType, pkAlgo, hashAlgo, sigClass, now, fpr)
}

func (w *StatusWriter) EmitGoodSig(cert *x509.Certificate) {
	subj := cert.Subject.String()
	fpr := internal.CertHexFingerprint(cert)

	w.Emitf(StatusGoodSig, "%s %s", fpr, subj)
}

func (w *StatusWriter) EmitBadSig(cert *x509.Certificate) {
	subj := cert.Subject.String()
	fpr := internal.CertHexFingerprint(cert)

	w.Emitf(StatusBadSig, "%s %s", fpr, subj)
}

func (w *StatusWriter) EmitTrustFully() {
	w.Emitf(StatusTrustFully, "0 shell")
}
