Challenge Generation API

Challenge Generation

Understand how Grantiva generates and manages cryptographic challenges for secure attestation.

Overview

Challenges are cryptographically secure random values used to prevent replay attacks during the attestation process. Each challenge is unique and can only be used once within a specific time window.

Important Note

With Grantiva's SDK, challenge generation is handled automatically. You don't need to manually request challenges - the SDK manages this for you. This documentation is provided for transparency and custom implementations.

How Challenges Work

  1. Generation: A cryptographically secure random value is generated
  2. Storage: The challenge is stored with an expiration timestamp
  3. Usage: The client includes the challenge in the attestation request
  4. Validation: The server verifies the challenge hasn't been used and isn't expired
  5. Invalidation: Once used, the challenge is marked as consumed

Challenge API Endpoint

GET /api/v1/attestation/challenge

Generate a new challenge for attestation. No authentication required as tenant identification happens during attestation validation.

Request

curl https://api.grantiva.com/api/v1/attestation/challenge

Response

{
  "challenge": "YmFzZTY0X2VuY29kZWRfcmFuZG9tX2RhdGE=",
  "expiresAt": "2024-01-15T12:05:00Z",
  "expiresIn": 300
}

Response Fields

  • challenge - Base64 encoded random data (32 bytes)
  • expiresAt - ISO 8601 timestamp when challenge expires
  • expiresIn - Seconds until expiration (default: 300)

SDK Implementation

The Grantiva SDK handles challenge generation automatically. Here's what happens behind the scenes:

// This is handled internally by the SDK
class GrantivaInternal {
    func generateChallenge() async throws -> Challenge {
        let url = URL(string: "https://api.grantiva.com/api/v1/attestation/challenge")!
        let (data, _) = try await URLSession.shared.data(from: url)
        
        return try JSONDecoder().decode(Challenge.self, from: data)
    }
    
    func attestWithChallenge(_ challenge: String) async throws -> Data {
        let service = DCAppAttestService.shared
        let keyId = try await service.generateKey()
        
        // Create client data hash from challenge
        let clientData = Data(challenge.utf8)
        let hash = SHA256.hash(data: clientData)
        
        // Generate attestation
        return try await service.attestKey(keyId, clientDataHash: hash)
    }
}

Challenge Security

Cryptographic Properties

  • Randomness: 256 bits of cryptographically secure random data
  • Uniqueness: Guaranteed unique across all tenants
  • Single-use: Each challenge can only be used once
  • Time-bound: Expires after 5 minutes by default

Replay Attack Prevention

// Server-side validation
async function validateChallenge(challenge: string, tenantId: string) {
  // Check if challenge exists and hasn't been used
  const storedChallenge = await db.challenges.findOne({
    value: challenge,
    tenantId: tenantId,
    used: false,
    expiresAt: { $gt: new Date() }
  });
  
  if (!storedChallenge) {
    throw new Error('Invalid or expired challenge');
  }
  
  // Mark as used
  await db.challenges.updateOne(
    { _id: storedChallenge._id },
    { $set: { used: true, usedAt: new Date() } }
  );
  
  return true;
}

Custom Implementation

If you're building a custom client, here's how to properly use challenges:

// Custom attestation implementation
class CustomAttestationClient {
    let apiBase = "https://api.grantiva.com/api/v1"
    
    func performAttestation() async throws -> AttestationResult {
        // Step 1: Get challenge
        let challengeURL = URL(string: "\(apiBase)/attestation/challenge")!
        let (challengeData, _) = try await URLSession.shared.data(from: challengeURL)
        let challenge = try JSONDecoder().decode(ChallengeResponse.self, from: challengeData)
        
        // Step 2: Generate attestation with challenge
        let service = DCAppAttestService.shared
        let keyId = try await service.generateKey()
        
        // Create hash of challenge
        let clientData = Data(challenge.challenge.utf8)
        let hash = SHA256.hash(data: clientData)
        
        // Generate attestation
        let attestation = try await service.attestKey(keyId, clientDataHash: hash)
        
        // Step 3: Submit attestation for validation
        var request = URLRequest(url: URL(string: "\(apiBase)/attestation/validate")!)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        let payload = AttestationRequest(
            keyId: keyId,
            attestation: attestation.base64EncodedString(),
            challenge: challenge.challenge
        )
        
        request.httpBody = try JSONEncoder().encode(payload)
        
        let (resultData, _) = try await URLSession.shared.data(for: request)
        return try JSONDecoder().decode(AttestationResult.self, from: resultData)
    }
}

Challenge Configuration

Challenge behavior can be customized per tenant (Enterprise tier and above):

Setting Default Range
Challenge Expiration 300 seconds 60-3600 seconds
Challenge Length 32 bytes 16-64 bytes
Rate Limiting 100/minute 10-1000/minute

Error Handling

// 429 Too Many Requests
{
  "error": "rate_limit_exceeded",
  "message": "Challenge generation rate limit exceeded",
  "retryAfter": 60
}

// 503 Service Unavailable
{
  "error": "service_unavailable",
  "message": "Challenge generation temporarily unavailable"
}

Best Practices

  • Use the SDK's automatic challenge handling when possible
  • Don't cache or reuse challenges
  • Complete attestation promptly after challenge generation
  • Implement exponential backoff for retries
  • Monitor challenge expiration in your error logs

Next Steps