//
// Copyright 2021 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 ssh

import (
	"crypto"
	"crypto/ecdsa"
	"crypto/ed25519"
	"crypto/elliptic"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"net/http"

	"github.com/asaskevich/govalidator"
	"github.com/sigstore/rekor/pkg/pki/identity"
	"github.com/sigstore/sigstore/pkg/cryptoutils"
	sigsig "github.com/sigstore/sigstore/pkg/signature"
	"golang.org/x/crypto/ssh"
)

type Signature struct {
	signature *ssh.Signature
	pk        ssh.PublicKey
	hashAlg   string
}

// NewSignature creates and Validates an ssh signature object
func NewSignature(r io.Reader) (*Signature, error) {
	b, err := io.ReadAll(r)
	if err != nil {
		return nil, err
	}
	sig, err := Decode(b)
	if err != nil {
		return nil, err
	}
	return sig, nil
}

// CanonicalValue implements the pki.Signature interface
func (s Signature) CanonicalValue() ([]byte, error) {
	if s.signature == nil {
		return nil, errors.New("ssh signature has not been initialized")
	}
	if s.pk == nil {
		return nil, errors.New("ssh signature public key has not been initialized")
	}
	return []byte(Armor(s.signature, s.pk)), nil
}

// Verify implements the pki.Signature interface
func (s Signature) Verify(r io.Reader, k interface{}, _ ...sigsig.VerifyOption) error {
	if s.signature == nil {
		return errors.New("ssh signature has not been initialized")
	}

	key, ok := k.(*PublicKey)
	if !ok || key == nil {
		return fmt.Errorf("invalid public key type for: %v", k)
	}

	ck, err := key.CanonicalValue()
	if err != nil {
		return err
	}
	cs, err := s.CanonicalValue()
	if err != nil {
		return err
	}
	return Verify(r, cs, ck)
}

// PublicKey contains an ssh PublicKey
type PublicKey struct {
	key     ssh.PublicKey
	comment string
}

// NewPublicKey implements the pki.PublicKey interface
func NewPublicKey(r io.Reader) (*PublicKey, error) {
	// 64K seems generous as a limit for valid SSH keys
	// we use http.MaxBytesReader and pass nil for ResponseWriter to reuse stdlib
	// and not reimplement this; There is a proposal for this to be fixed in 1.20
	// https://github.com/golang/go/issues/51115
	// TODO: switch this to stdlib once golang 1.20 comes out
	rawPub, err := io.ReadAll(http.MaxBytesReader(nil, io.NopCloser(r), 65536))
	if err != nil {
		return nil, err
	}

	key, comment, _, _, err := ssh.ParseAuthorizedKey(rawPub)
	if err != nil {
		return nil, err
	}

	return &PublicKey{key: key, comment: comment}, nil
}

// CanonicalValue implements the pki.PublicKey interface
func (k PublicKey) CanonicalValue() ([]byte, error) {
	if k.key == nil {
		return nil, errors.New("ssh public key has not been initialized")
	}
	return ssh.MarshalAuthorizedKey(k.key), nil
}

// EmailAddresses implements the pki.PublicKey interface
func (k PublicKey) EmailAddresses() []string {
	if govalidator.IsEmail(k.comment) {
		return []string{k.comment}
	}
	return nil
}

// Subjects implements the pki.PublicKey interface
func (k PublicKey) Subjects() []string {
	return k.EmailAddresses()
}

// Identities implements the pki.PublicKey interface
func (k PublicKey) Identities() ([]identity.Identity, error) {
	if k.key == nil {
		return nil, errors.New("ssh public key has not been initialized")
	}

	// extract key from SSH certificate if present
	var sshKey ssh.PublicKey
	switch v := k.key.(type) {
	case *ssh.Certificate:
		if v == nil {
			return nil, errors.New("ssh certificate is nil")
		}
		sshKey = v.Key
	default:
		sshKey = k.key
	}

	if sshKey == nil {
		return nil, errors.New("ssh public key is nil")
	}

	// Extract crypto.PublicKey from SSH key
	// Handle sk public keys which do not implement ssh.CryptoPublicKey
	// Inspired by x/ssh/keys.go
	// TODO: Simplify after https://github.com/golang/go/issues/62518
	var cryptoPubKey crypto.PublicKey
	if v, ok := sshKey.(ssh.CryptoPublicKey); ok {
		cryptoPubKey = v.CryptoPublicKey()
	} else {
		switch sshKey.Type() {
		case ssh.KeyAlgoSKECDSA256:
			var w struct {
				Curve       string
				KeyBytes    []byte
				Application string
				Rest        []byte `ssh:"rest"`
			}
			_, k, ok := parseString(sshKey.Marshal())
			if !ok {
				return nil, fmt.Errorf("error parsing ssh.KeyAlgoSKED25519 key")
			}
			if err := ssh.Unmarshal(k, &w); err != nil {
				return nil, err
			}
			if w.Curve != "nistp256" {
				return nil, errors.New("ssh: unsupported curve")
			}
			ecdsaPubKey := new(ecdsa.PublicKey)
			ecdsaPubKey.Curve = elliptic.P256()
			//nolint:staticcheck // ignore SA1019 for old code
			ecdsaPubKey.X, ecdsaPubKey.Y = elliptic.Unmarshal(ecdsaPubKey.Curve, w.KeyBytes)
			if ecdsaPubKey.X == nil || ecdsaPubKey.Y == nil {
				return nil, errors.New("ssh: invalid curve point")
			}
			cryptoPubKey = ecdsaPubKey
		case ssh.KeyAlgoSKED25519:
			var w struct {
				KeyBytes    []byte
				Application string
				Rest        []byte `ssh:"rest"`
			}
			_, k, ok := parseString(sshKey.Marshal())
			if !ok {
				return nil, fmt.Errorf("error parsing ssh.KeyAlgoSKED25519 key")
			}
			if err := ssh.Unmarshal(k, &w); err != nil {
				return nil, err
			}
			if l := len(w.KeyBytes); l != ed25519.PublicKeySize {
				return nil, fmt.Errorf("invalid size %d for Ed25519 public key", l)
			}
			cryptoPubKey = ed25519.PublicKey(w.KeyBytes)
		default:
			// Should not occur, as rsa, dsa, ecdsa, and ed25519 all implement ssh.CryptoPublicKey
			return nil, fmt.Errorf("unknown key type: %T", sshKey)
		}
	}

	pkixKey, err := cryptoutils.MarshalPublicKeyToDER(cryptoPubKey)
	if err != nil {
		return nil, err
	}
	fp := ssh.FingerprintSHA256(k.key)
	return []identity.Identity{{
		Crypto:      k.key,
		Raw:         pkixKey,
		Fingerprint: fp,
	}}, nil
}

// Copied by x/ssh/keys.go
// TODO: Remove after https://github.com/golang/go/issues/62518
func parseString(in []byte) (out, rest []byte, ok bool) {
	if len(in) < 4 {
		return
	}
	length := binary.BigEndian.Uint32(in)
	in = in[4:]
	if uint32(len(in)) < length {
		return
	}
	out = in[:length]
	rest = in[length:]
	ok = true
	return
}
