Generating Secure Nonces for Sign In with Ethereum using Verifiable Cosmic Randomness from Space

Generating Secure Nonces for Sign In with Ethereum using Verifiable Cosmic Randomness from Space

Introduction

True randomness is essential for bolstering security in decentralized applications (dApps), particularly in authentication mechanisms where predictability can lead to devastating exploits. SpaceComputer Orbitport tackles this head-on by harnessing satellite technology to deliver verifiable, tamper-proof randomness and secure compute through its robust APIs.

This guide is crafted to be beginner-friendly while catering to TypeScript developers. If you’re eager to code along or integrate the examples, some TypeScript familiarity is helpful but not mandatory. For those just reading to grasp the ideas, no programming background is required to uncover the value here.

Get Developer Support on Telegram: Click Here

What is SpaceComputer?

SpaceComputer is a space-based digital sovereignty layer designed as a cryptographically secure compute protocol for blockchain stack applications.

Every node operates as a Space Trusted Execution Environment (SpaceTEE). The underlying hardware consists of satellites outfitted with “crypto engines” and data modules, creating an orbital decentralized physical infrastructure network (DePIN).

The architecture employs a dual-layer model: the Celestial Chain in space acts as the definitive source for immutable data records, while the Uncelestial layer on Earth handles rapid transaction pre-finality. This setup delivers exceptional security via tamper-resistant hardware in the isolated void of space, maintaining functionality amid ground-based disruptions.

Founded by seasoned researchers, entrepreneurs, and developers in space and blockchain domains, SpaceComputer seeks to foster a space-native economy, where computation and commerce flourish beyond planetary confines through its satellite-driven blockchain ecosystem.

Understanding Orbitport and cTRNG

Orbitport serves as the portal to celestial compute services atop the SpaceComputer foundation. By tapping into SpaceComputer nodes, Orbitport empowers decentralized setups to draw from unalterable satellite hardware in low Earth orbit. It unlocks orbital computing that surpasses earthly constraints, with standout features like:

  • cTRNG (cosmic True Random Number Generator): Draws entropy from cosmic radiation captured by satellite sensors, yielding unbiased, cryptographically strong random numbers.
  • spaceTEE: Offers a fortified, isolated enclave for sensitive off-chain computations, prioritizing privacy and integrity.

Sectors reaping major benefits from cTRNG’s superior randomness encompass authentication systems, cryptographic protocols, on-chain transactions, and secure data exchanges. For Sign In with Ethereum (SIWE), this cosmic entropy fortifies nonces against prediction, thwarting replay attacks that could compromise user sessions or assets.

Authentication Flow Reference

Since this post delves into authentication specifics for added context especially for newcomers landing here first, we’ll outline the Orbitport auth process briefly. It builds on secure token management via Auth0, with server-side handling to safeguard credentials. For a deeper dive into the foundational setup, check our prior guide: Building with SpaceComputer Orbitport: A Guide to Cosmic Randomness in Web3. We’ve adapted that flow here for SIWE integration, ensuring seamless nonce generation.

Why Use Cosmic Randomness for SIWE Nonces?

SIWE (Sign In with Ethereum, per EIP-4361) lets users authenticate via wallet signatures on a structured message, including a nonce for freshness. But why cosmic randomness over local libraries or pseudo-random generators? Let’s unpack it, starting with vulnerabilities.

Traditional nonces from local RNGs (e.g., JavaScript’s Math.random() or even CSPRNGs like crypto.randomBytes) stem from deterministic seeds like system time or hardware noise. If an attacker predicts or compromises the seed via side-channel attacks or low-entropy environments , they can foresee nonces, forging signatures. Worse, repeating nonces enable replay attacks: an intercepted valid signature can be reused to impersonate users, hijacking sessions, draining wallets, or authorizing illicit transactions. For instance, if a nonce repeats, an attacker could replay a signature to log in repeatedly or trigger on-chain actions without consent.

Real-world parallels underscore the risk. The Ronin bridge hack (2022, ~$625M loss) exploited predictable behaviors in validator signatures, akin to weak RNGs allowing replay-like manipulations. In SIWE, a replay attack could escalate: imagine reusing a signature to approve token spends or execute smart contracts, leading to asset theft in DeFi apps.

