chore: add code

This commit is contained in:
blacktop
2025-10-20 10:18:43 -06:00
parent c6bbb88893
commit 773686edbb
10 changed files with 973 additions and 0 deletions
+11
View File
@@ -78,6 +78,17 @@ build-linux: ## Build ipsw (linux)
@echo " > Building ipswd (linux)"
@CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w --X github.com/blacktop/ipsw/api/types.BuildVersion=$(CUR_VERSION) -X github.com/blacktop/ipsw/api/types.BuildTime=$(date -u +%Y%m%d)" ./cmd/ipswd
.PHONY: build-passkey-helper
build-passkey-helper: ## Build macOS passkey helper for WebAuthn authentication
@echo " > Building passkey_helper (macOS only)"
@swiftc -o internal/download/webauthn/passkey_helper/passkey_helper \
-framework AuthenticationServices \
-framework Foundation \
-framework AppKit \
internal/download/webauthn/passkey_helper/main.swift
@chmod +x internal/download/webauthn/passkey_helper/passkey_helper
@echo " > ✅ Helper built at internal/download/webauthn/passkey_helper/passkey_helper"
.PHONY: docs
docs: ## Build the cli docs
@echo " > Updating CLI Docs"
+10
View File
@@ -0,0 +1,10 @@
# Compiled Swift helper binary
passkey_helper/passkey_helper
# macOS build artifacts
*.dSYM
*.swiftmodule
*.swiftdoc
# Test fixtures
*.test
+139
View File
@@ -0,0 +1,139 @@
//go:build darwin && !ios
package webauthn
import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
)
// Challenge represents a WebAuthn authentication challenge
type Challenge struct {
Challenge string `json:"challenge"`
RpId string `json:"rpId"`
Timeout *int `json:"timeout,omitempty"`
UserVerification string `json:"userVerification,omitempty"` // required, preferred, discouraged
AllowedCredentials []AllowedCredential `json:"allowedCredentials,omitempty"`
}
// AllowedCredential represents a credential descriptor
type AllowedCredential struct {
Type string `json:"type"`
ID string `json:"id"`
Transports []string `json:"transports,omitempty"`
}
// AssertionResponse represents the result of a WebAuthn assertion
type AssertionResponse struct {
CredentialID string `json:"credentialId"`
AuthenticatorData string `json:"authenticatorData"`
ClientDataJSON string `json:"clientDataJSON"`
Signature string `json:"signature"`
UserHandle *string `json:"userHandle,omitempty"`
}
// ErrorResponse represents an error from the passkey helper
type ErrorResponse struct {
Error string `json:"error"`
Code int `json:"code"`
}
// Client provides WebAuthn passkey authentication using macOS native APIs
type Client struct {
helperPath string
}
// NewClient creates a new WebAuthn client
// If helperPath is empty, it will look for the compiled helper binary
func NewClient(helperPath string) (*Client, error) {
if helperPath == "" {
// Try to find the helper in common locations
locations := []string{
"./passkey_helper",
"./internal/download/webauthn/passkey_helper/passkey_helper",
"/usr/local/bin/passkey_helper",
}
for _, loc := range locations {
if _, err := os.Stat(loc); err == nil {
helperPath = loc
break
}
}
if helperPath == "" {
return nil, fmt.Errorf("passkey_helper binary not found; please compile it first")
}
}
absPath, err := filepath.Abs(helperPath)
if err != nil {
return nil, fmt.Errorf("failed to resolve helper path: %w", err)
}
// Verify the helper exists and is executable
if _, err := os.Stat(absPath); err != nil {
return nil, fmt.Errorf("helper binary not found at %s: %w", absPath, err)
}
return &Client{helperPath: absPath}, nil
}
// Authenticate performs WebAuthn authentication using macOS passkeys
func (c *Client) Authenticate(challenge Challenge) (*AssertionResponse, error) {
// Encode challenge as JSON
challengeJSON, err := json.Marshal(challenge)
if err != nil {
return nil, fmt.Errorf("failed to marshal challenge: %w", err)
}
// Execute Swift helper
cmd := exec.Command(c.helperPath, string(challengeJSON))
output, err := cmd.CombinedOutput()
if err != nil {
// Try to parse error response
var errResp ErrorResponse
if jsonErr := json.Unmarshal(output, &errResp); jsonErr == nil {
return nil, fmt.Errorf("authentication failed: %s (code: %d)", errResp.Error, errResp.Code)
}
return nil, fmt.Errorf("helper execution failed: %w\nOutput: %s", err, string(output))
}
// Parse response
var response AssertionResponse
if err := json.Unmarshal(output, &response); err != nil {
return nil, fmt.Errorf("failed to parse response: %w\nOutput: %s", err, string(output))
}
return &response, nil
}
// CompileHelper compiles the Swift helper binary
// This is a convenience function for development
func CompileHelper(srcPath, outPath string) error {
if srcPath == "" {
srcPath = filepath.Join("internal", "download", "webauthn", "passkey_helper", "main.swift")
}
if outPath == "" {
outPath = filepath.Join("internal", "download", "webauthn", "passkey_helper", "passkey_helper")
}
// Compile Swift code
cmd := exec.Command("swiftc",
"-o", outPath,
"-framework", "AuthenticationServices",
"-framework", "Foundation",
"-framework", "AppKit",
srcPath,
)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("compilation failed: %w\nOutput: %s", err, string(output))
}
return nil
}
+43
View File
@@ -0,0 +1,43 @@
//go:build !darwin || ios
package webauthn
import "fmt"
// Challenge represents a WebAuthn authentication challenge
type Challenge struct {
Challenge string `json:"challenge"`
RpId string `json:"rpId"`
Timeout *int `json:"timeout,omitempty"`
UserVerification string `json:"userVerification,omitempty"`
AllowedCredentials []AllowedCredential `json:"allowedCredentials,omitempty"`
}
// AllowedCredential represents a credential descriptor
type AllowedCredential struct {
Type string `json:"type"`
ID string `json:"id"`
Transports []string `json:"transports,omitempty"`
}
// AssertionResponse represents the result of a WebAuthn assertion
type AssertionResponse struct {
CredentialID string `json:"credentialId"`
AuthenticatorData string `json:"authenticatorData"`
ClientDataJSON string `json:"clientDataJSON"`
Signature string `json:"signature"`
UserHandle *string `json:"userHandle,omitempty"`
}
// Client provides WebAuthn passkey authentication (stub for non-macOS)
type Client struct{}
// NewClient creates a new WebAuthn client (not supported on this platform)
func NewClient(helperPath string) (*Client, error) {
return nil, fmt.Errorf("WebAuthn passkey authentication is only supported on macOS")
}
// Authenticate performs WebAuthn authentication (not supported on this platform)
func (c *Client) Authenticate(challenge Challenge) (*AssertionResponse, error) {
return nil, fmt.Errorf("WebAuthn passkey authentication is only supported on macOS")
}
+88
View File
@@ -0,0 +1,88 @@
//go:build darwin && !ios
package webauthn
import (
"encoding/base64"
"testing"
)
func TestClientCreation(t *testing.T) {
// Test with non-existent path
client, err := NewClient("/non/existent/path")
if err == nil {
t.Error("Expected error for non-existent helper path")
}
if client != nil {
t.Error("Expected nil client for non-existent path")
}
// Test auto-detection (may fail if not compiled yet)
client, err = NewClient("")
if err != nil {
t.Logf("Auto-detection failed (expected if helper not compiled): %v", err)
} else if client == nil {
t.Error("Client should not be nil on success")
}
}
func TestChallengeMarshaling(t *testing.T) {
challenge := Challenge{
Challenge: base64.RawURLEncoding.EncodeToString([]byte("test-challenge")),
RpId: "apple.com",
UserVerification: "preferred",
}
// This just validates the struct is properly defined
if challenge.RpId != "apple.com" {
t.Errorf("Expected RpId 'apple.com', got '%s'", challenge.RpId)
}
}
// TestAuthenticationIntegration requires a compiled helper and user interaction
// Run manually with: go test -v -run TestAuthenticationIntegration
func TestAuthenticationIntegration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
client, err := NewClient("")
if err != nil {
t.Skipf("Skipping integration test: %v", err)
}
// Create a test challenge (this won't actually work without a real Apple challenge)
challenge := Challenge{
Challenge: base64.RawURLEncoding.EncodeToString([]byte("test-challenge-data")),
RpId: "apple.com",
UserVerification: "preferred",
}
// Note: This will fail unless you have:
// 1. A real challenge from Apple's auth endpoint
// 2. A registered passkey for the RP ID
// 3. User interaction (Touch ID/password)
_, err = client.Authenticate(challenge)
if err != nil {
t.Logf("Authentication failed (expected without real challenge): %v", err)
}
}
func TestBase64URLEncoding(t *testing.T) {
// Test data that should round-trip through base64url
testData := []byte("Hello, WebAuthn! 🔐")
// Encode to base64url
encoded := base64.RawURLEncoding.EncodeToString(testData)
// Decode from base64url
decoded, err := base64.RawURLEncoding.DecodeString(encoded)
if err != nil {
t.Fatalf("Failed to decode: %v", err)
}
// Verify round-trip
if string(decoded) != string(testData) {
t.Errorf("Round-trip failed: got '%s', want '%s'", string(decoded), string(testData))
}
}
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
#
# Debug helper to capture Apple authentication flow
# This script helps identify when and how Apple sends WebAuthn challenges
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_FILE="${SCRIPT_DIR}/auth_debug.log"
echo "🔍 Apple Auth Flow Debugger"
echo "=========================="
echo ""
echo "This will run ipsw with verbose logging to capture the auth flow."
echo "Log will be saved to: $LOG_FILE"
echo ""
echo "Steps to test:"
echo " 1. Make sure you have an Apple ID with passkey registered"
echo " 2. Clear any saved credentials: rm -rf ~/.config/ipsw"
echo " 3. Run this script"
echo " 4. Enter your credentials when prompted"
echo " 5. Check $LOG_FILE for WebAuthn challenge data"
echo ""
read -p "Press Enter to continue or Ctrl+C to cancel..."
# Clear old log
> "$LOG_FILE"
# Add debug helper to temporarily show all JSON responses
echo "Starting ipsw with debug logging..."
echo "====================================" >> "$LOG_FILE"
echo "Date: $(date)" >> "$LOG_FILE"
echo "====================================" >> "$LOG_FILE"
# Run ipsw with verbose output and capture everything
cd "$(git rev-parse --show-toplevel)" || exit 1
IPSW_DEBUG=1 go run ./cmd/ipsw download dev --os -v 2>&1 | tee -a "$LOG_FILE"
echo ""
echo "✅ Debug log saved to: $LOG_FILE"
echo ""
echo "Now analyze the log for:"
echo " 1. HTTP status codes (look for 412, 409, 200)"
echo " 2. 'publicKeyCredentialRequestOptions' field"
echo " 3. 'passkeyAuthentication' field"
echo " 4. 'authenticationType' field"
echo ""
echo "Search for WebAuthn data:"
echo " grep -i 'webauthn\\|passkey\\|publicKey' $LOG_FILE"
echo ""
+189
View File
@@ -0,0 +1,189 @@
//go:build ignore
// +build ignore
// Debug patch to add to dev_portal.go for WebAuthn challenge detection
// This shows how to temporarily instrument the code to see what Apple sends
package debug
// STEP 1: Add this function to dev_portal.go (temporarily for debugging)
func (dp *DevPortal) inspectAuthResponse(statusCode int, headers http.Header, body []byte) {
log.Info("🔍 Inspecting authentication response...")
log.Infof("Status Code: %d", statusCode)
// Log relevant headers
relevantHeaders := []string{
"X-Apple-Id-Session-Id",
"Scnt",
"X-Apple-Widget-Key",
"Content-Type",
"X-Apple-Auth-Attributes",
}
for _, h := range relevantHeaders {
if val := headers.Get(h); val != "" {
log.Debugf("Header %s: %s", h, val)
}
}
// Try to parse as JSON
var jsonResp map[string]interface{}
if err := json.Unmarshal(body, &jsonResp); err != nil {
log.Warnf("Response is not JSON: %v", err)
return
}
// Check for WebAuthn indicators
indicators := []struct{
key string
description string
}{
{"publicKeyCredentialRequestOptions", "WebAuthn challenge"},
{"passkeyAuthentication", "Passkey auth flag"},
{"authenticationType", "Auth type"},
{"webAuthnOptions", "WebAuthn options (alternative)"},
{"credentialRequestOptions", "Credential request (alternative)"},
{"authType", "Authentication type"},
}
for _, ind := range indicators {
if val, ok := jsonResp[ind.key]; ok {
log.Infof("✅ Found %s: %s", ind.description, ind.key)
// Pretty print the value
if jsonBytes, err := json.MarshalIndent(val, "", " "); err == nil {
log.Infof("%s:\n%s", ind.key, string(jsonBytes))
}
}
}
// Check for service errors
if errors, ok := jsonResp["serviceErrors"].([]interface{}); ok && len(errors) > 0 {
log.Warnf("Service errors present: %+v", errors)
}
// Log full response in debug mode
if viper.GetBool("verbose") {
if prettyJSON, err := json.MarshalIndent(jsonResp, "", " "); err == nil {
log.Debugf("Full response:\n%s", string(prettyJSON))
}
}
}
// STEP 2: Add this call in signIn() right after reading the response body
func (dp *DevPortal) signIn(username, password string) error {
// ... existing code ...
body, err := io.ReadAll(response.Body)
if err != nil {
return err
}
log.Debugf("POST Login: (%d):\n%s\n", response.StatusCode, string(body))
// ADD THIS LINE TO DEBUG
dp.inspectAuthResponse(response.StatusCode, response.Header, body)
// ... rest of existing code ...
}
// STEP 3: Example output you might see
/*
Expected output when WebAuthn is available:
🔍 Inspecting authentication response...
Status Code: 412
Header X-Apple-Id-Session-Id: abc123...
Header Scnt: xyz789...
✅ Found WebAuthn challenge: publicKeyCredentialRequestOptions
publicKeyCredentialRequestOptions:
{
"challenge": "rlHp7l62HW...",
"rpId": "apple.com",
"timeout": 60000,
"userVerification": "preferred",
"allowCredentials": [
{
"type": "public-key",
"id": "credential_id_here",
"transports": ["internal"]
}
]
}
✅ Found Auth type: authType
authType: "hsa2"
*/
// STEP 4: Alternative - add middleware to log all HTTP traffic
type loggingTransport struct {
transport http.RoundTripper
}
func (t *loggingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// Log request
log.Debugf("→ %s %s", req.Method, req.URL)
// Execute request
resp, err := t.transport.RoundTrip(req)
if err != nil {
return nil, err
}
// Log response status
log.Debugf("← %d %s", resp.StatusCode, req.URL)
// For Apple auth endpoints, log the response body
if strings.Contains(req.URL.String(), "idmsa.apple.com") {
bodyBytes, _ := io.ReadAll(resp.Body)
resp.Body = io.NopCloser(bytes.NewReader(bodyBytes))
log.Debugf("Response body:\n%s", string(bodyBytes))
// Check for WebAuthn
if bytes.Contains(bodyBytes, []byte("publicKeyCredential")) ||
bytes.Contains(bodyBytes, []byte("passkey")) {
log.Infof("🔑 WebAuthn/Passkey detected in response!")
}
}
return resp, nil
}
// Add to client initialization:
func (dp *DevPortal) Init() error {
// ... existing code ...
// Wrap transport with logging (only in debug mode)
if viper.GetBool("verbose") {
dp.Client.Transport = &loggingTransport{
transport: dp.Client.Transport,
}
}
return nil
}
// PRACTICAL USAGE:
/*
1. Temporarily add inspectAuthResponse() to dev_portal.go
2. Add the call in signIn() after reading response body
3. Run ipsw with verbose logging:
ipsw download dev --os -v
4. Look for output like:
✅ Found WebAuthn challenge: publicKeyCredentialRequestOptions
5. Copy the exact field names and structure Apple uses
6. Update the WebAuthn implementation with correct field names
7. Remove the debug code once you have the structure
*/
@@ -0,0 +1,238 @@
//go:build ignore
// +build ignore
// Example: How to integrate WebAuthn into dev_portal.go
//
// This file shows the key changes needed to add passkey support
// to the existing Apple Developer Portal authentication flow.
//
// NOTE: This is an example/documentation file and is not compiled.
package example
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"runtime"
"github.com/apex/log"
"github.com/blacktop/ipsw/internal/download/webauthn"
)
// STEP 1: Add WebAuthn response types to dev_portal.go
// Add this near the other response structs (around line 200)
type webAuthnChallengeResponse struct {
Challenge string `json:"challenge,omitempty"`
RpId string `json:"rpId,omitempty"`
Timeout int `json:"timeout,omitempty"`
AllowCredentials []webAuthnCredential `json:"allowCredentials,omitempty"`
UserVerification string `json:"userVerification,omitempty"`
Extensions map[string]json.RawMessage `json:"extensions,omitempty"`
}
type webAuthnCredential struct {
Type string `json:"type"`
ID string `json:"id"`
Transports []string `json:"transports,omitempty"`
}
// STEP 2: Modify signIn() to detect and handle WebAuthn challenges
// Insert this code in dev_portal.go after the initial login POST (around line 750)
func (dp *DevPortal) signInWithWebAuthnSupport(username, password string) error {
// ... existing hashcash and POST request code ...
response, err := dp.Client.Do(req)
if err != nil {
return err
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
return err
}
log.Debugf("POST Login: (%d):\n%s\n", response.StatusCode, string(body))
// NEW: Check for WebAuthn challenge in response
if response.StatusCode == 200 {
var authResp map[string]interface{}
if err := json.Unmarshal(body, &authResp); err == nil {
// Check if response contains WebAuthn challenge
if challengeData, ok := authResp["webauthn"].(map[string]interface{}); ok {
log.Info("🔐 Apple requires passkey authentication")
// Try WebAuthn first, fallback to 2FA if it fails
if err := dp.handleWebAuthnChallenge(challengeData); err != nil {
log.Warnf("Passkey authentication failed: %v", err)
log.Info("Falling back to 2FA...")
// Continue to existing 2FA code below
} else {
// WebAuthn succeeded
return dp.storeSession()
}
}
}
}
// ... existing SRP and 2FA code continues here ...
if response.StatusCode == 409 {
// ... existing 2FA handling ...
}
return nil
}
// STEP 3: Add WebAuthn handler methods
// Add these new methods to dev_portal.go
func (dp *DevPortal) handleWebAuthnChallenge(challengeData map[string]interface{}) error {
// Only available on macOS
if runtime.GOOS != "darwin" {
return fmt.Errorf("WebAuthn passkey authentication only supported on macOS")
}
// Parse challenge
var challenge webAuthnChallengeResponse
chalJSON, err := json.Marshal(challengeData)
if err != nil {
return fmt.Errorf("failed to marshal challenge: %w", err)
}
if err := json.Unmarshal(chalJSON, &challenge); err != nil {
return fmt.Errorf("failed to parse challenge: %w", err)
}
// Create WebAuthn client
client, err := webauthn.NewClient("")
if err != nil {
return fmt.Errorf("WebAuthn client unavailable: %w", err)
}
// Convert to internal format
webauthnChallenge := webauthn.AppleWebAuthnChallenge{
Challenge: challenge.Challenge,
RpId: challenge.RpId,
Timeout: challenge.Timeout,
UserVerification: challenge.UserVerification,
}
// Convert credentials
for _, cred := range challenge.AllowCredentials {
webauthnChallenge.AllowCredentials = append(webauthnChallenge.AllowCredentials,
webauthn.AppleAllowedCredential{
Type: cred.Type,
ID: cred.ID,
Transports: cred.Transports,
})
}
// Perform authentication
log.Info("Requesting passkey authentication (Touch ID)...")
assertion, err := webauthn.HandleAppleWebAuthn(client, webauthnChallenge)
if err != nil {
return fmt.Errorf("passkey authentication failed: %w", err)
}
// Send assertion back to Apple
return dp.sendWebAuthnAssertion(assertion)
}
func (dp *DevPortal) sendWebAuthnAssertion(assertion *webauthn.AppleWebAuthnResponse) error {
// Marshal the assertion
data, err := json.Marshal(assertion)
if err != nil {
return fmt.Errorf("failed to marshal assertion: %w", err)
}
// Send to Apple's verification endpoint
req, err := http.NewRequest("POST", completeURL, bytes.NewReader(data))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
// Set required headers
dp.updateRequestHeaders(req)
req.Header.Set("Content-Type", "application/json")
// Send request
resp, err := dp.Client.Do(req)
if err != nil {
return fmt.Errorf("failed to send assertion: %w", err)
}
defer resp.Body.Close()
// Check response
respBody, _ := io.ReadAll(resp.Body)
log.Debugf("POST WebAuthn Assertion: (%d):\n%s\n", resp.StatusCode, string(respBody))
if resp.StatusCode != 200 {
return fmt.Errorf("assertion verification failed: status %d: %s", resp.StatusCode, string(respBody))
}
// Update session from response headers
dp.config.SessionID = resp.Header.Get("X-Apple-Id-Session-Id")
dp.config.SCNT = resp.Header.Get("Scnt")
log.Info("✅ Passkey authentication successful")
return nil
}
// STEP 4: Optional - Add proactive WebAuthn check in Login()
// This could be added to the Login() method to try passkeys first
func (dp *DevPortal) tryPasskeyFirst(username string) bool {
// Only on macOS
if runtime.GOOS != "darwin" {
return false
}
// Check if passkey helper is available
if _, err := webauthn.NewClient(""); err != nil {
log.Debug("Passkey helper not available")
return false
}
// Could probe Apple's endpoint to see if passkey is available
// for this account (implementation left as exercise)
log.Info("🔑 Passkey authentication available for this device")
return true
}
// USAGE EXAMPLE:
//
// In cmd/ipsw/cmd/download/download_dev.go, the flow becomes:
//
// 1. User runs: ipsw download dev --os
// 2. Login() checks for saved credentials
// 3. If on macOS, tryPasskeyFirst() checks availability
// 4. POST to Apple's login endpoint
// 5. Apple responds with either:
// a) WebAuthn challenge → handleWebAuthnChallenge() → Touch ID
// b) 2FA required → existing code → SMS/device code
// 6. Session established, downloads proceed
// TESTING:
//
// Test the implementation:
// go run ./cmd/ipsw download dev --os -v
//
// Expected flow:
// 1. "🔐 Apple requires passkey authentication"
// 2. macOS shows Touch ID prompt
// 3. "✅ Passkey authentication successful"
// 4. Downloads begin
// FALLBACK STRATEGY:
//
// If passkey fails for any reason:
// - User cancellation → Prompt for 2FA
// - No passkey registered → Fall back to password + 2FA
// - Helper not found → Fall back to password + 2FA
// - macOS too old → Fall back to password + 2FA
+161
View File
@@ -0,0 +1,161 @@
//go:build darwin && !ios
package webauthn
import (
"encoding/json"
"fmt"
"net/http"
)
// AppleWebAuthnChallenge represents the challenge from Apple's authentication endpoint
type AppleWebAuthnChallenge struct {
Challenge string `json:"challenge"`
RpId string `json:"rpId"`
Timeout int `json:"timeout"`
AllowCredentials []AppleAllowedCredential `json:"allowCredentials"`
UserVerification string `json:"userVerification"`
Extensions map[string]json.RawMessage `json:"extensions,omitempty"`
}
// AppleAllowedCredential represents a credential from Apple
type AppleAllowedCredential struct {
Type string `json:"type"`
ID string `json:"id"`
Transports []string `json:"transports,omitempty"`
}
// AppleWebAuthnResponse represents the response to send back to Apple
type AppleWebAuthnResponse struct {
ID string `json:"id"`
RawID string `json:"rawId"`
Response AppleAuthenticatorResponse `json:"response"`
Type string `json:"type"`
}
// AppleAuthenticatorResponse contains the authenticator assertion
type AppleAuthenticatorResponse struct {
ClientDataJSON string `json:"clientDataJSON"`
AuthenticatorData string `json:"authenticatorData"`
Signature string `json:"signature"`
UserHandle *string `json:"userHandle,omitempty"`
}
// HandleAppleWebAuthn processes an Apple WebAuthn challenge and returns the response
func HandleAppleWebAuthn(client *Client, appleChallenge AppleWebAuthnChallenge) (*AppleWebAuthnResponse, error) {
// Convert Apple's challenge format to our internal format
challenge := Challenge{
Challenge: appleChallenge.Challenge,
RpId: appleChallenge.RpId,
UserVerification: appleChallenge.UserVerification,
}
if appleChallenge.Timeout > 0 {
challenge.Timeout = &appleChallenge.Timeout
}
// Convert allowed credentials
if len(appleChallenge.AllowCredentials) > 0 {
challenge.AllowedCredentials = make([]AllowedCredential, len(appleChallenge.AllowCredentials))
for i, cred := range appleChallenge.AllowCredentials {
challenge.AllowedCredentials[i] = AllowedCredential{
Type: cred.Type,
ID: cred.ID,
Transports: cred.Transports,
}
}
}
// Perform authentication
assertion, err := client.Authenticate(challenge)
if err != nil {
return nil, fmt.Errorf("authentication failed: %w", err)
}
// Convert response to Apple's expected format
response := &AppleWebAuthnResponse{
ID: assertion.CredentialID,
RawID: assertion.CredentialID,
Type: "public-key",
Response: AppleAuthenticatorResponse{
ClientDataJSON: assertion.ClientDataJSON,
AuthenticatorData: assertion.AuthenticatorData,
Signature: assertion.Signature,
UserHandle: assertion.UserHandle,
},
}
return response, nil
}
// SendWebAuthnResponse sends the WebAuthn assertion back to Apple
func SendWebAuthnResponse(httpClient *http.Client, endpoint string, response *AppleWebAuthnResponse, headers map[string]string) error {
// Marshal response
data, err := json.Marshal(response)
if err != nil {
return fmt.Errorf("failed to marshal response: %w", err)
}
// Create request
req, err := http.NewRequest("POST", endpoint, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}
// Set headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
for key, value := range headers {
req.Header.Set(key, value)
}
// Send request
resp, err := httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("server returned status %d", resp.StatusCode)
}
// Read response body
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return fmt.Errorf("failed to decode response: %w", err)
}
_ = data // Use the marshaled data in the actual implementation
return nil
}
// Example usage in dev_portal.go:
//
// func (dp *DevPortal) tryWebAuthnLogin() error {
// // 1. Detect WebAuthn challenge from Apple's response
// var appleChallenge AppleWebAuthnChallenge
// // ... parse from response ...
//
// // 2. Create WebAuthn client
// client, err := webauthn.NewClient("")
// if err != nil {
// return fmt.Errorf("failed to create WebAuthn client: %w", err)
// }
//
// // 3. Handle authentication
// response, err := webauthn.HandleAppleWebAuthn(client, appleChallenge)
// if err != nil {
// return fmt.Errorf("WebAuthn authentication failed: %w", err)
// }
//
// // 4. Send response back to Apple
// headers := map[string]string{
// "X-Apple-Id-Session-Id": dp.config.SessionID,
// "X-Apple-Widget-Key": dp.config.WidgetKey,
// "Scnt": dp.config.SCNT,
// }
//
// return webauthn.SendWebAuthnResponse(dp.Client, webauthnEndpoint, response, headers)
// }
+43
View File
@@ -0,0 +1,43 @@
#!/bin/bash
#
# Build the macOS passkey helper binary
# This script compiles the Swift code into a standalone executable
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SOURCE="$SCRIPT_DIR/main.swift"
OUTPUT="$SCRIPT_DIR/passkey_helper"
echo "🔨 Building passkey_helper..."
# Check if Swift is available
if ! command -v swiftc &> /dev/null; then
echo "❌ Error: swiftc not found. Please install Xcode Command Line Tools:"
echo " xcode-select --install"
exit 1
fi
# Check macOS version
macos_version=$(sw_vers -productVersion | cut -d. -f1)
if [ "$macos_version" -lt 13 ]; then
echo "⚠️ Warning: macOS 13+ recommended for full WebAuthn support"
fi
# Compile
swiftc -o "$OUTPUT" \
-framework AuthenticationServices \
-framework Foundation \
-framework AppKit \
"$SOURCE"
if [ $? -eq 0 ]; then
chmod +x "$OUTPUT"
echo "✅ Successfully built: $OUTPUT"
echo ""
echo "Test it with:"
echo " $OUTPUT '{\"challenge\":\"dGVzdA\",\"rpId\":\"apple.com\"}'"
else
echo "❌ Build failed"
exit 1
fi