mirror of
https://github.com/blacktop/ipsw.git
synced 2026-05-08 12:22:26 +00:00
chore: add code
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Compiled Swift helper binary
|
||||
passkey_helper/passkey_helper
|
||||
|
||||
# macOS build artifacts
|
||||
*.dSYM
|
||||
*.swiftmodule
|
||||
*.swiftdoc
|
||||
|
||||
# Test fixtures
|
||||
*.test
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
@@ -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 ""
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
Reference in New Issue
Block a user