Cosmic randomness counters this via cTRNG’s entropy from unpredictable cosmic rays and particle interactions in space , quantum-level chaos that’s inherently non-deterministic. Nonces become truly unpredictable, slashing replay vectors and boosting entropy to meet NIST standards. Plus, it’s verifiable: users can trace the orbital source for trust in Web3’s decentralized ethos.

This SIWE demo is a foundational example, but it’s the gateway to broader applications like nonces in on-chain signatures for transactions, multisig approvals, or zero-knowledge proofs. Replay attacks here could be catastrophic, enabling unauthorized fund transfers or governance manipulations. Upcoming demos will explore these, showing how cosmic nonces harden complex protocols against quantum threats.

The hybrid model shines: fetch cosmic seeds server-side from Orbitport, then process locally for flexibility. A local CSPRNG fallback ensures uptime, blending orbital integrity with resilience.

Technical Implementation

(Get your API access key here: https://spacecomputer.deform.cc/ctrngearlyaccess )

We’ll build a Cosmic SIWE app for wallet-based logins with cosmic nonces, runnable from the browser. Clone the demo branch:

git clone -b demo git@github.com:spacecomputer-io/cosmic-siwe.git

Frontend is pre-built; we’ll hone in on API integration and nonce logic for SIWE messages.

Before getting started, however, we will need to install the siwe package for the demo to work:

npm i siwe

Fetching Random Nonces

First, we need a server-side API endpoint that securely fetches a random value from Orbitport to use as a nonce. This endpoint acts as a proxy, using its own credentials to communicate with the Orbitport API, ensuring your access tokens are never exposed to the client.

Crucially, after fetching the cosmic nonce, the server stores it in a secure, HTTP-only cookie-based session using iron-session. This prevents tampering on the client side and allows our verification endpoint to retrieve the trusted nonce later. If the Orbitport API is unreachable, the endpoint gracefully falls back to a standard, cryptographically secure nonce using the siwe library, ensuring high availability.

Here is the complete code for src/app/api/nonce/route.ts:

// src/app/api/nonce/route.ts

import { getSession } from "@/lib/session";
import { NextRequest, NextResponse } from "next/server";
import { generateNonce } from "siwe";
import { getValidToken } from "@/lib/auth";
import { OrbitportSeedResponse } from "@/types/orbitport";

const ORBITPORT_API_URL = process.env.ORBITPORT_API_URL;

export async function GET(req: NextRequest) {
  let nonce: string;
  let usedFallback = false;

  try {
    // Try to get cosmic randomness first
    if (ORBITPORT_API_URL) {
      const res = NextResponse.next();
      const accessToken = await getValidToken(req, res);

      if (accessToken) {
        const response = await fetch(
          `${ORBITPORT_API_URL}/api/v1/services/trng`,
          {
            headers: {
              Authorization: `Bearer ${accessToken}`,
            },
          }
        );

        if (response.ok) {
          const data: OrbitportSeedResponse = await response.json();
          nonce = data.data; // Use cosmic randomness as nonce
          console.log("Generated cosmic nonce:", nonce);
        } else {
          throw new Error(`Orbitport API failed: ${response.status}`);
        }
      } else {
        throw new Error("Failed to get access token");
      }
    } else {
      throw new Error("Missing Orbitport API URL");
    }
  } catch (error) {
    console.warn("Using fallback nonce generation:", error);
    usedFallback = true;
    nonce = generateNonce(); // Fallback to standard nonce
    console.log("Generated fallback nonce:", nonce);
  }

  const session = await getSession();
  session.nonce = nonce;
  await session.save();

  return NextResponse.json({
    nonce: session.nonce,
    usedFallback,
  });
}

This securely grabs a cosmic nonce, falling back locally to a CSPRNG 96-bit nonce if needed, for SIWE message assembly.

Verifying the SIWE Signature

The verification endpoint ensures the signature is valid by checking it against the nonce stored in the user’s secure session. This prevents replay attacks by destroying the nonce after successful verification. We can inspect the src/api/verify/route.ts to understand the implementation:

// src/api/verify/route.ts

/* eslint-disable @typescript-eslint/no-explicit-any */
import { getSession } from "@/lib/session";
import { NextRequest, NextResponse } from "next/server";
import { SiweMessage } from "siwe";

export async function POST(req: NextRequest) {
  try {
    const { message, signature } = await req.json();

    if (!message || !signature) {
      return NextResponse.json(
        { ok: false, message: "Missing message or signature" },
        { status: 400 }
      );
    }

    const session = await getSession();

    if (!session.nonce) {
      return NextResponse.json(
        { ok: false, message: "No nonce found in session" },
        { status: 400 }
      );
    }

    const siweMessage = new SiweMessage(message);
    const { data: fields } = await siweMessage.verify({
      signature,
      nonce: session.nonce,
    });

    if (fields.nonce !== session.nonce) {
      return NextResponse.json(
        { ok: false, message: "Invalid nonce." },
        { status: 422 }
      );
    }

    session.destroy();
    return NextResponse.json({ ok: true });
  } catch (error) {
    console.error("Verification error:", error);

    // Try to get session for cleanup
    try {
      const session = await getSession();
      session.destroy();
    } catch (sessionError) {
      console.error("Error destroying session:", sessionError);
    }

    if (
      (error as any).error.type.includes(
        "Nonce does not match provided nonce for verification."
      )
    ) {
      return NextResponse.json(
        { ok: false, message: "Invalid nonce." },
        { status: 422 }
      );
    }

    return NextResponse.json(
      { ok: false, message: (error as Error).message || "Verification failed" },
      { status: 500 }
    );
  }
}

Frontend SIWE Message Generation Logic

With the nonce in hand and an API route to handle the verification of the signature, we can now focus on our frontend implementation. Crafting a SIWE message is easy with the help of the siwe package. All we need to do is fetch the nonce, construct a message using the library, and send it over to the wallet client to sign. These three steps can actually be combined into a single useSIWE hook to encapsulate the logic and help with and easier and cleaner implementation. Let’s look at src/hooks/useSIWE.ts :

//src/hooks/useSIWE.ts

import { useCallback, useState } from "react";
import { useAccount, useSignMessage } from "wagmi";
import { SiweMessage } from "siwe";

interface SIWEState {
  nonce: string | null;
  message: SiweMessage | null;
  signature: string | null;
  verificationStatus: string;
  isLoading: boolean;
  usedFallback: boolean;
}

export function useSIWE() {
  const { address, chainId, isConnected } = useAccount();
  const { signMessageAsync } = useSignMessage();
  const [state, setState] = useState<SIWEState>({
    nonce: null,
    message: null,
    signature: null,
    verificationStatus: "",
    isLoading: false,
    usedFallback: false,
  });

  const fetchNonce = useCallback(async () => {
    try {
      const response = await fetch(
        `/api/nonce`
      );
      if (!response.ok) {
        throw new Error("Failed to fetch nonce");
      }
      const data = await response.json();
      setState((prev) => ({
        ...prev,
        nonce: data.nonce,
        usedFallback: data.usedFallback || false,
      }));
      return data;
    } catch (error) {
      console.error("Error fetching nonce:", error);
      throw error;
    }
  }, []);

  const signMessageWithCustomNonce = useCallback(
    async (customNonce: string) => {
      if (!isConnected || !address || !chainId) {
        throw new Error("Missing required data for signing");
      }

      try {
        setState((prev) => ({ ...prev, isLoading: true }));

        const message = new SiweMessage({
          domain: window.location.host,
          address,
          statement:
            "Sign in with Ethereum using cosmic randomness for enhanced security.",
          uri: window.location.origin,
          version: "1",
          chainId,
          nonce: customNonce,
        });

        const preparedMessage = message.prepareMessage();
        const signature = await signMessageAsync({ message: preparedMessage });

        setState((prev) => ({
          ...prev,
          message,
          signature,
          isLoading: false,
        }));

        return { message: preparedMessage, signature };
      } catch (error) {
        setState((prev) => ({ ...prev, isLoading: false }));
        console.error("Error signing message:", error);
        throw error;
      }
    },
    [isConnected, address, chainId, signMessageAsync]
  );

  const signMessage = useCallback(async () => {
    if (!state.nonce) {
      throw new Error("Missing required data for signing");
    }

    return signMessageWithCustomNonce(state.nonce);
  }, [state.nonce, signMessageWithCustomNonce]);

  const verifySignature = useCallback(async () => {
    if (!state.message || !state.signature) {
      throw new Error("No message or signature to verify");
    }

    try {
      setState((prev) => ({
        ...prev,
        isLoading: true,
        verificationStatus: "Verifying...",
      }));

      const response = await fetch(
        `/api/verify`,
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            message: state.message.prepareMessage(),
            signature: state.signature,
          }),
        }
      );

      const data = await response.json();

      if (response.ok && data.ok) {
        setState((prev) => ({
          ...prev,
          verificationStatus: "Success!",
          isLoading: false,
        }));
        return true;
      } else {
        const errorMessage = data.message || "Verification failed";
        setState((prev) => ({
          ...prev,
          verificationStatus: `Failed: ${errorMessage}`,
          isLoading: false,
        }));
        return false;
      }
    } catch (error) {
      const errorMessage =
        error instanceof Error ? error.message : "Verification failed";
      setState((prev) => ({
        ...prev,
        verificationStatus: `Failed: ${errorMessage}`,
        isLoading: false,
      }));
      throw error;
    }
  }, [state.message, state.signature]);

  const reset = useCallback(() => {
    setState({
      nonce: null,
      message: null,
      signature: null,
      verificationStatus: "",
      isLoading: false,
      usedFallback: false,
    });
  }, []);

  return {
    ...state,
    fetchNonce,
    signMessage,
    signMessageWithCustomNonce,
    verifySignature,
    reset,
    isConnected,
    address,
    chainId,
  };
}

