packages feed

sd-jwt-0.1.0.0: test/interop/InteropSpec.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- | Interoperability tests with Python SD-JWT implementation.
--
-- These tests are only built when the 'interop-tests' flag is enabled:
--   stack build --flag sd-jwt:interop-tests
--   stack exec sd-jwt-interop-test
--
-- See internal-docs/INTEROPERABILITY_TESTING.md for details.
module Main (main) where

import Test.Hspec
import TestCaseParser
import TestCaseRunner (extractDisclosurePaths, convertClaimsToObject, compareClaims)
import TestCaseParser (extractSelectivelyDisclosablePaths)
import TestKeys
import SDJWT.Issuer
import SDJWT.Holder
import SDJWT.Verifier
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Key as Key
import qualified Data.Aeson.KeyMap as KeyMap
import qualified Data.Map.Strict as Map
import qualified Data.Text as T
import Data.List (nub)
import System.FilePath
import System.Directory
import Control.Monad
import Control.Exception (catch, SomeException)
import Data.Int (Int64)

main :: IO ()
main = do
  -- Find all test case directories
  vectorsDir <- getCurrentDirectory >>= \dir -> return $ dir </> "test" </> "interop" </> "vectors"
  testCases <- listDirectory vectorsDir `catch` (\(_::SomeException) -> return [])
  
  -- Filter to only directories with specification.yml
  validTestCases <- filterM (\name -> do
    let specFile = vectorsDir </> name </> "specification.yml"
    doesFileExist specFile
    ) testCases
  
  hspec $ describe "Python SD-JWT Interoperability Tests" $ do
    forM_ validTestCases $ \testCaseName -> do
      it ("can run test case: " ++ testCaseName) $ do
        let specFile = vectorsDir </> testCaseName </> "specification.yml"
        result <- loadTestCase specFile
        case result of
          Left err -> expectationFailure $ "Failed to parse test case: " ++ err
          Right testCase -> runTestCase specFile testCase

-- | Run a single test case using only the public API.
runTestCase :: FilePath -> TestCase -> IO ()
runTestCase specFile testCase = do
  -- Skip JSON serialization test cases (not yet supported)
  case tcSerializationFormat testCase of
    Just "json" -> return ()  -- Skip for now
    _ -> do
      -- Generate test keys
      keyPair <- generateTestRSAKeyPair
      
      -- Convert user_claims to Aeson.Object
      let userClaimsObj = convertClaimsToObject (tcUserClaims testCase)
      
      -- Extract selectively disclosable paths from YAML file (preserves !sd tags)
      sdPathsResult <- extractSelectivelyDisclosablePaths specFile
      selectiveClaimNames <- case sdPathsResult of
        Left err -> do
          expectationFailure $ "Failed to extract selectively disclosable paths from YAML: " ++ err
          return []  -- Never reached, but satisfies type checker
        Right paths -> return paths
      
      -- Debug: print selectively disclosable paths for array_recursive_sd and recursions
      when (Map.member "array_with_recursive_sd" (tcUserClaims testCase) || Map.member "animals" (tcUserClaims testCase)) $ do
        let testName = if Map.member "array_with_recursive_sd" (tcUserClaims testCase) then "array_recursive_sd" else "recursions"
        putStrLn $ "\n=== " ++ testName ++ " test case ==="
        putStrLn $ "Selectively disclosable paths (from YAML tags):"
        mapM_ (putStrLn . ("  " ++) . T.unpack) selectiveClaimNames
        putStrLn ""
      
      -- Create SD-JWT
      sdjwtResult <- createSDJWT Nothing Nothing SHA256 (privateKeyJWK keyPair) selectiveClaimNames userClaimsObj
      case sdjwtResult of
        Left err -> expectationFailure $ "Failed to create SD-JWT: " ++ show err
        Right sdjwt -> do
          -- Extract disclosure paths from holder_disclosed_claims
          let disclosurePaths = extractDisclosurePaths (tcHolderDisclosedClaims testCase)
          
          -- Create presentation
          presentationResult <- case selectDisclosuresByNames sdjwt disclosurePaths of
            Left err -> return $ Left $ "Failed to select disclosures: " ++ show err
            Right presentation -> do
              -- Add key binding if required
              if tcKeyBinding testCase
                then do
                  -- Generate holder key pair
                  holderKeyPair <- generateTestEd25519KeyPair
                  -- Add holder key to claims (for cnf claim)
                  let claimsWithCnf = addHolderKeyToClaims (publicKeyJWK holderKeyPair) userClaimsObj
                  -- Recreate SD-JWT with cnf claim
                  sdjwtWithCnfResult <- createSDJWT Nothing Nothing SHA256 (privateKeyJWK keyPair) selectiveClaimNames claimsWithCnf
                  case sdjwtWithCnfResult of
                    Left err -> return $ Left $ "Failed to create SD-JWT with cnf: " ++ show err
                    Right sdjwtWithCnf -> do
                      -- Select disclosures again
                      case selectDisclosuresByNames sdjwtWithCnf disclosurePaths of
                        Left err -> return $ Left $ "Failed to select disclosures: " ++ show err
                        Right pres -> do
                          -- Add key binding
                          let audience = "test-audience"
                          let nonce = "test-nonce"
                          let issuedAt = 1234567890 :: Int64
                          kbResult <- addKeyBindingToPresentation SHA256 (privateKeyJWK holderKeyPair) audience nonce issuedAt pres KeyMap.empty
                          case kbResult of
                            Left err -> return $ Left $ "Failed to add key binding: " ++ show err
                            Right presWithKB -> return $ Right presWithKB
                else return $ Right presentation
          
          case presentationResult of
            Left err -> expectationFailure err
            Right presentation -> do
              -- Verify SD-JWT
              verifyResult <- verifySDJWT (publicKeyJWK keyPair) presentation Nothing
              case verifyResult of
                Left err -> expectationFailure $ "Verification failed: " ++ show err
                Right processedPayload -> do
                  -- Compare with expected claims
                  let verifiedClaims = processedClaims processedPayload
                  let expectedClaimsObj = convertClaimsToObject (tcExpectedVerifiedClaims testCase)
                  
                  -- Compare claims using deep comparison
                  case compareClaims verifiedClaims expectedClaimsObj of
                    Right () -> return ()
                    Left err -> expectationFailure $ "Claims mismatch: " ++ err ++ "\nExpected: " ++ show expectedClaimsObj ++ "\nGot: " ++ show verifiedClaims