iOS: Registration
Register a Passkey with the Service and attest the knowledge of a an Algorand keypair. This creates a new credential that can be used for future authentication with the Liquid Auth service.
Who is this for?
iOS Wallet developers who want to enable their users to authenticate with dApps using Liquid Auth, and to setup the WebRTC connection.
This is specifically for Liquid URIs/QR Codes - provided by the Liquid Auth backend.
To handle FIDO:/
URIs/QR Codes, refer to the Autofill Credential Extension.
Overview
The registration (attestation) process involves:
- Requesting attestation options from the Liquid Auth service
- Creating a P256 key pair (deterministically or randomly)
- Signing the challenge with your Algorand Ed25519 key, to prove ownership
- Building the WebAuthn attestation response
- Submitting the credential to complete registration
Implementation
Complete Registration Function
import AuthenticationServicesimport CryptoKitimport Foundationimport SwiftCBORimport LiquidAuthSDKimport deterministicP256_swift
func registration( origin: String, requestId: String, algorandAddress: String, p256KeyPair: P256.Signing.PrivateKey, userAgent: String, device: String) async throws -> LiquidAuthResult { let attestationApi = AttestationApi()
// Step 1: Prepare attestation options let options: [String: Any] = [ "username": algorandAddress, "displayName": "Liquid Auth User", "authenticatorSelection": ["userVerification": "required"], "extensions": ["liquid": true], // Enable Liquid extension ]
// Step 2: Request attestation options from service let (data, sessionCookie) = try await attestationApi.postAttestationOptions( origin: origin, userAgent: userAgent, options: options )
// Step 3: Parse server response guard let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], let challengeBase64Url = json["challenge"] as? String, let rp = json["rp"] as? [String: Any], let rpId = rp["id"] as? String else { throw LiquidAuthError.invalidJSON("Missing required fields in attestation options response") }
// Validate origin matches rpId if origin != rpId { print("⚠️ Origin (\(origin)) and rpId (\(rpId)) are different.") }
// Step 4: Decode and sign the challenge let challengeBytes = Data([UInt8](Utility.decodeBase64Url(challengeBase64Url)!))
// INTEGRATION POINT: Sign with your Algorand Ed25519 private key let signature = try signChallenge(challengeBytes, with: yourAlgorandPrivateKey) // <-- Implement your own.
// Step 5: Create Liquid extension let liquidExt = [ "type": "algorand", "requestId": requestId, "address": algorandAddress, "signature": signature.base64URLEncodedString(), "device": device, ]
// Step 6: Build WebAuthn credential let credential = try buildAttestationCredential( challengeBase64Url: challengeBase64Url, rpId: rpId, p256KeyPair: p256KeyPair )
// Step 7: Submit credential to service let responseData = try await attestationApi.postAttestationResult( origin: origin, userAgent: userAgent, credential: credential, liquidExt: liquidExt, device: device )
// Step 8: Handle response return try parseRegistrationResponse(responseData) // Check for errors, return success if none.}
If everything went well, you have now registered a passkey, proved ownership of the Algorand address, and authenticated the requestId (UUID).
If you wish, you can now proceed with the requestId and use it to setup a WebRTC-based P2P communication channel.
Building the Attestation Credential
private func buildAttestationCredential( challengeBase64Url: String, rpId: String, p256KeyPair: P256.Signing.PrivateKey) throws -> [String: Any] {
// Deterministic credential ID from P256 public key let rawId = Data([UInt8](Utility.hashSHA256(p256KeyPair.publicKey.rawRepresentation)))
// Create clientDataJSON let clientData: [String: Any] = [ "type": "webauthn.create", "challenge": challengeBase64Url, "origin": "https://\(rpId)", ]
guard let clientDataJSONData = try? JSONSerialization.data(withJSONObject: clientData, options: []) else { throw LiquidAuthError.invalidJSON("Failed to serialize client data") }
let clientDataJSONBase64Url = clientDataJSONData.base64URLEncodedString()
// Create attestation object let attestedCredData = Utility.getAttestedCredentialData( aaguid: UUID(uuidString: "1F59713A-C021-4E63-9158-2CC5FDC14E52")!, // Your app's AAGUID credentialId: rawId, publicKey: p256KeyPair.publicKey.rawRepresentation )
let rpIdHash = Utility.hashSHA256(rpId.data(using: .utf8)!) let authData = AuthenticatorData.attestation( rpIdHash: rpIdHash, userPresent: true, userVerified: true, // Ensure user verification! backupEligible: true, backupState: true, signCount: 0, attestedCredentialData: attestedCredData, extensions: nil )
let attObj: [String: Any] = [ "attStmt": [:], "authData": authData.toData(), "fmt": "none", ]
let cborEncoded = try CBOR.encodeMap(attObj) let attestationObject = Data(cborEncoded)
return [ "id": rawId.base64URLEncodedString(), "type": "public-key", "rawId": rawId.base64URLEncodedString(), "response": [ "clientDataJSON": clientDataJSONBase64Url, "attestationObject": attestationObject.base64URLEncodedString(), ], ]}
Integration Requirements
1. Challenge Signing
func signChallenge(_ challenge: Data, with privateKey: Ed25519PrivateKey) throws -> Data { // IMPLEMENT: Use your wallet's signing mechanism, to sign arbitrary data. // In this case, 32 random bytes. return signature}
2. User Verification
You should ensure the user is prompted to proceed with the registration, using biometrics or pin-code to authenticate. This is known as user verification and it is a flag that in the example above was set to true.
// Example using LocalAuthenticationimport LocalAuthentication
func requireUserVerification() async throws -> Bool { let context = LAContext() var error: NSError?
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else { throw LiquidAuthError.authenticationFailed("Biometrics not available") }
return try await context.evaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, localizedReason: "Authenticate to register passkey" )}
3. Key Management
// Example: Generate deterministic P256 key pairfunc generateDeterministicKeyPair(phrase: String, origin: String, userHandle: String) throws -> P256.Signing.PrivateKey { // Use deterministic-P256-swift library let dp256 = DeterministicP256()
// Generate the derived main key from BIP39 mnemonic let derivedMainKey = try dp256.genDerivedMainKeyWithBIP39(phrase: phrase)
// Generate domain-specific key pair let keyPair = dp256.genDomainSpecificKeyPair(derivedMainKey: derivedMainKey, origin: origin, userHandle: userHandle)
return keyPair // This is actually a P256.Signing.PrivateKey}
// Example: Complete wallet setup with deterministic keysfunc setupWalletForOrigin(_ origin: String, algorandAddress: String) throws -> P256.Signing.PrivateKey { // Your app's BIP39 mnemonic (securely stored) let phrase = "salon zoo engage submit smile frost later decide wing sight chaos renew lizard rely canal coral scene hobby scare step bus leaf tobacco slice"
// Use Algorand address as userHandle for deterministic generation. // Assuming that there is no more approrpriate userHandle to pick. let userHandle = algorandAddress
return try generateDeterministicKeyPair(phrase: phrase, origin: origin, userHandle: userHandle)}
Security Considerations
- Always verify user presence and user verification flags
- Validate origin domains against your allowlist
- Use unique AAGUIDs for your application
- Store keys securely
- Implement proper error handling and user feedback
Next Steps
After successful registration:
- Authentication: Learn how to authenticate with already registered passkeys
- Peer Communication: Set up WebRTC for dApp communication