[GH-ISSUE #431] Feature: macOS Notarization via Notary REST API #123

Closed
opened 2026-02-26 21:33:35 +03:00 by kerem · 1 comment
Owner

Originally created by @rudrankriyam on GitHub (Feb 6, 2026).
Original GitHub issue: https://github.com/rudrankriyam/App-Store-Connect-CLI/issues/431

Overview

Add support for macOS notarization using Apple's Notary REST API. This enables developers to submit, track, and inspect notarization requests directly from asc — without requiring macOS, Xcode, or xcrun notarytool.

The Notary API is a separate REST API from the main App Store Connect API, but uses the same JWT authentication and API keys. It lives at https://appstoreconnect.apple.com/notary/v2/ (not in the App Store Connect OpenAPI spec).

Why This Matters

  • No existing CLI does this via the REST API. Every other tool (xcrun notarytool, gon) shells out to Apple's native CLI, requiring macOS + Xcode. The only pure REST API implementation is bep/macosnotarylib (Go library, no CLI).
  • Cross-platform. The REST API works from any OS — Linux CI servers, Docker containers, GitHub Actions runners — no Xcode needed.
  • Zero new auth setup. Developers already authenticated with asc auth can notarize immediately. Same key, same issuer, same JWT.
  • Consistent UX. JSON-first output, --output table/markdown, --paginate — same patterns as every other asc command.

Use Case

A developer distributes a macOS app outside the Mac App Store with Developer ID and needs to notarize it:

# Submit, upload, and wait for notarization to complete
asc notarize submit ./MyApp.zip --wait

# Check on a previous submission
asc notarize status --id "2efe2717-52ef-43a5-96dc-0797e4ca1041"

# Get the notarization log (warnings, errors)
asc notarize log --id "2efe2717-52ef-43a5-96dc-0797e4ca1041"

# List recent submissions
asc notarize history

# Optionally staple on macOS
asc notarize staple ./MyApp.dmg

API Documentation

Scope (Notary API endpoints)

The Notary API has 4 endpoints, all at https://appstoreconnect.apple.com/notary/v2/:

Method Path Purpose
POST /submissions Start a new submission (returns temporary upload credentials for Apple's cloud storage)
GET /submissions List previous submissions (up to 100)
GET /submissions/{id} Get submission status
GET /submissions/{id}/logs Get submission log URL

Note: This API is NOT in the App Store Connect OpenAPI spec (docs/openapi/latest.json). It is documented separately by Apple.

Submission Workflow

Apple's notarization uses a two-step upload process. The Notary API does not accept the file directly — instead, it returns temporary cloud storage credentials so you can upload the file to Apple's storage, and Apple picks it up from there for scanning.

  1. POST /submissions with { submissionName, sha256, notifications? } — returns temporary AWS S3 credentials (awsAccessKeyId, awsSecretAccessKey, awsSessionToken, bucket, object) and a submission id
  2. Upload the file to the provided S3 bucket using the temporary credentials (standard multipart upload with transfer acceleration)
  3. GET /submissions/{id} to poll status — returns Accepted, In Progress, Invalid, or Rejected
  4. GET /submissions/{id}/logs to retrieve a signed URL to the JSON log file

The temporary upload credentials expire after 12 hours. If not used before expiry, the submission must be restarted.

Authentication

Identical to App Store Connect API:

  • Same JWT format (ES256, 20-minute expiry recommended)
  • Same API keys (ASC_KEY_ID, ASC_ISSUER_ID, private key)
  • Header: Authorization: Bearer <token>

Proposed CLI

Command group

asc notarize <subcommand> [flags]

Subcommands

asc notarize submit

Submit a file for notarization, upload it, and optionally wait for completion.

asc notarize submit ./MyApp.zip
asc notarize submit ./MyApp.zip --wait
asc notarize submit ./MyApp.zip --wait --webhook "https://example.com/notify"
asc notarize submit ./MyApp.dmg --output table

Flags:

  • Positional arg or --file: path to .zip, .dmg, or .pkg
  • --wait: block until notarization completes (poll status)
  • --webhook: URL to receive notification on completion
  • --output, --pretty: standard output flags

Behavior:

  1. Compute SHA-256 of the file
  2. POST to /submissions with file name, hash, and optional webhook
  3. Upload file to Apple's cloud storage using returned credentials
  4. If --wait, poll /submissions/{id} until terminal state
  5. Print submission result (id, status, and log URL if complete)

asc notarize status

Check the status of a submission.

asc notarize status --id "2efe2717-52ef-43a5-96dc-0797e4ca1041"
asc notarize status --id "2efe2717-..." --wait

Flags:

  • --id: submission ID (required)
  • --wait: poll until terminal state
  • --output, --pretty

asc notarize log

Get the notarization log for a submission.

asc notarize log --id "2efe2717-52ef-43a5-96dc-0797e4ca1041"
asc notarize log --id "2efe2717-..." --download ./notarize-log.json

Flags:

  • --id: submission ID (required)
  • --download: save log to file instead of printing URL
  • --output, --pretty

asc notarize history

List recent notarization submissions.

asc notarize history
asc notarize history --output table

Flags:

  • --output, --pretty

asc notarize staple (optional, macOS only)

Staple the notarization ticket to a package. This shells out to xcrun stapler staple since stapling is not available via the REST API.

asc notarize staple ./MyApp.dmg

Flags:

  • Positional arg or --file: path to .dmg, .pkg, or .app
  • Fails with a clear error on non-macOS systems

Flag Patterns

Common: --id, --output, --pretty
New: --wait, --webhook, --file, --download

Output

  • JSON minified by default
  • --pretty for pretty-printed JSON
  • --output table/markdown for human-readable output

Submit output example:

{"id":"2efe2717-52ef-43a5-96dc-0797e4ca1041","status":"In Progress","name":"MyApp.zip"}

Status output example:

{"id":"2efe2717-52ef-43a5-96dc-0797e4ca1041","status":"Accepted","name":"MyApp.zip","createdDate":"2026-02-07T12:00:00Z"}

Detailed TODO

Core

  • Add Notary API base URL constant (NotaryBaseURL = "https://appstoreconnect.apple.com/notary/v2")
  • Add Notary API client methods in new file internal/asc/client_notarize.go
    • SubmitNotarization(ctx, name, sha256, webhookURL) (*NotarySubmissionResponse, error)
    • GetNotarizationStatus(ctx, submissionID) (*NotarySubmission, error)
    • GetNotarizationLog(ctx, submissionID) (*NotaryLogResponse, error)
    • GetNotarizationHistory(ctx) (*NotaryHistoryResponse, error)
  • Add Notary types in internal/asc/notarize.go
    • NotarySubmissionRequest, NotarySubmissionResponse
    • NotarySubmission, NotaryLogResponse, NotaryHistoryResponse
    • Upload credential types (NotaryUploadCredentials)
  • Add upload function in internal/asc/notarize_upload.go
    • Upload file to Apple's cloud storage using temporary credentials from submit response
    • Support transfer acceleration
    • Progress reporting for large files

CLI

  • Create internal/cli/notarize/ package
  • notarize.goNotarizeCommand() group with subcommands
  • submit.go — submit + upload + optional wait
  • status.go — check submission status
  • log.go — retrieve notarization log
  • history.go — list previous submissions
  • staple.go — optional macOS-only staple wrapper
  • shared_wrappers.go — standard shared wrappers
  • commands.goCommand() factory
  • Register in internal/cli/registry/registry.go
  • Set UsageFunc: shared.DefaultUsageFunc on all commands

Output

  • Add output helpers in internal/asc/notarize_output.go
  • Table/markdown formatters for submissions and history

Tests

  • CLI flag validation tests (missing --id, missing file path, invalid file)
  • HTTP client tests with mocked Notary API responses
  • Upload test with mocked storage endpoint
  • --wait polling test (status transitions: In Progress → Accepted)
  • --wait polling test (status transitions: In Progress → Rejected)
  • Staple error test on non-macOS
  • Add cmdtests in internal/cli/cmdtest/
  • SHA-256 computation test

Acceptance Criteria

  • asc notarize submit ./file.zip submits and uploads via the REST API (no xcrun dependency)
  • asc notarize submit --wait polls until completion and prints final status
  • asc notarize status --id ID returns current submission status
  • asc notarize log --id ID returns log URL or downloads log
  • asc notarize history lists recent submissions
  • Works on macOS, Linux, and in CI environments (no Xcode required except for staple)
  • Uses existing asc auth credentials — no additional auth setup
  • JSON-first output, --output table/markdown supported
  • All commands have proper error handling and validation

Dependencies

The file upload step requires uploading to an AWS S3 bucket using temporary credentials provided by Apple. Two approaches:

  1. AWS SDK (github.com/aws/aws-sdk-go-v2/service/s3) — handles multipart upload, retries, and transfer acceleration cleanly, but adds a heavier dependency
  2. Minimal HTTP upload — use standard net/http with S3's REST interface directly (like bep/macosnotarylib does), keeping dependencies minimal

Evaluate both approaches. The minimal approach aligns better with the project's "standard library first" philosophy from docs/GO_STANDARDS.md.

Implementation Notes

  • The Notary API uses a different base URL from the main ASC API (appstoreconnect.apple.com/notary/v2 vs api.appstoreconnect.apple.com/v1)
  • JWT auth is identical — reuse internal/auth token generation
  • Upload credentials expire after 12 hours
  • Notarization typically completes in < 10 minutes but can take longer
  • Supported file types: .zip, .dmg, .pkg
  • Stapling cannot be done via API — requires xcrun stapler staple on macOS
  • The bep/macosnotarylib Go library (MIT, single file) is a useful reference implementation
  • For --wait polling, use exponential backoff starting at ~5s, capping at ~30s

Prior Art

Tool Approach Cross-platform?
xcrun notarytool Native Apple CLI macOS only
gon (Bearer/gon) Shells out to xcrun notarytool macOS only
bep/macosnotarylib REST API (Go library, no CLI) Yes
asc notarize REST API (CLI, JSON-first) Yes

References

Originally created by @rudrankriyam on GitHub (Feb 6, 2026). Original GitHub issue: https://github.com/rudrankriyam/App-Store-Connect-CLI/issues/431 # Overview Add support for macOS notarization using Apple's Notary REST API. This enables developers to submit, track, and inspect notarization requests directly from `asc` — without requiring macOS, Xcode, or `xcrun notarytool`. The Notary API is a separate REST API from the main App Store Connect API, but uses **the same JWT authentication and API keys**. It lives at `https://appstoreconnect.apple.com/notary/v2/` (not in the App Store Connect OpenAPI spec). # Why This Matters - **No existing CLI does this via the REST API.** Every other tool (`xcrun notarytool`, gon) shells out to Apple's native CLI, requiring macOS + Xcode. The only pure REST API implementation is `bep/macosnotarylib` (Go library, no CLI). - **Cross-platform.** The REST API works from any OS — Linux CI servers, Docker containers, GitHub Actions runners — no Xcode needed. - **Zero new auth setup.** Developers already authenticated with `asc auth` can notarize immediately. Same key, same issuer, same JWT. - **Consistent UX.** JSON-first output, `--output table/markdown`, `--paginate` — same patterns as every other `asc` command. # Use Case A developer distributes a macOS app outside the Mac App Store with Developer ID and needs to notarize it: ```bash # Submit, upload, and wait for notarization to complete asc notarize submit ./MyApp.zip --wait # Check on a previous submission asc notarize status --id "2efe2717-52ef-43a5-96dc-0797e4ca1041" # Get the notarization log (warnings, errors) asc notarize log --id "2efe2717-52ef-43a5-96dc-0797e4ca1041" # List recent submissions asc notarize history # Optionally staple on macOS asc notarize staple ./MyApp.dmg ``` # API Documentation - [Submitting software for notarization over the web](https://developer.apple.com/documentation/notaryapi/submitting-software-for-notarization-over-the-web) - [Notary API reference](https://developer.apple.com/documentation/notaryapi) - Mirror: [sosumi.ai/documentation/notaryapi](https://sosumi.ai/documentation/notaryapi) # Scope (Notary API endpoints) The Notary API has **4 endpoints**, all at `https://appstoreconnect.apple.com/notary/v2/`: | Method | Path | Purpose | |--------|------|---------| | `POST` | `/submissions` | Start a new submission (returns temporary upload credentials for Apple's cloud storage) | | `GET` | `/submissions` | List previous submissions (up to 100) | | `GET` | `/submissions/{id}` | Get submission status | | `GET` | `/submissions/{id}/logs` | Get submission log URL | **Note:** This API is NOT in the App Store Connect OpenAPI spec (`docs/openapi/latest.json`). It is documented separately by Apple. ## Submission Workflow Apple's notarization uses a two-step upload process. The Notary API does not accept the file directly — instead, it returns temporary cloud storage credentials so you can upload the file to Apple's storage, and Apple picks it up from there for scanning. 1. **POST `/submissions`** with `{ submissionName, sha256, notifications? }` — returns temporary AWS S3 credentials (`awsAccessKeyId`, `awsSecretAccessKey`, `awsSessionToken`, `bucket`, `object`) and a submission `id` 2. **Upload the file** to the provided S3 bucket using the temporary credentials (standard multipart upload with transfer acceleration) 3. **GET `/submissions/{id}`** to poll status — returns `Accepted`, `In Progress`, `Invalid`, or `Rejected` 4. **GET `/submissions/{id}/logs`** to retrieve a signed URL to the JSON log file The temporary upload credentials expire after **12 hours**. If not used before expiry, the submission must be restarted. ## Authentication Identical to App Store Connect API: - Same JWT format (ES256, 20-minute expiry recommended) - Same API keys (`ASC_KEY_ID`, `ASC_ISSUER_ID`, private key) - Header: `Authorization: Bearer <token>` # Proposed CLI ## Command group ``` asc notarize <subcommand> [flags] ``` ## Subcommands ### `asc notarize submit` Submit a file for notarization, upload it, and optionally wait for completion. ```bash asc notarize submit ./MyApp.zip asc notarize submit ./MyApp.zip --wait asc notarize submit ./MyApp.zip --wait --webhook "https://example.com/notify" asc notarize submit ./MyApp.dmg --output table ``` Flags: - Positional arg or `--file`: path to `.zip`, `.dmg`, or `.pkg` - `--wait`: block until notarization completes (poll status) - `--webhook`: URL to receive notification on completion - `--output`, `--pretty`: standard output flags Behavior: 1. Compute SHA-256 of the file 2. POST to `/submissions` with file name, hash, and optional webhook 3. Upload file to Apple's cloud storage using returned credentials 4. If `--wait`, poll `/submissions/{id}` until terminal state 5. Print submission result (id, status, and log URL if complete) ### `asc notarize status` Check the status of a submission. ```bash asc notarize status --id "2efe2717-52ef-43a5-96dc-0797e4ca1041" asc notarize status --id "2efe2717-..." --wait ``` Flags: - `--id`: submission ID (required) - `--wait`: poll until terminal state - `--output`, `--pretty` ### `asc notarize log` Get the notarization log for a submission. ```bash asc notarize log --id "2efe2717-52ef-43a5-96dc-0797e4ca1041" asc notarize log --id "2efe2717-..." --download ./notarize-log.json ``` Flags: - `--id`: submission ID (required) - `--download`: save log to file instead of printing URL - `--output`, `--pretty` ### `asc notarize history` List recent notarization submissions. ```bash asc notarize history asc notarize history --output table ``` Flags: - `--output`, `--pretty` ### `asc notarize staple` (optional, macOS only) Staple the notarization ticket to a package. This shells out to `xcrun stapler staple` since stapling is not available via the REST API. ```bash asc notarize staple ./MyApp.dmg ``` Flags: - Positional arg or `--file`: path to `.dmg`, `.pkg`, or `.app` - Fails with a clear error on non-macOS systems # Flag Patterns Common: `--id`, `--output`, `--pretty` New: `--wait`, `--webhook`, `--file`, `--download` # Output - JSON minified by default - `--pretty` for pretty-printed JSON - `--output table/markdown` for human-readable output Submit output example: ```json {"id":"2efe2717-52ef-43a5-96dc-0797e4ca1041","status":"In Progress","name":"MyApp.zip"} ``` Status output example: ```json {"id":"2efe2717-52ef-43a5-96dc-0797e4ca1041","status":"Accepted","name":"MyApp.zip","createdDate":"2026-02-07T12:00:00Z"} ``` # Detailed TODO ## Core - [ ] Add Notary API base URL constant (`NotaryBaseURL = "https://appstoreconnect.apple.com/notary/v2"`) - [ ] Add Notary API client methods in new file `internal/asc/client_notarize.go` - [ ] `SubmitNotarization(ctx, name, sha256, webhookURL) (*NotarySubmissionResponse, error)` - [ ] `GetNotarizationStatus(ctx, submissionID) (*NotarySubmission, error)` - [ ] `GetNotarizationLog(ctx, submissionID) (*NotaryLogResponse, error)` - [ ] `GetNotarizationHistory(ctx) (*NotaryHistoryResponse, error)` - [ ] Add Notary types in `internal/asc/notarize.go` - [ ] `NotarySubmissionRequest`, `NotarySubmissionResponse` - [ ] `NotarySubmission`, `NotaryLogResponse`, `NotaryHistoryResponse` - [ ] Upload credential types (`NotaryUploadCredentials`) - [ ] Add upload function in `internal/asc/notarize_upload.go` - [ ] Upload file to Apple's cloud storage using temporary credentials from submit response - [ ] Support transfer acceleration - [ ] Progress reporting for large files ## CLI - [ ] Create `internal/cli/notarize/` package - [ ] `notarize.go` — `NotarizeCommand()` group with subcommands - [ ] `submit.go` — submit + upload + optional wait - [ ] `status.go` — check submission status - [ ] `log.go` — retrieve notarization log - [ ] `history.go` — list previous submissions - [ ] `staple.go` — optional macOS-only staple wrapper - [ ] `shared_wrappers.go` — standard shared wrappers - [ ] `commands.go` — `Command()` factory - [ ] Register in `internal/cli/registry/registry.go` - [ ] Set `UsageFunc: shared.DefaultUsageFunc` on all commands ## Output - [ ] Add output helpers in `internal/asc/notarize_output.go` - [ ] Table/markdown formatters for submissions and history ## Tests - [ ] CLI flag validation tests (missing `--id`, missing file path, invalid file) - [ ] HTTP client tests with mocked Notary API responses - [ ] Upload test with mocked storage endpoint - [ ] `--wait` polling test (status transitions: In Progress → Accepted) - [ ] `--wait` polling test (status transitions: In Progress → Rejected) - [ ] Staple error test on non-macOS - [ ] Add cmdtests in `internal/cli/cmdtest/` - [ ] SHA-256 computation test # Acceptance Criteria - [ ] `asc notarize submit ./file.zip` submits and uploads via the REST API (no xcrun dependency) - [ ] `asc notarize submit --wait` polls until completion and prints final status - [ ] `asc notarize status --id ID` returns current submission status - [ ] `asc notarize log --id ID` returns log URL or downloads log - [ ] `asc notarize history` lists recent submissions - [ ] Works on macOS, Linux, and in CI environments (no Xcode required except for staple) - [ ] Uses existing `asc auth` credentials — no additional auth setup - [ ] JSON-first output, `--output table/markdown` supported - [ ] All commands have proper error handling and validation # Dependencies The file upload step requires uploading to an AWS S3 bucket using temporary credentials provided by Apple. Two approaches: 1. **AWS SDK** (`github.com/aws/aws-sdk-go-v2/service/s3`) — handles multipart upload, retries, and transfer acceleration cleanly, but adds a heavier dependency 2. **Minimal HTTP upload** — use standard `net/http` with S3's REST interface directly (like `bep/macosnotarylib` does), keeping dependencies minimal Evaluate both approaches. The minimal approach aligns better with the project's "standard library first" philosophy from `docs/GO_STANDARDS.md`. # Implementation Notes - The Notary API uses a **different base URL** from the main ASC API (`appstoreconnect.apple.com/notary/v2` vs `api.appstoreconnect.apple.com/v1`) - JWT auth is identical — reuse `internal/auth` token generation - Upload credentials expire after **12 hours** - Notarization typically completes in **< 10 minutes** but can take longer - Supported file types: `.zip`, `.dmg`, `.pkg` - Stapling cannot be done via API — requires `xcrun stapler staple` on macOS - The `bep/macosnotarylib` Go library (MIT, single file) is a useful reference implementation - For `--wait` polling, use exponential backoff starting at ~5s, capping at ~30s # Prior Art | Tool | Approach | Cross-platform? | |------|----------|----------------| | `xcrun notarytool` | Native Apple CLI | macOS only | | gon (Bearer/gon) | Shells out to `xcrun notarytool` | macOS only | | `bep/macosnotarylib` | REST API (Go library, no CLI) | Yes | | **`asc notarize`** | REST API (CLI, JSON-first) | **Yes** | # References - Apple Notary API docs: https://developer.apple.com/documentation/notaryapi - Submission guide: https://sosumi.ai/documentation/notaryapi/submitting-software-for-notarization-over-the-web - `bep/macosnotarylib`: https://github.com/bep/macosnotarylib (Go reference implementation) - Existing auth: `internal/auth/` (JWT generation — reusable as-is) - Client core: `internal/asc/client_core.go` (BaseURL, HTTP helpers)
kerem 2026-02-26 21:33:35 +03:00
Author
Owner

@rudrankriyam commented on GitHub (Feb 6, 2026):

Implemented in PR #432 and released in 0.25.0.

<!-- gh-comment-id:3863039161 --> @rudrankriyam commented on GitHub (Feb 6, 2026): Implemented in PR #432 and released in 0.25.0.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/App-Store-Connect-CLI#123
No description provided.