attestation

package module
v0.1.14 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 20, 2026 License: MIT Imports: 8 Imported by: 0

README

📱 device-attestation

A Go library for verifying device attestations from iOS (App Attest) and Android (Play Integrity).

This library allows server-side verification of device authenticity to ensure requests are coming from legitimate, unmodified apps running on genuine devices.

Features

  • iOS App Attest: Full attestation and assertion verification
  • Android Play Integrity: Token verification via Google's official API
  • Challenge Management: Cryptographically secure challenge generation with expiration
  • Key Storage: Interface for persisting iOS attestation public keys
  • Replay Protection: Counter-based assertion replay detection for iOS
  • Configurable Security: Adjustable integrity requirements for Android

Installation

go get github.com/kacy/device-attestation

Quick Start

The simplest way to get started is with the Server type, which handles challenge management and key storage automatically:

package main

import (
    "context"
    "log"

    attestation "github.com/kacy/device-attestation"
)

func main() {
    // Create an attestation server (batteries included)
    server, err := attestation.NewServer(attestation.ServerConfig{
        IOS: &attestation.IOSConfig{
            BundleIDs: []string{"com.example.myapp"},
            TeamID:    "ABCD123456",
        },
        Android: &attestation.AndroidConfig{
            PackageNames: []string{"com.example.myapp"},
            GCPProjectID: "my-gcp-project",
        },
    })
    if err != nil {
        log.Fatal(err)
    }
    defer server.Close()

    // 1. Generate a challenge for the client
    challenge, _ := server.GenerateChallenge("user-123")
    // Send challenge to client...

    // 2. Verify the attestation from the client
    result, err := server.VerifyAttestation(context.Background(), "user-123", attestation.VerifyRequest{
        Platform:    attestation.PlatformIOS,
        Attestation: "<base64-attestation-from-client>",
        Challenge:   challenge,
        KeyID:       "<key-id-from-client>",
        BundleID:    "com.example.myapp",
    })
    if err != nil {
        log.Printf("Attestation failed: %v", err)
        return
    }

    log.Printf("Device verified: %s", result.DeviceID)
}

Advanced Usage

For more control, you can use the lower-level Verifier API with custom challenge and key stores:

import (
    attestation "github.com/kacy/device-attestation"
    "github.com/kacy/device-attestation/challenge"
    "github.com/kacy/device-attestation/ios"
)

// Create custom stores
challenges := challenge.NewMemoryStore(challenge.Config{Timeout: 5 * time.Minute})
keyStore := ios.NewMemoryKeyStore()

// Create verifier with full control
verifier, _ := attestation.NewVerifier(attestation.Config{
    IOSBundleIDs:           []string{"com.example.myapp"},
    IOSTeamID:              "ABCD123456",
    AndroidPackageNames:    []string{"com.example.myapp"},
    GCPProjectID:           "my-project",
    KeyStore:               keyStore,
    RequireStrongIntegrity: true,
})

// Manual challenge management
challenge, _ := challenges.Generate("user-123")
// ... send to client ...
if !challenges.Validate("user-123", clientChallenge) {
    // Invalid challenge
}

// Verify with the low-level API
result, err := verifier.Verify(ctx, &attestation.Request{...})

iOS App Attest

How It Works
  1. Client generates a key pair using DCAppAttestService.generateKey()
  2. Server generates a challenge and sends it to the client
  3. Client creates attestation using DCAppAttestService.attestKey()
  4. Server verifies attestation using this library
  5. Server stores the public key for future assertion verification
  6. Subsequent requests use assertions signed with the attested key
Attestation Verification
import "github.com/kacy/device-attestation/ios"

verifier, _ := ios.NewVerifier(ios.Config{
    BundleIDs: []string{"com.example.myapp"},
    TeamID:    "ABCD123456",
    KeyStore:  ios.NewMemoryKeyStore(), // Required for assertion verification
})

