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.
- 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
go get github.com/kacy/device-attestationThe 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)
}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{...})- Client generates a key pair using
DCAppAttestService.generateKey() - Server generates a challenge and sends it to the client
- Client creates attestation using
DCAppAttestService.attestKey() - Server verifies attestation using this library
- Server stores the public key for future assertion verification
- Subsequent requests use assertions signed with the attested key
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",
})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
- Enable the Play Integrity API in Google Cloud Console
- Link your app in Google Play Console
- Create a service account with Play Integrity API access
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,
})| 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 |
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)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.
The KeyStore is used for iOS assertion verification. Here's why it exists:
- Attestation (one-time): Device proves it's genuine, server extracts the public key
- 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.
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
})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
})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)
}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.
For multi-instance deployments where servers need to share state:
keyStore, _ := attestredis.NewKeyStore(attestredis.KeyStoreConfig{
Client: redisClient,
})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| 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 |
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
}
}- Always use HTTPS for transmitting attestation data
- Generate unique challenges per attestation request
- Set appropriate timeouts for challenges (recommended: 1-5 minutes)
- Store iOS public keys securely with proper access controls
- Monitor attestation failures for potential abuse patterns
- Consider rate limiting attestation endpoints
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
}
}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
}MIT License - see LICENSE for details.
Contributions are welcome! Please open an issue or submit a pull request.