# iOS security review

Review Keychain, Universal Links and release entitlements against business needs. Do not base token storage decisions on sandbox assumptions alone.

Original AI-assisted SecNet educational content. Fictional local examples; not a standards provider publication, endorsement or compliance evidence. Page exercises do not create account progress.

Sources checked: 2026-10-21

## Choose Keychain accessibility

UserDefaults is not a secret store. Select Keychain accessibility against lock state, background work and device migration needs. The example uses WhenUnlockedThisDeviceOnly; this is not universal and may restrict background access. Check result codes and design deletion alongside server token revocation. No storage API makes logging plaintext tokens safe.

## Universal Links are not authority

Domain-app association uses associated domains and apple-app-site-association. The app must also validate scheme, exact host, path and unexpected parameters. Opening a link in the app does not authorize access to another customer’s record. The downloadable pure Swift URL test checks parsing policy only, not OS association or device behavior.

## Release evidence and privacy

A successful archive is not evidence of a secure release. Review ATS exceptions, entitlement scope, signing profiles, data collection declarations and SDKs. Test stored-data lifecycle across lock, reinstall and account changes. Compare privacy declarations with actual network behavior; network testing must not lead to disabling production TLS controls.

## Device workpaper: access and transitions

Store a synthetic token using the selected Keychain accessibility class and compare locked/unlocked behavior against requirements. Account switching must not expose the previous account’s data, and a locally retained token must not bypass server revocation. Separately test the correct Universal Link, a lookalike host and an unauthorized ticket. Opening from Safari and handing off to the app are platform integration tests; seven pure URL assertions do not replace them. Capture iOS version, app archive, associated-domain scope and observed outcomes. Running the Keychain component on a developer computer with a real account token is not part of this workshop.

## Working sequence

1. Select Keychain accessibility against business needs.
2. Test token copies and revocation.
3. Test Universal Links association on-device.
4. Review ATS, entitlements and privacy declarations.
5. Match the release archive with device tests.

## 1. A token inside the sandbox

Sandboxed=true does not prove secret protection. The model requires this example’s Keychain/device-only policy and absence of log copies. Different business needs require a fresh accessibility decision.

This code is a local policy model; it does not replace target/device testing.

### Weak implementation
```js
s=>s.sandboxed
```

### Corrected implementation
```js
s=>s.sandboxed&&s.keychain&&s.deviceOnly&&!s.logged
```

### Synthetic input and actual model output
```json
{
  "before": [
    {
      "name": "defaults-token",
      "input": {
        "sandboxed": true,
        "keychain": false,
        "deviceOnly": false,
        "logged": false
      },
      "expected": false,
      "observed": true,
      "matched": false
    },
    {
      "name": "controlled-keychain",
      "input": {
        "sandboxed": true,
        "keychain": true,
        "deviceOnly": true,
        "logged": false
      },
      "expected": true,
      "observed": true,
      "matched": true
    },
    {
      "name": "logging-leak",
      "input": {
        "sandboxed": true,
        "keychain": true,
        "deviceOnly": true,
        "logged": true
      },
      "expected": false,
      "observed": true,
      "matched": false
    }
  ],
  "after": [
    {
      "name": "defaults-token",
      "input": {
        "sandboxed": true,
        "keychain": false,
        "deviceOnly": false,
        "logged": false
      },
      "expected": false,
      "observed": false,
      "matched": true
    },
    {
      "name": "controlled-keychain",
      "input": {
        "sandboxed": true,
        "keychain": true,
        "deviceOnly": true,
        "logged": false
      },
      "expected": true,
      "observed": true,
      "matched": true
    },
    {
      "name": "logging-leak",
      "input": {
        "sandboxed": true,
        "keychain": true,
        "deviceOnly": true,
        "logged": true
      },
      "expected": false,
      "observed": false,
      "matched": true
    }
  ]
}
```

## 2. Suffix confusion

endsWith accepts otherharbor.example. Require exact host, OS association and server object authority together. Host validation does not remove the need for object authorization.

This code is a local policy model; it does not replace target/device testing.

### Weak implementation
```js
s=>s.host.endsWith('harbor.example')
```

### Corrected implementation
```js
s=>s.scheme==='https'&&s.host==='harbor.example'&&s.associationVerified&&s.serverAuthorization
```

