/*
 * Copyright (c) 2018, Psiphon Inc.
 * All rights reserved.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

// Package accesscontrol implements an access control authorization scheme
// based on digital signatures.
//
// Authorizations for specified access types are issued by an entity that
// digitally signs each authorization. The digital signature is verified
// by service providers before granting the specified access type. Each
// authorization includes an expiry date and a unique ID that may be used
// to mitigate malicious reuse/sharing of authorizations.
//
// In a typical deployment, the signing keys will be present on issuing
// entities which are distinct from service providers. Only verification
// keys will be deployed to service providers.
//
// An authorization is represented in JSON, which is then base64-encoded
// for transport:
//
// {
//   "Authorization" : {
// 	 "ID" : <derived unique ID>,
// 	 "AccessType" : <access type name; e.g., "my-access">,
// 	 "Expires" : <RFC3339-encoded UTC time value>
//   },
//   "SigningKeyID" : <unique key ID>,
//   "Signature" : <Ed25519 digital signature>
// }
//
package accesscontrol

import (
	"crypto/ed25519"
	"crypto/rand"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/base64"
	"encoding/json"
	"io"
	"time"

	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/errors"
	"golang.org/x/crypto/hkdf"
)

const (
	keyIDLength              = 32
	authorizationIDKeyLength = 32
	authorizationIDLength    = 32
)

// SigningKey is the private key used to sign newly issued
// authorizations for the specified access type. The key ID
// is included in authorizations and identifies the
// corresponding verification keys.
//
// AuthorizationIDKey is used to produce a unique
// authentication ID that cannot be mapped back to its seed
// value.
type SigningKey struct {
	ID                 []byte
	AccessType         string
	AuthorizationIDKey []byte
	PrivateKey         []byte
}

// VerificationKey is the public key used to verify signed
// authentications issued for the specified access type. The
// authorization references the expected public key by ID.
type VerificationKey struct {
	ID         []byte
	AccessType string
	PublicKey  []byte
}

// NewKeyPair generates a new authorization signing key pair.
func NewKeyPair(
	accessType string) (*SigningKey, *VerificationKey, error) {

	ID := make([]byte, keyIDLength)
	_, err := rand.Read(ID)
	if err != nil {
		return nil, nil, errors.Trace(err)
	}

	authorizationIDKey := make([]byte, authorizationIDKeyLength)
	_, err = rand.Read(authorizationIDKey)
	if err != nil {
		return nil, nil, errors.Trace(err)
	}

	publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		return nil, nil, errors.Trace(err)
	}

	signingKey := &SigningKey{
		ID:                 ID,
		AccessType:         accessType,
		AuthorizationIDKey: authorizationIDKey,
		PrivateKey:         privateKey,
	}

	verificationKey := &VerificationKey{
		ID:         ID,
		AccessType: accessType,
		PublicKey:  publicKey,
	}

	return signingKey, verificationKey, nil
}

// Authorization describes an authorization, with a unique ID,
// granting access to a specified access type, and expiring at
// the specified time.
//
// An Authorization is embedded within a digitally signed
// object. This wrapping object adds a signature and a signing
// key ID.
type Authorization struct {
	ID         []byte
	AccessType string
	Expires    time.Time
}

type signedAuthorization struct {
	Authorization json.RawMessage
	SigningKeyID  []byte
	Signature     []byte
}

// ValidateSigningKey checks that a signing key is correctly configured.
func ValidateSigningKey(signingKey *SigningKey) error {
	if len(signingKey.ID) != keyIDLength ||
		len(signingKey.AccessType) < 1 ||
		len(signingKey.AuthorizationIDKey) != authorizationIDKeyLength ||
		len(signingKey.PrivateKey) != ed25519.PrivateKeySize {
		return errors.TraceNew("invalid signing key")
	}
	return nil
}

// IssueAuthorization issues an authorization signed with the
// specified signing key.
//
// seedAuthorizationID should be a value that uniquely identifies
// the purchase, subscription, or transaction that backs the
// authorization; a distinct unique authorization ID will be derived
// from the seed without revealing the original value. The authorization
// ID is to be used to mitigate malicious authorization reuse/sharing.
//
// The first return value is a base64-encoded, serialized JSON representation
// of the signed authorization that can be passed to VerifyAuthorization. The
// second return value is the unique ID of the signed authorization returned in
// the first value.
func IssueAuthorization(
	signingKey *SigningKey,
	seedAuthorizationID []byte,
	expires time.Time) (string, []byte, error) {

	err := ValidateSigningKey(signingKey)
	if err != nil {
		return "", nil, errors.Trace(err)
	}

	hkdf := hkdf.New(sha256.New, signingKey.AuthorizationIDKey, nil, seedAuthorizationID)
	ID := make([]byte, authorizationIDLength)
	_, err = io.ReadFull(hkdf, ID)
	if err != nil {
		return "", nil, errors.Trace(err)
	}

	auth := Authorization{
		ID:         ID,
		AccessType: signingKey.AccessType,
		Expires:    expires.UTC(),
	}

	authJSON, err := json.Marshal(auth)
	if err != nil {
		return "", nil, errors.Trace(err)
	}

	signature := ed25519.Sign(signingKey.PrivateKey, authJSON)

	signedAuth := signedAuthorization{
		Authorization: authJSON,
		SigningKeyID:  signingKey.ID,
		Signature:     signature,
	}

	signedAuthJSON, err := json.Marshal(signedAuth)
	if err != nil {
		return "", nil, errors.Trace(err)
	}

	encodedSignedAuth := base64.StdEncoding.EncodeToString(signedAuthJSON)

	return encodedSignedAuth, ID, nil
}

// VerificationKeyRing is a set of verification keys to be deployed
// to a service provider for verifying access authorizations.
type VerificationKeyRing struct {
	Keys []*VerificationKey
}

// ValidateVerificationKeyRing checks that a verification key ring is
// correctly configured.
func ValidateVerificationKeyRing(keyRing *VerificationKeyRing) error {
	for _, key := range keyRing.Keys {
		if len(key.ID) != keyIDLength ||
			len(key.AccessType) < 1 ||
			len(key.PublicKey) != ed25519.PublicKeySize {
			return errors.TraceNew("invalid verification key")
		}
	}
	return nil
}

// VerifyAuthorization verifies the signed authorization and, when
// verified, returns the embedded Authorization struct with the
// access control information.
//
// The key ID in the signed authorization is used to select the
// appropriate verification key from the key ring.
func VerifyAuthorization(
	keyRing *VerificationKeyRing,
	encodedSignedAuthorization string) (*Authorization, error) {

	err := ValidateVerificationKeyRing(keyRing)
	if err != nil {
		return nil, errors.Trace(err)
	}

	signedAuthorizationJSON, err := base64.StdEncoding.DecodeString(
		encodedSignedAuthorization)
	if err != nil {
		return nil, errors.Trace(err)
	}

	var signedAuth signedAuthorization
	err = json.Unmarshal(signedAuthorizationJSON, &signedAuth)
	if err != nil {
		return nil, errors.Trace(err)
	}

	if len(signedAuth.SigningKeyID) != keyIDLength {
		return nil, errors.TraceNew("invalid key ID length")
	}

	if len(signedAuth.Signature) != ed25519.SignatureSize {
		return nil, errors.TraceNew("invalid signature length")
	}

	var verificationKey *VerificationKey

	for _, key := range keyRing.Keys {
		if subtle.ConstantTimeCompare(signedAuth.SigningKeyID, key.ID) == 1 {
			verificationKey = key
		}
	}

	if verificationKey == nil {
		return nil, errors.TraceNew("invalid key ID")
	}

	if !ed25519.Verify(
		verificationKey.PublicKey, signedAuth.Authorization, signedAuth.Signature) {
		return nil, errors.TraceNew("invalid signature")
	}

	var auth Authorization

	err = json.Unmarshal(signedAuth.Authorization, &auth)
	if err != nil {
		return nil, errors.Trace(err)
	}

	if len(auth.ID) == 0 {
		return nil, errors.TraceNew("invalid authorization ID")
	}

	if auth.AccessType != verificationKey.AccessType {
		return nil, errors.TraceNew("invalid access type")
	}

	if auth.Expires.IsZero() {
		return nil, errors.TraceNew("invalid expiry")
	}

	if auth.Expires.Before(time.Now().UTC()) {
		return nil, errors.TraceNew("expired authorization")
	}

	return &auth, nil
}
