├── .github └── workflows │ └── release.yaml ├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── main.go └── screenshots └── demo.png /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | name: Build 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | goos: [linux] 14 | goarch: [amd64, arm64] 15 | steps: 16 | - name: Set up Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: '1.23.2' 20 | 21 | - name: Check out code 22 | uses: actions/checkout@v4 23 | 24 | - name: Build 25 | env: 26 | GOOS: ${{ matrix.goos }} 27 | GOARCH: ${{ matrix.goarch }} 28 | run: | 29 | go build -v -o resign-${{ matrix.goos }}-${{ matrix.goarch }} 30 | 31 | - name: Upload Artifacts 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: resign-${{ matrix.goos }}-${{ matrix.goarch }} 35 | path: resign-${{ matrix.goos }}-${{ matrix.goarch }} 36 | 37 | - name: Upload Artifact to Release 38 | uses: softprops/action-gh-release@v2 39 | env: 40 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 41 | with: 42 | files: ./resign-${{ matrix.goos }}-${{ matrix.goarch }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | 27 | # output directory 28 | output 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, Vincent Young 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IPA-Resign 2 | 3 | A RESTful API service for iOS IPA file analysis and re-signing. This service provides an alternative to Esign and Universal-sign tools. 4 | 5 | ## Features 6 | 7 | - Analyze IPA files to extract bundle ID and app name 8 | - Re-sign IPA files with custom certificates and provisioning profiles 9 | - Generate installation manifest files for iOS OTA installation 10 | - Consistent UUID-based storage of files 11 | - Caching of previously analyzed IPAs 12 | 13 | ## Prerequisites 14 | 15 | - [zsign](https://github.com/zhlynn/zsign) must be installed on your deployment server 16 | - Go 1.16 or later 17 | 18 | ## Installation 19 | 20 | ```bash 21 | git clone https://github.com/yourusername/ipa-resign.git 22 | cd ipa-resign 23 | go mod download 24 | go build . 25 | ``` 26 | 27 | ## Usage 28 | 29 | ```bash 30 | # Basic usage with default settings 31 | ./ipa-resign 32 | 33 | # Custom base URL and port 34 | ./ipa-resign --base-url=https://example.com --port=9900 35 | 36 | # Using environment variables 37 | export BASE_URL=https://example.com 38 | ./ipa-resign --port=9900 39 | ``` 40 | 41 | ## API Endpoints 42 | 43 | ### Analyze IPA 44 | 45 | ``` 46 | POST /analyze 47 | ``` 48 | 49 | Parameters: 50 | - `ipa_url`: Direct download URL to the IPA file 51 | 52 | Response: 53 | ```json 54 | { 55 | "uuid": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", 56 | "bundle_id": "com.example.app", 57 | "app_name": "Example App", 58 | "source_url": "https://example.com/download/6ba7b810-9dad-11d1-80b4-00c04fd430c8/source.ipa", 59 | "analyzed": true 60 | } 61 | ``` 62 | 63 | ### Re-sign IPA 64 | 65 | ``` 66 | POST /resign 67 | ``` 68 | 69 | Parameters: 70 | - Either `ipa_uuid` (from previous analysis) or `ipa_url` (direct download link) 71 | - `p12`: Upload your signing certificate (multipart/form-data) 72 | - `mobileprovision`: Upload your provisioning profile (multipart/form-data) 73 | - `p12_password`: Password for the p12 certificate 74 | - `bundle_id`: (Optional) Custom bundle ID for the resigned app 75 | - `app_name`: (Optional) Custom app name for the resigned app 76 | 77 | Response: 78 | ```json 79 | { 80 | "uuid": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", 81 | "plist_url": "https://example.com/download/6ba7b810-9dad-11d1-80b4-00c04fd430c8/manifest.plist", 82 | "source_url": "https://example.com/download/6ba7b810-9dad-11d1-80b4-00c04fd430c8/source.ipa", 83 | "ipa_url": "https://example.com/download/6ba7b810-9dad-11d1-80b4-00c04fd430c8/resigned.ipa", 84 | "bundle_id": "com.example.app", 85 | "app_name": "Example App" 86 | } 87 | ``` 88 | 89 | ### Download Files 90 | 91 | ``` 92 | GET /download/:uuid/:filename 93 | ``` 94 | 95 | Supported filenames: 96 | - `source.ipa`: Original IPA file 97 | - `resigned.ipa`: Re-signed IPA file 98 | - `manifest.plist`: Installation manifest for iOS OTA installation 99 | 100 | ## OTA Installation 101 | 102 | After re-signing an IPA, you can install it on iOS devices using the Safari browser with the following URL format: 103 | 104 | ``` 105 | itms-services://?action=download-manifest&url=https://example.com/download/UUID/manifest.plist 106 | ``` 107 | 108 | ## Demo 109 | 110 | A demo service is available at [https://sign.missuo.me](https://sign.missuo.me) 111 | 112 | ![demo](./screenshots/demo.png) 113 | 114 | **Note**: The front-end interface is not open source at this time. When using the service, all fields must be completed, and direct IPA file uploads are not supported. You must provide correct and complete download links. 115 | 116 | ## Project Structure 117 | 118 | ``` 119 | ipa-resign/ 120 | ├── main.go # Main application code 121 | ├── output/ # Generated files directory 122 | │ └── [UUID]/ # Unique directories for each IPA 123 | │ ├── source.ipa 124 | │ ├── resigned.ipa 125 | │ ├── manifest.plist 126 | │ ├── cert.p12 127 | │ └── profile.mobileprovision 128 | └── screenshots/ # Screenshots for documentation 129 | ``` 130 | 131 | ## License 132 | 133 | [BSD-3-Clause](./LICENSE) -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/missuo/resign 2 | 3 | go 1.23.2 4 | 5 | require ( 6 | github.com/gin-contrib/cors v1.7.2 7 | github.com/gin-gonic/gin v1.10.0 8 | github.com/google/uuid v1.6.0 9 | howett.net/plist v1.0.1 10 | ) 11 | 12 | require ( 13 | github.com/bytedance/sonic v1.11.6 // indirect 14 | github.com/bytedance/sonic/loader v0.1.1 // indirect 15 | github.com/cloudwego/base64x v0.1.4 // indirect 16 | github.com/cloudwego/iasm v0.2.0 // indirect 17 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 18 | github.com/gin-contrib/sse v0.1.0 // indirect 19 | github.com/go-playground/locales v0.14.1 // indirect 20 | github.com/go-playground/universal-translator v0.18.1 // indirect 21 | github.com/go-playground/validator/v10 v10.20.0 // indirect 22 | github.com/goccy/go-json v0.10.2 // indirect 23 | github.com/json-iterator/go v1.1.12 // indirect 24 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 25 | github.com/kr/text v0.2.0 // indirect 26 | github.com/leodido/go-urn v1.4.0 // indirect 27 | github.com/mattn/go-isatty v0.0.20 // indirect 28 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 29 | github.com/modern-go/reflect2 v1.0.2 // indirect 30 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 31 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 32 | github.com/ugorji/go/codec v1.2.12 // indirect 33 | golang.org/x/arch v0.8.0 // indirect 34 | golang.org/x/crypto v0.35.0 // indirect 35 | golang.org/x/net v0.36.0 // indirect 36 | golang.org/x/sys v0.30.0 // indirect 37 | golang.org/x/text v0.22.0 // indirect 38 | google.golang.org/protobuf v1.34.1 // indirect 39 | gopkg.in/yaml.v3 v3.0.1 // indirect 40 | ) 41 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= 2 | github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= 3 | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= 4 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 5 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 6 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 7 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 8 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 9 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 14 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 15 | github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= 16 | github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= 17 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 18 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 19 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 20 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 21 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 22 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 23 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 24 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 25 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 26 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 27 | github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= 28 | github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 29 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 30 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 31 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 32 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 33 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 34 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 35 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 36 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 37 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 38 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 39 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 40 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 41 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 42 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 43 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 44 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 45 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 46 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 47 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 48 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 49 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 50 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 51 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 52 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 53 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 54 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 55 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 56 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 57 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 58 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 59 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 60 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 61 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 62 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 63 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 64 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 65 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 66 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 67 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 68 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 69 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 70 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 71 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 72 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 73 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 74 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 75 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 76 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 77 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 78 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 79 | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= 80 | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 81 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 82 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 83 | golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= 84 | golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= 85 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 86 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 87 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 88 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 89 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 90 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 91 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 92 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 93 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= 94 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 95 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 96 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 97 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 98 | gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= 99 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 100 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 101 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 102 | howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= 103 | howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= 104 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 105 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 106 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: Vincent Yang 3 | * @Date: 2024-10-25 20:51:53 4 | * @LastEditors: Vincent Yang 5 | * @LastEditTime: 2025-04-15 11:37:46 6 | * @FilePath: /resign/main.go 7 | * @Telegram: https://t.me/missuo 8 | * @GitHub: https://github.com/missuo 9 | * 10 | * Copyright © 2025 by Vincent, All Rights Reserved. 11 | */ 12 | 13 | package main 14 | 15 | import ( 16 | "archive/zip" 17 | "flag" 18 | "fmt" 19 | "io" 20 | "net/http" 21 | "os" 22 | "os/exec" 23 | "path/filepath" 24 | "strings" 25 | "sync" 26 | "text/template" 27 | "time" 28 | 29 | "github.com/gin-contrib/cors" 30 | "github.com/gin-gonic/gin" 31 | "github.com/google/uuid" 32 | "howett.net/plist" 33 | ) 34 | 35 | const plistTemplate = ` 36 | 37 | 38 | 39 | items 40 | 41 | 42 | assets 43 | 44 | 45 | kind 46 | software-package 47 | url 48 | {{.IpaURL}} 49 | 50 | 51 | metadata 52 | 53 | bundle-identifier 54 | {{.BundleID}} 55 | bundle-version 56 | 1 57 | kind 58 | software 59 | title 60 | {{.AppName}} 61 | 62 | 63 | 64 | 65 | ` 66 | 67 | var ( 68 | baseURL string // Base URL for download links, configurable via command line args or env vars 69 | outputDir = "./output" // Root directory for storing output files 70 | port string // Server listening port 71 | ipaCache = make(map[string]IPAInfo) 72 | ipaCacheLock sync.RWMutex 73 | ) 74 | 75 | // IPAInfo stores information about analyzed IPA files 76 | type IPAInfo struct { 77 | OriginalURL string 78 | UUID string 79 | BundleID string 80 | AppName string 81 | UploadedAt time.Time 82 | } 83 | 84 | func init() { 85 | // Define command line flags 86 | flag.StringVar(&baseURL, "base-url", "", "Base URL for generated download links") 87 | flag.StringVar(&port, "port", "8080", "Port to listen on") 88 | 89 | // Parse command line arguments 90 | flag.Parse() 91 | 92 | // If base URL is not set via command line, try environment variable 93 | if baseURL == "" { 94 | baseURL = os.Getenv("BASE_URL") 95 | // If env var is also not set, use default value 96 | if baseURL == "" { 97 | fmt.Println("Warning: BASE_URL not set, using default value") 98 | baseURL = fmt.Sprintf("http://localhost:%s", port) 99 | } 100 | } 101 | 102 | // Ensure baseURL doesn't end with a slash 103 | baseURL = strings.TrimRight(baseURL, "/") 104 | 105 | // Print configuration info 106 | fmt.Printf("Using BASE_URL: %s\n", baseURL) 107 | fmt.Printf("Using PORT: %s\n", port) 108 | } 109 | 110 | func main() { 111 | gin.SetMode(gin.ReleaseMode) 112 | r := gin.Default() 113 | r.Use(cors.Default()) 114 | 115 | r.POST("/resign", resignHandler) 116 | r.POST("/analyze", analyzeIPAHandler) // New endpoint for analyzing IPA files 117 | r.GET("/download/:uuid/:filename", downloadHandler) 118 | 119 | // Ensure output directory exists 120 | if err := os.MkdirAll(outputDir, 0755); err != nil { 121 | panic(err) 122 | } 123 | 124 | // Start the server 125 | fmt.Printf("Server starting on port %s...\n", port) 126 | r.Run(":" + port) 127 | } 128 | 129 | // Download a file from URL and save it to the specified filepath 130 | func downloadFile(url, filepath string) error { 131 | resp, err := http.Get(url) 132 | if err != nil { 133 | return err 134 | } 135 | defer resp.Body.Close() 136 | 137 | if resp.StatusCode != http.StatusOK { 138 | return fmt.Errorf("bad status: %s", resp.Status) 139 | } 140 | 141 | out, err := os.Create(filepath) 142 | if err != nil { 143 | return err 144 | } 145 | defer out.Close() 146 | 147 | _, err = io.Copy(out, resp.Body) 148 | return err 149 | } 150 | 151 | // Extract Info.plist from IPA file and parse it 152 | func extractIPAInfo(ipaPath string) (string, string, error) { 153 | reader, err := zip.OpenReader(ipaPath) 154 | if err != nil { 155 | return "", "", fmt.Errorf("failed to open IPA file: %v", err) 156 | } 157 | defer reader.Close() 158 | 159 | var infoPlistFile *zip.File 160 | for _, file := range reader.File { 161 | if strings.HasSuffix(file.Name, ".app/Info.plist") { 162 | infoPlistFile = file 163 | break 164 | } 165 | } 166 | 167 | if infoPlistFile == nil { 168 | return "", "", fmt.Errorf("info.plist not found in IPA") 169 | } 170 | 171 | // Open the plist file 172 | rc, err := infoPlistFile.Open() 173 | if err != nil { 174 | return "", "", fmt.Errorf("failed to open Info.plist: %v", err) 175 | } 176 | defer rc.Close() 177 | 178 | // Read the plist content 179 | plistData, err := io.ReadAll(rc) 180 | if err != nil { 181 | return "", "", fmt.Errorf("failed to read Info.plist: %v", err) 182 | } 183 | 184 | // Parse the plist 185 | var plistObj map[string]interface{} 186 | if _, err := plist.Unmarshal(plistData, &plistObj); err != nil { 187 | return "", "", fmt.Errorf("failed to parse Info.plist: %v", err) 188 | } 189 | 190 | // Extract bundle ID and app name 191 | bundleID, ok := plistObj["CFBundleIdentifier"].(string) 192 | if !ok { 193 | return "", "", fmt.Errorf("CFBundleIdentifier not found or not a string") 194 | } 195 | 196 | appName, ok := plistObj["CFBundleDisplayName"].(string) 197 | if !ok { 198 | // Try CFBundleName as fallback 199 | appName, ok = plistObj["CFBundleName"].(string) 200 | if !ok { 201 | appName = "Unknown App" 202 | } 203 | } 204 | 205 | return bundleID, appName, nil 206 | } 207 | 208 | // Handler for the new analyze endpoint 209 | func analyzeIPAHandler(c *gin.Context) { 210 | // Get IPA download URL from form data 211 | ipaURL := c.PostForm("ipa_url") 212 | if ipaURL == "" { 213 | c.JSON(http.StatusBadRequest, gin.H{"error": "Missing ipa_url parameter"}) 214 | return 215 | } 216 | 217 | // Check if this URL has already been analyzed 218 | ipaCacheLock.RLock() 219 | for _, info := range ipaCache { 220 | if info.OriginalURL == ipaURL { 221 | ipaCacheLock.RUnlock() 222 | c.JSON(http.StatusOK, gin.H{ 223 | "uuid": info.UUID, 224 | "bundle_id": info.BundleID, 225 | "app_name": info.AppName, 226 | "source_url": fmt.Sprintf("%s/download/%s/source.ipa", baseURL, info.UUID), 227 | "analyzed": true, 228 | }) 229 | return 230 | } 231 | } 232 | ipaCacheLock.RUnlock() 233 | 234 | // Generate a UUID for this IPA 235 | uuidStr := uuid.New().String() 236 | 237 | // Create directory for this IPA 238 | workDir := filepath.Join(outputDir, uuidStr) 239 | if err := os.MkdirAll(workDir, 0755); err != nil { 240 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create directory"}) 241 | return 242 | } 243 | 244 | // Path for source IPA 245 | ipaPath := filepath.Join(workDir, "source.ipa") 246 | 247 | // Download the IPA file 248 | if err := downloadFile(ipaURL, ipaPath); err != nil { 249 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to download IPA file: " + err.Error()}) 250 | return 251 | } 252 | 253 | // Extract bundle ID and app name 254 | bundleID, appName, err := extractIPAInfo(ipaPath) 255 | if err != nil { 256 | // If extraction fails, delete the downloaded file 257 | os.RemoveAll(workDir) 258 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to extract IPA info: " + err.Error()}) 259 | return 260 | } 261 | 262 | // Store the IPA info in cache 263 | ipaCacheLock.Lock() 264 | ipaCache[uuidStr] = IPAInfo{ 265 | OriginalURL: ipaURL, 266 | UUID: uuidStr, 267 | BundleID: bundleID, 268 | AppName: appName, 269 | UploadedAt: time.Now(), 270 | } 271 | ipaCacheLock.Unlock() 272 | 273 | // Return the UUID and extracted info 274 | c.JSON(http.StatusOK, gin.H{ 275 | "uuid": uuidStr, 276 | "bundle_id": bundleID, 277 | "app_name": appName, 278 | "source_url": fmt.Sprintf("%s/download/%s/source.ipa", baseURL, uuidStr), 279 | "analyzed": true, 280 | }) 281 | } 282 | 283 | // Handler for downloading files 284 | func downloadHandler(c *gin.Context) { 285 | uuid := c.Param("uuid") 286 | filename := c.Param("filename") 287 | filePath := filepath.Join(outputDir, uuid, filename) 288 | 289 | // Check if file exists 290 | if _, err := os.Stat(filePath); os.IsNotExist(err) { 291 | c.JSON(http.StatusNotFound, gin.H{"error": "File not found"}) 292 | return 293 | } 294 | 295 | // Set appropriate Content-Type header 296 | if strings.HasSuffix(filename, ".ipa") { 297 | c.Header("Content-Type", "application/octet-stream") 298 | c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) 299 | } else if strings.HasSuffix(filename, ".plist") { 300 | c.Header("Content-Type", "application/xml") 301 | } 302 | 303 | c.File(filePath) 304 | } 305 | 306 | // Modified resign handler that uses UUID directories 307 | func resignHandler(c *gin.Context) { 308 | // Get UUID for the IPA to resign 309 | var uuidStr string 310 | var sourceIpaPath string 311 | var bundleID, appName string 312 | var workDir string 313 | 314 | ipaUUID := c.PostForm("ipa_uuid") 315 | ipaURL := c.PostForm("ipa_url") 316 | 317 | if ipaUUID != "" { 318 | // Use existing analyzed IPA if UUID is provided 319 | ipaCacheLock.RLock() 320 | info, exists := ipaCache[ipaUUID] 321 | ipaCacheLock.RUnlock() 322 | 323 | if !exists { 324 | c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ipa_uuid, IPA not found"}) 325 | return 326 | } 327 | 328 | uuidStr = ipaUUID 329 | workDir = filepath.Join(outputDir, uuidStr) 330 | sourceIpaPath = filepath.Join(workDir, "source.ipa") 331 | 332 | // Use the stored bundle ID and app name if not provided 333 | providedBundleID := c.PostForm("bundle_id") 334 | providedAppName := c.PostForm("app_name") 335 | 336 | if providedBundleID != "" { 337 | bundleID = providedBundleID 338 | } else { 339 | bundleID = info.BundleID 340 | } 341 | 342 | if providedAppName != "" { 343 | appName = providedAppName 344 | } else { 345 | appName = info.AppName 346 | } 347 | 348 | } else if ipaURL != "" { 349 | // Create a new UUID for this IPA 350 | uuidStr = uuid.New().String() 351 | workDir = filepath.Join(outputDir, uuidStr) 352 | 353 | // Create directory for this IPA 354 | if err := os.MkdirAll(workDir, 0755); err != nil { 355 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create directory"}) 356 | return 357 | } 358 | 359 | // Download the IPA file 360 | sourceIpaPath = filepath.Join(workDir, "source.ipa") 361 | if err := downloadFile(ipaURL, sourceIpaPath); err != nil { 362 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to download IPA file"}) 363 | os.RemoveAll(workDir) 364 | return 365 | } 366 | 367 | // Get bundle ID and app name from parameters 368 | bundleID = c.PostForm("bundle_id") 369 | appName = c.PostForm("app_name") 370 | 371 | // If not provided, try to extract from IPA 372 | if bundleID == "" || appName == "" { 373 | extractedBundleID, extractedAppName, err := extractIPAInfo(sourceIpaPath) 374 | if err == nil { 375 | if bundleID == "" { 376 | bundleID = extractedBundleID 377 | } 378 | if appName == "" { 379 | appName = extractedAppName 380 | } 381 | } 382 | } 383 | 384 | // Store the IPA info in cache 385 | ipaCacheLock.Lock() 386 | ipaCache[uuidStr] = IPAInfo{ 387 | OriginalURL: ipaURL, 388 | UUID: uuidStr, 389 | BundleID: bundleID, 390 | AppName: appName, 391 | UploadedAt: time.Now(), 392 | } 393 | ipaCacheLock.Unlock() 394 | } else { 395 | c.JSON(http.StatusBadRequest, gin.H{"error": "Either ipa_url or ipa_uuid must be provided"}) 396 | return 397 | } 398 | 399 | // Handle uploaded p12 certificate 400 | p12, err := c.FormFile("p12") 401 | if err != nil { 402 | c.JSON(http.StatusBadRequest, gin.H{"error": "Missing p12 file"}) 403 | return 404 | } 405 | p12Path := filepath.Join(workDir, "cert.p12") 406 | if err := c.SaveUploadedFile(p12, p12Path); err != nil { 407 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save p12 file"}) 408 | return 409 | } 410 | 411 | // Handle uploaded mobile provision profile 412 | mobileprovision, err := c.FormFile("mobileprovision") 413 | if err != nil { 414 | c.JSON(http.StatusBadRequest, gin.H{"error": "Missing mobileprovision file"}) 415 | return 416 | } 417 | mobileprovisionPath := filepath.Join(workDir, "profile.mobileprovision") 418 | if err := c.SaveUploadedFile(mobileprovision, mobileprovisionPath); err != nil { 419 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save mobileprovision file"}) 420 | return 421 | } 422 | 423 | // Get p12 password 424 | p12Password := c.PostForm("p12_password") 425 | if p12Password == "" { 426 | c.JSON(http.StatusBadRequest, gin.H{"error": "Missing p12_password parameter"}) 427 | return 428 | } 429 | 430 | // Verify we have bundle ID and app name 431 | if bundleID == "" || appName == "" { 432 | c.JSON(http.StatusBadRequest, gin.H{"error": "Bundle ID and App Name must be provided"}) 433 | return 434 | } 435 | 436 | // Fixed output filename 437 | outputPath := filepath.Join(workDir, "resigned.ipa") 438 | 439 | // Execute the signing command 440 | cmd := exec.Command( 441 | "zsign", 442 | "-k", p12Path, 443 | "-m", mobileprovisionPath, 444 | "-p", p12Password, 445 | "-b", bundleID, 446 | "-n", appName, 447 | "-o", outputPath, 448 | "-z", "9", 449 | sourceIpaPath, 450 | ) 451 | 452 | output, err := cmd.CombinedOutput() 453 | if err != nil { 454 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Signing failed", "output": string(output)}) 455 | return 456 | } 457 | 458 | // Verify output file was generated 459 | if _, err := os.Stat(outputPath); os.IsNotExist(err) { 460 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Output file was not generated"}) 461 | return 462 | } 463 | 464 | // Generate plist file with fixed name 465 | plistPath := filepath.Join(workDir, "manifest.plist") 466 | ipaDownloadURL := fmt.Sprintf("%s/download/%s/resigned.ipa", baseURL, uuidStr) 467 | plistContent := generatePlist(ipaDownloadURL, bundleID, appName) 468 | 469 | if err := os.WriteFile(plistPath, []byte(plistContent), 0644); err != nil { 470 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create plist file"}) 471 | return 472 | } 473 | 474 | // Generate URLs with consistent paths 475 | plistURL := fmt.Sprintf("%s/download/%s/manifest.plist", baseURL, uuidStr) 476 | sourceURL := fmt.Sprintf("%s/download/%s/source.ipa", baseURL, uuidStr) 477 | resignedURL := fmt.Sprintf("%s/download/%s/resigned.ipa", baseURL, uuidStr) 478 | 479 | // Return the download URLs 480 | c.JSON(http.StatusOK, gin.H{ 481 | "uuid": uuidStr, 482 | "plist_url": plistURL, 483 | "source_url": sourceURL, 484 | "ipa_url": resignedURL, 485 | "bundle_id": bundleID, 486 | "app_name": appName, 487 | }) 488 | } 489 | 490 | // Generate plist file content using the template 491 | func generatePlist(ipaURL, bundleID, appName string) string { 492 | tmpl, err := template.New("plist").Parse(plistTemplate) 493 | if err != nil { 494 | return "" 495 | } 496 | 497 | var result strings.Builder 498 | err = tmpl.Execute(&result, struct { 499 | IpaURL string 500 | BundleID string 501 | AppName string 502 | }{ 503 | IpaURL: ipaURL, 504 | BundleID: bundleID, 505 | AppName: appName, 506 | }) 507 | 508 | if err != nil { 509 | return "" 510 | } 511 | 512 | return result.String() 513 | } 514 | -------------------------------------------------------------------------------- /screenshots/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/missuo/resign/5156deef0120f465417cf581f5fa259ca983c710/screenshots/demo.png --------------------------------------------------------------------------------