Skip to content

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:

  1. Requesting attestation options from the Liquid Auth service
  2. Creating a P256 key pair (deterministically or randomly)
  3. Signing the challenge with your Algorand Ed25519 key, to prove ownership
  4. Building the WebAuthn attestation response
  5. Submitting the credential to complete registration

Implementation

Complete Registration Function

import AuthenticationServices
import CryptoKit
import Foundation
import SwiftCBOR
import LiquidAuthSDK
import 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 LocalAuthentication
import 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 pair
func 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 keys
func 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

  1. Always verify user presence and user verification flags
  2. Validate origin domains against your allowlist
  3. Use unique AAGUIDs for your application
  4. Store keys securely
  5. Implement proper error handling and user feedback

Next Steps

After successful registration: