defmodule DaProductApp.Crypto.Signature do @moduledoc """ Digital signature functionality for UPI QR codes using ECDSA with SHA-256. Implements signing similar to the NPCI specification requirements. """ @doc """ Generate ECDSA signature for UPI QR string Returns Base64-encoded signature on success or {:error, reason} on failure. """ def sign_qr_string(qr_string) when is_binary(qr_string) do with {:ok, private_key} <- get_private_key(), {:ok, signature} <- create_signature(qr_string, private_key) do {:ok, Base.encode64(signature)} else {:error, reason} -> {:error, "Signature generation failed: #{reason}"} end end @doc """ Verify ECDSA signature for UPI QR string. Expects signature in Base64. Returns {:ok, true} or {:ok, false} on success, or {:error, reason} on failures. """ def verify_qr_signature(qr_string, signature_base64) when is_binary(qr_string) and is_binary(signature_base64) do with {:ok, public_key} <- get_public_key(), {:ok, signature} <- Base.decode64(signature_base64), {:ok, result} <- verify_signature(qr_string, signature, public_key) do {:ok, result} else {:error, reason} -> {:error, "Signature verification failed: #{reason}"} end end @doc """ Generate a new ECDSA key pair for testing/development """ def generate_key_pair do # Generate secp256r1 (P-256) key pair using :crypto.generate_key/2 # Returns {public_key, private_key} case :crypto.generate_key(:ecdh, :secp256r1) do {pub, priv} -> %{ public_key: Base.encode64(pub), private_key: Base.encode64(priv), algorithm: "secp256r1" } other -> # In unexpected cases, return error tuple to caller usage {:error, "key_generation_failed: #{inspect(other)}"} end end # Private functions defp create_signature(message, private_key) do try do # Convert message to bytes (binary) message_bytes = :unicode.characters_to_binary(message, :utf8) # :crypto.sign/4 accepts [PrivateKeyBinary, Curve] when using the raw # key binary produced by :crypto.generate_key/2. We use secp256r1. signature = :crypto.sign(:ecdsa, :sha256, message_bytes, [private_key, :secp256r1]) {:ok, signature} rescue error -> {:error, "Signature creation failed: #{inspect(error)}"} end end defp verify_signature(message, signature, public_key) do try do # Convert message to bytes message_bytes = :unicode.characters_to_binary(message, :utf8) # Verify signature using the public key binary and curve result = :crypto.verify(:ecdsa, :sha256, message_bytes, signature, [public_key, :secp256r1]) {:ok, result} rescue error -> {:error, "Signature verification failed: #{inspect(error)}"} end end defp get_private_key do # In production, this should be loaded from secure storage (env vars, vault, etc.) case Application.get_env(:da_product_app, :upi_signing_private_key) do nil -> # For development, generate a test key and persist to runtime config case generate_key_pair() do %{:private_key => priv_b64} = key_pair when is_map(key_pair) -> Application.put_env(:da_product_app, :upi_signing_private_key, priv_b64) Application.put_env(:da_product_app, :upi_signing_public_key, key_pair.public_key) {:ok, Base.decode64!(priv_b64)} {:error, reason} -> {:error, reason} end private_key_base64 when is_binary(private_key_base64) -> case Base.decode64(private_key_base64) do {:ok, private_key} -> {:ok, private_key} :error -> {:error, "Invalid private key format"} end _ -> {:error, "Private key not configured"} end end defp get_public_key do # In production, this should be loaded from secure storage case Application.get_env(:da_product_app, :upi_signing_public_key) do nil -> # For development, generate a test key and persist to runtime config case generate_key_pair() do %{:public_key => pub_b64} = key_pair when is_map(key_pair) -> Application.put_env(:da_product_app, :upi_signing_private_key, key_pair.private_key) Application.put_env(:da_product_app, :upi_signing_public_key, pub_b64) {:ok, Base.decode64!(pub_b64)} {:error, reason} -> {:error, reason} end public_key_base64 when is_binary(public_key_base64) -> case Base.decode64(public_key_base64) do {:ok, public_key} -> {:ok, public_key} :error -> {:error, "Invalid public key format"} end _ -> {:error, "Public key not configured"} end end @doc """ Initialize signing keys in development environment """ def init_dev_keys do key_pair = generate_key_pair() # In a real application, you would save these securely Application.put_env(:da_product_app, :upi_signing_private_key, key_pair.private_key) Application.put_env(:da_product_app, :upi_signing_public_key, key_pair.public_key) key_pair end end