This builds a verifiable SIWE message, ready for wallet signing. Creating a hook allows tweaks like custom statements or expiry in a centralized location while keeping our code DRY.

Implementing in our app

With our backend and frontend hooks ready, it now becomes extremely straightforward to implement anywhere within our application. Since our useSIWE hook manages nonce fetching, message creation, loading, and more, we can just use it like below. Here is an example implementation:

 const {
    nonce,
    signature,
    verificationStatus,
    isLoading,
    usedFallback,
    fetchNonce,
    signMessage,
    signMessageWithCustomNonce,
    verifySignature,
    reset,
    isConnected,
    address,
    chainId,
  } = useSIWE();

  // 1. First step to fetch nonce
  const handleFetching = async () => {
    if (!isConnected || !address || !chainId) return;
    // fetch 
    await fetchNonce();
  }

  // 2. Once nonce is fetched, sign a message using it
  useEffect(() => {
    // sign message
    const handleSigning = async () => {
      await signMessage();
    }

    if (nonce) {
      handleSigning();
    }
  }, [nonce]);

  // 3. After signature is generated, we can verify it
  const handleVerify = async () => {
    if (isLoading) return;
    
    // verify signature
    await verifySignature();
  }

The frontend implementation has already been handled, but you can view it in src/app/page.tsx to learn how its been done. We’ve even included two flows for you to experience, one with the correct SIWE nonce flow and another one that simulates a mismatch.

Now, run npm run dev, visit http://localhost:3000, connect your wallet, and authenticate with a cosmic-secured SIWE flow!

This demo includes a step-by-step walkthrough in our frontend that breaks down the steps behind the scenes when performing the SIWE flows. You can inspect the nonce in the SIWE prompt of your wallet before signing the message.

Conclusion

Cosmic SIWE transcends basic authentication. It’s a showcase of space tech fortifying Web3 against replay and RNG exploits. By infusing Orbitport’s cosmic randomness, it achieves unparalleled nonce security, outpacing earthbound methods.

SpaceComputer Orbitport supplies a dependable, truly random oracle via satellites, with built-in tamper resistance making it vital for high-stakes dApps demanding ironclad integrity. As we push orbital infrastructure frontiers, anticipate more breakthroughs in secure, space-enhanced protocols.

Resources