result, err := verifier.VerifyAttestation(ctx, &ios.AttestationRequest{
    Attestation: attestationBase64,
    Challenge:   serverChallenge,
    KeyID:       keyID,
    BundleID:    "com.example.myapp",
})
Assertion Verification

After initial attestation, use assertions to verify subsequent requests:

result, err := verifier.VerifyAssertion(ctx, &ios.AssertionRequest{
    Assertion:  assertionBase64,
    ClientData: []byte("request-specific-data"),
    KeyID:      keyID,
    BundleID:   "com.example.myapp",
})

The library automatically:

  • Retrieves the stored public key
  • Verifies the signature
  • Checks the counter to prevent replay attacks
  • Updates the counter on success

Android Play Integrity

Prerequisites
  1. Enable the Play Integrity API in Google Cloud Console
  2. Link your app in Google Play Console
  3. Create a service account with Play Integrity API access
Verification
import "github.com/kacy/device-attestation/android"

verifier, _ := android.NewVerifier(android.Config{
    PackageNames:       []string{"com.example.myapp"},
    GCPProjectID:       "my-project",
    GCPCredentialsFile: "/path/to/credentials.json", // Optional, uses ADC if empty

    // Optional: APK signing certificate SHA-256 digests
    APKCertDigests: []string{"AA:BB:CC:..."},

    // Security requirements
    RequireStrongIntegrity: false, // Require hardware-backed attestation
    AllowBasicIntegrity:    false, // Allow potentially rooted devices
})

result, err := verifier.Verify(ctx, &android.Request{
    IntegrityToken: tokenFromClient,
    Challenge:      serverChallenge,
})
Device Integrity Levels
Verdict Meaning
MEETS_STRONG_INTEGRITY Genuine device with hardware-backed security
MEETS_DEVICE_INTEGRITY Genuine device with Google Play services
MEETS_BASIC_INTEGRITY Device may be rooted or running custom ROM

Challenge Store

The challenge store generates cryptographically secure challenges and handles expiration:

import "github.com/kacy/device-attestation/challenge"

store := challenge.NewMemoryStore(challenge.Config{
    Timeout:         5 * time.Minute,  // Challenge validity period
    CleanupInterval: 1 * time.Minute,  // Expired challenge cleanup interval
    ChallengeBytes:  32,               // Random bytes in challenge
})
defer store.Close()

// Generate a challenge for a user
ch, _ := store.Generate("user-123")

// Validate (consumes the challenge on success)
valid := store.Validate("user-123", ch)

Redis (Distributed Deployments)

For distributed systems where multiple server instances need to share state, use the Redis-backed stores:

import (
    "github.com/redis/go-redis/v9"
    attestredis "github.com/kacy/device-attestation/redis"
)

// Create your Redis client (you control the connection)
rdb := redis.NewClient(&redis.Options{
    Addr:     "localhost:6379",
    Password: "",
    DB:       0,
})

// Create Redis-backed challenge store
challenges, _ := attestredis.NewChallengeStore(attestredis.ChallengeStoreConfig{
    Client:    rdb,
    KeyPrefix: "myapp:challenge:",  // Optional, default: "attest:challenge:"
    Timeout:   5 * time.Minute,
})

// Create Redis-backed key store
keyStore, _ := attestredis.NewKeyStore(attestredis.KeyStoreConfig{
    Client:    rdb,
    KeyPrefix: "myapp:key:",  // Optional, default: "attest:key:"
    TTL:       0,             // 0 = no expiration
})

// Use with the advanced API
verifier, _ := attestation.NewVerifier(attestation.Config{
    IOSBundleIDs: []string{"com.example.app"},
    IOSTeamID:    "TEAM123",
    KeyStore:     keyStore,
})

The Redis package defines a Cmdable interface compatible with github.com/redis/go-redis/v9, so you can pass in a *redis.Client, *redis.ClusterClient, or any compatible client.

Key Storage (iOS)

