{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts #-}

module ECDSA (tests) where

import qualified Crypto.ECC as ECDSA
import Crypto.Error
import Crypto.Hash
import qualified Crypto.PubKey.ECC.ECDSA as ECC
import qualified Crypto.PubKey.ECC.Generate as ECC
import qualified Crypto.PubKey.ECC.Types as ECC
import qualified Crypto.PubKey.ECDSA as ECDSA
import qualified Data.ByteString as B
import Data.Maybe

import Imports

data Curve
    = forall curve.
        (ECDSA.EllipticCurveECDSA curve, Show (ECDSA.Scalar curve)) =>
      Curve curve ECC.Curve ECC.CurveName

instance Show Curve where
    showsPrec d (Curve _ _ name) = showsPrec d name

instance Arbitrary Curve where
    arbitrary =
        elements
            [ makeCurve ECDSA.Curve_P256R1 ECC.SEC_p256r1
            , makeCurve ECDSA.Curve_P384R1 ECC.SEC_p384r1
            , makeCurve ECDSA.Curve_P521R1 ECC.SEC_p521r1
            ]
      where
        makeCurve c name = Curve c (ECC.getCurveByName name) name

arbitraryScalar :: ECC.Curve -> Gen Integer
arbitraryScalar curve = choose (1, n - 1)
  where
    n = ECC.ecc_n (ECC.common_curve curve)

sigECDSAtoECC
    :: ECDSA.EllipticCurveECDSA curve
    => proxy curve -> ECDSA.Signature curve -> ECC.Signature
sigECDSAtoECC prx (ECDSA.Signature r s) = ECC.Signature (ECDSA.scalarToInteger prx r) (ECDSA.scalarToInteger prx s)

normalizeECC :: ECC.Curve -> ECC.Signature -> ECC.Signature
normalizeECC curve (ECC.Signature r s)
    | s <= n `div` 2 = ECC.Signature r s
    | otherwise = ECC.Signature r (n - s)
  where
    n = ECC.ecc_n $ ECC.common_curve curve

testRecover :: ECC.CurveName -> TestTree
testRecover name = testProperty (show name) $ \(ArbitraryBS0_2901 msg) -> do
    let curve = ECC.getCurveByName name
    let n = ECC.ecc_n $ ECC.common_curve curve
    k <- choose (1, n - 1)
    d <- choose (1, n - 1)
    let key = ECC.PrivateKey curve d
    let digest = hashWith SHA256 msg
    let pub =
            ECC.signExtendedDigestWith k key digest >>= \signature -> ECC.recoverDigest curve signature digest
    pure $
        propertyHold
            [eqTest "recovery" (Just $ ECC.generateQ curve d) (ECC.public_q <$> pub)]

testNormalize :: ECC.CurveName -> TestTree
testNormalize name = testProperty (show name) $ \(ArbitraryBS0_2901 msg) -> do
    let curve = ECC.getCurveByName name
    let n = ECC.ecc_n $ ECC.common_curve curve
    k <- choose (1, n - 1)
    d <- choose (1, n - 1)
    let key = ECC.PrivateKey curve d
    let digest = hashWith SHA256 msg
    let check =
            ECC.signExtendedDigestWith k key digest >>= \s -> pure $ ECC.sign_s (ECC.signature s) <= n `div` 2
    pure $ propertyHold [eqTest "normalized" (Just True) check]

tests :: TestTree
tests =
    testGroup
        "ECDSA"
        [ localOption (QuickCheckTests 5) $
            testGroup
                "verification"
                [ testProperty "SHA1" $ propertyECDSA SHA1
                , testProperty "SHA224" $ propertyECDSA SHA224
                , testProperty "SHA256" $ propertyECDSA SHA256
                , testProperty "SHA384" $ propertyECDSA SHA384
                , testProperty "SHA512" $ propertyECDSA SHA512
                ]
        , testGroup
            "recovery"
            [ localOption (QuickCheckTests 100) $ testRecover ECC.SEC_p128r1
            , localOption (QuickCheckTests 100) $ testRecover ECC.SEC_p128r2
            , localOption (QuickCheckTests 100) $ testRecover ECC.SEC_p256k1
            , localOption (QuickCheckTests 100) $ testRecover ECC.SEC_p256r1
            , localOption (QuickCheckTests 50) $ testRecover ECC.SEC_t131r1
            , localOption (QuickCheckTests 50) $ testRecover ECC.SEC_t131r2
            , localOption (QuickCheckTests 20) $ testRecover ECC.SEC_t233k1
            , localOption (QuickCheckTests 20) $ testRecover ECC.SEC_t233r1
            ]
        , testGroup
            "normalize"
            [ localOption (QuickCheckTests 100) $ testNormalize ECC.SEC_p128r1
            , localOption (QuickCheckTests 100) $ testNormalize ECC.SEC_p128r2
            , localOption (QuickCheckTests 100) $ testNormalize ECC.SEC_p256k1
            , localOption (QuickCheckTests 100) $ testNormalize ECC.SEC_p256r1
            , localOption (QuickCheckTests 50) $ testNormalize ECC.SEC_t131r1
            , localOption (QuickCheckTests 50) $ testNormalize ECC.SEC_t131r2
            , localOption (QuickCheckTests 20) $ testNormalize ECC.SEC_t233k1
            , localOption (QuickCheckTests 20) $ testNormalize ECC.SEC_t233r1
            ]
        ]
  where
    propertyECDSA hashAlg (Curve c curve _) (ArbitraryBS0_2901 msg) = do
        d <- arbitraryScalar curve
        kECC <- arbitraryScalar curve
        let privECC = ECC.PrivateKey curve d
            prx = Just c -- using Maybe as Proxy
            kECDSA = throwCryptoError $ ECDSA.scalarFromInteger prx kECC
            privECDSA = throwCryptoError $ ECDSA.scalarFromInteger prx d
            pubECDSA = ECDSA.toPublic prx privECDSA
            sigECC = fromJust $ ECC.signWith kECC privECC hashAlg msg
            sigECDSA = fromJust $ ECDSA.signWith prx kECDSA privECDSA hashAlg msg
            msg' = msg `B.append` B.singleton 42
        return $
            propertyHold
                [ eqTest "signature" sigECC $ normalizeECC curve $ sigECDSAtoECC prx sigECDSA
                , eqTest "verification" True (ECDSA.verify prx hashAlg pubECDSA sigECDSA msg)
                , eqTest "alteration" False (ECDSA.verify prx hashAlg pubECDSA sigECDSA msg')
                ]