### Synthetic input and actual model output
```json
{
  "before": [
    {
      "name": "associated-authorized",
      "input": {
        "scheme": "https",
        "host": "harbor.example",
        "associationVerified": true,
        "serverAuthorization": true
      },
      "expected": true,
      "observed": true,
      "matched": true
    },
    {
      "name": "suffix-confusion",
      "input": {
        "scheme": "https",
        "host": "otherharbor.example",
        "associationVerified": false,
        "serverAuthorization": true
      },
      "expected": false,
      "observed": true,
      "matched": false
    },
    {
      "name": "foreign-ticket",
      "input": {
        "scheme": "https",
        "host": "harbor.example",
        "associationVerified": true,
        "serverAuthorization": false
      },
      "expected": false,
      "observed": true,
      "matched": false
    }
  ],
  "after": [
    {
      "name": "associated-authorized",
      "input": {
        "scheme": "https",
        "host": "harbor.example",
        "associationVerified": true,
        "serverAuthorization": true
      },
      "expected": true,
      "observed": true,
      "matched": true
    },
    {
      "name": "suffix-confusion",
      "input": {
        "scheme": "https",
        "host": "otherharbor.example",
        "associationVerified": false,
        "serverAuthorization": true
      },
      "expected": false,
      "observed": false,
      "matched": true
    },
    {
      "name": "foreign-ticket",
      "input": {
        "scheme": "https",
        "host": "harbor.example",
        "associationVerified": true,
        "serverAuthorization": false
      },
      "expected": false,
      "observed": false,
      "matched": true
    }
  ]
}
```

## 3. Archive-only evidence

Producing an archive does not prove ATS, entitlement or privacy review. The model rejects missing device tests; collect actual review records separately.

This code is a local policy model; it does not replace target/device testing.

### Weak implementation
```js
s=>s.archived
```

### Corrected implementation
```js
s=>s.archived&&!s.arbitraryLoads&&s.entitlementsReviewed&&s.privacyReviewed&&s.deviceTests
```

### Synthetic input and actual model output
```json
{
  "before": [
    {
      "name": "archived-only",
      "input": {
        "archived": true,
        "arbitraryLoads": true,
        "entitlementsReviewed": false,
        "privacyReviewed": false,
        "deviceTests": false
      },
      "expected": false,
      "observed": true,
      "matched": false
    },
    {
      "name": "reviewed-release",
      "input": {
        "archived": true,
        "arbitraryLoads": false,
        "entitlementsReviewed": true,
        "privacyReviewed": true,
        "deviceTests": true
      },
      "expected": true,
      "observed": true,
      "matched": true
    },
    {
      "name": "missing-device-test",
      "input": {
        "archived": true,
        "arbitraryLoads": false,
        "entitlementsReviewed": true,
        "privacyReviewed": true,
        "deviceTests": false
      },
      "expected": false,
      "observed": true,
      "matched": false
    }
  ],
  "after": [
    {
      "name": "archived-only",
      "input": {
        "archived": true,
        "arbitraryLoads": true,
        "entitlementsReviewed": false,
        "privacyReviewed": false,
        "deviceTests": false
      },
      "expected": false,
      "observed": false,
      "matched": true
    },
    {
      "name": "reviewed-release",
      "input": {
        "archived": true,
        "arbitraryLoads": false,
        "entitlementsReviewed": true,
        "privacyReviewed": true,
        "deviceTests": true
      },
      "expected": true,
      "observed": true,
      "matched": true
    },
    {
      "name": "missing-device-test",
      "input": {
        "archived": true,
        "arbitraryLoads": false,
        "entitlementsReviewed": true,
        "privacyReviewed": true,
        "deviceTests": false
      },
      "expected": false,
      "observed": false,
      "matched": true
    }
  ]
}
```

## Exit evidence

- Keychain policy and failure behavior
- Release archive, entitlements and device test records

## References

- [OWASP MASVS](https://mas.owasp.org/MASVS/)
- [OWASP MASTG](https://mas.owasp.org/MASTG/0x03-Overview/)
- [Apple Keychain Services](https://developer.apple.com/documentation/security/keychain-services)
- [Apple Universal Links](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app)