The KeyStore is used for iOS assertion verification. Here's why it exists:

  1. Attestation (one-time): Device proves it's genuine, server extracts the public key
  2. Assertion (ongoing): Device signs requests with its private key, server verifies using the stored public key

The KeyStore persists the public key and tracks a counter to prevent replay attacks.

When You Don't Need a KeyStore

If you're only using attestation as a one-time device check (e.g., during account registration) and don't need ongoing assertion verification, you can skip the KeyStore:

// Attestation-only mode
server, _ := attestation.NewServer(attestation.ServerConfig{
    IOS: &attestation.IOSConfig{
        BundleIDs: []string{"com.example.app"},
        TeamID:    "TEAM123",
    },
    // No KeyStore configured - attestation works, assertions won't
})
When You Need a KeyStore

If you want to verify assertions on subsequent requests (recommended for ongoing API security):

// Full attestation + assertion support
verifier, _ := attestation.NewVerifier(attestation.Config{
    IOSBundleIDs: []string{"com.example.app"},
    IOSTeamID:    "TEAM123",
    KeyStore:     ios.NewMemoryKeyStore(), // or Redis for distributed
})
KeyStore Interface
type KeyStore interface {
    Store(ctx context.Context, keyID string, key *StoredKey) error
    Load(ctx context.Context, keyID string) (*StoredKey, error)
    Delete(ctx context.Context, keyID string) error
    IncrementCounter(ctx context.Context, keyID string) (uint32, error)
}
In-Memory (Development/Single Instance)

Suitable for development, testing, or single-server deployments:

keyStore := ios.NewMemoryKeyStore()

Note: Data is lost on server restart. For production single-instance deployments, consider Redis or a database.

Redis (Distributed)

For multi-instance deployments where servers need to share state:

keyStore, _ := attestredis.NewKeyStore(attestredis.KeyStoreConfig{
    Client: redisClient,
})
Custom Implementation

You can implement the interface with any backend (PostgreSQL, DynamoDB, etc.):

type MyKeyStore struct {
    // your fields
}

func (s *MyKeyStore) Store(ctx context.Context, keyID string, key *ios.StoredKey) error {
    // your implementation
}
// ... implement other methods

Configuration Reference

Main Verifier Config
Field Type Description
IOSBundleIDs []string Allowed iOS bundle identifiers
IOSTeamID string Apple Developer Team ID
AndroidPackageNames []string Allowed Android package names
AndroidAPKCertDigests []string APK signing certificate SHA-256 digests
GCPProjectID string Google Cloud project ID
GCPCredentialsFile string Path to service account JSON (optional)
ChallengeTimeout time.Duration Maximum challenge age (default: 5m)
RequireStrongIntegrity bool Require Android strong integrity
KeyStore ios.KeyStore Storage for iOS public keys

Error Handling

The library returns typed errors for different failure cases:

import attestation "github.com/kacy/device-attestation"

result, err := verifier.Verify(ctx, req)
if err != nil {
    switch {
    case errors.Is(err, attestation.ErrInvalidAttestation):
        // Malformed attestation data
    case errors.Is(err, attestation.ErrVerificationFailed):
        // Cryptographic verification failed
    case errors.Is(err, attestation.ErrInvalidBundleID):
        // Bundle ID not in allowed list
    case errors.Is(err, attestation.ErrDeviceCompromised):
        // Android device integrity check failed
    case errors.Is(err, attestation.ErrAppNotRecognized):
        // App not recognized by Play Store
    default:
        // Other error
    }
}

Security Considerations

  1. Always use HTTPS for transmitting attestation data
  2. Generate unique challenges per attestation request
  3. Set appropriate timeouts for challenges (recommended: 1-5 minutes)
  4. Store iOS public keys securely with proper access controls
  5. Monitor attestation failures for potential abuse patterns
  6. Consider rate limiting attestation endpoints

Client-Side Implementation

iOS (Swift)
import DeviceCheck

let service = DCAppAttestService.shared

