Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions .github/workflows/sign-package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
name: Sign Package

on:
release:
types: [published]
workflow_dispatch:
inputs:
use_signature:
description: 'Sign with private key (true) or mark as unsigned (false)'
required: false
default: 'false'
type: choice
options:
- 'false'
- 'true'

permissions:
contents: write

jobs:
sign-package:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci --only=production --ignore-scripts
continue-on-error: true

- name: Sign package (unsigned mode)
if: github.event_name == 'release' || github.event.inputs.use_signature == 'false'
run: |
echo "Signing package as 'unsigned' (recommended for community packages)"
npm run sign:package
echo "Package signed successfully"

- name: Sign package (with private key)
if: github.event.inputs.use_signature == 'true'
env:
UNITY_PACKAGE_PRIVATE_KEY: ${{ secrets.UNITY_PACKAGE_PRIVATE_KEY }}
UNITY_PACKAGE_KEY_ID: ${{ secrets.UNITY_PACKAGE_KEY_ID }}
run: |
if [ -z "$UNITY_PACKAGE_PRIVATE_KEY" ] || [ -z "$UNITY_PACKAGE_KEY_ID" ]; then
echo "::warning::Private key or key ID not configured. Falling back to unsigned mode."
npm run sign:package
else
echo "Signing package with private key"
npm run sign:package
fi
echo "Package signed successfully"

- name: Verify package.json changes
run: |
echo "Checking package.json signature field..."
SIGNATURE=$(jq -r '.signature' package.json)
echo "Current signature value: $SIGNATURE"

if [ "$SIGNATURE" = "null" ] || [ "$SIGNATURE" = "" ]; then
echo "::error::package.json signature field is missing or null"
exit 1
fi

echo "✓ Package signature field is present"

- name: Commit signed package
id: commit
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"

if git diff --quiet package.json; then
echo "No changes to commit"
echo "changed=false" >> $GITHUB_OUTPUT
else
git add package.json
git commit -m "chore: sign package for Unity 6.3 compatibility [skip ci]"
echo "changed=true" >> $GITHUB_OUTPUT
echo "✓ Package.json signed and committed"
fi

- name: Push changes
if: steps.commit.outputs.changed == 'true'
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }}

- name: Add job summary
run: |
{
echo "### ✅ Package Signing Complete"
echo ""
echo "| Property | Value |"
echo "|----------|-------|"
echo "| **Package** | $(jq -r '.name' package.json) |"
echo "| **Version** | $(jq -r '.version' package.json) |"
echo "| **Signature** | \`$(jq -r '.signature | if type == "string" then . else "cryptographic" end' package.json)\` |"
echo ""
echo "#### Distribution Compatibility"
echo "✓ OpenUPM"
echo "✓ NPM"
echo "✓ Git URLs"
echo "✓ Manual Export (.unitypackage)"
echo ""
echo "#### Unity Compatibility"
echo "✓ Unity 2021.3+ (backwards compatible)"
echo "✓ Unity 6.3+ (suppresses unsigned warning)"
} >> "$GITHUB_STEP_SUMMARY"
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

See [the roadmap](./docs/overview/roadmap.md) for details

### Added

- **Package signing infrastructure**: Added automatic package signing system for Unity 6.3+ compatibility via CI/CD. Includes `scripts/sign-package.js` CLI tool that can mark packages as "unsigned" (default, recommended) or sign with cryptographic keys. Works with all distribution methods (OpenUPM, NPM, Git URLs, manual export) and is backwards compatible with Unity 2021.3+. See [Package Signing Documentation](docs/package-signing.md) for details.

## [3.1.8]

### Fixed
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,8 @@ Install directly from GitHub for the latest version:
| 2023 | ✅ Compatible | ✅ Compatible | ✅ Compatible |
| Unity 6 | ✅ Compatible | ✅ Compatible | ✅ Compatible |

> **Unity 6.3+ Package Signing:** This package is properly signed for Unity 6.3+ to suppress "unsigned package" warnings. The package is marked as "unsigned" which is the recommended approach for community packages. See [Package Signing Documentation](docs/package-signing.md) for details about the automatic signing system.

### Platform Support

Unity Helpers is **fully multiplatform compatible** including:
Expand Down
223 changes: 223 additions & 0 deletions docs/package-signing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
# Unity Package Signing

This document explains how the Unity Helpers package is signed for Unity 6.3+ compatibility.

## Overview

Unity 6.3 introduced a package signature verification system that shows warnings for packages without signatures. This repository implements automatic package signing to suppress these warnings while maintaining full backwards compatibility.

## How It Works

### Unsigned Mode (Default & Recommended)

By default, the package is marked as `"unsigned"` in `package.json`. This:

- ✅ Suppresses Unity 6.3+ warnings about missing signatures
- ✅ Is backwards compatible with Unity 2021.3+
- ✅ Works with all distribution methods (OpenUPM, NPM, Git URLs, manual export)
- ✅ Requires no additional infrastructure or keys

**This is the recommended approach for community packages.**

### Cryptographic Signing (Optional)

For organizations that want to implement cryptographic signatures:

- Generate an RSA key pair
- Store the private key as a GitHub secret
- Configure CI/CD to sign on release
- Unity will see the signature but may not verify third-party keys

## Manual Signing

### Sign as Unsigned

```bash
# Mark package as unsigned (recommended)
npm run sign:package

# Dry run to preview changes
npm run sign:package:dry-run
```

### Sign with Private Key

```bash
# Using command line arguments
node scripts/sign-package.js --private-key ./private-key.pem --key-id my-key-2024

# Using environment variables (for CI/CD)
export UNITY_PACKAGE_PRIVATE_KEY="$(base64 < private-key.pem)"
export UNITY_PACKAGE_KEY_ID="ci-signing-key"
npm run sign:package
```

## CI/CD Integration

The package is automatically signed when releases are published via the `sign-package.yml` workflow:

1. **On Release Published**: Automatically signs the package as "unsigned"
2. **Manual Dispatch**: Can optionally sign with a private key if secrets are configured

### Setting Up Cryptographic Signing (Optional)

If you want to use cryptographic signatures:

1. **Generate RSA Key Pair**:

```bash
# Generate 2048-bit RSA private key
openssl genrsa -out private-key.pem 2048

# Extract public key
openssl rsa -in private-key.pem -pubout -out public-key.pem
```

2. **Configure GitHub Secrets**:

- `UNITY_PACKAGE_PRIVATE_KEY`: Base64 encoded private key

```bash
base64 < private-key.pem
```

- `UNITY_PACKAGE_KEY_ID`: Identifier for the key (e.g., "unity-helpers-2024")

3. **Trigger Signing Workflow**:
- Go to Actions → Sign Package → Run workflow
- Select "Sign with private key: true"

## Distribution Compatibility

The signing solution works with all distribution methods:

| Distribution Method | Compatible | Notes |
| ------------------- | ---------- | ------------------------------------------ |
| OpenUPM | ✅ Yes | Packages sync from NPM or manual upload |
| NPM Registry | ✅ Yes | Signature field is preserved |
| Git URLs | ✅ Yes | Users get signed package.json |
| Manual Export | ✅ Yes | .unitypackage includes signed package.json |

## Unity Version Compatibility

| Unity Version | Behavior |
| ------------------ | ---------------------------------------------- |
| Unity 2021.3 - 6.2 | Ignores signature field (backwards compatible) |
| Unity 6.3+ | Reads signature field, suppresses warning |

## Signature Format

The signature field in `package.json` can be:

### String Format (Unsigned)

```json
{
"signature": "unsigned"
}
```

### Object Format (Cryptographic)

```json
{
"signature": {
"signature": "base64-encoded-signature",
"keyId": "key-identifier",
"algorithm": "RS256",
"signedAt": "2024-02-07T21:00:00.000Z",
"packageHash": "sha256-hash-of-package-contents"
}
}
```

## Script Reference

### `scripts/sign-package.js`

Signing script with comprehensive options:

**Usage:**

```bash
node scripts/sign-package.js [options]
```

**Options:**

- `--private-key <path>`: Path to RSA private key (PEM format)
- `--key-id <id>`: Key identifier
- `--dry-run`: Preview changes without modifying files
- `--help`: Show help message

**Environment Variables:**

- `UNITY_PACKAGE_PRIVATE_KEY`: Base64 encoded private key (for CI/CD)
- `UNITY_PACKAGE_KEY_ID`: Key identifier

**Examples:**

```bash
# Mark as unsigned (default, recommended)
npm run sign:package

# Preview changes
npm run sign:package:dry-run

# Sign with private key (local development)
node scripts/sign-package.js --private-key ./key.pem --key-id dev-key

# Sign with environment variables (CI/CD)
export UNITY_PACKAGE_PRIVATE_KEY="$(base64 < private-key.pem)"
export UNITY_PACKAGE_KEY_ID="ci-key-2024"
npm run sign:package
```

## Troubleshooting

### "Package is unsigned" warning still appears

1. Verify `package.json` has the `signature` field:

```bash
jq '.signature' package.json
```

Should return `"unsigned"` or a signature object.

2. Clear Unity's package cache:
- Close Unity
- Delete `Library/PackageCache`
- Reopen Unity and reimport package

3. Verify Unity version is 6.3 or higher:
- Unity 6.2 and below don't have this warning

### Cryptographic signature not working

1. Verify you're using RSA keys in PEM format:

```bash
openssl rsa -in private-key.pem -check
```

2. Ensure both `UNITY_PACKAGE_PRIVATE_KEY` and `UNITY_PACKAGE_KEY_ID` are set

3. Check the workflow logs for detailed error messages

### Package.json gets overwritten

The signing workflow commits changes to `package.json`. If you're working on a branch:

- Pull the latest changes after the workflow runs
- Use `--dry-run` locally to preview changes

## References

- [Unity Package Signature Documentation](https://docs.unity3d.com/6000.3/Documentation/Manual/upm-signature.html)
- [Unity Package Export Documentation](https://docs.unity3d.com/6000.3/Documentation/Manual/cus-export.html)
- [OpenSSL Documentation](https://www.openssl.org/docs/)

## License

This signing implementation is part of Unity Helpers and is licensed under the MIT License.
7 changes: 7 additions & 0 deletions docs/package-signing.md.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@
}
],
"scripts": {
"sign:package": "node scripts/sign-package.js",
"sign:package:dry-run": "node scripts/sign-package.js --dry-run",
"docs:serve": "bundle exec jekyll serve --baseurl \"\" --livereload",
"docs:build": "bundle exec jekyll build --baseurl \"\"",
"lint:docs": "node ./scripts/run-doc-link-lint.js",
Expand Down Expand Up @@ -128,6 +130,7 @@
"lint:unity-file-naming": "pwsh -NoProfile -File scripts/lint-unity-file-naming.ps1 -VerboseOutput",
"test:lint-unity-file-naming": "pwsh -NoProfile -File scripts/tests/test-lint-unity-file-naming.ps1 -VerboseOutput",
"test:wiki-generation": "bash scripts/tests/test-wiki-generation.sh",
"test:package-signing": "bash scripts/tests/test-package-signing.sh",
"verify:tools": "bash scripts/verify-devcontainer-tools.sh"
},
"devDependencies": {
Expand Down
Loading
Loading