// Generate key
service.generateKey { keyId, error in
    guard let keyId = keyId else { return }
    
    // Get challenge from server, then attest
    let challenge = Data(challengeString.utf8)
    let hash = SHA256.hash(data: challenge)
    
    service.attestKey(keyId, clientDataHash: Data(hash)) { attestation, error in
        // Send attestation to server
    }
}
Android (Kotlin)
val integrityManager = IntegrityManagerFactory.create(context)

val request = IntegrityTokenRequest.builder()
    .setNonce(challengeFromServer)
    .build()

integrityManager.requestIntegrityToken(request)
    .addOnSuccessListener { response ->
        val token = response.token()
        // Send token to server
    }

License

MIT License - see LICENSE for details.

Contributing

Contributions are welcome! Please open an issue or submit a pull request.

Documentation

Overview

Package attestation provides device attestation verification for iOS App Attest and Android Play Integrity.

This library allows server-side verification of device authenticity to ensure requests are coming from legitimate, unmodified apps running on genuine devices.

iOS App Attest

Verifies attestations and assertions from Apple's App Attest service. See: https://developer.apple.com/documentation/devicecheck/establishing_your_app_s_integrity

Android Play Integrity

Verifies integrity tokens from Google's Play Integrity API. See: https://developer.android.com/google/play/integrity

Basic Usage

verifier, err := attestation.NewVerifier(attestation.Config{
    IOSBundleIDs: []string{"com.example.app"},
    IOSTeamID:    "TEAM123456",
    AndroidPackageNames: []string{"com.example.app"},
    GCPProjectID: "my-project",
})

result, err := verifier.Verify(ctx, &attestation.Request{
    Platform:    attestation.PlatformIOS,
    Attestation: attestationData,
    Challenge:   challenge,
    KeyID:       keyID,
    BundleID:    "com.example.app",
})

Package attestation provides device attestation verification for iOS App Attest and Android Play Integrity.

This library allows server-side verification of device authenticity to ensure requests are coming from legitimate, unmodified apps running on genuine devices.

iOS App Attest

Verifies attestations and assertions from Apple's App Attest service. See: https://developer.apple.com/documentation/devicecheck/establishing_your_app_s_integrity

Android Play Integrity

Verifies integrity tokens from Google's Play Integrity API. See: https://developer.android.com/google/play/integrity

Basic Usage

verifier, err := attestation.NewVerifier(attestation.Config{
    IOSBundleIDs: []string{"com.example.app"},
    IOSTeamID:    "TEAM123456",
    AndroidPackageNames: []string{"com.example.app"},
    GCPProjectID: "my-project",
})
if err != nil {
    log.Fatal(err)
}

result, err := verifier.Verify(ctx, &attestation.Request{
    Platform:    attestation.PlatformIOS,
    Attestation: attestationData,
    Challenge:   challenge,
    KeyID:       keyID,
    BundleID:    "com.example.app",
})

Subpackages

The library is organized into the following subpackages:

  • ios: iOS App Attest verification (attestation and assertion)
  • android: Android Play Integrity verification
  • challenge: Secure challenge generation and validation

For more details, see the README at https://github.com/kacy/device-attestation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidAttestation  = errors.New("invalid attestation")
	ErrAttestationExpired  = errors.New("attestation expired")
	ErrUnsupportedPlatform = errors.New("unsupported platform")
	ErrVerificationFailed  = errors.New("attestation verification failed")
	ErrMissingAttestation  = errors.New("missing attestation data")
	ErrInvalidBundleID     = errors.New("invalid bundle ID")
	ErrInvalidKeyID        = errors.New("invalid key ID")
	ErrInvalidChallenge    = errors.New("invalid challenge")
	ErrInvalidPackageName  = errors.New("invalid package name")
	ErrDeviceCompromised   = errors.New("device integrity check failed")
	ErrAppNotRecognized    = errors.New("app not recognized")
	ErrNotConfigured       = errors.New("platform not configured")
)

Common errors returned by the attestation package.

Functions

This section is empty.

Types

type AndroidConfig

type AndroidConfig struct {
	// PackageNames is the list of allowed app package names (required).
	PackageNames []string

	// GCPProjectID is your Google Cloud project ID (required).
	GCPProjectID string

	// GCPCredentialsFile is the path to service account credentials (optional).
	// If empty, uses Application Default Credentials.
	GCPCredentialsFile string

	// APKCertDigests is the list of allowed APK signing certificate SHA-256 digests (optional).
	APKCertDigests []string

	// RequireStrongIntegrity requires hardware-backed attestation (default: false).
	RequireStrongIntegrity bool
}

AndroidConfig holds Android-specific configuration.

type AssertionRequest

type AssertionRequest struct {
	// Assertion is the base64-encoded assertion from the device.
	Assertion string

	// ClientData is the request-specific data that was signed.
	ClientData []byte

	// KeyID is the key identifier from the original attestation.
	KeyID string

	// BundleID is the app bundle identifier.
	BundleID string
}

AssertionRequest contains the assertion data from the client.

type Config

type Config struct {
	// iOS App Attest configuration
	IOSBundleIDs []string
	IOSTeamID    string

	// Android Play Integrity configuration
	AndroidPackageNames   []string
	AndroidAPKCertDigests []string
	GCPProjectID          string
	GCPCredentialsFile    string

	// Shared configuration
	ChallengeTimeout time.Duration
	HTTPClient       *http.Client

	// Device integrity requirements
	// When true, Android requires MEETS_STRONG_INTEGRITY verdict
	RequireStrongIntegrity bool

	// KeyStore for storing iOS attestation public keys (optional).
	// Required for assertion verification.
	KeyStore ios.KeyStore

	// SkipCertificateVerification skips the certificate chain verification for iOS.
	// WARNING: Only use this for development/testing. Never in production!
	SkipCertificateVerification bool
}

Config holds configuration for the attestation verifier.

type IOSConfig

type IOSConfig struct {
	// BundleIDs is the list of allowed app bundle identifiers (required).
	BundleIDs []string

	// TeamID is your Apple Developer Team ID (required).
	TeamID string
}

IOSConfig holds iOS-specific configuration.

type Platform

type Platform string

Platform represents the mobile platform.

const (
	PlatformIOS     Platform = "ios"
	PlatformAndroid Platform = "android"
)

Platform constants for iOS and Android.

type Request

type Request struct {
	// Platform is the mobile platform (ios or android).
	Platform Platform

	// Attestation is the base64-encoded attestation data.
	// For iOS: the attestation object from DCAppAttestService.attestKey
	// For Android: the integrity token from Play Integrity API
	Attestation string

	// Challenge is the server-generated challenge that was signed.
	Challenge string

	// KeyID is the key identifier (iOS only).
	// This is the key ID returned by DCAppAttestService.generateKey
	KeyID string

	// BundleID is the app bundle identifier (iOS only).
	BundleID string
}

Request represents an attestation or assertion verification request.

type Result

type Result struct {
	// Valid indicates whether the attestation was successfully verified.
	Valid bool

	// DeviceID is a unique identifier for the device/key.
	// For iOS: the key ID
	// For Android: derived from the nonce
	DeviceID string

	// Platform is the verified platform.
	Platform Platform

	// Timestamp is when the verification was performed.
	Timestamp time.Time
}

Result represents the result of attestation verification.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server provides a batteries-included attestation server that handles challenge generation, validation, and key storage automatically.

This is the recommended way to use the library for most use cases. For advanced customization, use NewVerifier directly with your own challenge store and key store implementations.

func NewServer

func NewServer(cfg ServerConfig) (*Server, error)

NewServer creates a new attestation server with sensible defaults.

Example:

server, err := attestation.NewServer(attestation.ServerConfig{
    IOS: &attestation.IOSConfig{
        BundleIDs: []string{"com.example.app"},
        TeamID:    "ABCD123456",
    },
    Android: &attestation.AndroidConfig{
        PackageNames: []string{"com.example.app"},
        GCPProjectID: "my-project",
    },
})

func (*Server) Challenges

func (s *Server) Challenges() challenge.Store

Challenges returns the underlying challenge store for advanced use cases.

func (*Server) Close

func (s *Server) Close() error

Close releases resources used by the server.

func (*Server) GenerateChallenge

func (s *Server) GenerateChallenge(identifier string) (string, error)

GenerateChallenge creates a new challenge for the given identifier. The identifier should be unique per attestation flow (e.g., user ID, session ID).

Returns the challenge string that should be sent to the client.

func (*Server) KeyStore

func (s *Server) KeyStore() ios.KeyStore

KeyStore returns the underlying key store for advanced use cases.

func (*Server) Verifier

func (s *Server) Verifier() Verifier

Verifier returns the underlying verifier for advanced use cases.

func (*Server) VerifyAssertion

func (s *Server) VerifyAssertion(ctx context.Context, req AssertionRequest) (*Result, error)

VerifyAssertion verifies an iOS assertion for subsequent requests.

This requires a previous successful attestation for the given KeyID. The assertion counter is automatically tracked to prevent replay attacks.

func (*Server) VerifyAttestation

func (s *Server) VerifyAttestation(ctx context.Context, identifier string, req VerifyRequest) (*Result, error)

VerifyAttestation verifies a device attestation.

The identifier must match the one used in GenerateChallenge. On success, the challenge is consumed and cannot be reused.

Example:

result, err := server.VerifyAttestation(ctx, "user-123", attestation.VerifyRequest{
    Platform:    attestation.PlatformIOS,
    Attestation: attestationFromClient,
    Challenge:   challengeFromClient,
    KeyID:       keyIDFromClient,
    BundleID:    "com.example.app",
})

type ServerConfig

type ServerConfig struct {
	// iOS configuration (optional - omit to disable iOS support)
	IOS *IOSConfig

	// Android configuration (optional - omit to disable Android support)
	Android *AndroidConfig

	// ChallengeTimeout is how long challenges remain valid (default: 5 minutes).
	ChallengeTimeout time.Duration
}

ServerConfig holds configuration for the attestation server.

type Verifier

type Verifier interface {
	// Verify verifies an attestation request.
	Verify(ctx context.Context, req *Request) (*Result, error)

	// VerifyAssertion verifies an iOS assertion (requires KeyStore).
	VerifyAssertion(ctx context.Context, req *ios.AssertionRequest) (*Result, error)
}

Verifier verifies device attestations.

func NewVerifier

func NewVerifier(cfg Config) (Verifier, error)

NewVerifier creates a new attestation verifier.

type VerifyRequest

type VerifyRequest struct {
	// Platform is the device platform (PlatformIOS or PlatformAndroid).
	Platform Platform

	// Attestation is the base64-encoded attestation data from the device.
	Attestation string

	// Challenge is the challenge that was sent to the client.
	Challenge string

	// KeyID is the key identifier (iOS only).
	KeyID string

	// BundleID is the app bundle identifier (iOS only).
	BundleID string
}

VerifyRequest contains the attestation data from the client.

Directories

Path Synopsis
Package android provides Android Play Integrity verification.
Package android provides Android Play Integrity verification.
Package challenge provides secure challenge generation and validation for device attestation flows.
Package challenge provides secure challenge generation and validation for device attestation flows.
examples
basic command
Example: Basic device attestation verification server
Example: Basic device attestation verification server
Package ios provides iOS App Attest verification.
Package ios provides iOS App Attest verification.
Package redis provides Redis-backed implementations of the challenge store and key store interfaces for distributed deployments.
Package redis provides Redis-backed implementations of the challenge store and key store interfaces for distributed deployments.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL